diff --git a/Readme.md b/Readme.md index 7110fe2..237e759 100644 --- a/Readme.md +++ b/Readme.md @@ -23,7 +23,6 @@ There's also an easy mode for kids (slower ball). # Next - - extract sound logic, only set the params as a gamestate object - separate particles by type - reuse coins and particles diff --git a/dist/PWA/sw-b71.js b/dist/PWA/sw-b71.js index f32287c..536cb9e 100644 --- a/dist/PWA/sw-b71.js +++ b/dist/PWA/sw-b71.js @@ -1,2 +1,33 @@ -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]{ + 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 diff --git a/dist/PWA/sw-b71.js.map b/dist/PWA/sw-b71.js.map index 472a0d0..4983be7 100644 --- a/dist/PWA/sw-b71.js.map +++ b/dist/PWA/sw-b71.js.map @@ -1 +1 @@ -{"mappings":"A,S,E,C,C,C,C,C,C,C,C,C,C,C,C,C,E,G,C,I,E,C,C,E,C,G,E,E,K,A,C,M,E,C,E,G,M,C,E,I,C,E,G,Q,O,C,G,I,C,E,E,C,S,E,C,E,O,W,I,E,I,C,E,U,O,I,Q,S,C,C,C,E,I,E,E,K,C,E,G,S,E,C,E,E,E,E,E,E,E,O,E,C,S,E,C,E,E,E,E,E,E,E,Q,E,C,E,K,E,E,C,C,S,E,C,C,C,E,I,E,E,E,E,E,C,M,E,K,W,G,A,E,C,C,E,C,M,C,C,E,C,O,C,C,E,A,E,K,E,C,I,E,A,E,O,E,C,K,E,G,M,E,G,O,E,E,E,A,Y,O,Q,C,C,C,O,Q,C,C,W,O,I,A,C,E,E,S,E,C,E,O,S,C,E,O,A,S,C,E,G,E,M,A,U,mC,K,G,G,C,G,E,E,G,C,E,A,E,C,C,E,C,E,M,C,C,C,E,C,E,K,E,C,A,C,E,E,M,A,G,E,I,C,G,C,E,E,I,A,G,C,A,C,E,E,I,C,E,C,C,E,C,E,I,C,O,E,O,E,E,A,G,C,E,C,A,E,C,C,E,C,E,K,C,A,E,C,C,E,E,K,E,K,E,E,E,K,M,E,O,E,K,G,C,M,C,C,E,C,K,C,C,C,M,E,E,K,G,E,C,C,E,C,E,C,E,C,Q,M,E,E,E,G,C,G,G,E,I,C,G,G,Q,S,G,C,C,E,A,C,E,E,I,A,E,M,C,G,C,C,E,M,C,E,A,G,C,A,I,C,C,E,E,A,I,C,C,E,A,E,C,E,E,Q,C,G,A,I,C,C,E,E,C,C,G,C,C,E,C,C,C,E,E,C,C,E,C,C,C,E,A,E,C,E,K,C,C,C,E,C,K,C,G,A,I,C,C,E,E,E,K,C,C,C,E,C,C,E,K,C,C,C,E,C,E,E,K,C,G,G,E,K,C,C,C,E,C,C,E,K,C,C,C,E,C,E,G,C,I,C,G,K,C,C,C,E,E,E,G,C,G,G,E,I,C,G,G,Q,C,E,E,I,C,E,E,C,M,E,C,E,C,E,E,C,E,C,Q,C,E,E,C,C,G,A,E,C,C,E,C,M,C,C,E,C,M,C,M,C,C,E,C,C,C,E,C,K,E,K,C,C,C,E,C,E,E,C,C,C,CCIA,IAAM,EAAc,eAAsB,MAAA,CAH1B,YAMV,EAAuB,CAAC,IAAI,CAGlC,KAAK,gBAAgB,CAAC,UAAW,SAAC,CAAlC,EACE,EAAM,SAAS,CACb,EAAC,W,O,E,I,C,S,C,E,O,E,K,E,K,EACe,MAAA,C,EAAM,OAAO,IAAI,CAAC,G,A,M,E,OAChC,AADc,EAAR,IAAA,GACA,MAAM,CAAC,G,C,E,A,C,EACf,KAEJ,GAGA,KAAK,gBAAgB,CAAC,WAAY,SAAC,CAAnC,EACE,EAAM,SAAS,CACb,EAAC,W,O,E,I,C,S,C,E,O,E,K,E,K,EACe,MAAA,C,EAAM,OAAO,IAAI,G,A,M,EAC/B,MAAA,C,EAAM,QAAQ,GAAG,CACf,AAFY,EAAR,IAAA,GAEE,GAAG,CAAC,SAAC,CADP,EAEF,GAAI,IAAS,EACX,OAAO,OAAO,MAAM,CAAC,EAEzB,I,A,M,EAEF,OAPA,EAAA,IAAA,GAOA,C,EAAM,QAAQ,KAAK,G,A,M,E,OAAnB,EAAA,IAAA,G,C,E,A,C,EACF,KAEJ,GAEA,KAAK,gBAAgB,CAAC,QAAS,SAAC,CAAhC,EACE,GACE,AAAuB,aAAvB,EAAM,OAAO,CAAC,IAAI,EAClB,EAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,0BAC3B,CACA,EAAM,WAAW,CAAC,OAAO,KAAK,CAAC,MAC/B,MACF,CACF","sources":["","src/PWA/sw-b71.js"],"sourcesContent":["// The version of the cache.\nfunction asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {\n try {\n var info = gen[key](arg);\n var value = info.value;\n } catch (error) {\n reject(error);\n return;\n }\n if (info.done) {\n resolve(value);\n } else {\n Promise.resolve(value).then(_next, _throw);\n }\n}\nfunction _async_to_generator(fn) {\n return function() {\n var self1 = this, args = arguments;\n return new Promise(function(resolve, reject) {\n var gen = fn.apply(self1, args);\n function _next(value) {\n asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"next\", value);\n }\n function _throw(err) {\n asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"throw\", err);\n }\n _next(undefined);\n });\n };\n}\nfunction _ts_generator(thisArg, body) {\n var f, y, t, g, _ = {\n label: 0,\n sent: function() {\n if (t[0] & 1) throw t[1];\n return t[1];\n },\n trys: [],\n ops: []\n };\n return g = {\n next: verb(0),\n \"throw\": verb(1),\n \"return\": verb(2)\n }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() {\n return this;\n }), g;\n function verb(n) {\n return function(v) {\n return step([\n n,\n v\n ]);\n };\n }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while(_)try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [\n op[0] & 2,\n t.value\n ];\n switch(op[0]){\n case 0:\n case 1:\n t = op;\n break;\n case 4:\n _.label++;\n return {\n value: op[1],\n done: false\n };\n case 5:\n _.label++;\n y = op[1];\n op = [\n 0\n ];\n continue;\n case 7:\n op = _.ops.pop();\n _.trys.pop();\n continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {\n _ = 0;\n continue;\n }\n if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {\n _.label = op[1];\n break;\n }\n if (op[0] === 6 && _.label < t[1]) {\n _.label = t[1];\n t = op;\n break;\n }\n if (t && _.label < t[2]) {\n _.label = t[2];\n _.ops.push(op);\n break;\n }\n if (t[2]) _.ops.pop();\n _.trys.pop();\n continue;\n }\n op = body.call(thisArg, _);\n } catch (e) {\n op = [\n 6,\n e\n ];\n y = 0;\n } finally{\n f = t = 0;\n }\n if (op[0] & 5) throw op[1];\n return {\n value: op[0] ? op[1] : void 0,\n done: true\n };\n }\n}\nvar VERSION = \"29036807\";\n// The name of the cache\nvar CACHE_NAME = \"breakout-71-\".concat(VERSION);\n// The static resources that the app needs to function.\nvar APP_STATIC_RESOURCES = [\n \"/\"\n];\n// On install, cache the static resources\nself.addEventListener(\"install\", function(event) {\n event.waitUntil(_async_to_generator(function() {\n var cache;\n return _ts_generator(this, function(_state) {\n switch(_state.label){\n case 0:\n return [\n 4,\n caches.open(CACHE_NAME)\n ];\n case 1:\n cache = _state.sent();\n cache.addAll(APP_STATIC_RESOURCES);\n return [\n 2\n ];\n }\n });\n })());\n});\n// delete old caches on activate\nself.addEventListener(\"activate\", function(event) {\n event.waitUntil(_async_to_generator(function() {\n var names;\n return _ts_generator(this, function(_state) {\n switch(_state.label){\n case 0:\n return [\n 4,\n caches.keys()\n ];\n case 1:\n names = _state.sent();\n return [\n 4,\n Promise.all(names.map(function(name) {\n if (name !== CACHE_NAME) return caches[\"delete\"](name);\n }))\n ];\n case 2:\n _state.sent();\n return [\n 4,\n clients.claim()\n ];\n case 3:\n _state.sent();\n return [\n 2\n ];\n }\n });\n })());\n});\nself.addEventListener(\"fetch\", function(event) {\n if (event.request.mode === \"navigate\" && event.request.url.endsWith(\"/index.html?isPWA=true\")) {\n event.respondWith(caches.match(\"/\"));\n return;\n }\n});\n\n//# sourceMappingURL=sw-b71.js.map\n","// The version of the cache.\nconst VERSION = \"29036807\";\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":["asyncGeneratorStep","gen","resolve","reject","_next","_throw","key","arg","info","value","error","done","Promise","then","_async_to_generator","fn","self1","args","arguments","apply","err","undefined","_ts_generator","thisArg","body","f","y","t","g","_","label","sent","trys","ops","next","verb","Symbol","iterator","n","v","step","op","TypeError","call","pop","length","push","e","CACHE_NAME","concat","APP_STATIC_RESOURCES","self","addEventListener","event","waitUntil","_state","caches","open","cache","addAll","keys","all","names","map","name","clients","claim","request","mode","url","endsWith","respondWith","match"],"version":3,"file":"sw-b71.js.map"} \ No newline at end of file +{"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/PWA/sw-b71.js"],"sourcesContent":["// The version of the cache.\nconst VERSION = \"29036807\";\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/"} \ No newline at end of file diff --git a/dist/index.html b/dist/index.html index c3cca34..ecd10c1 100644 --- a/dist/index.html +++ b/dist/index.html @@ -765,6 +765,7 @@ function tick() { (0, _render.render)(gameState); } if (gameState.running) (0, _recording.recordOneFrame)(gameState); + if ((0, _options.isOptionOn)('sound')) (0, _sounds.playPendingSounds)(gameState); requestAnimationFrame(tick); } window.addEventListener("visibilitychange", ()=>{ @@ -1185,7 +1186,7 @@ window.stressTest = ()=>restart({ } }); -},{"./loadGameData":"l1B4x","./sounds":"dQKPV","./game_utils":"cEeac","./PWA/sw_loader":"2n0gK","./i18n/i18n":"eNPRm","./settings":"5blfu","./gameStateMutators":"9ZeQl","./render":"9AS2t","./recording":"godmD","./newGameState":"aQN6X","./asyncAlert":"rSqLY","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./getLevelBackground":"7OIPf"}],"l1B4x":[function(require,module,exports,__globalThis) { +},{"./loadGameData":"l1B4x","./sounds":"dQKPV","./game_utils":"cEeac","./PWA/sw_loader":"2n0gK","./i18n/i18n":"eNPRm","./settings":"5blfu","./gameStateMutators":"9ZeQl","./render":"9AS2t","./recording":"godmD","./newGameState":"aQN6X","./asyncAlert":"rSqLY","./options":"d5NoS","./getLevelBackground":"7OIPf","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l1B4x":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "appVersion", ()=>appVersion); @@ -1756,49 +1757,62 @@ function levelIconHTML(bricks, levelSize, color) { },{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"dQKPV":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); +parcelHelpers.export(exports, "playPendingSounds", ()=>playPendingSounds); parcelHelpers.export(exports, "sounds", ()=>sounds); parcelHelpers.export(exports, "getAudioContext", ()=>getAudioContext); parcelHelpers.export(exports, "getAudioRecordingTrack", ()=>getAudioRecordingTrack); -var _game = require("./game"); var _options = require("./options"); +let lastPlay = Date.now(); +function playPendingSounds(gameState) { + if (lastPlay > Date.now() - 60) return; + lastPlay = Date.now(); + for(let key in gameState.aboutToPlaySound){ + const soundName = key; + const ex = gameState.aboutToPlaySound[soundName]; + if (ex.vol) { + sounds[soundName](Math.min(2, ex.vol), pixelsToPan(gameState, ex.x), gameState.combo); + ex.vol = 0; + } + } +} const sounds = { - wallBeep: (pan)=>{ + wallBeep: (vol, pan, combo)=>{ if (!(0, _options.isOptionOn)("sound")) return; - createSingleBounceSound(800, pixelsToPan(pan)); + createSingleBounceSound(800, pan, vol); }, - comboIncreaseMaybe: (combo, x, volume)=>{ + comboIncreaseMaybe: (volume, pan, combo)=>{ if (!(0, _options.isOptionOn)("sound")) return; let delta = 0; if (!isNaN(lastComboPlayed)) { if (lastComboPlayed < combo) delta = 1; if (lastComboPlayed > combo) delta = -1; } - playShepard(delta, pixelsToPan(x), volume); + playShepard(delta, pan, volume); lastComboPlayed = combo; }, - comboDecrease () { + comboDecrease (volume, pan, combo) { if (!(0, _options.isOptionOn)("sound")) return; - playShepard(-1, 0.5, 0.5); + playShepard(-1, pan, volume); }, - coinBounce: (pan, volume)=>{ + coinBounce: (volume, pan, combo)=>{ if (!(0, _options.isOptionOn)("sound")) return; - createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, "triangle"); + createSingleBounceSound(1200, pan, volume, 0.1, "triangle"); }, - explode: (pan)=>{ + explode: (volume, pan, combo)=>{ if (!(0, _options.isOptionOn)("sound")) return; - createExplosionSound(pixelsToPan(pan)); + createExplosionSound(pan); }, - lifeLost (pan) { + lifeLost (volume, pan, combo) { if (!(0, _options.isOptionOn)("sound")) return; - createShatteredGlassSound(pixelsToPan(pan)); + createShatteredGlassSound(pan); }, - coinCatch (pan) { + coinCatch (volume, pan, combo) { if (!(0, _options.isOptionOn)("sound")) return; - createSingleBounceSound(900, pixelsToPan(pan), 0.8, 0.1, "triangle"); + createSingleBounceSound(900, pan, volume, 0.1, "triangle"); }, - colorChange (pan, volume) { - createSingleBounceSound(400, pixelsToPan(pan), volume, 0.5, "sine"); - createSingleBounceSound(800, pixelsToPan(pan), volume * 0.5, 0.2, "square"); + colorChange (volume, pan, combo) { + createSingleBounceSound(400, pan, volume, 0.5, "sine"); + createSingleBounceSound(800, pan, volume * 0.5, 0.2, "square"); } }; // How to play the code on the leftconst context = new window.AudioContext(); @@ -1879,8 +1893,8 @@ function createExplosionSound(pan = 0.5) { // Stop the noise source after the sound has played noiseSource.stop(context.currentTime + 1); } -function pixelsToPan(pan) { - return Math.max(0, Math.min(1, (pan - (0, _game.gameState).offsetXRoundedDown) / (0, _game.gameState).gameZoneWidthRoundedUp)); +function pixelsToPan(gameState, pan) { + return Math.max(0, Math.min(1, (pan - gameState.offsetXRoundedDown) / gameState.gameZoneWidthRoundedUp)); } let lastComboPlayed = NaN, shepard = 6; function playShepard(delta, pan, volume) { @@ -1935,7 +1949,7 @@ function createOscillator(context, frequency, type) { return oscillator; } -},{"./game":"edeGs","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"d5NoS":[function(require,module,exports,__globalThis) { +},{"./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"d5NoS":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "options", ()=>options); @@ -2001,6 +2015,7 @@ parcelHelpers.export(exports, "isTelekinesisActive", ()=>isTelekinesisActive); parcelHelpers.export(exports, "findLast", ()=>findLast); parcelHelpers.export(exports, "distance2", ()=>distance2); parcelHelpers.export(exports, "distanceBetween", ()=>distanceBetween); +parcelHelpers.export(exports, "defaultSounds", ()=>defaultSounds); var _loadGameData = require("./loadGameData"); function getMajorityValue(arr) { const count = {}; @@ -2058,6 +2073,44 @@ function distance2(a, b) { function distanceBetween(a, b) { return Math.sqrt(distance2(a, b)); } +function defaultSounds() { + return { + aboutToPlaySound: { + wallBeep: { + vol: 0, + x: 0 + }, + comboIncreaseMaybe: { + vol: 0, + x: 0 + }, + comboDecrease: { + vol: 0, + x: 0 + }, + coinBounce: { + vol: 0, + x: 0 + }, + explode: { + vol: 0, + x: 0 + }, + lifeLost: { + vol: 0, + x: 0 + }, + coinCatch: { + vol: 0, + x: 0 + }, + colorChange: { + vol: 0, + x: 0 + } + } + }; +} },{"./loadGameData":"l1B4x","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"2n0gK":[function(require,module,exports,__globalThis) { if ("serviceWorker" in navigator && window.location.search.includes("isPWA=true")) // @ts-ignore @@ -2115,6 +2168,7 @@ parcelHelpers.export(exports, "spawnExplosion", ()=>spawnExplosion); parcelHelpers.export(exports, "explodeBrick", ()=>explodeBrick); parcelHelpers.export(exports, "dontOfferTooSoon", ()=>dontOfferTooSoon); parcelHelpers.export(exports, "pickRandomUpgrades", ()=>pickRandomUpgrades); +parcelHelpers.export(exports, "schedulGameSound", ()=>schedulGameSound); parcelHelpers.export(exports, "addToScore", ()=>addToScore); parcelHelpers.export(exports, "setLevel", ()=>setLevel); parcelHelpers.export(exports, "rainbowColor", ()=>rainbowColor); @@ -2122,7 +2176,6 @@ parcelHelpers.export(exports, "repulse", ()=>repulse); parcelHelpers.export(exports, "attract", ()=>attract); parcelHelpers.export(exports, "gameStateTick", ()=>gameStateTick); parcelHelpers.export(exports, "ballTick", ()=>ballTick); -var _sounds = require("./sounds"); var _gameUtils = require("./game_utils"); var _i18N = require("./i18n/i18n"); var _loadGameData = require("./loadGameData"); @@ -2208,7 +2261,7 @@ function resetCombo(gameState, x, y) { if (prev > gameState.combo && gameState.perks.soft_reset) gameState.combo += Math.floor((prev - gameState.combo) * (gameState.perks.soft_reset * 10) / 100); const lost = Math.max(0, prev - gameState.combo); if (lost) { - for(let i = 0; i < lost && i < 8; i++)setTimeout(()=>(0, _sounds.sounds).comboDecrease(), i * 100); + for(let i = 0; i < lost && i < 8; i++)setTimeout(()=>schedulGameSound(gameState, 'comboDecrease', x, 1), i * 100); if (typeof x !== "undefined" && typeof y !== "undefined") gameState.flashes.push({ type: "text", text: "-" + lost, @@ -2227,7 +2280,7 @@ function decreaseCombo(gameState, by, x, y) { gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by); const lost = Math.max(0, prev - gameState.combo); if (lost) { - (0, _sounds.sounds).comboDecrease(); + schedulGameSound(gameState, 'comboDecrease', x, 1); if (typeof x !== "undefined" && typeof y !== "undefined") gameState.flashes.push({ type: "text", text: "-" + lost, @@ -2263,7 +2316,7 @@ function explodeBrick(gameState, index, ball, isExplosion) { if (color === "black") { delete gameState.bricks[index]; const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index); - (0, _sounds.sounds).explode(ball.x); + schedulGameSound(gameState, 'explode', ball.x, 1); const col = index % gameState.gridSize; const row = Math.floor(index / gameState.gridSize); const size = 1 + gameState.perks.bigger_explosions; @@ -2346,13 +2399,13 @@ function explodeBrick(gameState, index, ball, isExplosion) { // color change if ((gameState.perks.picky_eater || gameState.perks.pierce_color) && color !== gameState.ballsColor && color) { if (gameState.perks.picky_eater) resetCombo(gameState, ball.x, ball.y); - (0, _sounds.sounds).colorChange(ball.x, 0.8); + schedulGameSound(gameState, 'colorChange', ball.x, 0.8); gameState.lastExplosion = gameState.levelTime; gameState.ballsColor = color; if (!(0, _options.isOptionOn)("basic")) gameState.balls.forEach((ball)=>{ spawnExplosion(gameState, 7, ball.previousX, ball.previousY, color, 150, 15); }); - } else (0, _sounds.sounds).comboIncreaseMaybe(gameState.combo, ball.x, 1); + } else schedulGameSound(gameState, 'comboIncreaseMaybe', ball.x, 1); } gameState.flashes.push({ type: "ball", @@ -2390,6 +2443,14 @@ function pickRandomUpgrades(gameState, count) { help: u.help(gameState.perks[u.id] + 1) })); } +function schedulGameSound(gameState, sound, x, vol) { + if (!vol) return; + x ??= gameState.offsetX + gameState.gameZoneWidth / 2; + const ex = gameState.aboutToPlaySound[sound]; + if (ex.vol) console.log('Combined sounds for ' + sound); + ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol); + ex.vol += vol; +} function addToScore(gameState, coin) { coin.destroyed = true; gameState.score += coin.points; @@ -2413,7 +2474,7 @@ function addToScore(gameState, coin) { }); if (Date.now() - gameState.lastPlayedCoinGrab > 16) { gameState.lastPlayedCoinGrab = Date.now(); - (0, _sounds.sounds).coinCatch(coin.x); + schedulGameSound(gameState, 'coinCatch', coin.x, 1); } gameState.runStatistics.score += coin.points; } @@ -2553,7 +2614,6 @@ frames = 1) { score: gameState.score })); } else if (gameState.running || gameState.levelTime) { - let playedCoinBounce = false; const coinRadius = Math.round(gameState.coinSize / 2); gameState.coins.forEach((coin)=>{ if (coin.destroyed) return; @@ -2585,17 +2645,14 @@ frames = 1) { if (gameState.bricks[hitBrick] && coin.color !== gameState.bricks[hitBrick] && gameState.bricks[hitBrick] !== "black" && !coin.coloredABrick) { gameState.bricks[hitBrick] = coin.color; coin.coloredABrick = true; - (0, _sounds.sounds).colorChange(coin.x, 0.3); + schedulGameSound(gameState, 'colorChange', coin.x, 0.3); } } if (typeof hitBrick !== "undefined" || hitBorder) { coin.vx *= 0.8; coin.vy *= 0.8; coin.sa *= 0.9; - if (speed > 20 && !playedCoinBounce) { - playedCoinBounce = true; - (0, _sounds.sounds).coinBounce(coin.x, 0.2); - } + if (speed > 20) schedulGameSound(gameState, 'coinBounce', coin.x, 0.2); if (Math.abs(coin.vy) < 3) coin.vy = 0; } }); @@ -2739,7 +2796,7 @@ function ballTick(gameState, ball, delta) { if (gameState.perks.left_is_lava && borderHitCode % 2 && ball.x < gameState.offsetX + gameState.gameZoneWidth / 2) resetCombo(gameState, ball.x, ball.y); if (gameState.perks.right_is_lava && borderHitCode % 2 && ball.x > gameState.offsetX + gameState.gameZoneWidth / 2) resetCombo(gameState, ball.x, ball.y); if (gameState.perks.top_is_lava && borderHitCode >= 2) resetCombo(gameState, ball.x, ball.y + gameState.ballSize); - (0, _sounds.sounds).wallBeep(ball.x); + schedulGameSound(gameState, 'wallBeep', ball.x, 1); gameState.levelWallBounces++; gameState.runStatistics.wall_bounces++; } @@ -2752,11 +2809,11 @@ function ballTick(gameState, ball, delta) { const angle = Math.atan2(-gameState.puckWidth / 2, (ball.x - gameState.puckPosition) * (gameState.perks.concave_puck ? -0.5 : 1)); ball.vx = speed * Math.cos(angle); ball.vy = speed * Math.sin(angle); - (0, _sounds.sounds).wallBeep(ball.x); + schedulGameSound(gameState, 'wallBeep', ball.x, 1); } else { ball.vy *= -1; gameState.perks.extra_life = Math.max(0, gameState.perks.extra_life - 1); - (0, _sounds.sounds).lifeLost(ball.x); + schedulGameSound(gameState, 'lifeLost', ball.x, 1); if (!(0, _options.isOptionOn)("basic")) for(let i = 0; i < 10; i++)gameState.flashes.push({ type: "particle", ethereal: false, @@ -2831,7 +2888,7 @@ function ballTick(gameState, ball, delta) { } } if (sturdyBounce) { - (0, _sounds.sounds).wallBeep(x); + schedulGameSound(gameState, 'wallBeep', x, 1); return; } if (typeof hitBrick !== "undefined") { @@ -2863,7 +2920,7 @@ function ballTick(gameState, ball, delta) { } } -},{"./sounds":"dQKPV","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./loadGameData":"l1B4x","./settings":"5blfu","./render":"9AS2t","./gameOver":"caCAf","./game":"edeGs","./recording":"godmD","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"9AS2t":[function(require,module,exports,__globalThis) { +},{"./game_utils":"cEeac","./i18n/i18n":"eNPRm","./loadGameData":"l1B4x","./settings":"5blfu","./render":"9AS2t","./gameOver":"caCAf","./game":"edeGs","./recording":"godmD","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"9AS2t":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "gameCanvas", ()=>gameCanvas); @@ -3742,7 +3799,8 @@ function newGameState(params) { levelTime: 0, levelWallBounces: 0, needsRender: true, - autoCleanUses: 0 + autoCleanUses: 0, + ...(0, _gameUtils.defaultSounds)() }; (0, _gameStateMutators.resetBalls)(gameState); if (!(0, _gameUtils.sumOfKeys)(gameState.perks)) { diff --git a/src/game.ts b/src/game.ts index a9c177f..9125c9b 100644 --- a/src/game.ts +++ b/src/game.ts @@ -8,7 +8,7 @@ import { RunParams, Upgrade, } from "./types"; -import { getAudioContext } from "./sounds"; +import {getAudioContext, playPendingSounds} from "./sounds"; import { currentLevelInfo, getRowColIndex, @@ -429,7 +429,6 @@ export function bordersHitCheck( return hhit + vhit * 2; } - export function tick() { const currentTick = performance.now(); const timeDeltaMs = currentTick - gameState.lastTick; @@ -458,6 +457,9 @@ export function tick() { if (gameState.running) { recordOneFrame(gameState); } + if(isOptionOn('sound') ){ + playPendingSounds(gameState) + } requestAnimationFrame(tick); } diff --git a/src/gameStateMutators.ts b/src/gameStateMutators.ts index cfecb92..2e7a22e 100644 --- a/src/gameStateMutators.ts +++ b/src/gameStateMutators.ts @@ -1,1203 +1,1246 @@ -import { Ball, BallLike, Coin, colorString, GameState, PerkId } from "./types"; -import { sounds } from "./sounds"; -import { - brickCenterX, - brickCenterY, - currentLevelInfo, - distanceBetween, - getMajorityValue, - getPossibleUpgrades, - getRowColIndex, - isTelekinesisActive, - max_levels, -} from "./game_utils"; -import { t } from "./i18n/i18n"; -import { icons } from "./loadGameData"; +import {Ball, BallLike, Coin, colorString, GameState, PerkId} from "./types"; -import { addToTotalScore } from "./settings"; -import { background } from "./render"; -import { gameOver } from "./gameOver"; import { - bordersHitCheck, - brickIndex, - coinBrickHitCheck, - fitSize, - gameState, - hasBrick, - hitsSomething, - openUpgradesPicker, - pause, - shouldPierceByColor, + brickCenterX, + brickCenterY, + currentLevelInfo, + distanceBetween, + getMajorityValue, + getPossibleUpgrades, + getRowColIndex, + isTelekinesisActive, + max_levels, +} from "./game_utils"; +import {t} from "./i18n/i18n"; +import {icons} from "./loadGameData"; + +import {addToTotalScore} from "./settings"; +import {background} from "./render"; +import {gameOver} from "./gameOver"; +import { + bordersHitCheck, + brickIndex, + coinBrickHitCheck, + fitSize, + gameState, + hasBrick, + hitsSomething, + openUpgradesPicker, + pause, + shouldPierceByColor, } from "./game"; -import { stopRecording } from "./recording"; -import { isOptionOn } from "./options"; +import {stopRecording} from "./recording"; +import {isOptionOn} from "./options"; export function setMousePos(gameState: GameState, x: number) { - // Sets the puck position, and updates the ball position if they are supposed to follow it - gameState.puckPosition = x; - gameState.needsRender = true; + // Sets the puck position, and updates the ball position if they are supposed to follow it + gameState.puckPosition = x; + gameState.needsRender = true; } function getBallDefaultVx(gameState: GameState) { - return ( - (gameState.perks.concave_puck ? 0 : 1) * - (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed) - ); + return ( + (gameState.perks.concave_puck ? 0 : 1) * + (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed) + ); } export function resetBalls(gameState: GameState) { - const count = 1 + (gameState.perks?.multiball || 0); - const perBall = gameState.puckWidth / (count + 1); - gameState.balls = []; - gameState.ballsColor = "#FFF"; - if (gameState.perks.picky_eater || gameState.perks.pierce_color) { - gameState.ballsColor = - getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFF"; - } - for (let i = 0; i < count; i++) { - const x = - gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); - const vx = getBallDefaultVx(gameState); + const count = 1 + (gameState.perks?.multiball || 0); + const perBall = gameState.puckWidth / (count + 1); + gameState.balls = []; + gameState.ballsColor = "#FFF"; + if (gameState.perks.picky_eater || gameState.perks.pierce_color) { + gameState.ballsColor = + getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFF"; + } + for (let i = 0; i < count; i++) { + const x = + gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); + const vx = getBallDefaultVx(gameState); - gameState.balls.push({ - x, - previousX: x, - y: gameState.gameZoneHeight - 1.5 * gameState.ballSize, - previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize, - vx, - previousVX: vx, - vy: -gameState.baseSpeed, - previousVY: -gameState.baseSpeed, + gameState.balls.push({ + x, + previousX: x, + y: gameState.gameZoneHeight - 1.5 * gameState.ballSize, + previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize, + vx, + previousVX: vx, + vy: -gameState.baseSpeed, + previousVY: -gameState.baseSpeed, - sx: 0, - sy: 0, - sparks: 0, - piercedSinceBounce: 0, - hitSinceBounce: 0, - hitItem: [], - sapperUses: 0, - }); - } + sx: 0, + sy: 0, + sparks: 0, + piercedSinceBounce: 0, + hitSinceBounce: 0, + hitItem: [], + sapperUses: 0, + }); + } } export function putBallsAtPuck(gameState: GameState) { - // This reset could be abused to cheat quite easily - const count = gameState.balls.length; - const perBall = gameState.puckWidth / (count + 1); - const vx = getBallDefaultVx(gameState); - gameState.balls.forEach((ball, i) => { - const x = - gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); + // This reset could be abused to cheat quite easily + const count = gameState.balls.length; + const perBall = gameState.puckWidth / (count + 1); + const vx = getBallDefaultVx(gameState); + gameState.balls.forEach((ball, i) => { + const x = + gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); - ball.x = x; - ball.previousX = x; - ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize; - ball.previousY = ball.y; - ball.vx = vx; - ball.previousVX = ball.vx; - ball.vy = -gameState.baseSpeed; - ball.previousVY = ball.vy; - ball.sx = 0; - ball.sy = 0; - ball.hitItem = []; - ball.hitSinceBounce = 0; - ball.piercedSinceBounce = 0; - }); + ball.x = x; + ball.previousX = x; + ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize; + ball.previousY = ball.y; + ball.vx = vx; + ball.previousVX = ball.vx; + ball.vy = -gameState.baseSpeed; + ball.previousVY = ball.vy; + ball.sx = 0; + ball.sy = 0; + ball.hitItem = []; + ball.hitSinceBounce = 0; + ball.piercedSinceBounce = 0; + }); } export function normalizeGameState(gameState: GameState) { - // This function resets most parameters on the state to correct values, and should be used even when the game is paused + // This function resets most parameters on the state to correct values, and should be used even when the game is paused - gameState.baseSpeed = Math.max( - 3, - gameState.gameZoneWidth / 12 / 10 + - gameState.currentLevel / 3 + - gameState.levelTime / (30 * 1000) - - gameState.perks.slow_down * 2, - ); + gameState.baseSpeed = Math.max( + 3, + gameState.gameZoneWidth / 12 / 10 + + gameState.currentLevel / 3 + + gameState.levelTime / (30 * 1000) - + gameState.perks.slow_down * 2, + ); - gameState.puckWidth = - (gameState.gameZoneWidth / 12) * - (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck); + gameState.puckWidth = + (gameState.gameZoneWidth / 12) * + (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck); - if ( - gameState.puckPosition < - gameState.offsetXRoundedDown + gameState.puckWidth / 2 - ) { - gameState.puckPosition = - gameState.offsetXRoundedDown + gameState.puckWidth / 2; - } - if ( - gameState.puckPosition > - gameState.offsetXRoundedDown + - gameState.gameZoneWidthRoundedUp - - gameState.puckWidth / 2 - ) { - gameState.puckPosition = - gameState.offsetXRoundedDown + - gameState.gameZoneWidthRoundedUp - - gameState.puckWidth / 2; - } + if ( + gameState.puckPosition < + gameState.offsetXRoundedDown + gameState.puckWidth / 2 + ) { + gameState.puckPosition = + gameState.offsetXRoundedDown + gameState.puckWidth / 2; + } + if ( + gameState.puckPosition > + gameState.offsetXRoundedDown + + gameState.gameZoneWidthRoundedUp - + gameState.puckWidth / 2 + ) { + gameState.puckPosition = + gameState.offsetXRoundedDown + + gameState.gameZoneWidthRoundedUp - + gameState.puckWidth / 2; + } - if (!gameState.running && !gameState.levelTime) { - putBallsAtPuck(gameState); - } + if (!gameState.running && !gameState.levelTime) { + putBallsAtPuck(gameState); + } } export function baseCombo(gameState: GameState) { - return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5; + return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5; } export function resetCombo( - gameState: GameState, - x: number | undefined, - y: number | undefined, + gameState: GameState, + x: number | undefined, + y: number | undefined, ) { - const prev = gameState.combo; - gameState.combo = baseCombo(gameState); - if (!gameState.levelTime) { - gameState.combo += gameState.perks.hot_start * 15; - } - if (prev > gameState.combo && gameState.perks.soft_reset) { - gameState.combo += Math.floor( - ((prev - gameState.combo) * (gameState.perks.soft_reset * 10)) / 100, - ); - } - const lost = Math.max(0, prev - gameState.combo); - if (lost) { - for (let i = 0; i < lost && i < 8; i++) { - setTimeout(() => sounds.comboDecrease(), i * 100); + const prev = gameState.combo; + gameState.combo = baseCombo(gameState); + if (!gameState.levelTime) { + gameState.combo += gameState.perks.hot_start * 15; } - if (typeof x !== "undefined" && typeof y !== "undefined") { - gameState.flashes.push({ - type: "text", - text: "-" + lost, - time: gameState.levelTime, - color: "red", - x: x, - y: y, - duration: 150, - size: gameState.puckHeight, - }); + if (prev > gameState.combo && gameState.perks.soft_reset) { + gameState.combo += Math.floor( + ((prev - gameState.combo) * (gameState.perks.soft_reset * 10)) / 100, + ); } - } - return lost; + const lost = Math.max(0, prev - gameState.combo); + if (lost) { + for (let i = 0; i < lost && i < 8; i++) { + setTimeout(() => schedulGameSound( + gameState, + 'comboDecrease', x, 1) + , + i * 100 + ); + } + if (typeof x !== "undefined" && typeof y !== "undefined") { + gameState.flashes.push({ + type: "text", + text: "-" + lost, + time: gameState.levelTime, + color: "red", + x: x, + y: y, + duration: 150, + size: gameState.puckHeight, + }); + } + } + return lost; } export function decreaseCombo( - gameState: GameState, - by: number, - x: number, - y: number, + gameState: GameState, + by: number, + x: number, + y: number, ) { - const prev = gameState.combo; - gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by); - const lost = Math.max(0, prev - gameState.combo); + const prev = gameState.combo; + gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by); + const lost = Math.max(0, prev - gameState.combo); - if (lost) { - sounds.comboDecrease(); - if (typeof x !== "undefined" && typeof y !== "undefined") { - gameState.flashes.push({ - type: "text", - text: "-" + lost, - time: gameState.levelTime, - color: "red", - x: x, - y: y, - duration: 300, - size: gameState.puckHeight, - }); + if (lost) { + schedulGameSound( + gameState, + 'comboDecrease', x, 1) + if (typeof x !== "undefined" && typeof y !== "undefined") { + gameState.flashes.push({ + type: "text", + text: "-" + lost, + time: gameState.levelTime, + color: "red", + x: x, + y: y, + duration: 300, + size: gameState.puckHeight, + }); + } } - } } export function spawnExplosion( - gameState: GameState, - count: number, - x: number, - y: number, - color: string, - duration = 150, - size = gameState.coinSize, + gameState: GameState, + count: number, + x: number, + y: number, + color: string, + duration = 150, + size = gameState.coinSize, ) { - if (!!isOptionOn("basic")) return; - if (gameState.flashes.length > gameState.MAX_PARTICLES) { - // Avoid freezing when lots of explosion happen at once - count = 1; - } - for (let i = 0; i < count; i++) { - gameState.flashes.push({ - type: "particle", - time: gameState.levelTime, - size, - x: x + ((Math.random() - 0.5) * gameState.brickWidth) / 2, - y: y + ((Math.random() - 0.5) * gameState.brickWidth) / 2, - vx: (Math.random() - 0.5) * 30, - vy: (Math.random() - 0.5) * 30, - color, - duration, - ethereal: false, - }); - } + if (!!isOptionOn("basic")) return; + if (gameState.flashes.length > gameState.MAX_PARTICLES) { + // Avoid freezing when lots of explosion happen at once + count = 1; + } + for (let i = 0; i < count; i++) { + gameState.flashes.push({ + type: "particle", + time: gameState.levelTime, + size, + x: x + ((Math.random() - 0.5) * gameState.brickWidth) / 2, + y: y + ((Math.random() - 0.5) * gameState.brickWidth) / 2, + vx: (Math.random() - 0.5) * 30, + vy: (Math.random() - 0.5) * 30, + color, + duration, + ethereal: false, + }); + } } export function explodeBrick( - gameState: GameState, - index: number, - ball: Ball, - isExplosion: boolean, + gameState: GameState, + index: number, + ball: Ball, + isExplosion: boolean, ) { - const color = gameState.bricks[index]; - if (!color) return; + const color = gameState.bricks[index]; + if (!color) return; - if (color === "black") { - delete gameState.bricks[index]; - const x = brickCenterX(gameState, index), - y = brickCenterY(gameState, index); + if (color === "black") { + delete gameState.bricks[index]; + const x = brickCenterX(gameState, index), + y = brickCenterY(gameState, index); +schedulGameSound( + gameState, + 'explode', ball.x, 1) - sounds.explode(ball.x); - - const col = index % gameState.gridSize; - const row = Math.floor(index / gameState.gridSize); - const size = 1 + gameState.perks.bigger_explosions; - // Break bricks around - for (let dx = -size; dx <= size; dx++) { - for (let dy = -size; dy <= size; dy++) { - const i = getRowColIndex(gameState, row + dy, col + dx); - if (gameState.bricks[i] && i !== -1) { - // Study bricks resist explosions too - if ( - gameState.bricks[i] !== "black" && - gameState.perks.sturdy_bricks > Math.random() * 5 - ) - continue; - explodeBrick(gameState, i, ball, true); + const col = index % gameState.gridSize; + const row = Math.floor(index / gameState.gridSize); + const size = 1 + gameState.perks.bigger_explosions; + // Break bricks around + for (let dx = -size; dx <= size; dx++) { + for (let dy = -size; dy <= size; dy++) { + const i = getRowColIndex(gameState, row + dy, col + dx); + if (gameState.bricks[i] && i !== -1) { + // Study bricks resist explosions too + if ( + gameState.bricks[i] !== "black" && + gameState.perks.sturdy_bricks > Math.random() * 5 + ) + continue; + explodeBrick(gameState, i, ball, true); + } + } } - } - } - // Blow nearby coins - gameState.coins.forEach((c) => { - const dx = c.x - x; - const dy = c.y - y; - const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy)); - c.vx += ((dx / d2) * 10 * size) / c.weight; - c.vy += ((dy / d2) * 10 * size) / c.weight; - }); - gameState.lastExplosion = Date.now(); + // Blow nearby coins + gameState.coins.forEach((c) => { + const dx = c.x - x; + const dy = c.y - y; + const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy)); + c.vx += ((dx / d2) * 10 * size) / c.weight; + c.vy += ((dy / d2) * 10 * size) / c.weight; + }); + gameState.lastExplosion = Date.now(); - gameState.flashes.push({ - type: "ball", - duration: 150, - time: gameState.levelTime, - size: gameState.brickWidth * 2, - color: "white", - x, - y, - }); - spawnExplosion( - gameState, - 7 * (1 + gameState.perks.bigger_explosions), - x, - y, - "white", - 150, - gameState.coinSize, - ); - ball.hitSinceBounce++; - gameState.runStatistics.bricks_broken++; - } else if (color) { - // Even if it bounces we don't want to count that as a miss - ball.hitSinceBounce++; + gameState.flashes.push({ + type: "ball", + duration: 150, + time: gameState.levelTime, + size: gameState.brickWidth * 2, + color: "white", + x, + y, + }); + spawnExplosion( + gameState, + 7 * (1 + gameState.perks.bigger_explosions), + x, + y, + "white", + 150, + gameState.coinSize, + ); + ball.hitSinceBounce++; + gameState.runStatistics.bricks_broken++; + } else if (color) { + // Even if it bounces we don't want to count that as a miss + ball.hitSinceBounce++; - // Flashing is take care of by the tick loop - const x = brickCenterX(gameState, index), - y = brickCenterY(gameState, index); + // Flashing is take care of by the tick loop + const x = brickCenterX(gameState, index), + y = brickCenterY(gameState, index); - gameState.bricks[index] = ""; + gameState.bricks[index] = ""; - // coins = coins.filter((c) => !c.destroyed); - let coinsToSpawn = gameState.combo; - if (gameState.perks.sturdy_bricks) { - // +10% per level - coinsToSpawn += Math.ceil( - ((10 + gameState.perks.sturdy_bricks) / 10) * coinsToSpawn, - ); - } - - gameState.levelSpawnedCoins += coinsToSpawn; - gameState.runStatistics.coins_spawned += coinsToSpawn; - gameState.runStatistics.bricks_broken++; - const maxCoins = gameState.MAX_COINS * (isOptionOn("basic") ? 0.5 : 1); - const spawnableCoins = - gameState.coins.length > gameState.MAX_COINS - ? 1 - : Math.floor(maxCoins - gameState.coins.length) / 3; - - const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins)); - - while (coinsToSpawn > 0) { - const points = Math.min(pointsPerCoin, coinsToSpawn); - if (points < 0 || isNaN(points)) { - console.error({ points }); - debugger; - } - - coinsToSpawn -= points; - - const cx = - x + - (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize), - cy = - y + - (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize); - - gameState.coins.push({ - points, - size: gameState.coinSize, //-Math.floor(Math.log2(points)), - color: gameState.perks.metamorphosis ? color : "gold", - x: cx, - y: cy, - previousX: cx, - previousY: cy, - // Use previous speed because the ball has already bounced - vx: ball.previousVX * (0.5 + Math.random()), - vy: ball.previousVY * (0.5 + Math.random()), - sx: 0, - sy: 0, - a: Math.random() * Math.PI * 2, - sa: Math.random() - 0.5, - weight: 0.8 + Math.random() * 0.2, - }); - } - - gameState.combo += Math.max( - 0, - gameState.perks.streak_shots + - gameState.perks.compound_interest + - gameState.perks.left_is_lava + - gameState.perks.right_is_lava + - gameState.perks.top_is_lava + - gameState.perks.picky_eater, - ); - - if (!isExplosion) { - // color change - if ( - (gameState.perks.picky_eater || gameState.perks.pierce_color) && - color !== gameState.ballsColor && - color - ) { - if (gameState.perks.picky_eater) { - resetCombo(gameState, ball.x, ball.y); - } - sounds.colorChange(ball.x, 0.8); - gameState.lastExplosion = gameState.levelTime; - gameState.ballsColor = color; - if (!isOptionOn("basic")) { - gameState.balls.forEach((ball) => { - spawnExplosion( - gameState, - 7, - ball.previousX, - ball.previousY, - color, - 150, - 15, + // coins = coins.filter((c) => !c.destroyed); + let coinsToSpawn = gameState.combo; + if (gameState.perks.sturdy_bricks) { + // +10% per level + coinsToSpawn += Math.ceil( + ((10 + gameState.perks.sturdy_bricks) / 10) * coinsToSpawn, ); - }); } - } else { - sounds.comboIncreaseMaybe(gameState.combo, ball.x, 1); - } + + gameState.levelSpawnedCoins += coinsToSpawn; + gameState.runStatistics.coins_spawned += coinsToSpawn; + gameState.runStatistics.bricks_broken++; + const maxCoins = gameState.MAX_COINS * (isOptionOn("basic") ? 0.5 : 1); + const spawnableCoins = + gameState.coins.length > gameState.MAX_COINS + ? 1 + : Math.floor(maxCoins - gameState.coins.length) / 3; + + const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins)); + + while (coinsToSpawn > 0) { + const points = Math.min(pointsPerCoin, coinsToSpawn); + if (points < 0 || isNaN(points)) { + console.error({points}); + debugger; + } + + coinsToSpawn -= points; + + const cx = + x + + (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize), + cy = + y + + (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize); + + gameState.coins.push({ + points, + size: gameState.coinSize, //-Math.floor(Math.log2(points)), + color: gameState.perks.metamorphosis ? color : "gold", + x: cx, + y: cy, + previousX: cx, + previousY: cy, + // Use previous speed because the ball has already bounced + vx: ball.previousVX * (0.5 + Math.random()), + vy: ball.previousVY * (0.5 + Math.random()), + sx: 0, + sy: 0, + a: Math.random() * Math.PI * 2, + sa: Math.random() - 0.5, + weight: 0.8 + Math.random() * 0.2, + }); + } + + gameState.combo += Math.max( + 0, + gameState.perks.streak_shots + + gameState.perks.compound_interest + + gameState.perks.left_is_lava + + gameState.perks.right_is_lava + + gameState.perks.top_is_lava + + gameState.perks.picky_eater, + ); + + if (!isExplosion) { + // color change + if ( + (gameState.perks.picky_eater || gameState.perks.pierce_color) && + color !== gameState.ballsColor && + color + ) { + if (gameState.perks.picky_eater) { + resetCombo(gameState, ball.x, ball.y); + } + schedulGameSound( + gameState, + 'colorChange', ball.x, 0.8) + gameState.lastExplosion = gameState.levelTime; + gameState.ballsColor = color; + if (!isOptionOn("basic")) { + gameState.balls.forEach((ball) => { + spawnExplosion( + gameState, + 7, + ball.previousX, + ball.previousY, + color, + 150, + 15, + ); + }); + } + } else { + schedulGameSound( + gameState, + 'comboIncreaseMaybe', ball.x, 1) + } + } + + gameState.flashes.push({ + type: "ball", + duration: 40, + time: gameState.levelTime, + size: gameState.brickWidth, + color: color, + x, + y, + }); + spawnExplosion( + gameState, + 5 + Math.min(gameState.combo, 30), + x, + y, + color, + 150, + gameState.coinSize / 2, + ); } - gameState.flashes.push({ - type: "ball", - duration: 40, - time: gameState.levelTime, - size: gameState.brickWidth, - color: color, - x, - y, - }); - spawnExplosion( - gameState, - 5 + Math.min(gameState.combo, 30), - x, - y, - color, - 150, - gameState.coinSize / 2, - ); - } - - if (!gameState.bricks[index] && color !== "black") { - ball.hitItem?.push({ - index, - color, - }); - } + if (!gameState.bricks[index] && color !== "black") { + ball.hitItem?.push({ + index, + color, + }); + } } export function dontOfferTooSoon(gameState: GameState, id: PerkId) { - gameState.lastOffered[id] = Math.round(Date.now() / 1000); + gameState.lastOffered[id] = Math.round(Date.now() / 1000); } export function pickRandomUpgrades(gameState: GameState, count: number) { - let list = getPossibleUpgrades(gameState) - .map((u) => ({ - ...u, - score: Math.random() + (gameState.lastOffered[u.id] || 0), - })) - .sort((a, b) => a.score - b.score) - .filter((u) => gameState.perks[u.id] < u.max) - .slice(0, count) - .sort((a, b) => (a.id > b.id ? 1 : -1)); + let list = getPossibleUpgrades(gameState) + .map((u) => ({ + ...u, + score: Math.random() + (gameState.lastOffered[u.id] || 0), + })) + .sort((a, b) => a.score - b.score) + .filter((u) => gameState.perks[u.id] < u.max) + .slice(0, count) + .sort((a, b) => (a.id > b.id ? 1 : -1)); - list.forEach((u) => { - dontOfferTooSoon(gameState, u.id); - }); + list.forEach((u) => { + dontOfferTooSoon(gameState, u.id); + }); + + return list.map((u) => ({ + text: + u.name + + (gameState.perks[u.id] + ? t("level_up.upgrade_perk_to_level", { + level: gameState.perks[u.id] + 1, + }) + : ""), + icon: icons["icon:" + u.id], + value: u.id as PerkId, + help: u.help(gameState.perks[u.id] + 1), + })); +} + +export function schedulGameSound(gameState: GameState, + sound: keyof GameState["aboutToPlaySound"], + x: number|void, + vol: number) { + if (!vol) return + x??=gameState.offsetX+gameState.gameZoneWidth/2 + const ex = gameState.aboutToPlaySound[sound] as { vol: number, x: number } + if(ex.vol){ + console.log('Combined sounds for '+sound) + } + ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol) + ex.vol += vol - return list.map((u) => ({ - text: - u.name + - (gameState.perks[u.id] - ? t("level_up.upgrade_perk_to_level", { - level: gameState.perks[u.id] + 1, - }) - : ""), - icon: icons["icon:" + u.id], - value: u.id as PerkId, - help: u.help(gameState.perks[u.id] + 1), - })); } export function addToScore(gameState: GameState, coin: Coin) { - coin.destroyed = true; - gameState.score += coin.points; - gameState.lastScoreIncrease = gameState.levelTime; + coin.destroyed = true; + gameState.score += coin.points; + gameState.lastScoreIncrease = gameState.levelTime; - addToTotalScore(gameState, coin.points); - if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) { - gameState.highScore = gameState.score; - localStorage.setItem("breakout-3-hs", gameState.score.toString()); - } - if (!isOptionOn("basic")) { - gameState.flashes.push({ - type: "particle", - duration: 100 + Math.random() * 50, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: coin.color, - x: coin.previousX, - y: coin.previousY, - vx: (gameState.canvasWidth - coin.x) / 100, - vy: -coin.y / 100, - ethereal: true, - }); - } + addToTotalScore(gameState, coin.points); + if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) { + gameState.highScore = gameState.score; + localStorage.setItem("breakout-3-hs", gameState.score.toString()); + } + if (!isOptionOn("basic")) { + gameState.flashes.push({ + type: "particle", + duration: 100 + Math.random() * 50, + time: gameState.levelTime, + size: gameState.coinSize / 2, + color: coin.color, + x: coin.previousX, + y: coin.previousY, + vx: (gameState.canvasWidth - coin.x) / 100, + vy: -coin.y / 100, + ethereal: true, + }); + } - if (Date.now() - gameState.lastPlayedCoinGrab > 16) { - gameState.lastPlayedCoinGrab = Date.now(); - sounds.coinCatch(coin.x); - } - gameState.runStatistics.score += coin.points; + if (Date.now() - gameState.lastPlayedCoinGrab > 16) { + gameState.lastPlayedCoinGrab = Date.now(); + + schedulGameSound( + gameState, + 'coinCatch', coin.x, 1) + } + gameState.runStatistics.score += coin.points; } export function setLevel(gameState: GameState, l: number) { - stopRecording(); - pause(false); - if (l > 0) { - openUpgradesPicker(gameState); - } - gameState.currentLevel = l; + stopRecording(); + pause(false); + if (l > 0) { + openUpgradesPicker(gameState); + } + gameState.currentLevel = l; - gameState.levelTime = 0; - gameState.levelWallBounces = 0; - gameState.autoCleanUses = 0; - gameState.lastTickDown = gameState.levelTime; - gameState.levelStartScore = gameState.score; - gameState.levelSpawnedCoins = 0; - gameState.levelMisses = 0; - gameState.runStatistics.levelsPlayed++; + gameState.levelTime = 0; + gameState.levelWallBounces = 0; + gameState.autoCleanUses = 0; + gameState.lastTickDown = gameState.levelTime; + gameState.levelStartScore = gameState.score; + gameState.levelSpawnedCoins = 0; + gameState.levelMisses = 0; + gameState.runStatistics.levelsPlayed++; - resetCombo(gameState, undefined, undefined); - resetBalls(gameState); + resetCombo(gameState, undefined, undefined); + resetBalls(gameState); - const lvl = currentLevelInfo(gameState); - if (lvl.size !== gameState.gridSize) { - gameState.gridSize = lvl.size; - fitSize(); - } - gameState.coins = []; - gameState.bricks = [...lvl.bricks]; - gameState.flashes = []; + const lvl = currentLevelInfo(gameState); + if (lvl.size !== gameState.gridSize) { + gameState.gridSize = lvl.size; + fitSize(); + } + gameState.coins = []; + gameState.bricks = [...lvl.bricks]; + gameState.flashes = []; - // This caused problems with accented characters like the ô of côte d'ivoire for odd reasons - // background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg) - background.src = "data:image/svg+xml;UTF8," + lvl.svg; + // This caused problems with accented characters like the ô of côte d'ivoire for odd reasons + // background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg) + background.src = "data:image/svg+xml;UTF8," + lvl.svg; } 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, - a: Ball, - b: BallLike, - power: number, - impactsBToo: boolean, + gameState: GameState, + a: Ball, + b: BallLike, + power: number, + impactsBToo: boolean, ) { - const distance = distanceBetween(a, b); - // Ensure we don't get soft locked - const max = gameState.gameZoneWidth / 2; - if (distance > max) return; - // Unit vector - const dx = (a.x - b.x) / distance; - const dy = (a.y - b.y) / distance; - const fact = - (((-power * (max - distance)) / (max * 1.2) / 3) * - Math.min(500, gameState.levelTime)) / - 500; - if ( - impactsBToo && - typeof b.vx !== "undefined" && - typeof b.vy !== "undefined" - ) { - b.vx += dx * fact; - b.vy += dy * fact; - } - a.vx -= dx * fact; - a.vy -= dy * fact; - - const speed = 10; - const rand = 2; - gameState.flashes.push({ - type: "particle", - duration: 100, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: rainbowColor(), - ethereal: true, - x: a.x, - y: a.y, - vx: -dx * speed + a.vx + (Math.random() - 0.5) * rand, - vy: -dy * speed + a.vy + (Math.random() - 0.5) * rand, - }); - if ( - impactsBToo && - typeof b.vx !== "undefined" && - typeof b.vy !== "undefined" - ) { - gameState.flashes.push({ - type: "particle", - duration: 100, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: rainbowColor(), - ethereal: true, - x: b.x, - y: b.y, - vx: dx * speed + b.vx + (Math.random() - 0.5) * rand, - vy: dy * speed + b.vy + (Math.random() - 0.5) * rand, - }); - } -} - -export function attract(gameState: GameState, a: Ball, b: Ball, power: number) { - const distance = distanceBetween(a, b); - // Ensure we don't get soft locked - const min = gameState.gameZoneWidth * 0.5; - if (distance < min) return; - // Unit vector - const dx = (a.x - b.x) / distance; - const dy = (a.y - b.y) / distance; - - const fact = - (((power * (distance - min)) / min) * Math.min(500, gameState.levelTime)) / - 500; - b.vx += dx * fact; - b.vy += dy * fact; - a.vx -= dx * fact; - a.vy -= dy * fact; - - const speed = 10; - const rand = 2; - gameState.flashes.push({ - type: "particle", - duration: 100, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: rainbowColor(), - ethereal: true, - x: a.x, - y: a.y, - vx: dx * speed + a.vx + (Math.random() - 0.5) * rand, - vy: dy * speed + a.vy + (Math.random() - 0.5) * rand, - }); - gameState.flashes.push({ - type: "particle", - duration: 100, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: rainbowColor(), - ethereal: true, - x: b.x, - y: b.y, - vx: -dx * speed + b.vx + (Math.random() - 0.5) * rand, - vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand, - }); -} - -export function gameStateTick( - gameState: GameState, - // How many frames to compute at once, can go above 1 to compensate lag - frames = 1, -) { - gameState.runStatistics.max_combo = Math.max( - gameState.runStatistics.max_combo, - gameState.combo, - ); - - gameState.coins = gameState.coins.filter((coin) => !coin.destroyed); - gameState.balls = gameState.balls.filter((ball) => !ball.destroyed); - - const remainingBricks = gameState.bricks.filter( - (b) => b && b !== "black", - ).length; - - if ( - gameState.levelTime > gameState.lastTickDown + 1000 && - gameState.perks.hot_start - ) { - gameState.lastTickDown = gameState.levelTime; - decreaseCombo( - gameState, - gameState.perks.hot_start, - gameState.puckPosition, - gameState.gameZoneHeight - 2 * gameState.puckHeight, - ); - } - - if ( - remainingBricks <= gameState.perks.skip_last && - !gameState.autoCleanUses - ) { - gameState.bricks.forEach((type, index) => { - if (type) { - explodeBrick(gameState, index, gameState.balls[0], true); - } - }); - gameState.autoCleanUses++; - } - if (!remainingBricks && !gameState.coins.length) { - if (gameState.currentLevel + 1 < max_levels(gameState)) { - setLevel(gameState, gameState.currentLevel + 1); - } else { - gameOver( - t("gameOver.win.title"), - t("gameOver.win.summary", { score: gameState.score }), - ); + const distance = distanceBetween(a, b); + // Ensure we don't get soft locked + const max = gameState.gameZoneWidth / 2; + if (distance > max) return; + // Unit vector + const dx = (a.x - b.x) / distance; + const dy = (a.y - b.y) / distance; + const fact = + (((-power * (max - distance)) / (max * 1.2) / 3) * + Math.min(500, gameState.levelTime)) / + 500; + if ( + impactsBToo && + typeof b.vx !== "undefined" && + typeof b.vy !== "undefined" + ) { + b.vx += dx * fact; + b.vy += dy * fact; } - } else if (gameState.running || gameState.levelTime) { - let playedCoinBounce = false; - const coinRadius = Math.round(gameState.coinSize / 2); + a.vx -= dx * fact; + a.vy -= dy * fact; - gameState.coins.forEach((coin) => { - if (coin.destroyed) return; - if (gameState.perks.coin_magnet) { - const attractionX = - ((frames * (gameState.puckPosition - coin.x)) / - (100 + - Math.pow(coin.y - gameState.gameZoneHeight, 2) + - Math.pow(coin.x - gameState.puckPosition, 2))) * - gameState.perks.coin_magnet * - 100; - coin.vx += attractionX; - coin.sa -= attractionX / 10; - } - - const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * frames; - - coin.vy *= ratio; - coin.vx *= ratio; - if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed; - if (coin.vx < -7 * gameState.baseSpeed) - coin.vx = -7 * gameState.baseSpeed; - if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed; - if (coin.vy < -7 * gameState.baseSpeed) - coin.vy = -7 * gameState.baseSpeed; - coin.a += coin.sa; - - // Gravity - coin.vy += frames * coin.weight * 0.8; - - const speed = Math.abs(coin.sx) + Math.abs(coin.sx); - const hitBorder = bordersHitCheck(coin, coin.size / 2, frames); - - if ( - coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight && - coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy && - Math.abs(coin.x - gameState.puckPosition) < - coinRadius + - gameState.puckWidth / 2 + // a bit of margin to be nice - gameState.puckHeight - ) { - addToScore(gameState, coin); - } else if (coin.y > gameState.canvasHeight + coinRadius) { - coin.destroyed = true; - if (gameState.perks.compound_interest) { - resetCombo(gameState, coin.x, coin.y); - } - } - - const hitBrick = coinBrickHitCheck(coin); - - if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") { - if ( - gameState.bricks[hitBrick] && - coin.color !== gameState.bricks[hitBrick] && - gameState.bricks[hitBrick] !== "black" && - !coin.coloredABrick - ) { - gameState.bricks[hitBrick] = coin.color; - coin.coloredABrick = true; - sounds.colorChange(coin.x, 0.3); - } - } - if (typeof hitBrick !== "undefined" || hitBorder) { - coin.vx *= 0.8; - coin.vy *= 0.8; - coin.sa *= 0.9; - if (speed > 20 && !playedCoinBounce) { - playedCoinBounce = true; - sounds.coinBounce(coin.x, 0.2); - } - - if (Math.abs(coin.vy) < 3) { - coin.vy = 0; - } - } + const speed = 10; + const rand = 2; + gameState.flashes.push({ + type: "particle", + duration: 100, + time: gameState.levelTime, + size: gameState.coinSize / 2, + color: rainbowColor(), + ethereal: true, + x: a.x, + y: a.y, + vx: -dx * speed + a.vx + (Math.random() - 0.5) * rand, + vy: -dy * speed + a.vy + (Math.random() - 0.5) * rand, }); - - gameState.balls.forEach((ball) => ballTick(gameState, ball, frames)); - - if (gameState.perks.wind) { - const windD = - ((gameState.puckPosition - - (gameState.offsetX + gameState.gameZoneWidth / 2)) / - gameState.gameZoneWidth) * - 2 * - gameState.perks.wind; - for (let i = 0; i < gameState.perks.wind; i++) { - if (Math.random() * Math.abs(windD) > 0.5) { - gameState.flashes.push({ + if ( + impactsBToo && + typeof b.vx !== "undefined" && + typeof b.vy !== "undefined" + ) { + gameState.flashes.push({ type: "particle", - duration: 150, - ethereal: true, + duration: 100, time: gameState.levelTime, size: gameState.coinSize / 2, color: rainbowColor(), - x: - gameState.offsetXRoundedDown + - Math.random() * gameState.gameZoneWidthRoundedUp, - y: Math.random() * gameState.gameZoneHeight, - vx: windD * 8, - vy: 0, - }); - } - } + ethereal: true, + x: b.x, + y: b.y, + vx: dx * speed + b.vx + (Math.random() - 0.5) * rand, + vy: dy * speed + b.vy + (Math.random() - 0.5) * rand, + }); } +} - gameState.flashes.forEach((flash) => { - if (flash.type === "particle") { - flash.x += flash.vx * frames; - flash.y += flash.vy * frames; - if (!flash.ethereal) { - flash.vy += 0.5; - if (hasBrick(brickIndex(flash.x, flash.y))) { - flash.destroyed = true; - } - } - } - }); - } +export function attract(gameState: GameState, a: Ball, b: Ball, power: number) { + const distance = distanceBetween(a, b); + // Ensure we don't get soft locked + const min = gameState.gameZoneWidth * 0.5; + if (distance < min) return; + // Unit vector + const dx = (a.x - b.x) / distance; + const dy = (a.y - b.y) / distance; - if (gameState.combo > baseCombo(gameState)) { - // The red should still be visible on a white bg - const baseParticle = !isOptionOn("basic") && - (gameState.combo - baseCombo(gameState)) * Math.random() > 5 && - gameState.running && { - type: "particle" as const, - duration: 100 * (Math.random() + 1), + const fact = + (((power * (distance - min)) / min) * Math.min(500, gameState.levelTime)) / + 500; + b.vx += dx * fact; + b.vy += dy * fact; + a.vx -= dx * fact; + a.vy -= dy * fact; + + const speed = 10; + const rand = 2; + gameState.flashes.push({ + type: "particle", + duration: 100, time: gameState.levelTime, size: gameState.coinSize / 2, - color: "red", + color: rainbowColor(), ethereal: true, - }; + x: a.x, + y: a.y, + vx: dx * speed + a.vx + (Math.random() - 0.5) * rand, + vy: dy * speed + a.vy + (Math.random() - 0.5) * rand, + }); + gameState.flashes.push({ + type: "particle", + duration: 100, + time: gameState.levelTime, + size: gameState.coinSize / 2, + color: rainbowColor(), + ethereal: true, + x: b.x, + y: b.y, + vx: -dx * speed + b.vx + (Math.random() - 0.5) * rand, + vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand, + }); +} - if (gameState.perks.top_is_lava) { - baseParticle && - gameState.flashes.push({ - ...baseParticle, - x: - gameState.offsetXRoundedDown + - Math.random() * gameState.gameZoneWidthRoundedUp, - y: 0, - vx: (Math.random() - 0.5) * 10, - vy: 5, +export function gameStateTick( + gameState: GameState, + // How many frames to compute at once, can go above 1 to compensate lag + frames = 1, +) { + gameState.runStatistics.max_combo = Math.max( + gameState.runStatistics.max_combo, + gameState.combo, + ); + + gameState.coins = gameState.coins.filter((coin) => !coin.destroyed); + gameState.balls = gameState.balls.filter((ball) => !ball.destroyed); + + const remainingBricks = gameState.bricks.filter( + (b) => b && b !== "black", + ).length; + + if ( + gameState.levelTime > gameState.lastTickDown + 1000 && + gameState.perks.hot_start + ) { + gameState.lastTickDown = gameState.levelTime; + decreaseCombo( + gameState, + gameState.perks.hot_start, + gameState.puckPosition, + gameState.gameZoneHeight - 2 * gameState.puckHeight, + ); + } + + if ( + remainingBricks <= gameState.perks.skip_last && + !gameState.autoCleanUses + ) { + gameState.bricks.forEach((type, index) => { + if (type) { + explodeBrick(gameState, index, gameState.balls[0], true); + } + }); + gameState.autoCleanUses++; + } + if (!remainingBricks && !gameState.coins.length) { + if (gameState.currentLevel + 1 < max_levels(gameState)) { + setLevel(gameState, gameState.currentLevel + 1); + } else { + gameOver( + t("gameOver.win.title"), + t("gameOver.win.summary", {score: gameState.score}), + ); + } + } else if (gameState.running || gameState.levelTime) { + const coinRadius = Math.round(gameState.coinSize / 2); + + gameState.coins.forEach((coin) => { + if (coin.destroyed) return; + if (gameState.perks.coin_magnet) { + const attractionX = + ((frames * (gameState.puckPosition - coin.x)) / + (100 + + Math.pow(coin.y - gameState.gameZoneHeight, 2) + + Math.pow(coin.x - gameState.puckPosition, 2))) * + gameState.perks.coin_magnet * + 100; + coin.vx += attractionX; + coin.sa -= attractionX / 10; + } + + const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * frames; + + coin.vy *= ratio; + coin.vx *= ratio; + if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed; + if (coin.vx < -7 * gameState.baseSpeed) + coin.vx = -7 * gameState.baseSpeed; + if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed; + if (coin.vy < -7 * gameState.baseSpeed) + coin.vy = -7 * gameState.baseSpeed; + coin.a += coin.sa; + + // Gravity + coin.vy += frames * coin.weight * 0.8; + + const speed = Math.abs(coin.sx) + Math.abs(coin.sx); + const hitBorder = bordersHitCheck(coin, coin.size / 2, frames); + + if ( + coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight && + coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy && + Math.abs(coin.x - gameState.puckPosition) < + coinRadius + + gameState.puckWidth / 2 + // a bit of margin to be nice + gameState.puckHeight + ) { + addToScore(gameState, coin); + } else if (coin.y > gameState.canvasHeight + coinRadius) { + coin.destroyed = true; + if (gameState.perks.compound_interest) { + resetCombo(gameState, coin.x, coin.y); + } + } + + const hitBrick = coinBrickHitCheck(coin); + + if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") { + if ( + gameState.bricks[hitBrick] && + coin.color !== gameState.bricks[hitBrick] && + gameState.bricks[hitBrick] !== "black" && + !coin.coloredABrick + ) { + gameState.bricks[hitBrick] = coin.color; + coin.coloredABrick = true; + + schedulGameSound( + gameState, + 'colorChange', coin.x, 0.3) + } + } + if (typeof hitBrick !== "undefined" || hitBorder) { + coin.vx *= 0.8; + coin.vy *= 0.8; + coin.sa *= 0.9; + if (speed > 20 ) { + schedulGameSound( + gameState, + 'coinBounce', coin.x, 0.2) + } + + if (Math.abs(coin.vy) < 3) { + coin.vy = 0; + } + } + }); + + gameState.balls.forEach((ball) => ballTick(gameState, ball, frames)); + + if (gameState.perks.wind) { + const windD = + ((gameState.puckPosition - + (gameState.offsetX + gameState.gameZoneWidth / 2)) / + gameState.gameZoneWidth) * + 2 * + gameState.perks.wind; + for (let i = 0; i < gameState.perks.wind; i++) { + if (Math.random() * Math.abs(windD) > 0.5) { + gameState.flashes.push({ + type: "particle", + duration: 150, + ethereal: true, + time: gameState.levelTime, + size: gameState.coinSize / 2, + color: rainbowColor(), + x: + gameState.offsetXRoundedDown + + Math.random() * gameState.gameZoneWidthRoundedUp, + y: Math.random() * gameState.gameZoneHeight, + vx: windD * 8, + vy: 0, + }); + } + } + } + + gameState.flashes.forEach((flash) => { + if (flash.type === "particle") { + flash.x += flash.vx * frames; + flash.y += flash.vy * frames; + if (!flash.ethereal) { + flash.vy += 0.5; + if (hasBrick(brickIndex(flash.x, flash.y))) { + flash.destroyed = true; + } + } + } }); } - if (gameState.perks.left_is_lava && baseParticle) { - gameState.flashes.push({ - ...baseParticle, - x: gameState.offsetXRoundedDown, - y: Math.random() * gameState.gameZoneHeight, - vx: 5, - vy: (Math.random() - 0.5) * 10, - }); - } + if (gameState.combo > baseCombo(gameState)) { + // The red should still be visible on a white bg + const baseParticle = !isOptionOn("basic") && + (gameState.combo - baseCombo(gameState)) * Math.random() > 5 && + gameState.running && { + type: "particle" as const, + duration: 100 * (Math.random() + 1), + time: gameState.levelTime, + size: gameState.coinSize / 2, + color: "red", + ethereal: true, + }; - if (gameState.perks.right_is_lava && baseParticle) { - gameState.flashes.push({ - ...baseParticle, - x: gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp, - y: Math.random() * gameState.gameZoneHeight, - vx: -5, - vy: (Math.random() - 0.5) * 10, - }); - } + if (gameState.perks.top_is_lava) { + baseParticle && + gameState.flashes.push({ + ...baseParticle, + x: + gameState.offsetXRoundedDown + + Math.random() * gameState.gameZoneWidthRoundedUp, + y: 0, + vx: (Math.random() - 0.5) * 10, + vy: 5, + }); + } - if (gameState.perks.compound_interest) { - let x = gameState.puckPosition, - attemps = 0; - do { - x = - gameState.offsetXRoundedDown + - gameState.gameZoneWidthRoundedUp * Math.random(); - attemps++; - } while ( - Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 && - attemps < 10 - ); - baseParticle && - gameState.flashes.push({ - ...baseParticle, - x, - y: gameState.gameZoneHeight, - vx: (Math.random() - 0.5) * 10, - vy: -5, - }); + if (gameState.perks.left_is_lava && baseParticle) { + gameState.flashes.push({ + ...baseParticle, + x: gameState.offsetXRoundedDown, + y: Math.random() * gameState.gameZoneHeight, + vx: 5, + vy: (Math.random() - 0.5) * 10, + }); + } + + if (gameState.perks.right_is_lava && baseParticle) { + gameState.flashes.push({ + ...baseParticle, + x: gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp, + y: Math.random() * gameState.gameZoneHeight, + vx: -5, + vy: (Math.random() - 0.5) * 10, + }); + } + + if (gameState.perks.compound_interest) { + let x = gameState.puckPosition, + attemps = 0; + do { + x = + gameState.offsetXRoundedDown + + gameState.gameZoneWidthRoundedUp * Math.random(); + attemps++; + } while ( + Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 && + attemps < 10 + ); + baseParticle && + gameState.flashes.push({ + ...baseParticle, + x, + y: gameState.gameZoneHeight, + vx: (Math.random() - 0.5) * 10, + vy: -5, + }); + } + if (gameState.perks.streak_shots) { + const pos = 0.5 - Math.random(); + baseParticle && + gameState.flashes.push({ + ...baseParticle, + duration: 100, + x: gameState.puckPosition + gameState.puckWidth * pos, + y: gameState.gameZoneHeight - gameState.puckHeight, + vx: pos * 10, + vy: -5, + }); + } } - if (gameState.perks.streak_shots) { - const pos = 0.5 - Math.random(); - baseParticle && - gameState.flashes.push({ - ...baseParticle, - duration: 100, - x: gameState.puckPosition + gameState.puckWidth * pos, - y: gameState.gameZoneHeight - gameState.puckHeight, - vx: pos * 10, - vy: -5, - }); - } - } } export function ballTick(gameState: GameState, ball: Ball, delta: number) { - ball.previousVX = ball.vx; - ball.previousVY = ball.vy; + ball.previousVX = ball.vx; + ball.previousVY = ball.vy; - let speedLimitDampener = - 1 + - gameState.perks.telekinesis + - gameState.perks.ball_repulse_ball + - gameState.perks.puck_repulse_ball + - gameState.perks.ball_attract_ball; - if (isTelekinesisActive(gameState, ball)) { - speedLimitDampener += 3; - ball.vx += - ((gameState.puckPosition - ball.x) / 1000) * - delta * - gameState.perks.telekinesis; - } - - if ( - ball.vx * ball.vx + ball.vy * ball.vy < - gameState.baseSpeed * gameState.baseSpeed * 2 - ) { - ball.vx *= 1 + 0.02 / speedLimitDampener; - ball.vy *= 1 + 0.02 / speedLimitDampener; - } else { - ball.vx *= 1 - 0.02 / speedLimitDampener; - ball.vy *= 1 - 0.02 / speedLimitDampener; - } - // Ball could get stuck horizontally because of ball-ball interactions in repulse/attract - if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) { - ball.vy += ((ball.vy > 0 ? 1 : -1) * 0.02) / speedLimitDampener; - } - - if (gameState.perks.ball_repulse_ball) { - for (let b2 of gameState.balls) { - // avoid computing this twice, and repulsing itself - if (b2.x >= ball.x) continue; - repulse(gameState, ball, b2, gameState.perks.ball_repulse_ball, true); - } - } - if (gameState.perks.ball_attract_ball) { - for (let b2 of gameState.balls) { - // avoid computing this twice, and repulsing itself - if (b2.x >= ball.x) continue; - attract(gameState, ball, b2, gameState.perks.ball_attract_ball); - } - } - if ( - gameState.perks.puck_repulse_ball && - Math.abs(ball.x - gameState.puckPosition) < - gameState.puckWidth / 2 + - (gameState.ballSize * (9 + gameState.perks.puck_repulse_ball)) / 10 - ) { - repulse( - gameState, - ball, - { - x: gameState.puckPosition, - y: gameState.gameZoneHeight, - }, - gameState.perks.puck_repulse_ball + 1, - false, - ); - } - - if ( - gameState.perks.respawn && - ball.hitItem?.length > 1 && - !isOptionOn("basic") - ) { - for ( - let i = 0; - i < ball.hitItem?.length - 1 && i < gameState.perks.respawn; - i++ - ) { - const { index, color } = ball.hitItem[i]; - if (gameState.bricks[index] || color === "black") continue; - const vertical = Math.random() > 0.5; - const dx = Math.random() > 0.5 ? 1 : -1; - const dy = Math.random() > 0.5 ? 1 : -1; - - gameState.flashes.push({ - type: "particle", - duration: 250, - ethereal: true, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color, - x: brickCenterX(gameState, index) + (dx * gameState.brickWidth) / 2, - y: brickCenterY(gameState, index) + (dy * gameState.brickWidth) / 2, - vx: vertical ? 0 : -dx * gameState.baseSpeed, - vy: vertical ? -dy * gameState.baseSpeed : 0, - }); - } - } - - const borderHitCode = bordersHitCheck(ball, gameState.ballSize / 2, delta); - if (borderHitCode) { - if ( - gameState.perks.left_is_lava && - borderHitCode % 2 && - ball.x < gameState.offsetX + gameState.gameZoneWidth / 2 - ) { - resetCombo(gameState, ball.x, ball.y); + let speedLimitDampener = + 1 + + gameState.perks.telekinesis + + gameState.perks.ball_repulse_ball + + gameState.perks.puck_repulse_ball + + gameState.perks.ball_attract_ball; + if (isTelekinesisActive(gameState, ball)) { + speedLimitDampener += 3; + ball.vx += + ((gameState.puckPosition - ball.x) / 1000) * + delta * + gameState.perks.telekinesis; } if ( - gameState.perks.right_is_lava && - borderHitCode % 2 && - ball.x > gameState.offsetX + gameState.gameZoneWidth / 2 + ball.vx * ball.vx + ball.vy * ball.vy < + gameState.baseSpeed * gameState.baseSpeed * 2 ) { - resetCombo(gameState, ball.x, ball.y); - } - - if (gameState.perks.top_is_lava && borderHitCode >= 2) { - resetCombo(gameState, ball.x, ball.y + gameState.ballSize); - } - sounds.wallBeep(ball.x); - gameState.levelWallBounces++; - gameState.runStatistics.wall_bounces++; - } - - // Puck collision - const ylimit = - gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2; - const ballIsUnderPuck = - Math.abs(ball.x - gameState.puckPosition) < - gameState.ballSize / 2 + gameState.puckWidth / 2; - if ( - ball.y > ylimit && - ball.vy > 0 && - (ballIsUnderPuck || - (gameState.perks.extra_life && - ball.y > ylimit + gameState.puckHeight / 2)) - ) { - if (ballIsUnderPuck) { - const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); - const angle = Math.atan2( - -gameState.puckWidth / 2, - (ball.x - gameState.puckPosition) * - (gameState.perks.concave_puck ? -0.5 : 1), - ); - ball.vx = speed * Math.cos(angle); - ball.vy = speed * Math.sin(angle); - sounds.wallBeep(ball.x); + ball.vx *= 1 + 0.02 / speedLimitDampener; + ball.vy *= 1 + 0.02 / speedLimitDampener; } else { - ball.vy *= -1; - gameState.perks.extra_life = Math.max(0, gameState.perks.extra_life - 1); - sounds.lifeLost(ball.x); - if (!isOptionOn("basic")) { - for (let i = 0; i < 10; i++) - gameState.flashes.push({ - type: "particle", - ethereal: false, - color: "red", - destroyed: false, - duration: 150, - size: gameState.coinSize / 2, - time: gameState.levelTime, - x: ball.x, - y: ball.y, - vx: Math.random() * gameState.baseSpeed * 3, - vy: gameState.baseSpeed * 3, - }); - } + ball.vx *= 1 - 0.02 / speedLimitDampener; + ball.vy *= 1 - 0.02 / speedLimitDampener; } - if (gameState.perks.streak_shots) { - resetCombo(gameState, ball.x, ball.y); + // Ball could get stuck horizontally because of ball-ball interactions in repulse/attract + if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) { + ball.vy += ((ball.vy > 0 ? 1 : -1) * 0.02) / speedLimitDampener; } - if (gameState.perks.respawn) { - ball.hitItem - .slice(0, -1) - .slice(0, gameState.perks.respawn) - .forEach(({ index, color }) => { - if (!gameState.bricks[index] && color !== "black") - gameState.bricks[index] = color; - }); + if (gameState.perks.ball_repulse_ball) { + for (let b2 of gameState.balls) { + // avoid computing this twice, and repulsing itself + if (b2.x >= ball.x) continue; + repulse(gameState, ball, b2, gameState.perks.ball_repulse_ball, true); + } } - ball.hitItem = []; - if (!ball.hitSinceBounce) { - gameState.runStatistics.misses++; - gameState.levelMisses++; - resetCombo(gameState, ball.x, ball.y); - gameState.flashes.push({ - type: "text", - text: t("play.missed_ball"), - duration: 500, - time: gameState.levelTime, - size: gameState.puckHeight * 1.5, - color: "red", - x: gameState.puckPosition, - y: gameState.gameZoneHeight - gameState.puckHeight * 2, - }); + if (gameState.perks.ball_attract_ball) { + for (let b2 of gameState.balls) { + // avoid computing this twice, and repulsing itself + if (b2.x >= ball.x) continue; + attract(gameState, ball, b2, gameState.perks.ball_attract_ball); + } } - gameState.runStatistics.puck_bounces++; - ball.hitSinceBounce = 0; - ball.sapperUses = 0; - ball.piercedSinceBounce = 0; - } - - if ( - ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 && - gameState.running - ) { - ball.destroyed = true; - gameState.runStatistics.balls_lost++; - if (!gameState.balls.find((b) => !b.destroyed)) { - gameOver( - t("gameOver.lost.title"), - t("gameOver.lost.summary", { score: gameState.score }), - ); + if ( + gameState.perks.puck_repulse_ball && + Math.abs(ball.x - gameState.puckPosition) < + gameState.puckWidth / 2 + + (gameState.ballSize * (9 + gameState.perks.puck_repulse_ball)) / 10 + ) { + repulse( + gameState, + ball, + { + x: gameState.puckPosition, + y: gameState.gameZoneHeight, + }, + gameState.perks.puck_repulse_ball + 1, + false, + ); } - } - const radius = gameState.ballSize / 2; - // Make ball/coin bonce, and return bricks that were hit - const { x, y, previousX, previousY } = ball; - - const vhit = hitsSomething(previousX, y, radius); - const hhit = hitsSomething(x, previousY, radius); - const chit = - (typeof vhit == "undefined" && - typeof hhit == "undefined" && - hitsSomething(x, y, radius)) || - undefined; - - const hitBrick = vhit ?? hhit ?? chit; - let sturdyBounce = - hitBrick && - gameState.bricks[hitBrick] !== "black" && - gameState.perks.sturdy_bricks && - gameState.perks.sturdy_bricks > Math.random() * 5; - - let pierce = false; - if (sturdyBounce || typeof hitBrick === "undefined") { - // cannot pierce - } else if (shouldPierceByColor(vhit, hhit, chit)) { - pierce = true; - } else if (ball.piercedSinceBounce < gameState.perks.pierce * 3) { - pierce = true; - ball.piercedSinceBounce++; - } - - if (typeof vhit !== "undefined" || typeof chit !== "undefined") { - if (!pierce) { - ball.y = ball.previousY; - ball.vy *= -1; - } - } - if (typeof hhit !== "undefined" || typeof chit !== "undefined") { - if (!pierce) { - ball.x = ball.previousX; - ball.vx *= -1; - } - } - - if (sturdyBounce) { - sounds.wallBeep(x); - return; - } - if (typeof hitBrick !== "undefined") { - const initialBrickColor = gameState.bricks[hitBrick]; - - explodeBrick(gameState, hitBrick, ball, false); if ( - ball.sapperUses < gameState.perks.sapper && - initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks - !gameState.bricks[hitBrick] + gameState.perks.respawn && + ball.hitItem?.length > 1 && + !isOptionOn("basic") ) { - gameState.bricks[hitBrick] = "black"; - ball.sapperUses++; - } - } + for ( + let i = 0; + i < ball.hitItem?.length - 1 && i < gameState.perks.respawn; + i++ + ) { + const {index, color} = ball.hitItem[i]; + if (gameState.bricks[index] || color === "black") continue; + const vertical = Math.random() > 0.5; + const dx = Math.random() > 0.5 ? 1 : -1; + const dy = Math.random() > 0.5 ? 1 : -1; - if (!isOptionOn("basic")) { - ball.sparks += (delta * (gameState.combo - 1)) / 30; - if (ball.sparks > 1) { - gameState.flashes.push({ - type: "particle", - duration: 100 * ball.sparks, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: gameState.ballsColor, - x: ball.x, - y: ball.y, - vx: (Math.random() - 0.5) * gameState.baseSpeed, - vy: (Math.random() - 0.5) * gameState.baseSpeed, - ethereal: false, - }); - ball.sparks = 0; + gameState.flashes.push({ + type: "particle", + duration: 250, + ethereal: true, + time: gameState.levelTime, + size: gameState.coinSize / 2, + color, + x: brickCenterX(gameState, index) + (dx * gameState.brickWidth) / 2, + y: brickCenterY(gameState, index) + (dy * gameState.brickWidth) / 2, + vx: vertical ? 0 : -dx * gameState.baseSpeed, + vy: vertical ? -dy * gameState.baseSpeed : 0, + }); + } + } + + const borderHitCode = bordersHitCheck(ball, gameState.ballSize / 2, delta); + if (borderHitCode) { + if ( + gameState.perks.left_is_lava && + borderHitCode % 2 && + ball.x < gameState.offsetX + gameState.gameZoneWidth / 2 + ) { + resetCombo(gameState, ball.x, ball.y); + } + + if ( + gameState.perks.right_is_lava && + borderHitCode % 2 && + ball.x > gameState.offsetX + gameState.gameZoneWidth / 2 + ) { + resetCombo(gameState, ball.x, ball.y); + } + + if (gameState.perks.top_is_lava && borderHitCode >= 2) { + resetCombo(gameState, ball.x, ball.y + gameState.ballSize); + } + + schedulGameSound( + gameState, + 'wallBeep', ball.x, 1) + gameState.levelWallBounces++; + gameState.runStatistics.wall_bounces++; + } + + // Puck collision + const ylimit = + gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2; + const ballIsUnderPuck = + Math.abs(ball.x - gameState.puckPosition) < + gameState.ballSize / 2 + gameState.puckWidth / 2; + if ( + ball.y > ylimit && + ball.vy > 0 && + (ballIsUnderPuck || + (gameState.perks.extra_life && + ball.y > ylimit + gameState.puckHeight / 2)) + ) { + if (ballIsUnderPuck) { + const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); + const angle = Math.atan2( + -gameState.puckWidth / 2, + (ball.x - gameState.puckPosition) * + (gameState.perks.concave_puck ? -0.5 : 1), + ); + ball.vx = speed * Math.cos(angle); + ball.vy = speed * Math.sin(angle); + schedulGameSound( + gameState, + 'wallBeep', ball.x, 1) + } else { + ball.vy *= -1; + gameState.perks.extra_life = Math.max(0, gameState.perks.extra_life - 1); + + schedulGameSound( + gameState, + 'lifeLost', ball.x, 1) + if (!isOptionOn("basic")) { + for (let i = 0; i < 10; i++) + gameState.flashes.push({ + type: "particle", + ethereal: false, + color: "red", + destroyed: false, + duration: 150, + size: gameState.coinSize / 2, + time: gameState.levelTime, + x: ball.x, + y: ball.y, + vx: Math.random() * gameState.baseSpeed * 3, + vy: gameState.baseSpeed * 3, + }); + } + } + if (gameState.perks.streak_shots) { + resetCombo(gameState, ball.x, ball.y); + } + + if (gameState.perks.respawn) { + ball.hitItem + .slice(0, -1) + .slice(0, gameState.perks.respawn) + .forEach(({index, color}) => { + if (!gameState.bricks[index] && color !== "black") + gameState.bricks[index] = color; + }); + } + ball.hitItem = []; + if (!ball.hitSinceBounce) { + gameState.runStatistics.misses++; + gameState.levelMisses++; + resetCombo(gameState, ball.x, ball.y); + gameState.flashes.push({ + type: "text", + text: t("play.missed_ball"), + duration: 500, + time: gameState.levelTime, + size: gameState.puckHeight * 1.5, + color: "red", + x: gameState.puckPosition, + y: gameState.gameZoneHeight - gameState.puckHeight * 2, + }); + } + gameState.runStatistics.puck_bounces++; + ball.hitSinceBounce = 0; + ball.sapperUses = 0; + ball.piercedSinceBounce = 0; + } + + if ( + ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 && + gameState.running + ) { + ball.destroyed = true; + gameState.runStatistics.balls_lost++; + if (!gameState.balls.find((b) => !b.destroyed)) { + gameOver( + t("gameOver.lost.title"), + t("gameOver.lost.summary", {score: gameState.score}), + ); + } + } + const radius = gameState.ballSize / 2; + // Make ball/coin bonce, and return bricks that were hit + const {x, y, previousX, previousY} = ball; + + const vhit = hitsSomething(previousX, y, radius); + const hhit = hitsSomething(x, previousY, radius); + const chit = + (typeof vhit == "undefined" && + typeof hhit == "undefined" && + hitsSomething(x, y, radius)) || + undefined; + + const hitBrick = vhit ?? hhit ?? chit; + let sturdyBounce = + hitBrick && + gameState.bricks[hitBrick] !== "black" && + gameState.perks.sturdy_bricks && + gameState.perks.sturdy_bricks > Math.random() * 5; + + let pierce = false; + if (sturdyBounce || typeof hitBrick === "undefined") { + // cannot pierce + } else if (shouldPierceByColor(vhit, hhit, chit)) { + pierce = true; + } else if (ball.piercedSinceBounce < gameState.perks.pierce * 3) { + pierce = true; + ball.piercedSinceBounce++; + } + + if (typeof vhit !== "undefined" || typeof chit !== "undefined") { + if (!pierce) { + ball.y = ball.previousY; + ball.vy *= -1; + } + } + if (typeof hhit !== "undefined" || typeof chit !== "undefined") { + if (!pierce) { + ball.x = ball.previousX; + ball.vx *= -1; + } + } + + if (sturdyBounce) { + schedulGameSound( + gameState, + 'wallBeep', x, 1) + return; + } + if (typeof hitBrick !== "undefined") { + const initialBrickColor = gameState.bricks[hitBrick]; + + explodeBrick(gameState, hitBrick, ball, false); + + if ( + ball.sapperUses < gameState.perks.sapper && + initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks + !gameState.bricks[hitBrick] + ) { + gameState.bricks[hitBrick] = "black"; + ball.sapperUses++; + } + } + + if (!isOptionOn("basic")) { + ball.sparks += (delta * (gameState.combo - 1)) / 30; + if (ball.sparks > 1) { + gameState.flashes.push({ + type: "particle", + duration: 100 * ball.sparks, + time: gameState.levelTime, + size: gameState.coinSize / 2, + color: gameState.ballsColor, + x: ball.x, + y: ball.y, + vx: (Math.random() - 0.5) * gameState.baseSpeed, + vy: (Math.random() - 0.5) * gameState.baseSpeed, + ethereal: false, + }); + ball.sparks = 0; + } } - } } diff --git a/src/game_utils.ts b/src/game_utils.ts index a6739f2..afe30da 100644 --- a/src/game_utils.ts +++ b/src/game_utils.ts @@ -1,5 +1,5 @@ -import { Ball, GameState, PerkId, PerksMap } from "./types"; -import { icons, upgrades } from "./loadGameData"; +import {Ball, GameState, PerkId, PerksMap} from "./types"; +import {icons, upgrades} from "./loadGameData"; export function getMajorityValue(arr: string[]): string { const count: { [k: string]: number } = {}; @@ -99,3 +99,18 @@ export function distanceBetween( ) { return Math.sqrt(distance2(a, b)); } + +export function defaultSounds() { + return { + aboutToPlaySound: { + wallBeep: {vol: 0, x: 0}, + comboIncreaseMaybe: {vol: 0, x: 0}, + comboDecrease: {vol: 0, x: 0}, + coinBounce: {vol: 0, x: 0}, + explode: {vol: 0, x: 0}, + lifeLost: {vol: 0, x: 0}, + coinCatch: {vol: 0, x: 0}, + colorChange: {vol: 0, x: 0}, + } + } +} \ No newline at end of file diff --git a/src/newGameState.ts b/src/newGameState.ts index ebbf744..8b124d6 100644 --- a/src/newGameState.ts +++ b/src/newGameState.ts @@ -1,13 +1,9 @@ -import { GameState, RunParams } from "./types"; -import { getTotalScore } from "./settings"; -import { allLevels, upgrades } from "./loadGameData"; -import { - getPossibleUpgrades, - makeEmptyPerksMap, - sumOfKeys, -} from "./game_utils"; -import { dontOfferTooSoon, resetBalls } from "./gameStateMutators"; -import { isOptionOn } from "./options"; +import {GameState, RunParams} from "./types"; +import {getTotalScore} from "./settings"; +import {allLevels, upgrades} from "./loadGameData"; +import {defaultSounds, getPossibleUpgrades, makeEmptyPerksMap, sumOfKeys,} from "./game_utils"; +import {dontOfferTooSoon, resetBalls} from "./gameStateMutators"; +import {isOptionOn} from "./options"; export function newGameState(params: RunParams): GameState { const totalScoreAtRunStart = getTotalScore(); @@ -91,6 +87,7 @@ export function newGameState(params: RunParams): GameState { levelWallBounces: 0, needsRender: true, autoCleanUses: 0, + ...defaultSounds() }; resetBalls(gameState); @@ -109,3 +106,4 @@ export function newGameState(params: RunParams): GameState { } return gameState; } + diff --git a/src/sounds.ts b/src/sounds.ts index f288880..2e2fb7f 100644 --- a/src/sounds.ts +++ b/src/sounds.ts @@ -1,48 +1,65 @@ -import { gameState } from "./game"; import { isOptionOn } from "./options"; +import {GameState} from "./types"; +let lastPlay = Date.now() + +export function playPendingSounds(gameState:GameState){ + if(lastPlay>Date.now()-60){ + return + } + lastPlay=Date.now() + for(let key in gameState.aboutToPlaySound){ + const soundName = key as keyof GameState["aboutToPlaySound"] + const ex = gameState.aboutToPlaySound[soundName] as {vol:number, x:number} + if(ex.vol){ + sounds[soundName](Math.min(2,ex.vol),pixelsToPan(gameState, ex.x), gameState.combo) + ex.vol=0 + } + } + +} export const sounds = { - wallBeep: (pan: number) => { + wallBeep: (vol:number, pan: number, combo:number) => { if (!isOptionOn("sound")) return; - createSingleBounceSound(800, pixelsToPan(pan)); + createSingleBounceSound(800, pan, vol); }, - comboIncreaseMaybe: (combo: number, x: number, volume: number) => { + comboIncreaseMaybe: ( volume: number,pan: number,combo: number, ) => { if (!isOptionOn("sound")) return; let delta = 0; if (!isNaN(lastComboPlayed)) { if (lastComboPlayed < combo) delta = 1; if (lastComboPlayed > combo) delta = -1; } - playShepard(delta, pixelsToPan(x), volume); + playShepard(delta, pan, volume); lastComboPlayed = combo; }, - comboDecrease() { + comboDecrease(volume: number,pan: number,combo: number) { if (!isOptionOn("sound")) return; - playShepard(-1, 0.5, 0.5); + playShepard(-1, pan, volume); }, - coinBounce: (pan: number, volume: number) => { + coinBounce: (volume: number,pan: number,combo: number) => { if (!isOptionOn("sound")) return; - createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, "triangle"); + createSingleBounceSound(1200, pan, volume, 0.1, "triangle"); }, - explode: (pan: number) => { + explode: (volume: number,pan: number,combo: number) => { if (!isOptionOn("sound")) return; - createExplosionSound(pixelsToPan(pan)); + createExplosionSound(pan); }, - lifeLost(pan: number) { + lifeLost(volume: number,pan: number,combo: number) { if (!isOptionOn("sound")) return; - createShatteredGlassSound(pixelsToPan(pan)); + createShatteredGlassSound(pan); }, - coinCatch(pan: number) { + coinCatch(volume: number,pan: number,combo: number) { if (!isOptionOn("sound")) return; - createSingleBounceSound(900, pixelsToPan(pan), 0.8, 0.1, "triangle"); + createSingleBounceSound(900, (pan), volume, 0.1, "triangle"); }, - colorChange(pan: number, volume: number) { - createSingleBounceSound(400, pixelsToPan(pan), volume, 0.5, "sine"); - createSingleBounceSound(800, pixelsToPan(pan), volume * 0.5, 0.2, "square"); + colorChange(volume: number,pan: number,combo: number) { + createSingleBounceSound(400, pan, volume, 0.5, "sine"); + createSingleBounceSound(800, pan, volume * 0.5, 0.2, "square"); }, }; @@ -158,7 +175,7 @@ function createExplosionSound(pan = 0.5) { noiseSource.stop(context.currentTime + 1); } -function pixelsToPan(pan: number) { +function pixelsToPan(gameState:GameState, pan: number) { return Math.max( 0, Math.min( diff --git a/src/types.d.ts b/src/types.d.ts index 6966533..7272db1 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -237,6 +237,16 @@ export type GameState = { levelTime: number; levelWallBounces: number; autoCleanUses: number; + aboutToPlaySound:{ + wallBeep:{vol:number, x:number}, + comboIncreaseMaybe:{vol:number, x:number}, + comboDecrease:{vol:number, x:number}, + coinBounce:{vol:number, x:number}, + explode:{vol:number, x:number}, + lifeLost:{vol:number, x:number}, + coinCatch:{vol:number, x:number}, + colorChange:{vol:number, x:number}, + } }; export type RunParams = {