5 colors /level, sound when ball or brick change color

This commit is contained in:
Renan LE CARO 2025-03-14 15:49:04 +01:00
parent b6fe46c9bc
commit 2e3ab3011f
21 changed files with 1379 additions and 598 deletions

115
Readme.md
View file

@ -32,41 +32,40 @@ There's also an easy mode for kids (slower ball).
- add pwe manifest
- offline mode with service worker
- see how to do fullscreen on ios, or at least explain to do aA/hide toolbars
- translation
- when game resumes near bottom, be unvulnerable for .5s ? , once per level
# Game engine features
- shinier coins by applying glow to them
- ask for permanent storage
- experiment with showing the combo somewhere else, maybe top center, maybe instead of score.
- more help somewhere accessible
- limit GC by reusing coins and particles
- convert captures to mp4 unsing ffmpeg wasm because reddit refuses webm files
- disable zooming (for ios double tap)
- few puck bounces = more choices / upgrades
- show total score on end screen (score added to total)
- show stats on end screen compared to other runs
- handle back bouton in menu
- mouvement relatif du puck
- balls should collide with each other
- when game resumes near bottom, be unvulnerable for .5s ? , once per level
- apply global curve / brightness to canvas when things blow, or just always to make neon effect better
- balls could collide with each other
- manifest for PWA (android and apple)
- lights shadows
- Offline mode web for iphone
- controller support on web/mobile
- webgl rendering
- enable export of gameplay capture in webview
- endgame histograms could work as filters, when you hover a bar, all other histograms would show the stats of those runs only, without changing reference of categories
- sound when ball changes color
- option : don't pause on mobile when lifting finger
- option : accelerated relative movements on mobile
- maybe just have 10 background, and always use the same one for the nth level of each run ?
- would be nice to have a leaderboard for not using each perk too. Like "best runs without hot start"
- restart run on r
- when missing, redo particle trail, but give speed to particle that matches ball direction
# graphics
- apply global curve / brightness to canvas when things blow, or just always to make neon effect better
- lights shadows
- webgl rendering
- shinier coins by applying glow to them
- different visual effects on ball to represent which perks it's imbued with (pierce, sapper…). remove visual while it's not affected (can't pierce/sap anymore until touching the puck).
- experiment with showing the combo somewhere else, maybe top center, maybe instead of score.
- the white outline on bricks associated with picky eater kinda works but i feel it's more distracting than anything. maybe try something different ? put a cross on matching coloured bricks, or the contrary, grey out other bricks.
# New perks ideas
- second puck (symmeric to the first one)
- second puck (symmetric to the first one)
- keep combo between level, loose half your run score when missing any bricks
- offer next level choice after upgrade pick
- ban 3 random perks from pool, doesn't tell you which ones, gain 2 upgrades
@ -99,7 +98,7 @@ There's also an easy mode for kids (slower ball).
- bricks attract coins
- breaking bricks stains neighbours
- extra kick after bouncing on puck
- transparent coins
- transparents coins
- coins of different colors repulse
- bricks follow game of life pattern with one update every second
- 2x coins when ball goes downward / upward, half that amount otherwise ?
@ -138,18 +137,8 @@ There's also an easy mode for kids (slower ball).
- ball avoids brick of wrong color
- coins avoid ball of different color
- colored coins only (coins should be of the color of the ball to count )
# Balancing ideas
The dominant strategy now is Compound Interest lvl 3 + coin magnet/viscosity/
and hot start + explosives and multiball
- make Compound Interest less OP making it full reset when coins lost
- cap hot start to lvl 2, or make it decrease faster
- make puck smaller as combo increases ?
- coin magnet has no effect when too close, or coins might overshoot, or coins bounce and spread more ?
- add red anti-coins, they destroy your combo and your score, and they behave like heavier coins.
- level flips horizontally every time a ball bounces on puck
- coins that hit the puck disappear, missed ones are scored
# extra levels
@ -159,17 +148,30 @@ and hot start + explosives and multiball
- animals
- countries flags and shapes, with name as background
# big features
# extra settings
- use ts and a bundler to get fewer bugs and compatibility with old browsers / webviews
- final bosses (large vertical level that scrolls down faster and faster)
- split screen multiplayer
- translation
- Add color schemes into the game (ex : Catppuccin, Dracula, Terminal, etc)
- add a toggle to switch between the “coin” design and colored bubbles
- sandbox mode
- on mobile, relative movement of the touch would be amplified and added to the puck
- option : don't pause on mobile when lifting finger
# Unlockable infinite mode
Allow players to loop the game, adding one hasard per loop, making it harder and harder to exploit each strategy. The high score are separated from the main mode. The scores are added for unlock. You no longer get upgrades after the first 7 levels. The score you make in each level is instead multiplied by the number of "upgrades" and "choices" you would have had.
Possible challenges :
- Add negative coins that make the coin magnet less usage
- add negative bricks that clear coins and reset combo
- add a brick eating enemy that forces you to play fast
- add a force field for 10s that negates hots start
- other perks can be randomly turned off
- ball keeps accelerating until unplayable
- graphical effects like trail, contrast, blur to make it harder to see what's going on
# extend re-playability
- hard mode : bricks take many hits, perks more rare, missing clears level score, missing coins deducts score..
- stats by lack of perk, like "best score without using hot start".
- split screen multiplayer
- Add color schemes into the game (ex : Catppuccin, Dracula, Terminal, etc)
Instead of automatically unlocking things at the end of each run, add the coins to the user's account,
and let them spend those coins on upgrades. The upgrades would then be explained. They could have a condition like
@ -177,27 +179,36 @@ and let them spend those coins on upgrades. The upgrades would then be explained
This requires recording a bit more info about each run.
I could unlock the "pro stand" at $999 that just holds the play area higher.
# bad ideas
-
- particles when bouncing on sides / top
-
# increase skill ceiling
- make puck smaller as combo increases ?
- nerf coin magnet :
- no effect when too close
- coins overshoot
- coins bounce and spread more ?
- add red anti-coins that apply downgrades
- destroy your combo
- hurt your score
- behave like heavier coins.
- deactivate a perk for this level
- reduce your number of coins
- destroy all coins on screen
- lowers your combo
- reduce your choice for your next perk
- final bosses (large vertical level that scrolls down faster and faster)
- when the player reaches the last level, allow them to loop the run, unlocking a permanent bonus for this run. For example: +5 combo, +1 life per loop… the counterpart would be hazards that slowly populate the levels.
# Colin's feedback (cwpute/obigre)
IMPROVEMENTS ON EXISTING PERKS :
* limit levels to only a handful of coulours, like 5 max, so that the colour-related perks are more viable.
GENERAL REMARKS ON DIFFERENT ASPECTS :
* when the player reaches the last level, alow them to loop the run, unlocking a permanent bonus for this run. For example: +5 combo, +1 life per loop… the counterpart would be hazards that slowly populate the levels.
* different visual effects on ball to represent which perks it's imbued with (pierce, sapper…). remove visual while it's not affected (can't pierce/sap anymore until touching the puck).
* always visually put the ball on top of coins so as to clearly see it. sometimes a black outline appears to distinguesh it from coins, this should be used more often imo.
* not brick-shaped bricks, or tilted bricks, that can bounce the ball into fun angles to spice up the game. or even moving blocks !
* reward the player with more choices/perks for breaking a brick while having reached an increasing combo thresholds. 5 combo, then 10, then 20, then 40 etc… once a threshold is reached you aren't rewarded for that threshold again until you start a rew run
* inspired by Balatro's score system : have some perks add to the multiplicator, and some others to the amount of coins in a brick (or the raw value of coins inside), so that you users want to improve both for maximized profit ! maybe tie one of the to perks that help you, and the other to perks that are bad to you, so that gambling players are forced to make their life harder
* the white outline on bricks asociated with picky eater kinda works but i feel it's more distracting than anything. maybe try something different ? put a cross on matching coloured bricks, or the contrary, grey out other bricks.
* also regarding colour : make it so the ball always start with a colour that matches one currently present in the level. sometimes you don't have white present and it's a waste of a combo :/
* negative coins, they would spawn from bricks as a hazard and do any of the following: -deactivate a perk for this level -reduce your number of coins -reduce your choice for your next perk -despawn all current coins on screen -lowers your combo. they could either be a negative perk with a bonus, like the small puck, or a hazard that spawns in later levels.
* the way combos look on the puck was better when you didn't see the coin visual on it ! now it easily overflows out of the puck with reduced visibility
# other
* not brick-shaped bricks, or tilted bricks, that can bounce the ball into fun angles to spice up the game. or even moving blocks !

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

358
dist/index.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head><script src="/index.c0fd3053.js"></script>
<meta charset="UTF-8">
<title>Level editor</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎨</text></svg>">

View file

@ -1,6 +1,6 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
testEnvironment: "node",
testEnvironment: "jsdom",
transform: {
"^.+\.tsx?$": ["ts-jest",{}],
},

619
package-lock.json generated
View file

@ -15,6 +15,7 @@
"body-parser": "^1.20.3",
"express": "^4.21.2",
"http-server": "^14.1.1",
"jest-environment-jsdom": "^29.7.0",
"nodemon": "^3.1.9",
"npm-run-all": "^4.1.5",
"parcel": "^2.13.3",
@ -737,7 +738,6 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
"integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/fake-timers": "^29.7.0",
@ -780,7 +780,6 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz",
"integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@ -858,7 +857,6 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
"integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.27.8"
@ -945,7 +943,6 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
@ -2792,14 +2789,12 @@
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true,
"license": "MIT"
},
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
"integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"type-detect": "4.0.8"
@ -2809,7 +2804,6 @@
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.0"
@ -3023,6 +3017,15 @@
"@swc/counter": "^0.1.3"
}
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@ -3090,14 +3093,12 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-report": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
"integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/istanbul-lib-coverage": "*"
@ -3107,7 +3108,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
"integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/istanbul-lib-report": "*"
@ -3123,11 +3123,21 @@
"pretty-format": "^29.0.0"
}
},
"node_modules/@types/jsdom": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
"integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/tough-cookie": "*",
"parse5": "^7.0.0"
}
},
"node_modules/@types/node": {
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
@ -3155,14 +3165,18 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"license": "MIT"
},
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
"integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/yargs-parser": "*"
@ -3172,9 +3186,15 @@
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
"dev": true,
"license": "MIT"
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"deprecated": "Use your platform's native atob() and btoa() methods instead",
"license": "BSD-3-Clause"
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -3187,6 +3207,75 @@
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-globals": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
"integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
"license": "MIT",
"dependencies": {
"acorn": "^8.1.0",
"acorn-walk": "^8.0.2"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/agent-base/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/agent-base/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@ -3316,6 +3405,12 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -3775,7 +3870,6 @@
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
"type": "github",
@ -3851,6 +3945,18 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@ -4114,12 +4220,50 @@
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="
},
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
"license": "MIT"
},
"node_modules/cssstyle": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
"integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
"license": "MIT",
"dependencies": {
"cssom": "~0.3.6"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cssstyle/node_modules/cssom": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
"integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
"license": "MIT",
"dependencies": {
"abab": "^2.0.6",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^11.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@ -4179,6 +4323,12 @@
"ms": "2.0.0"
}
},
"node_modules/decimal.js": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
"license": "MIT"
},
"node_modules/dedent": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
@ -4238,6 +4388,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -4318,6 +4477,19 @@
}
]
},
"node_modules/domexception": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
"integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
"deprecated": "Use your platform's native DOMException instead",
"license": "MIT",
"dependencies": {
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
@ -4622,11 +4794,31 @@
"node": ">=0.8.0"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
@ -4636,6 +4828,24 @@
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -4954,6 +5164,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -5419,6 +5644,43 @@
"node": ">=8.0.0"
}
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
"license": "MIT",
"dependencies": {
"@tootallnate/once": "2",
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/http-proxy-agent/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/http-proxy-agent/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/http-server": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
@ -5445,6 +5707,42 @@
"node": ">=12"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/https-proxy-agent/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/https-proxy-agent/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -5824,6 +6122,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"license": "MIT"
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@ -6349,6 +6653,33 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-environment-jsdom": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz",
"integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==",
"license": "MIT",
"dependencies": {
"@jest/environment": "^29.7.0",
"@jest/fake-timers": "^29.7.0",
"@jest/types": "^29.6.3",
"@types/jsdom": "^20.0.0",
"@types/node": "*",
"jest-mock": "^29.7.0",
"jest-util": "^29.7.0",
"jsdom": "^20.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"peerDependencies": {
"canvas": "^2.5.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jest-environment-node": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
@ -6437,7 +6768,6 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
"integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.12.13",
@ -6458,7 +6788,6 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
"integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@ -6645,7 +6974,6 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
"integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@ -6768,6 +7096,51 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsdom": {
"version": "20.0.3",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
"integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
"license": "MIT",
"dependencies": {
"abab": "^2.0.6",
"acorn": "^8.8.1",
"acorn-globals": "^7.0.0",
"cssom": "^0.5.0",
"cssstyle": "^2.3.0",
"data-urls": "^3.0.2",
"decimal.js": "^10.4.2",
"domexception": "^4.0.0",
"escodegen": "^2.0.0",
"form-data": "^4.0.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.2",
"parse5": "^7.1.1",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.2",
"w3c-xmlserializer": "^4.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^11.0.0",
"ws": "^8.11.0",
"xml-name-validator": "^4.0.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"canvas": "^2.5.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@ -7674,6 +8047,12 @@
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
"integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="
},
"node_modules/nwsapi": {
"version": "2.2.18",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.18.tgz",
"integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==",
"license": "MIT"
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@ -7905,6 +8284,30 @@
"node": ">= 0.10"
}
},
"node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -8103,7 +8506,6 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
@ -8118,7 +8520,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -8169,11 +8570,32 @@
"license": "MIT",
"optional": true
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
@ -8205,6 +8627,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -8257,7 +8685,6 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/react-refresh": {
@ -8499,6 +8926,18 @@
"license": "ISC",
"optional": true
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@ -8752,7 +9191,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -8844,7 +9282,6 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^2.0.0"
@ -8857,7 +9294,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -9074,6 +9510,12 @@
"node": ">= 10"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"license": "MIT"
},
"node_modules/term-size": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
@ -9139,6 +9581,33 @@
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"license": "MIT",
"dependencies": {
"punycode": "^2.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/ts-jest": {
"version": "29.2.6",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz",
@ -9196,7 +9665,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@ -9339,7 +9807,6 @@
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},
"node_modules/union": {
@ -9353,6 +9820,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -9395,6 +9871,16 @@
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/utility-types": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
@ -9444,6 +9930,18 @@
"node": ">= 0.8"
}
},
"node_modules/w3c-xmlserializer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
"integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
"license": "MIT",
"dependencies": {
"xml-name-validator": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@ -9459,6 +9957,15 @@
"resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz",
"integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw=="
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
@ -9481,6 +9988,28 @@
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
"license": "MIT",
"dependencies": {
"tr46": "^3.0.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -9617,6 +10146,42 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
"license": "Apache-2.0",
"engines": {
"node": ">=12"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View file

@ -19,6 +19,7 @@
"body-parser": "^1.20.3",
"express": "^4.21.2",
"http-server": "^14.1.1",
"jest-environment-jsdom": "^29.7.0",
"nodemon": "^3.1.9",
"npm-run-all": "^4.1.5",
"parcel": "^2.13.3",

58
src/combo.ts Normal file
View file

@ -0,0 +1,58 @@
import {GameState} from "./types";
import {sounds} from "./sounds";
export function baseCombo(gameState: GameState) {
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
}
export function resetCombo(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) / (1 + gameState.perks.soft_reset));
}
const lost = Math.max(0, prev - gameState.combo);
if (lost) {
for (let i = 0; i < lost && i < 8; i++) {
setTimeout(() => sounds.comboDecrease(), 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) {
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,
});
}
}
}

View file

@ -6,18 +6,19 @@ import {
colorString,
GameState,
PerkId,
PerksMap,
RunHistoryItem, RunParams,
RunHistoryItem,
RunParams,
RunStats,
Upgrade,
} from "./types";
import {OptionId, options} from "./options";
import {getAudioContext, getAudioRecordingTrack, sounds} from "./sounds";
import {putBallsAtPuck, resetBalls} from "./resetBalls";
import {sumOfKeys} from "./game_utils";
import {makeEmptyPerksMap, sumOfKeys} from "./game_utils";
import {baseCombo, decreaseCombo, resetCombo} from "./combo";
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
const ctx = gameCanvas.getContext("2d", {
alpha: false,
}) as CanvasRenderingContext2D;
@ -29,69 +30,6 @@ bombSVG.src =
<path d="m6.1528 26.516c-2.6992-3.4942-2.9332-8.281-.58305-11.981a10.454 10.454 0 017.3701-4.7582c1.962-.27726 4.1646.05953 5.8835.90027l.45013.22017.89782-.87417c.83748-.81464.91169-.87499 1.0992-.90271.40528-.058713.58876.03425 1.1971.6116l.55451.52679 1.0821-1.0821c1.1963-1.1963 1.383-1.3357 2.1039-1.5877.57898-.20223 1.5681-.19816 2.1691.00897 1.4613.50314 2.3673 1.7622 2.3567 3.2773-.0058.95654-.24464 1.5795-.90924 2.3746-.40936.48928-.55533.81057-.57898 1.2737-.02039.41018.1109.77714.42322 1.1792.30172.38816.3694.61323.2797.93044-.12803.45666-.56674.71598-1.0242.60507-.601-.14597-1.3031-1.3088-1.3969-2.3126-.09459-1.0161.19245-1.8682.92392-2.7432.42567-.50885.5643-.82851.5643-1.3031 0-.50151-.14026-.83177-.51211-1.2028-.50966-.50966-1.0968-.64829-1.781-.41996l-.37348.12477-2.1006 2.1006.52597.55696c.45421.48194.5325.58876.57898.78855.09622.41588.07502.45014-.88396 1.4548l-.87173.9125.26339.57979a10.193 10.193 0 01.9231 4.1001c.03996 2.046-.41996 3.8082-1.4442 5.537-.55044.928-1.0185 1.5013-1.8968 2.3241-.83503.78284-1.5526 1.2827-2.4904 1.7361-3.4266 1.657-7.4721 1.3422-10.549-.82035-.73473-.51782-1.7312-1.4621-2.2515-2.1357zm21.869-4.5584c-.0579-.19734-.05871-2.2662 0-2.4545.11906-.39142.57898-.63361 1.0038-.53005.23812.05708.54147.32455.6116.5382.06279.19163.06769 2.1805.0065 2.3811-.12558.40773-.61649.67602-1.0462.57164-.234-.05708-.51615-.30498-.57568-.50722m3.0417-2.6013c-.12313-.6222.37837-1.1049 1.0479-1.0079.18348.0261.25279.08399 1.0071.83911.75838.75838.81301.82362.84074 1.0112.10193.68499-.40365 1.1938-1.034 1.0405-.1949-.0473-.28786-.12558-1.0144-.85216-.7649-.76409-.80241-.81057-.84645-1.0316m.61323-3.0629a.85623.85623 0 01.59284-.99975c.28949-.09214 2.1814-.08318 2.3917.01141.38734.17369.6279.61078.53984.98181-.06035.25606-.35391.57327-.60181.64992-.25279.07747-2.2278.053-2.4097-.03017-.26013-.11906-.46318-.36125-.51374-.61323" fill="#fff" opacity="0.3"/>
</svg>`);
const makeEmptyPerksMap = () => {
const p = {} as any;
upgrades.forEach((u) => (p[u.id] = 0));
return p as PerksMap;
};
export function baseCombo() {
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
}
export function resetCombo(x: number | undefined, y: number | undefined) {
const prev = gameState.combo;
gameState.combo = baseCombo();
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) / (1 + gameState.perks.soft_reset));
}
const lost = Math.max(0, prev - gameState.combo);
if (lost) {
for (let i = 0; i < lost && i < 8; i++) {
setTimeout(() => sounds.comboDecrease(), 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(by: number, x: number, y: number) {
const prev = gameState.combo;
gameState.combo = Math.max(baseCombo(), 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,
});
}
}
}
export function play() {
if (gameState.running) return;
@ -341,7 +279,7 @@ async function openUpgradesPicker() {
gameState.runStatistics.upgrades_picked++;
}
resetCombo(undefined, undefined);
resetCombo(gameState, undefined, undefined);
resetBalls(gameState);
}
@ -361,7 +299,7 @@ export function setLevel(l: number) {
gameState.levelMisses = 0;
gameState.runStatistics.levelsPlayed++;
resetCombo(undefined, undefined);
resetCombo(gameState, undefined, undefined);
recomputeTargetBaseSpeed();
resetBalls(gameState);
@ -536,7 +474,7 @@ export function shouldPierceByColor(
export function coinBrickHitCheck(coin: Coin) {
// Make ball/coin bonce, and return bricks that were hit
const radius = gameState.coinSize / 2;
const radius = coin.size / 2;
const {x, y, previousX, previousY} = coin;
const vhit = hitsSomething(previousX, y, radius);
@ -646,7 +584,7 @@ export function tick() {
if (gameState.levelTime > gameState.lastTickDown + 1000 && gameState.perks.hot_start) {
gameState.lastTickDown = gameState.levelTime;
decreaseCombo(gameState.perks.hot_start, gameState.puckPosition, gameState.gameZoneHeight - 2 * gameState.puckHeight);
decreaseCombo(gameState, gameState.perks.hot_start, gameState.puckPosition, gameState.gameZoneHeight - 2 * gameState.puckHeight);
}
if (remainingBricks <= gameState.perks.skip_last && !gameState.autoCleanUses) {
@ -662,8 +600,8 @@ export function tick() {
setLevel(gameState.currentLevel + 1);
} else {
gameOver(
"Run finished with " + gameState.score + " points",
"You cleared all levels for this run.",
"Run finished ",
`You cleared all levels for this run, catching ${gameState.score} coins in total.`,
);
}
} else if (gameState.running || gameState.levelTime) {
@ -698,7 +636,7 @@ export function tick() {
coin.vy += delta * coin.weight * 0.8;
const speed = Math.abs(coin.sx) + Math.abs(coin.sx);
const hitBorder = bordersHitCheck(coin, coinRadius, delta);
const hitBorder = bordersHitCheck(coin, coin.size / 2, delta);
if (
coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&
@ -712,7 +650,7 @@ export function tick() {
} else if (coin.y > gameState.canvasHeight + coinRadius) {
coin.destroyed = true;
if (gameState.perks.compound_interest) {
resetCombo(coin.x, coin.y);
resetCombo(gameState, coin.x, coin.y);
}
}
@ -727,6 +665,7 @@ export function tick() {
) {
gameState.bricks[hitBrick] = coin.color;
coin.coloredABrick = true;
sounds.colorChange(coin.x,0.3)
}
}
if (typeof hitBrick !== "undefined" || hitBorder) {
@ -783,10 +722,10 @@ export function tick() {
});
}
if (gameState.combo > baseCombo()) {
if (gameState.combo > baseCombo(gameState)) {
// The red should still be visible on a white bg
const baseParticle = !isSettingOn("basic") &&
(gameState.combo - baseCombo()) * Math.random() > 5 &&
(gameState.combo - baseCombo(gameState)) * Math.random() > 5 &&
gameState.running && {
type: "particle" as const,
duration: 100 * (Math.random() + 1),
@ -955,7 +894,7 @@ export function ballTick(ball: Ball, delta: number) {
borderHitCode % 2 &&
ball.x < gameState.offsetX + gameState.gameZoneWidth / 2
) {
resetCombo(ball.x, ball.y);
resetCombo(gameState, ball.x, ball.y);
}
if (
@ -963,11 +902,11 @@ export function ballTick(ball: Ball, delta: number) {
borderHitCode % 2 &&
ball.x > gameState.offsetX + gameState.gameZoneWidth / 2
) {
resetCombo(ball.x, ball.y);
resetCombo(gameState, ball.x, ball.y);
}
if (gameState.perks.top_is_lava && borderHitCode >= 2) {
resetCombo(ball.x, ball.y + gameState.ballSize);
resetCombo(gameState, ball.x, ball.y + gameState.ballSize);
}
sounds.wallBeep(ball.x);
ball.bouncesList?.push({x: ball.previousX, y: ball.previousY});
@ -1010,7 +949,7 @@ export function ballTick(ball: Ball, delta: number) {
}
}
if (gameState.perks.streak_shots) {
resetCombo(ball.x, ball.y);
resetCombo(gameState, ball.x, ball.y);
}
if (gameState.perks.respawn) {
@ -1025,7 +964,7 @@ export function ballTick(ball: Ball, delta: number) {
if (!ball.hitSinceBounce) {
gameState.runStatistics.misses++;
gameState.levelMisses++;
resetCombo(ball.x, ball.y);
resetCombo(gameState, ball.x, ball.y);
gameState.flashes.push({
type: "text",
text: "miss",
@ -1245,6 +1184,11 @@ export function gameOver(title: string, intro: string) {
});
}
let unlockedItems = list.filter((u) => u.threshold > startTs && u.threshold < endTs);
if (unlockedItems.length) {
unlocksInfo += `<p>You unlocked ${unlockedItems.length} item(s) : ${unlockedItems.map(u => u.title).join(', ')}</p>`
}
// Avoid the sad sound right as we restart a new games
gameState.combo = 1;
@ -1254,6 +1198,7 @@ export function gameOver(title: string, intro: string) {
text: `
${gameState.isCreativeModeRun ? "<p>This test run and its score are not being recorded</p>" : ""}
<p>${intro}</p>
<p>Your total cumulative score went from ${startTs} to ${endTs}.</p>
${unlocksInfo}
`,
actions: [
@ -1480,6 +1425,7 @@ export function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
gameState.coins.push({
points,
size: gameState.coinSize,//-Math.floor(Math.log2(points)),
color: gameState.perks.metamorphosis ? color : "gold",
x: cx,
y: cy,
@ -1515,10 +1461,16 @@ export function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
color
) {
if (gameState.perks.picky_eater) {
resetCombo(ball.x, ball.y);
resetCombo(gameState, ball.x, ball.y);
}
sounds.colorChange(ball.x,0.8)
gameState.lastExplosion=gameState.levelTime
gameState.ballsColor = color;
if(!isSettingOn('basic')) {
gameState.balls.forEach(ball=>{
spawnExplosion(7, ball.previousX, ball.previousY, color, 150, 15)
})
}
} else {
sounds.comboIncreaseMaybe(gameState.combo, ball.x, 1);
}
@ -1668,7 +1620,7 @@ export function render() {
drawCoin(
ctx,
coin.color,
gameState.coinSize,
coin.size,
coin.x,
coin.y,
level.color || "black",
@ -1738,7 +1690,7 @@ export function render() {
// The puck
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
if (gameState.perks.streak_shots && gameState.combo > baseCombo()) {
if (gameState.perks.streak_shots && gameState.combo > baseCombo(gameState)) {
drawPuck(ctx, "red", gameState.puckWidth, gameState.puckHeight, -2);
}
drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight);
@ -1781,7 +1733,7 @@ export function render() {
}
}
// Borders
const hasCombo = gameState.combo > baseCombo();
const hasCombo = gameState.combo > baseCombo(gameState);
ctx.globalCompositeOperation = "source-over";
if (gameState.offsetXRoundedDown) {
// draw outside of gaming area to avoid capturing borders in recordings
@ -1795,11 +1747,11 @@ export function render() {
if (hasCombo && gameState.perks.right_is_lava) ctx.fillRect(width - 1, 0, 1, height);
}
if (gameState.perks.top_is_lava && gameState.combo > baseCombo()) {
if (gameState.perks.top_is_lava && gameState.combo > baseCombo(gameState)) {
ctx.fillStyle = "red";
ctx.fillRect(gameState.offsetXRoundedDown, 0, gameState.gameZoneWidthRoundedUp, 1);
}
const redBottom = gameState.perks.compound_interest && gameState.combo > baseCombo();
const redBottom = gameState.perks.compound_interest && gameState.combo > baseCombo(gameState);
ctx.fillStyle = redBottom ? "red" : gameState.puckColor;
if (isSettingOn("mobile-mode")) {
ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight, gameState.gameZoneWidthRoundedUp, 1);
@ -1837,7 +1789,7 @@ export function renderAllBricks() {
ctx.globalAlpha = 1;
const redBorderOnBricksWithWrongColor =
gameState.combo > baseCombo() && gameState.perks.picky_eater;
gameState.combo > baseCombo(gameState) && gameState.perks.picky_eater;
const newKey =
gameState.gameZoneWidth +
@ -2447,11 +2399,11 @@ async function openSettingsPanel() {
}
}
actions.push({
text: "Creative mode",
text: "Sandbox mode",
help:
getTotalScore() < creativeModeThreshold
? "Unlocks at total score $" + creativeModeThreshold
: "Test runs with custom perks",
: "Test any perk combination",
disabled: getTotalScore() < creativeModeThreshold,
async value() {
let creativeModePerks: Partial<{ [id in PerkId]: number }> = {},
@ -2549,7 +2501,7 @@ async function openUnlocksList() {
help:
ts >= threshold ? fullHelp : `Unlocks at total score ${threshold}.`,
disabled: ts < threshold,
value: {perk: id} as RunParams,
value: {perks: {[id]: 1}} as RunParams,
icon,
})),
...allLevels
@ -2944,7 +2896,7 @@ document.addEventListener("keydown", (e) => {
e.preventDefault();
});
document.addEventListener("keyup", (e) => {
document.addEventListener("keyup", async (e) => {
const focused = document.querySelector("button:focus");
if (e.key in pressed) {
setKeyPressed(e.key, 0);
@ -2966,6 +2918,8 @@ document.addEventListener("keyup", (e) => {
openSettingsPanel();
} else if (e.key.toLowerCase() === "s" && !alertsOpen) {
openScorePanel();
} else if (e.key.toLowerCase() === "r" && !alertsOpen) {
// TODO
} else {
return;
}
@ -2987,7 +2941,7 @@ function newGameState(params: RunParams): GameState {
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
);
const perks = {...makeEmptyPerksMap(), ...(params?.perks || {})}
const perks = {...makeEmptyPerksMap(upgrades), ...(params?.perks || {})}
const gameState: GameState = {
runLevels,

42
src/game_utils.test.ts Normal file
View file

@ -0,0 +1,42 @@
import {getMajorityValue, sample, sumOfKeys} from "./game_utils";
describe('getMajorityValue', ()=>{
it('returns the most common string',()=>{
expect(getMajorityValue(['1','1','2','2','3','2','3','2','2','1'])).toStrictEqual('2')
})
it('returns the only string',()=>{
expect(getMajorityValue(['1'])).toStrictEqual('1')
})
it('returns nothing for empty array',()=>{
expect(getMajorityValue([])).toStrictEqual(undefined)
})
})
describe('sample', ()=>{
it('returns a random pick from the array',()=>{
expect(['1','2','3'].includes(sample(['1','2','3']))).toBeTruthy()
})
it('returns the only item if there is just one',()=>{
expect(sample(['1'])).toStrictEqual('1')
})
it('returns nothing for empty array',()=>{
expect(sample([])).toStrictEqual(undefined)
})
})
describe('sumOfKeys', ()=>{
it('returns the sum of the keys of an array',()=>{
expect(sumOfKeys({a:1,b:2})).toEqual(3)
})
it('returns 0 for an empty object',()=>{
expect(sumOfKeys({})).toEqual(0)
})
it('returns 0 for undefined',()=>{
expect(sumOfKeys(undefined)).toEqual(0)
})
it('returns 0 for null',()=>{
expect(sumOfKeys(null)).toEqual(0)
})
})

View file

@ -1,4 +1,5 @@
import {PerksMap, Upgrade} from "./types";
export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {};
@ -17,3 +18,9 @@ export function sumOfKeys(obj:{[key:string]:number} | undefined | null){
if(!obj) return 0
return Object.values(obj)?.reduce((a,b)=>a+b,0) ||0
}
export const makeEmptyPerksMap = (upgrades:Upgrade[]) => {
const p = {} as any;
upgrades.forEach((u) => (p[u.id] = 0));
return p as PerksMap;
};

View file

@ -41,7 +41,7 @@
{
"name": "Dots",
"size": 9,
"bricks": "b_t_a_c_C__________b_t_a_c__________v_b_t_a_c__________v_b_t_a__________p_v_b_t_a",
"bricks": "b_t_a_c____________b_t_a_c__________P_b_t_a_c__________P_b_t_a____________P_b_t_a",
"svg": null
},
{
@ -95,7 +95,7 @@
{
"name": "Temple",
"size": 11,
"bricks": "_______________WWW______WWWWWWW___WWWWWWWWW___t_t_t_t____b_b_b_b____v_v_v_v____p_p_p_p____P_P_P_P____WWWWWWW___WWWWWWWWW_",
"bricks": "_______________WWW______WWWWWWW___WWWWWWWWW___b_b_b_b____b_b_b_b____v_v_v_v____P_P_P_P____P_P_P_P____WWWWWWW___WWWWWWWWW_",
"svg": null,
"color": ""
},
@ -109,7 +109,7 @@
{
"name": "Ship",
"size": 11,
"bricks": "____sWW________sWWW_______sWWW_______s___OOOOOOOOOOOOOO_OBOBOBOBOO__OOOOOOOO_bbbbbbbbgbbbbgbbbbggbbbggbbbbbbbb",
"bricks": "____sWW________sWWW_______sWWW_______s___OOOOOOOOOOOOOO_OBOBOBOBOO__OOOOOOOO_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb___________",
"svg": 19
},
{
@ -279,7 +279,7 @@
{
"name": "Ocean Sunrise",
"size": 8,
"bricks": "SSSSSSSSSSSyySSSSSyyyySSSyyWWyySbttaattbbbttttbbbbbttbbbbbbbbbbb",
"bricks": "SSSSSSSSSSSyySSSSSyyyySSSyyyyyySbttttttbbbttttbbbbbttbbbbbbbbbbb",
"svg": 12,
"color": ""
},
@ -648,7 +648,7 @@
{
"name": "Bird",
"size": 13,
"bricks": "_______RRR____R____RSSSR___RR__RSSWWWR__RSR_RSWWBWR__RSSRRSW_WWyy_RSSSRSWWWR___RSSSSSSRR_____RRSSyyyy______RSyyyyy___RRRRSyyyy____RSSSRyyy_____RRRR________",
"bricks": "_______RRR____R____RSSSR___RR__RSSWWWR__RSR_RSWWBWR__RSSRRSWWWWyy_RSSSRSWWWR___RSSSSSSRR_____RRSSyyyy______RSyyyyy___RRRRSyyyy____RSSSRyyy_____RRRR______________________",
"svg": null,
"color": ""
},

View file

@ -45,7 +45,10 @@ function App() {
return <div onMouseUp={() => setApplying('')} onMouseLeave={() => setApplying('')}>
return <div onMouseUp={() => {
console.log('mouse up')
setApplying('')
}} >
<div id={"levels"}>
{
levels.map((level, li) => {
@ -58,9 +61,12 @@ function App() {
brickButtons.push(<button
key={index}
onMouseDown={() => {
const color = selected === bricks[index] ? '_' : applying
if(!applying){
console.log(selected, bricks[index],applying)
const color = selected === bricks[index] ? '_' : selected
setApplying(color)
updateLevel(li, setBrick(level, index, color))
}
}}
onMouseEnter={() => {
if (applying) {

View file

@ -33,6 +33,7 @@ export function moveLevel(level: RawLevel, dx: number, dy: number) {
}
export function setBrick(level: RawLevel, index: number, colorCode: string) {
console.log('setBrick', level.name, index, colorCode)
const {size} = level
const newBricks=[]
for (let x = 0; x < size; x++) {

28
src/loadGameData.test.ts Normal file
View file

@ -0,0 +1,28 @@
import _palette from "./palette.json";
import _rawLevelsList from "./levels.json";
import _appVersion from "./version.json";
describe('json data checks', ()=>{
it('_rawLevelsList has icon levels', ()=>{
expect(_rawLevelsList.filter(l=>l.name.startsWith('icon:')).length).toBeGreaterThan(10)
})
it('_rawLevelsList has non-icon few levels', ()=>{
expect(_rawLevelsList.filter(l=>!l.name.startsWith('icon:')).length).toBeGreaterThan(10)
})
it('_rawLevelsList has max 5 colors per level', ()=>{
const levelsWithManyBrickColors=_rawLevelsList.filter(l=>{
const uniqueBricks = l.bricks.split('').filter(b=>b!=='_' && b!=='black').filter((a,b,c)=>c.indexOf(a)===b)
return uniqueBricks.length>5
}).map(l=>l.name)
expect(levelsWithManyBrickColors).toEqual([])
})
it('Has a few colors', ()=>{
expect(Object.keys(_palette).length).toBeGreaterThan(10)
})
it('Has an _appVersion', ()=>{
expect(parseInt(_appVersion)).toBeGreaterThan(2000)
})
})

View file

@ -19,7 +19,6 @@ const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {
function levelIconHTML(
bricks: string[],
levelSize: number,
levelName: string,
color: string,
) {
const size = 40;
@ -49,8 +48,8 @@ function levelIconHTML(
}
}
}
// I don't think many blind people will benefit for this, but it's nice to have something to put in "alt"
return `<img alt="${levelName}" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
return `<img alt="" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
}
export const icons = {} as { [k: string]: string };
@ -61,7 +60,7 @@ export const allLevels = rawLevelsList
.split("")
.map((c) => palette[c])
.slice(0, level.size * level.size);
const icon = levelIconHTML(bricks, level.size, level.name, level.color);
const icon = levelIconHTML(bricks, level.size, level.color);
icons[level.name] = icon;
return {
...level,

View file

@ -41,6 +41,10 @@ export const sounds = {
if (!isSettingOn("sound")) return;
createSingleBounceSound(900, pixelsToPan(pan), 0.8, 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")
}
};
// How to play the code on the leftconst context = new window.AudioContext();

1
src/types.d.ts vendored
View file

@ -69,6 +69,7 @@ export type Coin = {
color: colorString;
x: number;
y: number;
size: number;
previousX: number;
previousY: number;
vx: number;