From d4f7784ce3626f33d2f1fb747a26525390c073a2 Mon Sep 17 00:00:00 2001 From: david-fong Date: Wed, 6 May 2020 16:35:54 -0700 Subject: [PATCH] a14dc40 get home screen + rendering perf improvements --- .eslintrc.json | 5 +- .gitignore | 1 + .templates/tsconfig.json | 5 +- .vscode/cSpell.json | 9 +- .vscode/settings.json | 1 + TODO.md | 91 +++-- assets/images/square-ghost.png | Bin 0 -> 68 bytes assets/style/colour/index.css | 1 + assets/style/colour/smoothstone.css | 14 + assets/style/game/coordsys/beehive.css | 6 +- assets/style/game/coordsys/euclid2.css | 6 +- assets/style/game/grid.css | 51 ++- assets/style/game/player.css | 55 ++- assets/style/game/tile.css | 20 +- assets/style/game/zindex.css | 20 +- assets/style/initial/colour/index.css | 21 -- assets/style/initial/colour/snakey.css | 17 - assets/style/initial/components/index.css | 1 + assets/style/initial/components/pickone.css | 12 + assets/style/initial/index.css | 4 +- assets/style/initial/root.css | 9 + assets/style/initial/screen.css | 40 ++- assets/style/initial/screen/__play.css | 33 ++ assets/style/initial/screen/home.css | 62 ++++ assets/style/initial/screen/index.css | 2 + assets/style/initial/snakey.css | 14 + assets/style/initial/utils.css | 6 +- dev-guide.md | 11 +- dist/1.index.css | 1 - dist/1/index.js | 2 - dist/1/index.js.map | 1 - dist/client/chunk/game/offline.css | 1 + dist/client/chunk/game/offline.js | 2 + dist/client/chunk/lang/Emote-ts.js | 2 + dist/client/chunk/lang/English-ts.js | 2 + dist/client/chunk/lang/Japanese-ts.js | 2 + dist/client/chunk/lang/Korean-ts.js | 2 + dist/client/chunk/lang/Morse-ts.js | 2 + dist/{ => client}/favicon.ico | Bin dist/client/index.css | 1 + dist/client/index.js | 2 +- dist/client/index.js.map | 1 - dist/index.css | 1 - dist/server/index.js | 2 + index.ejs | 18 +- index.html | 33 +- manifest.webmanifest | 5 +- package-lock.json | 8 +- package.json | 11 +- scripts/Sandbox.js | 11 +- scripts/pack.sh | 2 +- scripts/webpack/tsconfig.json | 24 ++ scripts/webpack/webpack.config.ts | 159 ++++---- src/.templates/tsconfig.json | 2 +- src/base/.templates/tsconfig.json | 2 +- src/base/browser/ColourScheme.ts | 86 ----- src/base/browser/OmHooks.ts | 101 ------ src/base/browser/Storage.ts | 15 - src/base/browser/StorageHooks.ts | 16 - src/base/browser/TopLevel.ts | 6 - src/base/browser/screen/AllSkScreens.ts | 51 --- src/base/browser/screen/SkScreen.ts | 76 ---- src/base/browser/screen/impl/Home.ts | 12 - src/base/browser/screen/impl/PlayGame.ts | 15 - src/base/browser/tsconfig.json | 6 - src/base/defs/OmHooks.ts | 136 +++++++ src/base/defs/StorageHooks.ts | 58 +++ src/base/{utils => defs}/TypeDefs.ts | 177 +++++---- src/base/{utils => defs}/readme.md | 0 src/base/{utils => defs}/tsconfig.json | 0 src/base/floor/Grid.ts | 61 +--- src/base/floor/Tile.ts | 13 +- src/base/floor/VisibleGrid.ts | 56 ++- src/base/floor/VisibleTile.ts | 63 ++-- src/base/floor/impl/Beehive.ts | 9 +- src/base/floor/impl/Euclid2.ts | 20 +- src/base/floor/impl/readme.md | 12 +- src/base/floor/tsconfig.json | 3 +- src/base/game/Game.ts | 17 +- src/base/game/ScoreInfo.ts | 56 +++ src/base/game/__gameparts/Base.ts | 127 ++++--- src/base/game/__gameparts/Events.ts | 64 ++-- src/base/game/__gameparts/Manager.ts | 106 ++++-- src/base/game/__gameparts/pause.md | 6 +- src/base/game/events/PlayerActionEvent.ts | 12 +- src/base/game/player/ArtificialPlayer.ts | 14 +- src/base/game/player/OperatorPlayer.ts | 12 +- src/base/game/player/Player.ts | 40 +-- src/base/game/player/PlayerSkeleton.ts | 5 +- src/base/game/player/PlayerStatus.ts | 11 +- src/base/game/player/VisiblePlayerStatus.ts | 95 +++-- src/base/game/player/artificials/Chaser.ts | 4 +- src/base/game/tsconfig.json | 3 +- src/base/lang/Lang.ts | 39 +- src/base/lang/LangSeqTreeNode.ts | 7 +- src/base/lang/impl/Emote.ts | 17 + src/base/lang/impl/English.ts | 24 +- src/base/lang/impl/Japanese.ts | 24 +- src/base/lang/impl/Korean.ts | 104 +++--- src/base/lang/impl/Morse.ts | 30 ++ src/base/lang/impl/readme.md | 6 +- src/base/lang/tsconfig.json | 2 +- src/base/tsconfig.json | 3 +- src/client/OfflineGame.ts | 103 ------ src/client/ScratchMakeGame.ts | 53 --- src/{base/browser => client}/Sound.ts | 0 src/client/TopLevel.ts | 19 + src/client/game/GamePreset.ts | 9 + src/client/game/OfflineGame.ts | 74 ++++ src/client/{ => game}/OnlineGame.ts | 54 ++- src/client/game/VisibleGame.ts | 10 + src/client/index.ts | 30 +- src/{base/browser => client}/readme.md | 2 +- src/client/screen/AllSkScreens.ts | 64 ++++ src/client/screen/SkScreen.ts | 132 +++++++ src/client/screen/impl/ColourCtrl.ts | 145 ++++++++ .../screen/impl/GameSetup.ts | 29 +- src/client/screen/impl/Home.ts | 105 ++++++ .../screen/impl/HowToHost.ts | 10 +- .../screen/impl/HowToPlay.ts | 5 +- src/client/screen/impl/PlayOffline.ts | 83 +++++ src/client/screen/impl/PlayOnline.ts | 35 ++ .../screen/impl/SeshJoiner.ts | 2 +- src/client/screen/impl/__Play.ts | 340 ++++++++++++++++++ src/{base/browser => client}/screen/readme.md | 2 +- src/client/tsconfig.json | 2 +- src/client/utils/SkPickOne.ts | 152 ++++++++ src/server/GroupSession.ts | 9 +- src/server/ServerGame.ts | 30 +- src/server/SnakeyServer.ts | 71 ++-- src/server/index.ts | 3 +- src/tsconfig.json | 2 +- test/.templates/tsconfig.json | 2 +- test/lang/Lang.ts | 31 +- test/tsconfig.json | 2 +- tsconfig.json | 2 +- 136 files changed, 2746 insertions(+), 1502 deletions(-) create mode 100644 assets/images/square-ghost.png create mode 100644 assets/style/colour/index.css create mode 100644 assets/style/colour/smoothstone.css delete mode 100644 assets/style/initial/colour/index.css delete mode 100644 assets/style/initial/colour/snakey.css create mode 100644 assets/style/initial/components/index.css create mode 100644 assets/style/initial/components/pickone.css create mode 100644 assets/style/initial/screen/__play.css create mode 100644 assets/style/initial/screen/home.css create mode 100644 assets/style/initial/screen/index.css create mode 100644 assets/style/initial/snakey.css delete mode 100644 dist/1.index.css delete mode 100644 dist/1/index.js delete mode 100644 dist/1/index.js.map create mode 100644 dist/client/chunk/game/offline.css create mode 100644 dist/client/chunk/game/offline.js create mode 100644 dist/client/chunk/lang/Emote-ts.js create mode 100644 dist/client/chunk/lang/English-ts.js create mode 100644 dist/client/chunk/lang/Japanese-ts.js create mode 100644 dist/client/chunk/lang/Korean-ts.js create mode 100644 dist/client/chunk/lang/Morse-ts.js rename dist/{ => client}/favicon.ico (100%) create mode 100644 dist/client/index.css delete mode 100644 dist/client/index.js.map delete mode 100644 dist/index.css create mode 100644 dist/server/index.js create mode 100644 scripts/webpack/tsconfig.json delete mode 100644 src/base/browser/ColourScheme.ts delete mode 100644 src/base/browser/OmHooks.ts delete mode 100644 src/base/browser/Storage.ts delete mode 100644 src/base/browser/StorageHooks.ts delete mode 100644 src/base/browser/TopLevel.ts delete mode 100644 src/base/browser/screen/AllSkScreens.ts delete mode 100644 src/base/browser/screen/SkScreen.ts delete mode 100644 src/base/browser/screen/impl/Home.ts delete mode 100644 src/base/browser/screen/impl/PlayGame.ts delete mode 100644 src/base/browser/tsconfig.json create mode 100644 src/base/defs/OmHooks.ts create mode 100644 src/base/defs/StorageHooks.ts rename src/base/{utils => defs}/TypeDefs.ts (52%) rename src/base/{utils => defs}/readme.md (100%) rename src/base/{utils => defs}/tsconfig.json (100%) create mode 100644 src/base/game/ScoreInfo.ts create mode 100644 src/base/lang/impl/Emote.ts create mode 100644 src/base/lang/impl/Morse.ts delete mode 100644 src/client/OfflineGame.ts delete mode 100644 src/client/ScratchMakeGame.ts rename src/{base/browser => client}/Sound.ts (100%) create mode 100644 src/client/TopLevel.ts create mode 100644 src/client/game/GamePreset.ts create mode 100644 src/client/game/OfflineGame.ts rename src/client/{ => game}/OnlineGame.ts (77%) create mode 100644 src/client/game/VisibleGame.ts rename src/{base/browser => client}/readme.md (85%) create mode 100644 src/client/screen/AllSkScreens.ts create mode 100644 src/client/screen/SkScreen.ts create mode 100644 src/client/screen/impl/ColourCtrl.ts rename src/{base/browser => client}/screen/impl/GameSetup.ts (55%) create mode 100644 src/client/screen/impl/Home.ts rename src/{base/browser => client}/screen/impl/HowToHost.ts (57%) rename src/{base/browser => client}/screen/impl/HowToPlay.ts (67%) create mode 100644 src/client/screen/impl/PlayOffline.ts create mode 100644 src/client/screen/impl/PlayOnline.ts rename src/{base/browser => client}/screen/impl/SeshJoiner.ts (71%) create mode 100644 src/client/screen/impl/__Play.ts rename src/{base/browser => client}/screen/readme.md (90%) create mode 100644 src/client/utils/SkPickOne.ts diff --git a/.eslintrc.json b/.eslintrc.json index c8a25e2e..ba6a3c7f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,10 +8,9 @@ // infinite loop. :( "project": [ // webpack: - "./scripts/webpack.tsconfig.json", + "./scripts/webpack/tsconfig.json", // basecode: - "./src/base/utils/tsconfig.json", - "./src/base/browser/tsconfig.json", + "./src/base/defs/tsconfig.json", "./src/base/lang/tsconfig.json", "./src/base/floor/tsconfig.json", "./src/base/game/tsconfig.json", diff --git a/.gitignore b/.gitignore index 773a9648..7a2b11c0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /dist/ /scripts/eslint-cache.json /scripts/webpack/webpack.config.js +/scripts/webpack/*.tsbuildinfo # vim *.swp diff --git a/.templates/tsconfig.json b/.templates/tsconfig.json index 9f3ba0b1..a03714b8 100644 --- a/.templates/tsconfig.json +++ b/.templates/tsconfig.json @@ -11,12 +11,12 @@ // Language Settings: "target": "es6", - "lib": [ "es6", "dom", "es2019.array" ], + "lib": [ "es6", "dom", "ES2019.Array" ], "module": "ES2020", // Emit Flags: "sourceMap": true, - "removeComments": true, + //"removeComments": true, "alwaysStrict": true, // Grammar Checking: @@ -25,5 +25,6 @@ "noImplicitReturns": true, "strictBindCallApply": true, "noImplicitThis": true, + "noImplicitAny": true, }, } \ No newline at end of file diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 791027bc..e792a4ff 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -17,6 +17,7 @@ "typescriptreact" ], "words": [ + // Project-specific Terms: "snakey", "fong", "artif", @@ -29,12 +30,15 @@ "typeable", "unts", "engl", "japn", "kore", + "smoothstone", + // Programming terminology: + // (Functions and Variables) "coord", "dict", "desc", "descs", "dests", - "elem", + "elem", "elems", "minmax", "nsps", "retval", @@ -44,6 +48,7 @@ "typeof", "unshift", + // Tooling and Programming Languages: "API's", "devtool", "entrypoint", @@ -61,6 +66,7 @@ "hiragana" , "katakana" , "dubeolsik", "sebeolsik", + // English: "constraining", "gameplay", "hinderance", @@ -72,6 +78,7 @@ "ness", "pipelining", "placeholder", + "presets", "programmatically", "requester's", "screenshots", diff --git a/.vscode/settings.json b/.vscode/settings.json index 6fd8f355..a2f881d8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ // project specific excludes: "package-lock.json": true, + "scripts/webpack/webpack.config.js": true, // don't always need this: "node_modules/": true, diff --git a/TODO.md b/TODO.md index f0e8ce5d..41162e71 100644 --- a/TODO.md +++ b/TODO.md @@ -5,16 +5,17 @@ 1. Get a basic, working implementation of an offline game. 1. Write the stylesheets. -1. Record music + find out how to play tracks together. 1. Get working bundles for networked games. +1. Add player sprites. +1. Record music + find out how to play tracks together. +1. Make the website accessible by ARIA standards. ## Concrete TODOs ### High Priority -1. Brainstorm ways to split up the js and css to defer loading. - - Make lang files dynamically imported. This will save loading if the user only plays online (no game-manager implementation needed). -1. Make and hook up lang registry (initialize in PostInit, define under Lang). +1. Implement operator switching. + - Make the whole client-side only have one copy of the spotlight elements and always give it to the current operator. 1. Fill in implementation of bubble event handler. 1. Design decision: Change bubble mechanism: - Activates automatically and immediately upon players entering each others' (mutual) attack range, or by pressing space in the (mutual) attack range of other players. @@ -31,8 +32,14 @@ ### Low Priority +- Tell WebPack to split out styling for each screen? +- Test performance when using `cloneNode` to create Tile elements versus all those calls to `document.createElement`. + - [](https://developers.google.com/web/fundamentals/web-components) - If we start using SASS, make classes that always have .center-contents or .stack-contents use an extension mechanism so we don't have to manually specify those utility classes in the javascript. That makes it easier to see whats happening from looking just at the stylesheets. +- [Create a mask-safe icon that is large enough (min 144px)](https://web.dev/maskable-icon/) - Read about these topics and see how they might be useful + - [](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Basic_Concepts_Behind_IndexedDB) + - [](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB) - [](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API) - [](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen) - [](https://www.npmjs.com/package/bad-words) @@ -42,10 +49,12 @@ - [](https://devcenter.heroku.com/articles/node-best-practices) - [](https://devcenter.heroku.com/articles/nodejs-support) - [](https://medium.com/deployplace/heroku-vs-docker-the-ultimate-comparison-with-hidden-pitfalls-revealed-f6b7f4075de5) + - [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) - To discourage players from spamming the keyboard, which would make them move chaotically really fast and defeat the educational purpose of the game, detect their success rate of pressing relevant keys, or the rate in terms of time. If they seem to be spamming, then somehow throttle their requests. Maybe stop responding for a brief period of time. - For classes implementing some swappable component or ones in a long class hierarchy, see if there are elegance-improvements to be made by using re-exports. - Look into switching from JsDoc to TsDoc - [eslint plugin](https://www.npmjs.com/package/eslint-plugin-tsdoc) +- [custom mouse images!](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Basic_User_Interface/Using_URL_values_for_the_cursor_property) ### Dependency Management @@ -55,6 +64,10 @@ - WebPack 5: - [Magic dynamic import strings](https://webpack.js.org/migrate/5/#cleanup-the-code) will start getting useful values by default. - `output.ecmaVersion` is `6` by default. If we have set it to `6` manually, we can delete the manual field specification. +- [TypeScript / tslib bug](https://github.com/microsoft/TypeScript/issues/36841) + - This is on the roadmap for TypeScript 2.9.1... That may be a while. + - When it is fixed, we can take out the ts-loader compiler option forcing `importHelpers` to be off. +- In package.json's scripts field, use node's `--enable-source-maps` flag when there is better support for it / we update node to a version with better support for it / I find out that there is good support and I was just using it wrong. --- @@ -64,25 +77,35 @@ - make BGM have a track that varies with lang and different selectable style variations such as jazz cafe/elevator music, fast 13/8. - Make movement sound effects able to depend on translated key input like morse sounds. -## Research / Learning Links - -### Dynamic imports +## Good Reads ```text -https://javascript.info/modules-dynamic-imports -https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import -https://github.com/tc39/proposal-dynamic-import/#import -https://v8.dev/features/dynamic-import +https://javascript.info/class-inheritance +https://medium.com/better-programming/prototypes-in-javascript-5bba2990e04b +https://www.quirksmode.org/js/events_order.html#link4 +https://www.mikedoesweb.com/2017/dynamic-super-classes-extends-in-es6/ +https://javascript.info/mixins +https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/JavaScript + +https://github.com/whatwg/html/issues/4078 + +https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values ``` -### ES6 modules in NodeJS +## Research / Learning Links + +### Licensing ```text -https://stackoverflow.com/questions/45854169/how-can-i-use-an-es6-import-in-node -https://medium.com/@iamstan/typescript-es-modules-micheal-jackson-2040216be793 -https://nodejs.org/api/esm.html#esm_enabling +https://creativecommons.org/faq/#can-i-apply-a-creative-commons-license-to-software +https://www.gnu.org/licenses/copyleft.html +https://opensource.org/licenses ``` +### Dynamic imports + +Links no longer needed. Good things to know: both TypeScript and WebPack implement handling for dynamic imports. TypeScript will provide type information about the exports from a module, and WebPack will intercept the dynamic import to create a deferred-loading split chunk. + ### Web API's I might use [this](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API) for choosing which team you want to be part of. @@ -97,12 +120,7 @@ https://www.w3schools.com/html/html5_webstorage.asp https://www.w3schools.com/html/html5_serversentevents.asp Navigator.{keyboard,online,connection,language,languages,battery} https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API -``` - -### JsDoc - -```text -https://devdocs.io/jsdoc/howto-es2015-classes +https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API ``` ### Audio @@ -115,6 +133,8 @@ For the background music track, I will have multiple layers. They will all be th I should keep my audio loops as short as possible. Ie. Every audio file 4 measures long except the melody file which is maybe 12 measures long. +Here's [something fun I can do](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) (but don't have to). + ```text https://devdocs.io/dom/web_audio_api/best_practices https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API @@ -140,23 +160,32 @@ https://developer.mozilla.org/en-US/docs/Web/CSS/display flex playground: https://codepen.io/enxaneta/full/adLPwv/ ``` -### Handling Network Latency +### ARIA ```text -https://martinfowler.com/eaaDev/EventSourcing.html -https://stackoverflow.com/a/9283222/11107541 +https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles +https://a11yproject.com/checklist/ ``` -## Good Reads +### ES6 modules in NodeJS ```text -https://javascript.info/class-inheritance -https://medium.com/better-programming/prototypes-in-javascript-5bba2990e04b -https://www.quirksmode.org/js/events_order.html#link4 -https://www.mikedoesweb.com/2017/dynamic-super-classes-extends-in-es6/ -https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/JavaScript +https://stackoverflow.com/questions/45854169/how-can-i-use-an-es6-import-in-node +https://medium.com/@iamstan/typescript-es-modules-micheal-jackson-2040216be793 +https://nodejs.org/api/esm.html#esm_enabling +``` -https://github.com/whatwg/html/issues/4078 +### JsDoc + +```text +https://devdocs.io/jsdoc/howto-es2015-classes +``` + +### Handling Network Latency + +```text +https://martinfowler.com/eaaDev/EventSourcing.html +https://stackoverflow.com/a/9283222/11107541 ``` ## Things I have Tried that Haven't Worked (and that's okay) diff --git a/assets/images/square-ghost.png b/assets/images/square-ghost.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/assets/style/colour/index.css b/assets/style/colour/index.css new file mode 100644 index 00000000..43777565 --- /dev/null +++ b/assets/style/colour/index.css @@ -0,0 +1 @@ +@import "./smoothstone.css"; \ No newline at end of file diff --git a/assets/style/colour/smoothstone.css b/assets/style/colour/smoothstone.css new file mode 100644 index 00000000..d7f6bc11 --- /dev/null +++ b/assets/style/colour/smoothstone.css @@ -0,0 +1,14 @@ +[data-sk-colour-scheme="smooth-stone"] { + --colour-mainFg: #c3c5ce; + --colour-mainBg: #72888d; + --colour-tileFg: #a2b6bb; + --colour-tileBg: #546164; + --colour-tileBd: #ffffff; + --colour-healthFg: #21352e; + --colour-healthBg: #398f5a; + --colour-pFaceMe: #43aec9; + --colour-pFaceTeammate: #bdae58; + --colour-pFaceImtlTeammate: #f8df50; + --colour-pFaceOpponent: #a34e59; + --colour-pFaceImtlOpponent: #e23fa3; +} \ No newline at end of file diff --git a/assets/style/game/coordsys/beehive.css b/assets/style/game/coordsys/beehive.css index 38b38f6e..1ee77c2e 100644 --- a/assets/style/game/coordsys/beehive.css +++ b/assets/style/game/coordsys/beehive.css @@ -2,9 +2,11 @@ /* https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path */ -.game-grid[data-coord-sys="BEEHIVE"] .tile { +/* +.game-grid-impl-body[data-coord-sys="BEEHIVE"] .tile { --nothing: "yet"; } -.game-grid[data-coord-sys="BEEHIVE"] .tile > * { +.game-grid-impl-body[data-coord-sys="BEEHIVE"] .tile > * { --nothing: "yet"; } + */ diff --git a/assets/style/game/coordsys/euclid2.css b/assets/style/game/coordsys/euclid2.css index 817a4610..945396ea 100644 --- a/assets/style/game/coordsys/euclid2.css +++ b/assets/style/game/coordsys/euclid2.css @@ -1,12 +1,12 @@ -.game-grid[data-coord-sys="EUCLID2"] .game-grid-impl-body { - --track-size: minmax(27px, 1fr); +.game-grid-impl-body[data-coord-sys="EUCLID2"] { + --track-size: minmax(1.67em, 1fr); display: grid; grid-template-columns: repeat(var(--euclid2-grid-width), var(--track-size)); grid-auto-rows: var(--track-size); } -.game-grid[data-coord-sys="EUCLID2"] .tile { +.game-grid-impl-body[data-coord-sys="EUCLID2"] .tile { margin: 5%; } /* .game-grid[data-coord-sys="EUCLID2"] .tile > * { diff --git a/assets/style/game/grid.css b/assets/style/game/grid.css index 9c2e35f9..63bad80e 100644 --- a/assets/style/game/grid.css +++ b/assets/style/game/grid.css @@ -1,50 +1,41 @@ -#game-grid-container { - height: 100vh; - flex-flow: column nowrap; -} - - .game-grid { /* Since this is given the `center-contents` and `stack-contents` classes, it defaults to taking up no space (its children all have their position property set to `absolute`). We rectify this case by giving `.game-grid-impl-body` `position: static`. */ + border: 5px double var(--colour-tileBd); + border-radius: 15px; + padding: 5px; } .game-grid-impl-body { position: static; - padding: 4px 4px; - border: 5px double var(--colour-tileBd); - border-radius: 15px; color: var(--colour-tileFg); - - /* To hide the spotlight off the edges */ - overflow: hidden hidden; } -.game-grid-kbd-dc { - /* Take up the whole space of the parent, and - make sure that (regardless of element ordering,) - This doesn't prevent `.game-grid-impl-body` from - receiving focus via pointer. */ - pointer-events: none; +.game-grid-kbd-dc, +.game-grid-pause-overlay { top: 0; left: 0; bottom: 0; right: 0; -} -.game-grid-kbd-dc { - color: var(--colour-tileBd); - text-shadow: 0 0 0.5em black; + color: var(--colour-tileBd); + text-shadow: 0em 0.01em 0.5em black; + background-color: #00000040; visibility: hidden; } - -.game-grid-impl-body:focus { - /* cursor: help; */ - outline: none; +.game-grid-kbd-dc { + cursor: pointer; } -.game-grid-impl-body:not(:focus) { - filter: brightness(70%); +.game-grid-pause-overlay { + cursor: not-allowed; } -.game-grid-impl-body:not(:focus) ~ .game-grid-kbd-dc { - visibility: visible; + +@media screen { + .game-grid:focus { + outline: none; + } + .game-grid[data-game-state="paused"] > .game-grid-pause-overlay, + .game-grid:not([data-game-state="paused"]):not(:focus) > .game-grid-kbd-dc { + visibility: visible; + } } diff --git a/assets/style/game/player.css b/assets/style/game/player.css index c5743ea7..f7d24836 100644 --- a/assets/style/game/player.css +++ b/assets/style/game/player.css @@ -6,14 +6,44 @@ major hinderance, then consider making it an HTML child of a tile instead of a player (would double the number of DOM operations per operator movement. */ + --colour-spotlight: var(--colour-mainBg); + contain: size; } .player__face { - border-color: var(--colour-tileBd); + border-color: var(--colour-tileBd); + contain: strict; +} +.player__spotlight-short, +.player__spotlight-long { + will-change: transform; +} +.player[data-face="me"] > .player__face { + /* Both `contain` and `will-change` are required + here to constrain repaints (due to animations) on + this element to the smallest possible box. It is + also required to put the spotlights on separate + compositing layers. */ + will-change: transform; } .player__downed-overlay { background-color: red; opacity: 0.2; } +.player[data-downed="no"] .player__downed-overlay { + visibility: hidden; +} + +/* Only applied for client-side operator player since +using certain properties will create a new stacking +context, and we decided that we will allow this for +such players' face element. */ +/* .player[data-face="me"] > .anim-player__on-move { + animation: 0.2s + anim-frames-player__on-move; +} */@keyframes anim-frames-player__on-move { + 0% { transform: scale(1.1); } + 37% { transform: scale(1.3); } +} /* @@ -24,23 +54,30 @@ https://www.w3schools.com/cssref/pr_background-blend-mode.asp .player__spotlight-short { background-image: radial-gradient( farthest-side, - transparent 30%, var(--colour-mainBg) + transparent 30%, var(--colour-spotlight) ); padding: 1100%; - border: 100vmax solid var(--colour-mainBg); + border: 100vmax solid var(--colour-spotlight); } .player__spotlight-long { background-image: radial-gradient( farthest-side, - transparent 15%, var(--colour-mainBg) + transparent 15%, var(--colour-spotlight) ); padding: 1700%; - border: 100vmax solid var(--colour-mainBg); + border: 100vmax solid var(--colour-spotlight); opacity: 0.92; } -.player[data-downed="no"] > .player__downed-overlay { - visibility: hidden; +@media print { + .player__spotlight-short { + visibility: hidden; + } + .player__spotlight-long { + visibility: hidden; + } } + + .player[data-face="me"] > .player__face/* , .player[data-face="me"] ~ .tile__char */ { background-color: var(--colour-pFaceMe); @@ -55,9 +92,9 @@ https://www.w3schools.com/cssref/pr_background-blend-mode.asp } .player[data-face="teammate"][data-downed="team"] > .player__face/* , .player[data-face="teammate"][data-downed="team"] ~ .tile__char */ { - background-color: var(--colour-pFaceImmortalTeammate); + background-color: var(--colour-pFaceImtlTeammate); } .player[data-face="opponent"][data-downed="team"] > .player__face/* , .player[data-face="opponent"][data-downed="team"] ~ .tile__char */ { - background-color: var(--colour-pFaceImmortalOpponent); + background-color: var(--colour-pFaceImtlOpponent); } diff --git a/assets/style/game/tile.css b/assets/style/game/tile.css index fafdc803..68af3355 100644 --- a/assets/style/game/tile.css +++ b/assets/style/game/tile.css @@ -3,12 +3,14 @@ /* DO NOT create a new stacking context, which breaks the spotlight display. */ pointer-events: none; + contain: size; } .tile * { height: 100%; width: 100%; } .tile__pointer-hitbox { pointer-events: initial; + contain: strict; top: 40%; left: 40%; /* border-color: black; */ } @@ -16,20 +18,23 @@ .tile__char, .tile__seq, .player__face { box-sizing: border-box; border: 0.14em solid transparent; - border-radius: 16%; + border-radius: 0.35em; } + .tile__char { background-color: var(--colour-tileBg); overflow: hidden hidden; - transition-property: color, background-color; - transition-duration: 0.5s; - transition-timing-function: ease-in; } .tile[data-health] .tile__char { border-color: var(--colour-tileBd); color: var(--colour-healthFg); background-color: var(--colour-healthBg); + + transition-property: color, background-color; + transition-duration: 0.5s; + transition-timing-function: ease-in; } + .tile__seq { padding: 0ch 0.4ch; /* bottom: 0%; */ @@ -40,9 +45,12 @@ border-color: white; visibility: hidden; } -.tile__pointer-hitbox:hover ~ .tile__seq { - visibility: visible; +.tile__pointer-hitbox:hover ~ .player > .player__face { + visibility: hidden; } .tile__pointer-hitbox:hover ~ .tile__char { color: transparent; } +.tile__pointer-hitbox:hover ~ .tile__seq { + visibility: visible; +} diff --git a/assets/style/game/zindex.css b/assets/style/game/zindex.css index 2007522a..6add3529 100644 --- a/assets/style/game/zindex.css +++ b/assets/style/game/zindex.css @@ -1,13 +1,23 @@ -/* The only way I'm going to stay sane managing this -is by giving it its own file. */ -.game-grid { z-index: 0; } +/* The only way I'm going to stay sane managing +this is by giving it its own file. -.game-grid-impl-body { z-index: 0; } /* ROOT */ +Pay special attention to the containment property +specifications made here, which help the browser +avoid performing unnecessary reflow calculations +over the whole grid every time a player moves. +Turn on "Paint Flashing" in a browser inspector. */ + +.game-grid { z-index: 0; contain: content; } + +.game-grid-impl-body { z-index: 0; contain: strict; } /* ROOT */ +.player__face[data-face="me"] { z-index: 21; } .player__spotlight-long { z-index: 20; } /* SPOTLIGHT (long-range) */ -.tile__seq { z-index: 12; } +.tile__seq { z-index: 13; } +.player__downed-overlay { z-index: 12; } .player__face { z-index: 11; } .tile[data-health] .tile__char { z-index: 11; } .player__spotlight-short { z-index: 10; } /* SPOTLIGHT (short-range) */ .game-grid-kbd-dc { z-index: 1; } +.game-grid-pause-overlay { z-index: 2; } diff --git a/assets/style/initial/colour/index.css b/assets/style/initial/colour/index.css deleted file mode 100644 index 5720e847..00000000 --- a/assets/style/initial/colour/index.css +++ /dev/null @@ -1,21 +0,0 @@ -@import "./snakey.css"; -/* -This is a special file to get a view of the programmatically-set -swatch based on user selection, with the extra behaviour of falling -back to default values to prevent a flash of uncoloured content -before the javascript programmatically selects the default scheme. -*/ -:root { - --colour-tileFg: var(--colour-selected-tileFg, var(--colour-snakey-tileFg)); - --colour-tileBg: var(--colour-selected-tileBg, var(--colour-snakey-tileBg)); - --colour-tileBd: var(--colour-selected-tileBd, var(--colour-snakey-tileBd)); - --colour-healthFg: var(--colour-selected-healthFg, var(--colour-snakey-healthFg)); - --colour-healthBg: var(--colour-selected-healthBg, var(--colour-snakey-healthBg)); - --colour-mainFg: var(--colour-selected-mainFg, var(--colour-snakey-mainFg)); - --colour-mainBg: var(--colour-selected-mainBg, var(--colour-snakey-mainBg)); - --colour-pFaceMe: var(--colour-selected-pFaceMe, var(--colour-snakey-pFaceMe)); - --colour-pFaceTeammate: var(--colour-selected-pFaceTeammate, var(--colour-snakey-pFaceTeammate)); - --colour-pFaceOpponent: var(--colour-selected-pFaceOpponent, var(--colour-snakey-pFaceOpponent)); - --colour-pFaceImmortalTeammate: var(--colour-selected-pFaceImmortalTeammate, var(--colour-snakey-pFaceImmortalTeammate)); - --colour-pFaceImmortalOpponent: var(--colour-selected-pFaceImmortalOpponent, var(--colour-snakey-pFaceImmortalOpponent)); -} \ No newline at end of file diff --git a/assets/style/initial/colour/snakey.css b/assets/style/initial/colour/snakey.css deleted file mode 100644 index 7ae772c4..00000000 --- a/assets/style/initial/colour/snakey.css +++ /dev/null @@ -1,17 +0,0 @@ -:root { - --colour-snakey-tileFg: #aebde2; - --colour-snakey-tileBg: #2e3b48; - --colour-snakey-tileBd: #ffffff; - --colour-snakey-healthFg: #21352e; - --colour-snakey-healthBg: #4edf8f; - - --colour-snakey-mainFg: #e9eef3; - --colour-snakey-mainBg: #3f5e77; - - --colour-snakey-pFaceMe: #2fdef5; - --colour-snakey-pFaceTeammate: #f8df50; - --colour-snakey-pFaceImmortalTeammate: #f8df50; - - --colour-snakey-pFaceOpponent: #ec4daf; - --colour-snakey-pFaceImmortalOpponent: #e23fa3; -} \ No newline at end of file diff --git a/assets/style/initial/components/index.css b/assets/style/initial/components/index.css new file mode 100644 index 00000000..42fdb8c1 --- /dev/null +++ b/assets/style/initial/components/index.css @@ -0,0 +1 @@ +@import "./pickone.css"; \ No newline at end of file diff --git a/assets/style/initial/components/pickone.css b/assets/style/initial/components/pickone.css new file mode 100644 index 00000000..634d0260 --- /dev/null +++ b/assets/style/initial/components/pickone.css @@ -0,0 +1,12 @@ + +.sk-pick-one { + display: flex; + flex-direction: column; + align-items: stretch; + overflow-x: hidden; + overflow-y: auto; +} + +.sk-pick-one--opt { + overflow: hidden hidden; +} \ No newline at end of file diff --git a/assets/style/initial/index.css b/assets/style/initial/index.css index 3febd949..e59c47a8 100644 --- a/assets/style/initial/index.css +++ b/assets/style/initial/index.css @@ -1,5 +1,7 @@ @import "./utils.css"; -@import "./colour/index.css"; +@import "./snakey.css"; @import "./root.css"; @import "./screen.css"; +@import "./screen/index.css"; +@import "./components/index.css"; diff --git a/assets/style/initial/root.css b/assets/style/initial/root.css index 1c2c3c88..894660d7 100644 --- a/assets/style/initial/root.css +++ b/assets/style/initial/root.css @@ -19,3 +19,12 @@ body { table { border-spacing: 0; } +button { + font: inherit; + color: inherit; + padding: 0px; + background-color: var(--colour-tileBg); +} +button:disabled { + opacity: 0.7; +} diff --git a/assets/style/initial/screen.css b/assets/style/initial/screen.css index 9070e8a1..63eca8f8 100644 --- a/assets/style/initial/screen.css +++ b/assets/style/initial/screen.css @@ -1,13 +1,41 @@ -.screen { - /* Make sure that each display - starts its own stacking context. */ - z-index: 0; - position: fixed; +#all-screens-container { + display: contents; +} + +.sk-screen { + /* Make sure that each display starts its own + stacking context. Note that as a result of using + the contain property on this class and possibly + on some of its children, the visibility property + will not be sufficient to hide inactive screens. */ + contain: strict; + position: absolute; top: 0; right: 0; bottom: 0; left: 0; + overflow: auto; +} +.sk-screen:not([data-current]) { + display: none; } + +/* Special instances of non-navigated screens that +are explicitly marked up in the HTML. The background +is part of the markup- not generated by javascript, +so it will always appear even if the initial script +takes a long time to arrive. */ #background { - z-index: -1000; + will-change: transform; + z-index: -10000; + display: initial; background-color: var(--colour-mainBg); } +#screen-tint { + pointer-events: none; + z-index: 10000; + visibility: visible; + opacity: 0.0; + transition-property: opacity, background-color; + /* transition-duration will be assigned programmatically. */ + transition-timing-function: ease-in-out; +} diff --git a/assets/style/initial/screen/__play.css b/assets/style/initial/screen/__play.css new file mode 100644 index 00000000..9edb3ad3 --- /dev/null +++ b/assets/style/initial/screen/__play.css @@ -0,0 +1,33 @@ + +.screen-play { + display: grid; + grid-template: + "controls main scoreboard" 1fr / + minmax( 5em, 1fr) + minmax(24em, auto) + minmax(12em, 1fr); +} +.screen-play--grid-container { + grid-area: main; + flex-flow: column nowrap; + min-width: max-content; +} +.screen-play--controls-bar { + grid-area: controls; + display: flex; + flex-direction: column; + align-items: stretch; +} +.screen-play--scoreboard-bar { + grid-area: scoreboard; +} +@media screen and (max-aspect-ratio: 1/1) { + .screen-play { + grid-template: + "main" 1fr + "controls scoreboard" 1fr / + minmax( 5em, 1fr) + minmax(24em, auto) + minmax(12em, 1fr); + } +} diff --git a/assets/style/initial/screen/home.css b/assets/style/initial/screen/home.css new file mode 100644 index 00000000..a36d48e2 --- /dev/null +++ b/assets/style/initial/screen/home.css @@ -0,0 +1,62 @@ + +.screen-home { + display: flex; +} + +.screen-home--nav { + --spacing: 6px; + --border-radius: 15px; + height: 30em; max-height: 80vmin; + width: 30em; max-width: 80vmin; + display: grid; + grid-template: + "pofl pofl pofl ponl" 3fr + "tuto tuto tuto . " 1fr + "colr colr colr . " 1fr + "repo bugs . . " 1fr + / 1fr 1fr 1fr 3fr ; + background-color: var(--colour-mainBg); + padding: var(--spacing); + border-radius: calc( + var(--border-radius) + (2.0 * var(--spacing)) + ); +} +.screen-home--nav > * { + position: relative; + overflow: hidden; + border-radius: var(--border-radius); + margin: var(--spacing); + border: 0px double var(--colour-tileBg); + color: var(--colour-mainFg); + background-color: var(--colour-tileBg); + transition: all 0.15s ease-in; +} +/* Note: The javascript focuses these on pointer entry. */ +.screen-home--nav > :focus { + outline: none; + border-color: var(--colour-tileBd); + box-shadow: inset 0px 0px 20px #00000040; + text-shadow: 0px 0px 8px #000000ff; + animation: 0.4s ease-in-out -0.5s infinite alternate nav-button-focus; +} +.screen-home--nav:focus-within > :not(:focus) { + filter: brightness(0.9); + opacity: 0.6; +} +.screen-home--nav--play-offline { grid-area: pofl; } +.screen-home--nav--play-online { grid-area: ponl; } +.screen-home--nav--tutorial { grid-area: tuto; } +.screen-home--nav--colour-scheme { grid-area: colr; } +.screen-home--nav--goto-repo { grid-area: repo; } +.screen-home--nav--report-issue { grid-area: bugs; } + +@keyframes nav-button-focus { + from { + margin: calc(var(--spacing) / 3.0); + border-width: calc(1.2 * var(--spacing)); + } + to { + margin: calc(var(--spacing) * 2.0 / 3.0); + border-width: calc(1.4 * var(--spacing)); + } +} diff --git a/assets/style/initial/screen/index.css b/assets/style/initial/screen/index.css new file mode 100644 index 00000000..0360d24c --- /dev/null +++ b/assets/style/initial/screen/index.css @@ -0,0 +1,2 @@ +@import "./home.css"; +@import "./__play.css"; \ No newline at end of file diff --git a/assets/style/initial/snakey.css b/assets/style/initial/snakey.css new file mode 100644 index 00000000..1447fd1e --- /dev/null +++ b/assets/style/initial/snakey.css @@ -0,0 +1,14 @@ +[data-sk-colour-scheme="snakey"] { + --colour-mainFg: #e9eef3; + --colour-mainBg: #3f5e77; + --colour-tileFg: #e9eef3; + --colour-tileBg: #2e3b48; + --colour-tileBd: #ffffff; + --colour-healthFg: #21352e; + --colour-healthBg: #4edf8f; + --colour-pFaceMe: #35e7ff; + --colour-pFaceTeammate: #f8df50; + --colour-pFaceImtlTeammate: #f8df50; + --colour-pFaceOpponent: #ec4daf; + --colour-pFaceImtlOpponent: #e23fa3; +} \ No newline at end of file diff --git a/assets/style/initial/utils.css b/assets/style/initial/utils.css index 2d381880..e929ba75 100644 --- a/assets/style/initial/utils.css +++ b/assets/style/initial/utils.css @@ -13,9 +13,9 @@ } .center-contents { - display: inline-flex; - align-items: center; - justify-content: center; + display: inline-flex; + justify-content: center; + align-items: center; } .stack-contents { position: relative; diff --git a/dev-guide.md b/dev-guide.md index 878b2609..18c4cf96 100644 --- a/dev-guide.md +++ b/dev-guide.md @@ -8,6 +8,7 @@ Update the `version` field in `package.json`. ```shell git switch gh-pages git checkout dev -- . +# delete old artifacts of folder moving. export NODE_ENV='production' echo 'y' | rm -r dist/* ./scripts/pack.sh -t @@ -20,14 +21,16 @@ git add -u # Force git tracking of dynamic chunks. git commit git push +npm publish --dry-run # Check included files. npm publish ``` ```shell # Now let's go back to development: -git switch - +git switch dev export NODE_ENV='development' -echo 'y' | rm -r dist/* +echo 'y' | rm -r dist/{client,server} +npx tsc -b --force ./scripts/pack.sh -t ``` @@ -37,6 +40,10 @@ TODO.build Should we be maintaining some sort of release notes / making annotate This is to cover more abstract practices than rules that are covered by linting. +### Markdown + +Use single-underscore enclosures to italicize. Use double-asterisk enclosures to embolden. + ### ES6 #private Fields Methodology: Use #private fields for fields that back accessors- Ie. Fields that need to be internally reassigned, but should never be directly reassigned externally. If such a field does not have a get-accessor (because it doesn't need one, leave it- do not switch it to use hard privacy. diff --git a/dist/1.index.css b/dist/1.index.css deleted file mode 100644 index 5ed8477c..00000000 --- a/dist/1.index.css +++ /dev/null @@ -1 +0,0 @@ -.game-grid,.game-grid-impl-body{z-index:0}.player__spotlight-long{z-index:20}.tile__seq{z-index:12}.player__face,.tile[data-health] .tile__char{z-index:11}.player__spotlight-short{z-index:10}.game-grid-kbd-dc{z-index:1}#game-grid-container{height:100vh;flex-flow:column nowrap}.game-grid-impl-body{position:static;padding:4px;border:5px double var(--colour-tileBd);border-radius:15px;color:var(--colour-tileFg);overflow:hidden hidden}.game-grid-kbd-dc{pointer-events:none;top:0;left:0;bottom:0;right:0;color:var(--colour-tileBd);text-shadow:0 0 .5em #000;visibility:hidden}.game-grid-impl-body:focus{outline:none}.game-grid-impl-body:not(:focus){filter:brightness(70%)}.game-grid-impl-body:not(:focus)~.game-grid-kbd-dc{visibility:visible}.tile{pointer-events:none}.tile *{height:100%;width:100%}.tile__pointer-hitbox{pointer-events:auto;top:40%;left:40%}.player__face,.tile__char,.tile__seq{box-sizing:border-box;border:.14em solid transparent;border-radius:16%}.tile__char{background-color:var(--colour-tileBg);overflow:hidden hidden;transition-property:color,background-color;transition-duration:.5s;transition-timing-function:ease-in}.tile[data-health] .tile__char{border-color:var(--colour-tileBd);color:var(--colour-healthFg);background-color:var(--colour-healthBg)}.tile__seq{padding:0 .4ch;min-height:100%;height:max-content;min-width:100%;width:max-content;color:#fff;background-color:rgba(0,0,0,.5);border-color:#fff;visibility:hidden}.tile__pointer-hitbox:hover~.tile__seq{visibility:visible}.tile__pointer-hitbox:hover~.tile__char{color:transparent}.player__face{border-color:var(--colour-tileBd)}.player__downed-overlay{background-color:red;opacity:.2}.player__spotlight-short{background-image:radial-gradient(farthest-side,transparent 30%,var(--colour-mainBg));padding:1100%;border:100vmax solid var(--colour-mainBg)}.player__spotlight-long{background-image:radial-gradient(farthest-side,transparent 15%,var(--colour-mainBg));padding:1700%;border:100vmax solid var(--colour-mainBg);opacity:.92}.player[data-downed=no]>.player__downed-overlay{visibility:hidden}.player[data-face=me]>.player__face{background-color:var(--colour-pFaceMe)}.player[data-face=teammate]>.player__face{background-color:var(--colour-pFaceTeammate)}.player[data-face=opponent]>.player__face{background-color:var(--colour-pFaceOpponent)}.player[data-face=teammate][data-downed=team]>.player__face{background-color:var(--colour-pFaceImmortalTeammate)}.player[data-face=opponent][data-downed=team]>.player__face{background-color:var(--colour-pFaceImmortalOpponent)}.game-grid[data-coord-sys=EUCLID2] .game-grid-impl-body{--track-size:minmax(27px,1fr);display:grid;grid-template-columns:repeat(var(--euclid2-grid-width),var(--track-size));grid-auto-rows:var(--track-size)}.game-grid[data-coord-sys=EUCLID2] .tile{margin:5%}.game-grid[data-coord-sys=BEEHIVE] .tile,.game-grid[data-coord-sys=BEEHIVE] .tile>*{--nothing:"yet"} \ No newline at end of file diff --git a/dist/1/index.js b/dist/1/index.js deleted file mode 100644 index 70ab50cc..00000000 --- a/dist/1/index.js +++ /dev/null @@ -1,2 +0,0 @@ -(window.webpackJsonpsnakey3=window.webpackJsonpsnakey3||[]).push([[1],{4:function(e,t,s){},5:function(e,t,s){"use strict";s.r(t),s.d(t,"game",(function(){return Ce}));var r,i,a=s(1),n=s(0);!function(e){let t,s,r,i;!function(e){e.SERVER="SERVER",e.ONLINE="ONLINE",e.OFFLINE="OFFLINE"}(t=e.Type||(e.Type={})),function(e){e.EVENT_NAME="game-create"}(s=e.CtorArgs||(e.CtorArgs={})),function(e){e.EVENT_NAME="game-reset"}(r=e.Serialization||(e.Serialization={})),function(e){e.PLAYING="PLAYING",e.PAUSED="PAUSED",e.OVER="OVER"}(i=e.Status||(e.Status={})),e.K=Object.freeze({HEALTH_UPDATE_CHANCE:.1,PCT_MOVES_THAT_ARE_BOOST:.05,HEALTH_EFFECT_FOR_DOWNED_PLAYER:.6,EVENT_RECORD_WRAPPING_BUFFER_LENGTH:128,EVENT_RECORD_FORWARD_WINDOW_LENGTH:64})}(r||(r={})),Object.freeze(r),function(e){let t;!function(e){e.EUCLID2="EUCLID2",e.BEEHIVE="BEEHIVE"}(t=e.System||(e.System={}));class s{constructor(e){}}e.Abstract=s,function(t){class s extends e.Abstract{}t.Mathy=s}(s=e.Abstract||(e.Abstract={})),Object.freeze(s),Object.freeze(s.prototype)}(i||(i={})),Object.freeze(i);var o,h,c,l,d=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},u=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class m{constructor(e){o.set(this,void 0),h.set(this,void 0),c.set(this,void 0),l.set(this,void 0),this.coord=e,d(this,o,n.b.Id.NULL)}reset(){this.evictOccupant(),this.lastKnownUpdateId=0,this.freeHealth=0,this.setLangCharSeqPair(n.a.CharSeqPair.NULL)}visualBell(){}__setOccupant(e,t){d(this,o,e)}get isOccupied(){return this.occupantId!==n.b.Id.NULL}evictOccupant(){d(this,o,n.b.Id.NULL)}get occupantId(){return u(this,o)}get freeHealth(){return u(this,h)}set freeHealth(e){d(this,h,e)}setLangCharSeqPair(e){d(this,c,e.char),d(this,l,e.seq)}get langChar(){return u(this,c)}get langSeq(){return u(this,l)}}o=new WeakMap,h=new WeakMap,c=new WeakMap,l=new WeakMap,Object.freeze(m),Object.freeze(m.prototype);var p,g,f,E,y,w=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},T=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class C extends m{constructor(e){super(e),p.set(this,void 0);{const e=document.createElement("div");e.classList.add(a.a.Tile.Class.BASE,a.a.General.Class.CENTER_CONTENTS,a.a.General.Class.STACK_CONTENTS),w(this,p,e)}{const e=document.createElement("div");e.classList.add(a.a.Tile.Class.POINTER_HB),T(this,p).appendChild(e)}{const e=document.createElement("div");e.classList.add(a.a.Tile.Class.LANG_CHAR),T(this,p).appendChild(e),this.langCharElem=e}{const e=document.createElement("div");e.classList.add(a.a.Tile.Class.LANG_SEQ),T(this,p).appendChild(e),this.langSeqElem=e}}__addToDom(e){e.appendChild(T(this,p))}__setOccupant(e,t){super.__setOccupant(e,t),T(this,p).insertBefore(t.playerElem,this.langCharElem),this.langSeqElem.innerText=t.username}visualBell(){T(this,p)}evictOccupant(){super.evictOccupant(),this.langSeqElem.innerText=this.langSeq}set freeHealth(e){super.freeHealth=e,this.freeHealth>0?T(this,p).dataset[a.a.Tile.Dataset.HEALTH]=this.freeHealth.toString():delete T(this,p).dataset[a.a.Tile.Dataset.HEALTH]}get freeHealth(){return super.freeHealth}setLangCharSeqPair(e){super.setLangCharSeqPair(e),this.langCharElem.innerText=this.langChar,this.langSeqElem.innerText=this.langSeq}}p=new WeakMap,Object.freeze(C),Object.freeze(C.prototype),function(e){e.getImplementation=t=>e.__Constructors[t]}(g||(g={}));class v{constructor(e,t,s){this.sequence=t,this.characters=s,this.parent=e,this.children=[]}static CREATE_TREE_MAP(e){const t=new Map;for(const s in e){const r=e[s].seq,i=new _(s,e[s].weight),a=t.get(r);a?a.push(i):t.set(r,[i])}const s=new v.Root;return Array.from(t).sort((e,t)=>e[0].length-t[0].length).forEach(e=>{s.addCharMapping(...e)}),s.finalize(),s}finalize(){this.validateConstruction(),Object.freeze(this.characters),Object.freeze(this.children),this.children.forEach(e=>e.finalize())}validateConstruction(){if(!this.sequence.startsWith(this.parent.sequence))throw new Error("Child node's sequence must start with that of its parent.")}reset(){this.children.forEach(e=>e.reset()),this.inheritingHitCount=0,this.inheritingWeightedHitCount=0,this.characters.forEach(e=>{e.reset();for(let t=0;t<10*Math.random();t++)this.incrementNumHits(e)})}addCharMapping(e,t){if(!n.a.Seq.REGEXP.test(e))throw new RangeError(`Mapping-sequence "${e}" did not match the required regular expression "${n.a.Seq.REGEXP.source}".`);if(0===t.length)throw new Error("Must not make mapping without written characters.");let s=this;{let t=this;for(;t;)s=t,t=t.children.find(t=>e.startsWith(t.sequence))}if(s.sequence===e)throw new Error(`Mappings for all written-characters with a commoncorresponding typeable-sequence should be registered together,but an existing mapping for the sequence "${e}" was found.`);s.children.push(new v(s,e,t))}chooseOnePair(e){const t=this.characters.slice(0).sort(_.CMP[e]).shift(),s={char:t.char,seq:this.sequence};return this.incrementNumHits(t),s}incrementNumHits(e){e.incrementNumHits(),this.__recursiveIncrementNumHits(e.weightInv)}__recursiveIncrementNumHits(e){this.inheritingHitCount+=1,this.inheritingWeightedHitCount+=e,this.children.forEach(t=>t.__recursiveIncrementNumHits(e))}get personalHitCount(){return this.inheritingHitCount-this.parent.inheritingHitCount}get averageCharHitCount(){return this.characters.reduce((e,t)=>e+t.hitCount,0)/this.characters.length}get personalWeightedHitCount(){return this.inheritingWeightedHitCount-this.parent.inheritingWeightedHitCount}andNonRootParents(){const e=[];let t=this;for(;t.parent;)e.push(t),t=t.parent;return e}getLeafNodes(){const e=[];return this.__recursiveGetLeafNodes(e),e}__recursiveGetLeafNodes(e){this.children.length?this.children.forEach(t=>{t.__recursiveGetLeafNodes(e)}):e.push(this)}simpleView(){let e=this.characters.map(e=>e.simpleView());return Object.assign(Object.create(null),{seq:this.sequence,chars:1===e.length?e[0]:e,hits:this.personalHitCount,kids:this.children.map(e=>e.simpleView()),__proto__:void 0})}}v.LEAF_CMP=Object.freeze({[n.a.BalancingScheme.SEQ]:(e,t)=>e.inheritingHitCount-t.inheritingHitCount,[n.a.BalancingScheme.CHAR]:(e,t)=>e.inheritingHitCount-t.inheritingHitCount,[n.a.BalancingScheme.WEIGHT]:(e,t)=>e.inheritingWeightedHitCount-t.inheritingWeightedHitCount}),v.PATH_CMP=Object.freeze({[n.a.BalancingScheme.SEQ]:(e,t)=>e.personalHitCount-t.personalHitCount,[n.a.BalancingScheme.CHAR]:(e,t)=>e.averageCharHitCount-t.averageCharHitCount,[n.a.BalancingScheme.WEIGHT]:(e,t)=>e.personalWeightedHitCount-t.personalWeightedHitCount}),(f=v||(v={})).Root=class extends f{constructor(){super(void 0,"",[])}validateConstruction(){}chooseOnePair(e){throw new TypeError("Must never hit on the root.")}get personalHitCount(){throw new TypeError("Must never hit on the root.")}get personalWeightedHitCount(){throw new TypeError("Must never hit on the root.")}andNonRootParents(){throw new TypeError}simpleView(){return this.children.map(e=>e.simpleView())}},Object.freeze(v),Object.freeze(v.prototype);class _{constructor(e,t){if(t<=0)throw new RangeError(`All weights must be positive, but we were passed the value "${t}" for the character "${e}".`);this.char=e,this.weightInv=1/t}reset(){this.hitCount=0,this.weightedHitCount=0}incrementNumHits(){this.hitCount+=1,this.weightedHitCount+=this.weightInv}simpleView(){return Object.assign(Object.create(null),{char:this.char,hits:this.hitCount})}}_.CMP=Object.freeze({[n.a.BalancingScheme.SEQ]:(e,t)=>e.hitCount-t.hitCount,[n.a.BalancingScheme.CHAR]:(e,t)=>e.hitCount-t.hitCount,[n.a.BalancingScheme.WEIGHT]:(e,t)=>e.weightedHitCount-t.weightedHitCount}),Object.freeze(_),Object.freeze(_.prototype);class b extends n.a{constructor(e,t){super(),this.static=e,this.treeMap=v.CREATE_TREE_MAP(t),this.leafNodes=this.treeMap.getLeafNodes()}get numLeaves(){return this.leafNodes.length}reset(){this.treeMap.reset()}getNonConflictingChar(e,t){this.leafNodes.sort(v.LEAF_CMP[t]);let s=void 0;for(const r of this.leafNodes){const i=r.andNonRootParents();for(let t=0;te.startsWith(i[t].sequence));if(s){s===i[t].sequence?i.splice(0):i.splice(t);break}}if(i.length){i.sort(v.PATH_CMP[t]),s=i[0];break}}if(!s)throw new Error("Invariants guaranteeing that a LangSeq canalways be shuffled-in were not met.");return s.chooseOnePair(t)}simpleView(){return Object.assign(Object.create(null),{name:this.static.getName(),desc:this.static.getBlurb(),root:this.treeMap.simpleView()})}}b||(b={}),Object.freeze(b),Object.freeze(b.prototype),function(e){e.EVENT_ID_REJECT=-1}(E||(E={})),Object.freeze(E),function(e){e.INITIAL_REQUEST_ID=-1,e.EVENT_NAME=Object.freeze({Bubble:"player-bubble",Movement:"player-movement"});class t{constructor(e,t){this.eventId=E.EVENT_ID_REJECT,this.affectedNeighbours=void 0,this.playerId=e,this.playerLastAcceptedRequestId=t}}e.Bubble=t;e.Movement=class extends t{constructor(e,t,s,r){super(e,t),this.newPlayerHealth=void 0,this.tilesWithHealthUpdates=void 0,this.dest={coord:s.coord,lastKnownUpdateId:s.lastKnownUpdateId,newCharSeqPair:void 0,newFreeHealth:void 0},this.moveType=r}}}(y||(y={})),Object.freeze(y);class I{constructor(e){this.source=e}at(...e){return this.source.__getTileAt(...e)}destsFrom(...e){return new O(this.source.__getTileDestsFrom(...e))}sourcesTo(...e){return new O(this.source.__getTileSourcesTo(...e))}}Object.freeze(I),Object.freeze(I.prototype);class O{constructor(e){this.contents=e}get occupied(){return this.contents=this.contents.filter(e=>e.isOccupied),this}get unoccupied(){return this.contents=this.contents.filter(e=>!e.isOccupied),this}get get(){return this.contents}}Object.freeze(O),Object.freeze(O.prototype);var S,N=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},A=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class R extends n.b{constructor(e,t){if(super(),S.set(this,void 0),Math.trunc(t.playerId)!==t.playerId)throw new RangeError("Player ID's must be integer values.");this.playerId=t.playerId,this.game=e,this.status=new this.game.__playerStatusCtor(this,t.noCheckGameOver),this.tile=new I(new R.TileGetterSource(this))}__afterAllPlayersConstruction(){this.status.__afterAllPlayersConstruction()}reset(e){N(this,S,e),this.hostTile.__setOccupant(this.playerId,this.status.immigrantInfo)}get coord(){return this.hostTile.coord}get hostTile(){return A(this,S)}onGoBesideOtherPlayer(){}moveTo(e){if(this.hostTile.occupantId!==this.playerId){if(this.game.gameType!==r.Type.ONLINE)throw new Error("Linkage between player and occupied tile disagrees.")}else this.hostTile.evictOccupant();if(e.isOccupied){if(this.game.gameType!==r.Type.ONLINE)throw new Error("Only one player can occupy a tile at a time.")}else N(this,S,e),e.__setOccupant(this.playerId,this.status.immigrantInfo)}}S=new WeakMap,function(e){class t{constructor(e){this.player=e}__getTileAt(){return this.player.game.grid.tile.at(this.player.coord)}__getTileDestsFrom(){return this.player.game.grid.tile.destsFrom(this.player.coord).get}__getTileSourcesTo(){return this.player.game.grid.tile.sourcesTo(this.player.coord).get}}e.TileGetterSource=t,Object.freeze(t),Object.freeze(t.prototype)}(R||(R={})),Object.freeze(R),Object.freeze(R.prototype);var M,H=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},L=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class P{constructor(e,t){if(M.set(this,void 0),0===t.length)throw new Error("teams must have at least one member.");this.id=e,this.members=t,H(this,M,this.members.every(e=>e.status.noCheckGameOver)?P.ElimOrder.IMMORTAL:P.ElimOrder.STANDING)}reset(){this.elimOrder!==P.ElimOrder.IMMORTAL&&(this.elimOrder=P.ElimOrder.STANDING)}get elimOrder(){return L(this,M)}set elimOrder(e){if(this.elimOrder===P.ElimOrder.IMMORTAL)throw new Error("Cannot change the elimination status of an immortal team.");H(this,M,e)}}M=new WeakMap,function(e){let t;!function(e){e.IMMORTAL=-1,e.STANDING=0}(t=e.ElimOrder||(e.ElimOrder={}))}(P||(P={})),Object.freeze(P),Object.freeze(P.prototype);var q,x,G=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)},D=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s};class z{constructor(e,t){q.set(this,void 0),x.set(this,void 0),this.player=e,this.noCheckGameOver=t}reset(){this.score=0,this.health=0}__afterAllPlayersConstruction(){}get immigrantInfo(){}get score(){return G(this,q)}set score(e){D(this,q,e)}get health(){return G(this,x)}set health(e){const t=this.isDowned;if(D(this,x,e),t||!this.isDowned||this.noCheckGameOver)return;const s=this.player.team,r=this.player.game.teams;if(s.elimOrder===P.ElimOrder.STANDING&&s.members.every(e=>e.status.noCheckGameOver||e.status.isDowned)){const e=1+r.filter(e=>e.elimOrder!==P.ElimOrder.STANDING).length;s.elimOrder=1+r.filter(e=>e.elimOrder!==P.ElimOrder.STANDING&&e.elimOrder!==P.ElimOrder.IMMORTAL).length,e===r.length&&this.player.game.statusBecomeOver()}}get isDowned(){return this.health<0}}q=new WeakMap,x=new WeakMap,Object.freeze(z),Object.freeze(z.prototype);class j extends R{constructor(e,t){if(super(e,t),!j.Username.REGEXP.test(t.username))throw new RangeError(`Username "${t.username}" does not match the required regular expression, "${j.Username.REGEXP.source}".`);this.familyId=t.familyId,this.teamId=t.teamId,this.username=t.username}reset(e){super.reset(e),this.status.reset(),this.lastAcceptedRequestId=y.INITIAL_REQUEST_ID,this.requestInFlight=!1}__abstractNotifyThatGameStatusBecamePlaying(){}__abstractNotifyThatGameStatusBecamePaused(){}__abstractNotifyThatGameStatusBecameOver(){}makeMovementRequest(e,t){if(this.game.status!==r.Status.PLAYING)throw new Error("This is not a necessary precondition, but we're doing it anyway.");if(this.requestInFlight)throw new Error("Only one request should ever be in flight at a time.");this.requestInFlight=!0,this.game.processMoveRequest(new y.Movement(this.playerId,this.lastAcceptedRequestId,e,t))}get team(){return this.game.teams[this.teamId]}isTeamedWith(e){return this.team.members.includes(e)}}!function(e){let t,s;!function(e){e.REGEXP=/[a-zA-Z](?:[ ]?[a-zA-Z0-9:-]+?){4,}/}(t=e.Username||(e.Username={})),function(e){e.finalize=e=>{const t=Array.from(new Set(e.map(e=>e.teamId))).sort((e,t)=>e-t).reduce((e,t,s)=>(e[t]=s,e),[]);return e.slice().sort((e,s)=>t[e.teamId]-t[s.teamId]).map((e,s)=>({playerId:s,familyId:e.familyId,teamId:t[e.teamId],socketId:e.socketId,username:e.username,noCheckGameOver:e.noCheckGameOver,familyArgs:e.familyArgs}))}}(s=e.CtorArgs||(e.CtorArgs={})),Object.freeze(s)}(j||(j={})),Object.freeze(j),Object.freeze(j.prototype);var B,F=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},k=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class U extends j{constructor(e,t){super(e,t),B.set(this,void 0),this.langRemappingFunc=b.RemappingFunctions[this.game.langName]}reset(e){super.reset(e),this.prevCoord=e.coord,F(this,B,"")}processKeyboardInput(e){this.game.status===r.Status.PLAYING&&(this.requestInFlight||(32===e.keyCode?this.coord.equals(this.prevCoord)||this.makeMovementRequest(this.game.grid.getUntAwayFrom(this.coord,this.prevCoord),j.MoveType.BOOST):1===e.key.length&&this.seqBufferAcceptKey(e.key)))}seqBufferAcceptKey(e){const t=this.tile.destsFrom().unoccupied.get;if(0!==t.length)if(e){if(e=this.langRemappingFunc(e),b.Seq.REGEXP.test(e)){for(let s=this.seqBuffer+e;s.length;s=s.substring(1)){const e=t.find(e=>e.langSeq.startsWith(s));if(e)return F(this,B,s),void(e.langSeq===s&&this.makeMovementRequest(e,j.MoveType.NORMAL))}F(this,B,""),this.hostTile.visualBell()}}else{t.find(e=>e.langSeq.startsWith(this.seqBuffer))||F(this,B,"")}}moveTo(e){F(this,B,""),this.prevCoord=this.coord,super.moveTo(e)}get seqBuffer(){return k(this,B)}}B=new WeakMap,Object.freeze(U),Object.freeze(U.prototype);var W,V,K,Y=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},$=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class X extends z{constructor(e,t){super(e,t),W.set(this,void 0);{const e=document.createElement("div");e.classList.add(a.a.Player.Class.BASE,a.a.General.Class.CENTER_CONTENTS,a.a.General.Class.STACK_CONTENTS),Y(this,W,e)}{const e=document.createElement("div");e.classList.add(a.a.Player.Class.FACE),$(this,W).appendChild(e)}{const e=document.createElement("div");e.classList.add(a.a.Player.Class.DOWNED_OVERLAY),$(this,W).appendChild(e)}}__afterAllPlayersConstruction(){const e=this.player,t=this.player.game.operator;if(!t)throw new Error("this never happens. see comment in source.");if($(this,W).dataset[a.a.Player.Dataset.FACE_SWATCH]=e===t?"me":e.teamId===t.teamId?"teammate":"opponent",e===t){const e=document.createElement("div");e.classList.add(a.a.Player.Class.SHORT_SPOTLIGHT),$(this,W).appendChild(e);const t=document.createElement("div");t.classList.add(a.a.Player.Class.LONG_SPOTLIGHT),$(this,W).appendChild(t)}this.__immigrantInfoCache=Object.freeze({playerElem:$(this,W),username:e.username})}get immigrantInfo(){return this.__immigrantInfoCache}get score(){return super.score}set score(e){super.score=e}get health(){return super.health}set health(e){const t=this.isDowned;super.health=e,t!==this.isDowned&&($(this,W)[a.a.Player.Dataset.DOWNED]=this.isDowned?this.player.team.elimOrder?"team":"self":"no")}}W=new WeakMap,Object.freeze(X),Object.freeze(X.prototype);class J extends j{constructor(e,t){if(super(e,t),e.gameType===r.Type.ONLINE)throw new TypeError("OnlineGames should be using regular Players instead.")}__abstractNotifyThatGameStatusBecamePlaying(){this.movementContinueWithInitialDelay()}__abstractNotifyThatGameStatusBecamePaused(){this.game.cancelTimeout(this.scheduledMovementCallbackId),this.scheduledMovementCallbackId=void 0}__abstractNotifyThatGameStatusBecameOver(){this.game.cancelTimeout(this.scheduledMovementCallbackId),this.scheduledMovementCallbackId=void 0}movementContinue(){this.makeMovementRequest(this.game.grid.getUntToward(this.coord,this.computeDesiredDestination()),this.getNextMoveType()),this.movementContinueWithInitialDelay()}movementContinueWithInitialDelay(){this.scheduledMovementCallbackId=this.game.setTimeout(()=>this.movementContinue(),this.computeNextMovementTimer())}}(V=J||(J={})).of=(e,t)=>new V.__Constructors[t.familyId](e,t),function(e){class t extends b{constructor(){super(t,Object.entries(r).reduce((e,t)=>{const s=t[0],r=t[0],i=t[1];return e[s]={seq:r,weight:i},e},{}))}static getName(){return b.Names.ENGLISH__LOWERCASE}static getBlurb(){return""}static getInstance(){return this.SINGLETON||(this.SINGLETON=new t),this.SINGLETON}}t.SINGLETON=void 0,e.Lowercase=t,Object.seal(t),Object.freeze(t.prototype);class s extends b{constructor(){let e={};const t=t=>{e=Object.entries(r).reduce((e,s)=>{const r=t(s[0]),i=t(s[0]),a=s[1];return e[r]={seq:i,weight:a},e},e)};t(e=>e.toLowerCase()),t(e=>e.toUpperCase()),super(s,e)}static getName(){return b.Names.ENGLISH__MIXEDCASE}static getBlurb(){return""}static getInstance(){return this.SINGLETON||(this.SINGLETON=new s),this.SINGLETON}}s.SINGLETON=void 0,e.MixedCase=s,Object.seal(s),Object.freeze(s.prototype);const r=Object.freeze({a:8.167,b:1.492,c:2.202,d:4.253,e:12.702,f:2.228,g:2.015,h:6.094,i:6.966,j:.153,k:1.292,l:4.025,m:2.406,n:6.749,o:7.507,p:1.929,q:.095,r:5.987,s:6.327,t:9.356,u:2.758,v:.978,w:2.56,x:.15,y:1.994,z:.077})}(K||(K={})),Object.seal(K);var Q,Z=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},ee=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class te{constructor(e,t,s){Q.set(this,void 0),this.gameType=e;const r=this.__getGridImplementation(s.coordSys);this.grid=new r({gridClass:r,tileClass:t.tileClass,coordSys:s.coordSys,dimensions:s.gridDimensions,domParentHtmlIdHook:s.gridHtmlIdHook||"n/a"}),this.langName=s.languageName,this.__playerStatusCtor=t.playerStatusCtor,this.players=this.createPlayers(s),void 0!==s.operatorIndex&&(this.operator=this.players[s.operatorIndex]);const i=[];if(this.players.forEach(e=>{i[e.teamId]||(i[e.teamId]=[]),i[e.teamId].push(e)}),this.teams=i.map((e,t)=>new P(t,e)),this.players.forEach(e=>e.__afterAllPlayersConstruction()),this.teams.every(e=>e.id===P.ElimOrder.IMMORTAL))throw new Error("All teams are immortal. The game will never end.")}reset(){this.grid.reset(),Z(this,Q,r.Status.PAUSED)}createPlayers(e){return(e.playerDescs=this.gameType===r.Type.ONLINE?e.playerDescs:j.CtorArgs.finalize(e.playerDescs)).map((t,s)=>t.familyId===j.Family.HUMAN?s===e.operatorIndex?this.__createOperatorPlayer(t):new j(this,t):this.__createArtifPlayer(t))}serializeResetState(){const e=[],t=this.players.map(e=>e.coord),s=[];return this.grid.forEachTile(t=>{e.push({char:t.langChar,seq:t.langSeq}),t.freeHealth&&s.push({coord:t.coord,health:t.freeHealth})}),{csps:e,playerCoords:t,healthCoords:s}}deserializeResetState(e){{let t=0;this.grid.forEachTile(s=>{s.setLangCharSeqPair(e.csps[t++]),s.lastKnownUpdateId=1})}e.playerCoords.forEach((e,t)=>{this.players[t].moveTo(this.grid.tile.at(e))}),e.healthCoords.forEach(e=>{this.grid.tile.at(e.coord).freeHealth=e.health})}get status(){return ee(this,Q)}statusBecomePlaying(){if(this.status!==r.Status.PAUSED)throw new Error("Can only resume a game that is currently paused.");this.players.forEach(e=>{e.__abstractNotifyThatGameStatusBecamePlaying()}),this.__abstractStatusBecomePlaying(),Z(this,Q,r.Status.PLAYING),this.grid.baseElem&&this.grid.baseElem.focus()}statusBecomePaused(){if(this.status!==r.Status.PLAYING)throw new Error("Can only pause a game that is currently playing.");this.players.forEach(e=>{e.__abstractNotifyThatGameStatusBecamePaused()}),this.__abstractStatusBecomePaused(),Z(this,Q,r.Status.PAUSED)}statusBecomeOver(){if(this.status!==r.Status.PLAYING)throw new Error("Can only end a game that is currently playing.");this.players.forEach(e=>{e.__abstractNotifyThatGameStatusBecameOver()}),this.__abstractStatusBecomeOver(),Z(this,Q,r.Status.OVER),console.log("game is over!")}__abstractStatusBecomePlaying(){}__abstractStatusBecomePaused(){}__abstractStatusBecomeOver(){}}Q=new WeakMap,Object.freeze(te),Object.freeze(te.prototype);var se,re=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},ie=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class ae extends te{constructor(e,t,s){super(e,t,s),se.set(this,void 0),this.eventRecordBitmap=[]}reset(){super.reset(),this.eventRecordBitmap.fill(!1,0,r.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH),re(this,se,0)}get nextUnusedEventId(){return ie(this,se)}recordEvent(e){const t=e.eventId,s=t%r.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH;if(t===E.EVENT_ID_REJECT)throw new TypeError("Do not try to record events for rejected requests.");if(t<0||t!==Math.trunc(t))throw new RangeError("Event ID's must only be assigned positive, integer values.");if(this.eventRecordBitmap[s])throw new Error("Event ID's must be assigned unique values.");this.eventRecordBitmap[s]=!0,this.eventRecordBitmap[(t+r.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH-r.K.EVENT_RECORD_FORWARD_WINDOW_LENGTH)%r.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH]=!1,re(this,se,+ie(this,se)+1)}executeTileModEvent(e,t=!0){const s=this.grid.tile.at(e.coord);s.lastKnownUpdateId{this.executeTileModEvent(e)}),i>1){if(s===this.operator)throw new Error("This never happens. See comment in source.")}else{if(s.requestInFlight=!1,!(s===this.operator?1===i:i<=1))throw new Error("This never happens. See comment in source");s.status.score=e.newPlayerHealth.score,s.status.health=e.newPlayerHealth.health,s.moveTo(r),s.lastAcceptedRequestId=e.playerLastAcceptedRequestId}else 0===i&&(s.requestInFlight=!1)}processBubbleExecute(e){this.players[e.playerId].requestInFlight=!1,e.eventId!==E.EVENT_ID_REJECT&&this.recordEvent(e)}}se=new WeakMap,Object.freeze(ae),Object.freeze(ae.prototype);var ne,oe,he,ce,le,de=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},ue=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class me extends ae{constructor(e,t,s){super(e,t,s),ne.set(this,void 0),oe.set(this,void 0),this.averageFreeHealth=s.averageFreeHealthPerTile*this.grid.area,this.averageFreeHealthPerTile=s.averageFreeHealthPerTile,de(this,oe,new Set),this.lang=K.Lowercase.getInstance();const r=this.grid.static.getAmbiguityThreshold();if(this.lang.numLeaves{e.setLangCharSeqPair(this.dryRunShuffleLangCharSeqAt(e))}),this.teams.forEach(e=>e.reset());const e=this.grid.static.getSpawnCoords(this.teams.map(e=>e.members.length),this.grid.dimensions);this.teams.forEach((t,s)=>{t.members.forEach((t,r)=>{t.reset(this.grid.tile.at(e[s][r]))})})}dryRunShuffleLangCharSeqAt(e){e.setLangCharSeqPair(b.CharSeqPair.NULL);const t=Array.from(new Set(this.grid.tile.sourcesTo(e.coord).get.flatMap(e=>this.grid.tile.destsFrom(e.coord).get)));return this.lang.getNonConflictingChar(t.map(e=>e.langSeq).filter(e=>e),this.langBalancingScheme)}get currentFreeHealth(){return ue(this,ne)}get freeHealthTiles(){return ue(this,oe)}dryRunSpawnFreeHealth(e){let t=this.averageFreeHealth-this.currentFreeHealth;if(t<=0)return;const s=[];for(;t>0;){let i;do{i=this.grid.tile.at(this.grid.getRandomCoord())}while(i.isOccupied||s.find(e=>i.coord.equals(e.coord)));const a=1;if(Math.random()i.coord.equals(e.coord)))?t.newFreeHealth=(t.newFreeHealth||0)+a:s.push({coord:i.coord,lastKnownUpdateId:1+i.lastKnownUpdateId,newCharSeqPair:void 0,newFreeHealth:i.freeHealth+a})}t-=a}return s}getHealthCostOfBoost(){return this.averageFreeHealthPerTile/r.K.PCT_MOVES_THAT_ARE_BOOST}executeTileModEvent(e,t=!0){const s=this.grid.tile.at(e.coord);if(e.lastKnownUpdateId!==1+s.lastKnownUpdateId)throw new Error("this never happens. see comment in source.");de(this,ne,ue(this,ne)+(e.newFreeHealth-s.freeHealth)),0===e.newFreeHealth?ue(this,oe).delete(s):ue(this,oe).add(s),super.executeTileModEvent(e,t)}managerCheckGamePlayingRequest(e){if(this.status!==r.Status.PLAYING)return;const t=this.players[e.playerId];if(!t)throw new Error("No such player exists.");if(e.playerLastAcceptedRequestId!==t.lastAcceptedRequestId)throw new RangeError(e.playerLastAcceptedRequestId(this.operator.processKeyboardInput(e),32!==e.keyCode||(e.preventDefault(),!1)))}__createOperatorPlayer(e){return new U(this,e)}__createArtifPlayer(e){return J.of(this,e)}setTimeout(e,t,...s){return setTimeout(e,t,s)}cancelTimeout(e){clearTimeout(e)}}Object.freeze(pe),Object.freeze(pe.prototype);class ge{constructor(e){this.static=e.gridClass,this.dimensions=e.dimensions,this.tile=new I(this)}get area(){return this.static.getArea(this.dimensions)}reset(){this.forEachTile(e=>e.reset())}check(){}getRandomCoord(){return this.static.getRandomCoord(this.dimensions)}__VisibleGrid_super(e,t){const s=a.a.Grid;t.tabIndex=0,t.classList.add(s.Class.IMPL_BODY);const r=document.getElementById(e.domParentHtmlIdHook);if(!r)throw new RangeError(`The ID "${e.domParentHtmlIdHook}" did not refer to an existing html element.`);r.dataset[s.Dataset.COORD_SYS]=e.coordSys,r.classList.add(s.Class.GRID,a.a.General.Class.TEXT_SELECT_DISABLED,a.a.General.Class.CENTER_CONTENTS,a.a.General.Class.STACK_CONTENTS),r.querySelectorAll("."+s.Class.IMPL_BODY).forEach(e=>e.remove()),r.insertAdjacentElement("afterbegin",t),this.baseElem=t;if(!r.querySelector(":scope ."+s.Class.KBD_DC_BASE)){const e=document.createElement("div");e.classList.add(s.Class.KBD_DC_BASE,a.a.General.Class.CENTER_CONTENTS);{const t=document.createElement("div");t.classList.add(s.Class.KBD_DC_ICON),t.innerText="(click grid to continue typing)",e.appendChild(t)}r.appendChild(e)}}}(he=ge||(ge={})).getImplementation=e=>he.__Constructors[e],function(e){class t extends i.Abstract.Mathy{constructor(e){super(e),this.x=e.x,this.y=e.y,Object.freeze(this)}equals(e){return this.x===e.x&&this.y===e.y}round(){return new t({x:Math.round(this.x),y:Math.round(this.y)})}oneNorm(e){return this.sub(e).originOneNorm()}originOneNorm(){return Math.abs(this.x)+Math.abs(this.y)}infNorm(e){return this.sub(e).originInfNorm()}originInfNorm(){return Math.max(Math.abs(this.x),Math.abs(this.y))}axialAlignment(e){return this.sub(e).originAxialAlignment()}originAxialAlignment(){return Math.abs(Math.abs(this.x)-Math.abs(this.y))/(Math.abs(this.x)+Math.abs(this.y))}add(e){return new t({x:this.x+e.x,y:this.y+e.y})}sub(e){return new t({x:this.x-e.x,y:this.y-e.y})}mul(e){return new t({x:e*this.x,y:e*this.y})}}e.Coord=t,Object.freeze(t),Object.freeze(t.prototype);class s extends ge{constructor(e){super(e);const s=[];for(let r=0;rs.forEach(t=>{e(t)},t),t)}getUntToward(e,t){const s=this.tile.destsFrom(e).unoccupied.get;if(0===s.length)return this.tile.at(e);if(1===s.length)return s[0];s.sort((e,s)=>e.coord.oneNorm(t)-s.coord.oneNorm(t)).sort((e,s)=>e.coord.infNorm(t)-s.coord.infNorm(t));for(let e=1;es[0].coord.infNorm(t)){s.splice(e);break}if(1===s.length)return s[0];if(s[0].coord.x-e.x==0||s[0].coord.y-e.y==0){if(e.axialAlignment(e.sub(t))-.5>0)return s[0];s.shift()}return s[Math.floor(s.length*Math.random())]}getUntAwayFrom(e,t){return this.getUntToward(e,e.add(e.sub(t)))}getRandomCoordAround(e,s){return new t({x:e.x+Math.trunc(2*s*(Math.random()-.5)),y:e.y+Math.trunc(2*s*(Math.random()-.5))})}__getTileAt(e){if(e.x<0||e.x>=this.dimensions.width||e.y<0||e.y>=this.dimensions.height)throw new RangeError("Out of bounds. No such tile exists.");return this.grid[e.y][e.x]}__getTileDestsFrom(e,t=1){let s=e.y-t,r=e.y+t+1,i=e.x-t,a=e.x+t+1;return s>=this.dimensions.height||r<0||i>=this.dimensions.width||a<0?[]:this.grid.slice(Math.max(0,s),Math.min(this.dimensions.height,r)).flatMap(e=>e.slice(Math.max(0,i),Math.min(this.dimensions.width,a)))}__getTileSourcesTo(e,t=1){return this.__getTileDestsFrom(e,t)}minMovesFromTo(e,t){return Math.min(Math.abs(t.x-e.x),Math.abs(t.y-e.y))}static getSpawnCoords(e,t){const r=[];return e.map(e=>{const i=[];for(;e>0;){let a;do{a=s.getRandomCoord(t)}while(r.find(e=>a.equals(e)));i.push(a),r.push(a),e--}return i})}static getArea(e){return e.height*e.width}static getRandomCoord(e){const s=Math.floor(e.width*Math.random()),r=Math.floor(e.height*Math.random());return new t({x:s,y:r})}}s.SIZE_LIMITS=Object.freeze({height:Object.freeze({min:11,max:51}),width:Object.freeze({min:11,max:51})}),e.Grid=s,function(e){e.Visible=class extends e{constructor(e){super(e);const t=document.createElement("div");t.style.setProperty("--euclid2-grid-width",this.dimensions.width.toString());for(const e of this.grid)for(const s of e)s.__addToDom(t);this.__VisibleGrid_super(e,t)}}}(s=e.Grid||(e.Grid={})),Object.freeze(s),Object.freeze(s.prototype)}(ce||(ce={})),Object.freeze(ce),function(e){class t extends i.Abstract.Mathy{constructor(e){super(e),this.dash=e.dash,this.bash=e.bash,Object.freeze(this)}equals(e){return this.dash===e.dash&&this.bash===e.bash}round(){const e=Math.floor(this.dash),s=Math.floor(this.bash),r=e-this.dash,i=s-this.bash;return r>2*i?new t({dash:e+1,bash:s}):r<.5*i?new t({dash:e,bash:s+1}):Math.min(r,i)>.5?new t({dash:e+1,bash:s+1}):new t({dash:e,bash:s})}add(e){return new t({dash:this.dash+e.dash,bash:this.bash+e.bash})}sub(e){return new t({dash:this.dash-e.dash,bash:this.bash-e.bash})}mul(e){return new t({dash:e*this.dash,bash:e*this.bash})}}e.Coord=t,Object.freeze(t),Object.freeze(t.prototype);class s extends ge{constructor(e){super(e)}static getAmbiguityThreshold(){return 18}static getSizeLimits(){return this.SIZE_LIMITS}forEachTile(e,t=this){this.grid.forEach(s=>s.forEach(t=>{e(t)},t),t)}getUntToward(e,t){}getUntAwayFrom(e,t){return this.getUntToward(e,e.add(e.sub(t)))}getRandomCoordAround(e,t){}__getTileAt(e){}__getTileDestsFrom(e){}__getTileSourcesTo(e){}minMovesFromTo(e,t){}static getSpawnCoords(e,t){}static getArea(e){const t=Math.min(e.fslash,e.bslash),s=Math.max(e.fslash,e.bslash),r=-1+e.dash+t;let i=2*t*(e.dash+r);return i+=(s-t-1)*r,i}static getRandomCoord(e){return new t(void 0)}}s.SIZE_LIMITS=Object.freeze({dash:Object.freeze({min:10,max:50}),bslash:Object.freeze({min:10,max:50}),fslash:Object.freeze({min:10,max:50})}),e.Grid=s,function(e){e.Visible=class extends e{constructor(e){super(e);this.__VisibleGrid_super(e,void 0)}}}(s=e.Grid||(e.Grid={})),Object.freeze(s),Object.freeze(s.prototype)}(le||(le={})),Object.freeze(le);var fe,Ee,ye=function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},we=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class Te extends J{constructor(e,t){super(e,t),fe.set(this,void 0),this.behaviour=Object.freeze(t.familyArgs),this.grid=this.game.grid}__afterAllPlayersConstruction(){super.__afterAllPlayersConstruction(),this.threatProximity=this.game.teams.filter(e=>e.id!==this.teamId).flatMap(e=>e.members),this.targetProximity=this.threatProximity.slice()}reset(e){super.reset(e),ye(this,fe,this.coord)}moveTo(e){ye(this,fe,this.coord),super.moveTo(e)}computeDesiredDestination(){this.threatProximity.sort((e,t)=>this.grid.minMovesFromTo(e.coord,this.coord)-this.grid.minMovesFromTo(t.coord,this.coord));for(const e of this.threatProximity){if(this.grid.minMovesFromTo(e.coord,this.coord)>this.behaviour.fearDistance)break;if(!e.status.isDowned&&e.status.health>this.status.health)return this.grid.getUntAwayFrom(this.coord,e.coord).coord}if(this.targetProximity.sort((e,t)=>this.grid.minMovesFromTo(this.coord,e.coord)-this.grid.minMovesFromTo(this.coord,t.coord)),this.status.isDowned)for(const e of this.targetProximity){if(this.grid.minMovesFromTo(this.coord,e.coord)>this.behaviour.bloodThirstDistance)break;if(e.status.health.game-grid-kbd-dc,.game-grid[data-game-state=paused]>.game-grid-pause-overlay{visibility:visible}}.tile{pointer-events:none;contain:size}.tile *{height:100%;width:100%}.tile__pointer-hitbox{pointer-events:auto;contain:strict;top:40%;left:40%}.player__face,.tile__char,.tile__seq{box-sizing:border-box;border:.14em solid transparent;border-radius:.35em}.tile__char{background-color:var(--colour-tileBg);overflow:hidden hidden}.tile[data-health] .tile__char{border-color:var(--colour-tileBd);color:var(--colour-healthFg);background-color:var(--colour-healthBg);transition-property:color,background-color;transition-duration:.5s;transition-timing-function:ease-in}.tile__seq{padding:0 .4ch;min-height:100%;height:max-content;min-width:100%;width:max-content;color:#fff;background-color:rgba(0,0,0,.5);border-color:#fff}.tile__pointer-hitbox:hover~.player>.player__face,.tile__seq{visibility:hidden}.tile__pointer-hitbox:hover~.tile__char{color:transparent}.tile__pointer-hitbox:hover~.tile__seq{visibility:visible}.player{--colour-spotlight:var(--colour-mainBg);contain:size}.player__face{border-color:var(--colour-tileBd);contain:strict}.player[data-face=me]>.player__face,.player__spotlight-long,.player__spotlight-short{will-change:transform}.player__downed-overlay{background-color:red;opacity:.2}.player[data-downed=no] .player__downed-overlay{visibility:hidden}@keyframes anim-frames-player__on-move{0%{transform:scale(1.1)}37%{transform:scale(1.3)}}.player__spotlight-short{background-image:radial-gradient(farthest-side,transparent 30%,var(--colour-spotlight));padding:1100%;border:100vmax solid var(--colour-spotlight)}.player__spotlight-long{background-image:radial-gradient(farthest-side,transparent 15%,var(--colour-spotlight));padding:1700%;border:100vmax solid var(--colour-spotlight);opacity:.92}@media print{.player__spotlight-long,.player__spotlight-short{visibility:hidden}}.player[data-face=me]>.player__face{background-color:var(--colour-pFaceMe)}.player[data-face=teammate]>.player__face{background-color:var(--colour-pFaceTeammate)}.player[data-face=opponent]>.player__face{background-color:var(--colour-pFaceOpponent)}.player[data-face=teammate][data-downed=team]>.player__face{background-color:var(--colour-pFaceImtlTeammate)}.player[data-face=opponent][data-downed=team]>.player__face{background-color:var(--colour-pFaceImtlOpponent)}.game-grid-impl-body[data-coord-sys=EUCLID2]{--track-size:minmax(1.67em,1fr);display:grid;grid-template-columns:repeat(var(--euclid2-grid-width),var(--track-size));grid-auto-rows:var(--track-size)}.game-grid-impl-body[data-coord-sys=EUCLID2] .tile{margin:5%} \ No newline at end of file diff --git a/dist/client/chunk/game/offline.js b/dist/client/chunk/game/offline.js new file mode 100644 index 00000000..4aca162d --- /dev/null +++ b/dist/client/chunk/game/offline.js @@ -0,0 +1,2 @@ +(window.webpackJsonpsnakey3=window.webpackJsonpsnakey3||[]).push([[0],{13:function(e,t,r){"use strict";var s;r.r(t),r.d(t,"OfflineGame",(function(){return Te})),function(e){let t,r,s,i;!function(e){e.SERVER="SERVER",e.ONLINE="ONLINE",e.OFFLINE="OFFLINE"}(t=e.Type||(e.Type={})),function(e){e.EVENT_NAME="game-create"}(r=e.CtorArgs||(e.CtorArgs={})),function(e){e.EVENT_NAME="game-reset"}(s=e.Serialization||(e.Serialization={})),function(e){e.PLAYING="PLAYING",e.PAUSED="PAUSED",e.OVER="OVER"}(i=e.Status||(e.Status={})),e.K=Object.freeze({HEALTH_UPDATE_CHANCE:.1,PCT_MOVES_THAT_ARE_BOOST:.05,HEALTH_EFFECT_FOR_DOWNED_PLAYER:.6,EVENT_RECORD_WRAPPING_BUFFER_LENGTH:128,EVENT_RECORD_FORWARD_WINDOW_LENGTH:64})}(s||(s={})),Object.freeze(s);var i,a=r(0),n=r(1);!function(e){let t;!function(e){e.EUCLID2="EUCLID2",e.BEEHIVE="BEEHIVE"}(t=e.System||(e.System={}));class r{constructor(e){}}e.Abstract=r,function(t){class r extends e.Abstract{}t.Mathy=r}(r=e.Abstract||(e.Abstract={})),Object.freeze(r),Object.freeze(r.prototype)}(i||(i={})),Object.freeze(i);var o,h,c,l,d=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},u=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class m{constructor(e){o.set(this,void 0),h.set(this,void 0),c.set(this,void 0),l.set(this,void 0),this.coord=e,d(this,o,n.b.Id.NULL)}reset(){this.evictOccupant(),this.lastKnownUpdateId=0,this.freeHealth=0,this.setLangCharSeqPair(n.a.CharSeqPair.NULL)}__setOccupant(e,t){d(this,o,e)}get isOccupied(){return this.occupantId!==n.b.Id.NULL}evictOccupant(){d(this,o,n.b.Id.NULL)}get occupantId(){return u(this,o)}get freeHealth(){return u(this,h)}set freeHealth(e){d(this,h,e)}setLangCharSeqPair(e){d(this,c,e.char),d(this,l,e.seq)}get langChar(){return u(this,c)}get langSeq(){return u(this,l)}}o=new WeakMap,h=new WeakMap,c=new WeakMap,l=new WeakMap,Object.freeze(m),Object.freeze(m.prototype);var p,f,g=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},E=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class y extends m{constructor(e){super(e),p.set(this,void 0);{const e=g(this,p,document.createElement("div"));e.classList.add(a.a.General.Class.CENTER_CONTENTS,a.a.General.Class.STACK_CONTENTS,a.a.Tile.Class.BASE),e.setAttribute("aria-label","Tile")}{const e=document.createElement("div");e.classList.add(a.a.Tile.Class.POINTER_HB),e.setAttribute("aria-hidden","true"),E(this,p).appendChild(e)}{const e=this.langCharElem=document.createElement("div");e.classList.add(a.a.Tile.Class.LANG_CHAR),E(this,p).appendChild(e)}{const e=this.langSeqElem=document.createElement("div");e.classList.add(a.a.Tile.Class.LANG_SEQ),e.setAttribute("role","tooltip"),E(this,p).appendChild(e)}}__addToDom(e){e.appendChild(E(this,p))}__setOccupant(e,t){super.__setOccupant(e,t),E(this,p).insertBefore(t.playerElem,this.langCharElem),this.langSeqElem.innerText=t.username}evictOccupant(){super.evictOccupant(),this.langSeqElem.innerText=this.langSeq}set freeHealth(e){super.freeHealth=e,this.freeHealth>0?E(this,p).dataset[a.a.Tile.Dataset.HEALTH]=this.freeHealth.toString():delete E(this,p).dataset[a.a.Tile.Dataset.HEALTH]}get freeHealth(){return super.freeHealth}setLangCharSeqPair(e){super.setLangCharSeqPair(e),this.langCharElem.innerText=this.langChar,this.langSeqElem.innerText=this.langSeq}}p=new WeakMap,Object.freeze(y),Object.freeze(y.prototype),function(e){e.getImplementation=t=>e.__Constructors[t]}(f||(f={}));class w{__VisibleGrid_super(e,t){const r=a.a.Grid;t.classList.add(r.Class.IMPL_BODY),t.dataset[r.Dataset.IMPL_COORD_SYS]=e.coordSys,this.baseElem=t;const s=document.createElement("div");s.classList.add(a.a.Player.Class.SHORT_SPOTLIGHT);const i=document.createElement("div");i.classList.add(a.a.Player.Class.LONG_SPOTLIGHT),this.spotlightElems=Object.freeze([s,i])}}Object.freeze(w),Object.freeze(w.prototype);var v,T,b=r(5);!function(e){e.EVENT_ID_REJECT=-1}(v||(v={})),Object.freeze(v),function(e){e.INITIAL_REQUEST_ID=-1,e.EVENT_NAME=Object.freeze({Bubble:"player-bubble",Movement:"player-movement"});class t{constructor(e,t){this.eventId=v.EVENT_ID_REJECT,this.affectedNeighbours=void 0,this.playerId=e,this.playerLastAcceptedRequestId=t}}e.Bubble=t;e.Movement=class extends t{constructor(e,t,r,s){super(e,t),this.newPlayerHealth=void 0,this.tileHealthModDescs=void 0,this.destModDesc={coord:r.coord,lastKnownUpdateId:r.lastKnownUpdateId,newCharSeqPair:void 0,newFreeHealth:void 0},this.moveType=s}}}(T||(T={})),Object.freeze(T);class O{constructor(e){this.source=e}at(...e){return this.source.__getTileAt(...e)}destsFrom(...e){return new _(this.source.__getTileDestsFrom(...e))}sourcesTo(...e){return new _(this.source.__getTileSourcesTo(...e))}}Object.freeze(O),Object.freeze(O.prototype);class _{constructor(e){this.contents=e}get occupied(){return this.contents=this.contents.filter(e=>e.isOccupied),this}get unoccupied(){return this.contents=this.contents.filter(e=>!e.isOccupied),this}get get(){return this.contents}}Object.freeze(_),Object.freeze(_.prototype);var C,I=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},A=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class S extends n.b{constructor(e,t){if(super(),C.set(this,void 0),Math.trunc(t.playerId)!==t.playerId)throw new RangeError("Player ID's must be integer values.");this.playerId=t.playerId,this.isALocalOperator=t.isALocalOperator,this.game=e,this.status=new this.game.__playerStatusCtor(this,t.noCheckGameOver),this.tile=new O(new S.TileGetterSource(this))}__afterAllPlayersConstruction(){this.status.__afterAllPlayersConstruction()}reset(e){I(this,C,e),this.hostTile.__setOccupant(this.playerId,this.status.immigrantInfo)}get coord(){return this.hostTile.coord}get hostTile(){return A(this,C)}onGoBesideOtherPlayer(){}moveTo(e){if(this.hostTile.occupantId!==this.playerId){if(this.game.gameType!==s.Type.ONLINE)throw new Error("Linkage between player and occupied tile disagrees.")}else this.hostTile.evictOccupant();if(e.isOccupied){if(this.game.gameType!==s.Type.ONLINE)throw new Error("Only one player can occupy a tile at a time.")}else I(this,C,e),e.__setOccupant(this.playerId,this.status.immigrantInfo)}}C=new WeakMap,function(e){class t{constructor(e){this.player=e}__getTileAt(){return this.player.game.grid.tile.at(this.player.coord)}__getTileDestsFrom(){return this.player.game.grid.tile.destsFrom(this.player.coord).get}__getTileSourcesTo(){return this.player.game.grid.tile.sourcesTo(this.player.coord).get}}e.TileGetterSource=t,Object.freeze(t),Object.freeze(t.prototype)}(S||(S={})),Object.freeze(S),Object.freeze(S.prototype);var M,N=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},P=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class R{constructor(e,t){if(M.set(this,void 0),0===t.length)throw new Error("teams must have at least one member.");this.id=e,this.members=t,N(this,M,this.members.every(e=>e.status.noCheckGameOver)?R.ElimOrder.IMMORTAL:R.ElimOrder.STANDING)}reset(){this.elimOrder!==R.ElimOrder.IMMORTAL&&(this.elimOrder=R.ElimOrder.STANDING)}get elimOrder(){return P(this,M)}set elimOrder(e){if(this.elimOrder===R.ElimOrder.IMMORTAL)throw new Error("Cannot change the elimination status of an immortal team.");N(this,M,e)}}M=new WeakMap,function(e){let t;!function(e){e.IMMORTAL=-1,e.STANDING=0}(t=e.ElimOrder||(e.ElimOrder={}))}(R||(R={})),Object.freeze(R),Object.freeze(R.prototype);var H,L=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)},z=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r};class j{constructor(e,t){H.set(this,void 0),this.player=e,this.noCheckGameOver=t}reset(){this.health=0}__afterAllPlayersConstruction(){}get immigrantInfo(){}get health(){return L(this,H)}set health(e){const t=this.isDowned;if(z(this,H,e),t||!this.isDowned||this.noCheckGameOver)return;const r=this.player.team,s=this.player.game.teams;if(r.elimOrder===R.ElimOrder.STANDING&&r.members.every(e=>e.status.noCheckGameOver||e.status.isDowned)){const e=1+s.filter(e=>e.elimOrder!==R.ElimOrder.STANDING).length;r.elimOrder=1+s.filter(e=>e.elimOrder!==R.ElimOrder.STANDING&&e.elimOrder!==R.ElimOrder.IMMORTAL).length,e===s.length&&this.player.game.statusBecomeOver()}}get isDowned(){return this.health<0}}H=new WeakMap,Object.freeze(j),Object.freeze(j.prototype);class q extends S{constructor(e,t){if(super(e,t),!q.Username.REGEXP.test(t.username))throw new RangeError(`Username "${t.username}" does not match the required regular expression, "${q.Username.REGEXP.source}".`);this.familyId=t.familyId,this.teamId=t.teamId,this.username=t.username}reset(e){super.reset(e),this.status.reset(),this.lastAcceptedRequestId=T.INITIAL_REQUEST_ID,this.requestInFlight=!1}__abstractNotifyThatGameStatusBecamePlaying(){}__abstractNotifyThatGameStatusBecamePaused(){}__abstractNotifyThatGameStatusBecameOver(){}makeMovementRequest(e,t){if(this.game.status!==s.Status.PLAYING)throw new Error("This is not a necessary precondition, but we're doing it anyway.");if(this.requestInFlight)throw new Error("Only one request should ever be in flight at a time.");this.requestInFlight=!0,this.game.processMoveRequest(new T.Movement(this.playerId,this.lastAcceptedRequestId,e,t))}get team(){return this.game.teams[this.teamId]}isTeamedWith(e){return this.team.members.includes(e)}}!function(e){let t,r;!function(e){e.REGEXP=/[a-zA-Z](?:[ ]?[a-zA-Z0-9:-]+?){4,}/}(t=e.Username||(e.Username={})),function(e){e.finalize=e=>{const t=Array.from(new Set(e.map(e=>e.teamId))).sort((e,t)=>e-t).reduce((e,t,r)=>(e[t]=r,e),[]);return e.slice().sort((e,r)=>t[e.teamId]-t[r.teamId]).map((e,r)=>Object.assign(e,{playerId:r,teamId:t[e.teamId]}))}}(r=e.CtorArgs||(e.CtorArgs={})),Object.freeze(r)}(q||(q={})),Object.freeze(q),Object.freeze(q.prototype);var D,x=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},F=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class G extends q{constructor(e,t){super(e,t),D.set(this,void 0),this.langRemappingFunc=this.game.langFrontend.remapFunc}reset(e){super.reset(e),this.prevCoord=e.coord,x(this,D,"")}processKeyboardInput(e){this.game.status===s.Status.PLAYING&&(this.requestInFlight||(32===e.keyCode?this.coord.equals(this.prevCoord)||this.makeMovementRequest(this.game.grid.getUntAwayFrom(this.coord,this.prevCoord),q.MoveType.BOOST):1===e.key.length&&this.seqBufferAcceptKey(e.key)))}seqBufferAcceptKey(e){const t=this.tile.destsFrom().unoccupied.get;if(0!==t.length)if(e){if(e=this.langRemappingFunc(e),b.a.Seq.REGEXP.test(e)){for(let r=this.seqBuffer+e;r.length;r=r.substring(1)){const e=t.find(e=>e.langSeq.startsWith(r));if(e)return x(this,D,r),void(e.langSeq===r&&this.makeMovementRequest(e,q.MoveType.NORMAL))}x(this,D,""),this.status.visualBell()}}else{t.find(e=>e.langSeq.startsWith(this.seqBuffer))||x(this,D,"")}}moveTo(e){x(this,D,""),this.prevCoord=this.coord,super.moveTo(e)}__abstractNotifyBecomeCurrent(){this.status.__notifyBecomeCurrent(this.game.grid.spotlightElems)}get seqBuffer(){return F(this,D)}}D=new WeakMap,Object.freeze(G),Object.freeze(G.prototype);var B,U,W,k=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},V=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class K extends j{constructor(e,t){super(e,t),B.set(this,void 0),U.set(this,void 0);k(this,B,document.createElement("div")).classList.add(a.a.General.Class.CENTER_CONTENTS,a.a.General.Class.STACK_CONTENTS,a.a.Player.Class.BASE);{const e=document.createElement("div");e.classList.add(a.a.Player.Class.FACE);k(this,U,this.player.isALocalOperator?[e.animate({filter:["brightness(0.7)","brightness(1.0)"]},{duration:300,easing:"ease-in"}),e.animate({transform:K.makeWiggleAnimation(10,2)},{duration:270,easing:"ease-out"})]:[]);{const t=document.createElement("div");t.classList.add(a.a.Player.Class.DOWNED_OVERLAY),e.appendChild(t)}V(this,B).appendChild(e)}}__afterAllPlayersConstruction(){const e=this.player,t=this.player.game.operators[0].teamId;V(this,B).dataset[a.a.Player.Dataset.FACE_SWATCH]=e.isALocalOperator?"me":e.teamId===t?"teammate":"opponent",this.__immigrantInfoCache=Object.freeze({playerElem:V(this,B),username:e.username})}reset(){super.reset();const e=a.a.Player.Dataset.DOWNED;V(this,B).dataset[e.KEY]=e.VALUES.NO}get immigrantInfo(){return this.__immigrantInfoCache}__notifyBecomeCurrent(e){e.forEach(e=>{V(this,B).appendChild(e)})}visualBell(){window.requestAnimationFrame(e=>{V(this,U).forEach(e=>e.play())})}get health(){return super.health}set health(e){const t=this.isDowned;if(super.health=e,t!==this.isDowned){const e=a.a.Player.Dataset.DOWNED;V(this,B).dataset[e.KEY]=this.isDowned?this.player.team.elimOrder?e.VALUES.TEAM:e.VALUES.SELF:e.VALUES.NO}}}B=new WeakMap,U=new WeakMap,(K||(K={})).makeWiggleAnimation=function(e,t){const r=Array(2*t).fill(e);return r.unshift(0),r.push(0),r.map((e,t)=>`translate(${t%2?e:-e}%)`)},Object.freeze(K),Object.freeze(K.prototype);class Y extends q{constructor(e,t){if(super(e,t),e.gameType===s.Type.ONLINE)throw new TypeError("OnlineGames should be using regular Players instead.")}__abstractNotifyThatGameStatusBecamePlaying(){this.movementContinueWithInitialDelay()}__abstractNotifyThatGameStatusBecamePaused(){this.game.cancelTimeout(this.scheduledMovementCallbackId),this.scheduledMovementCallbackId=void 0}__abstractNotifyThatGameStatusBecameOver(){this.game.cancelTimeout(this.scheduledMovementCallbackId),this.scheduledMovementCallbackId=void 0}movementContinue(){this.makeMovementRequest(this.game.grid.getUntToward(this.coord,this.computeDesiredDestination()),this.getNextMoveType()),this.movementContinueWithInitialDelay()}movementContinueWithInitialDelay(){this.scheduledMovementCallbackId=this.game.setTimeout(()=>this.movementContinue(),this.computeNextMovementTimer())}}(W=Y||(Y={})).of=(e,t)=>{const r=t.familyId;return new W.__Constructors[r](e,t)};var $,J,Q=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},X=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class Z{constructor(e,t,r){$.set(this,void 0),J.set(this,void 0),this.gameType=e;const s=this.__getGridImplementation(r.coordSys);if(this.grid=new s({gridClass:s,tileClass:t.tileClass,coordSys:r.coordSys,dimensions:r.gridDimensions}),this.langFrontend=n.a.GET_FRONTEND_DESC_BY_ID(r.langId),this.__playerStatusCtor=t.playerStatusCtor,this.players=this.createPlayers(r),this.operators=this.players.filter(e=>e.isALocalOperator),this.currentOperator=this.operators[0],this.operators.some(e=>e.teamId!==this.operators[0].teamId))throw new Error("All local operators must be on the same team.");{const e=[];if(this.players.forEach(t=>{e[t.teamId]||(e[t.teamId]=[]),e[t.teamId].push(t)}),this.teams=e.map((e,t)=>new R(t,e)),this.teams.every(e=>e.id===R.ElimOrder.IMMORTAL))throw new Error("All teams are immortal. The game will never end.")}this.players.forEach(e=>e.__afterAllPlayersConstruction())}reset(){return this.grid.reset(),Q(this,J,s.Status.PAUSED),Promise.resolve()}createPlayers(e){const t=e.playerDescs=this.gameType===s.Type.ONLINE?e.playerDescs:q.CtorArgs.finalize(e.playerDescs);return Object.freeze(t.map(e=>e.familyId===q.Family.HUMAN?e.isALocalOperator?this.__createOperatorPlayer(e):new q(this,e):this.__createArtifPlayer(e)))}serializeResetState(){const e=[],t=this.players.map(e=>e.coord),r=[];return this.grid.forEachTile(t=>{e.push({char:t.langChar,seq:t.langSeq}),t.freeHealth&&r.push({coord:t.coord,health:t.freeHealth})}),{csps:e,playerCoords:t,healthCoords:r}}deserializeResetState(e){{let t=0;this.grid.forEachTile(r=>{r.setLangCharSeqPair(e.csps[t++]),r.lastKnownUpdateId=1})}e.playerCoords.forEach((e,t)=>{this.players[t].moveTo(this.grid.tile.at(e))}),e.healthCoords.forEach(e=>{this.grid.tile.at(e.coord).freeHealth=e.health})}get currentOperator(){return X(this,$)}set currentOperator(e){e&&this.currentOperator!==e&&this.operators.includes(e)&&(Q(this,$,e),e.__abstractNotifyBecomeCurrent())}get status(){return X(this,J)}statusBecomePlaying(){if(this.status!==s.Status.PLAYING){if(this.status!==s.Status.PAUSED)throw new Error("Can only resume a game that is currently paused.");this.players.forEach(e=>{e.__abstractNotifyThatGameStatusBecamePlaying()}),this.__abstractStatusBecomePlaying(),Q(this,J,s.Status.PLAYING)}else console.log("[statusBecomePlaying]: Game is already playing")}statusBecomePaused(){if(this.status!==s.Status.PAUSED){if(this.status!==s.Status.PLAYING)throw new Error("Can only pause a game that is currently playing.");this.players.forEach(e=>{e.__abstractNotifyThatGameStatusBecamePaused()}),this.__abstractStatusBecomePaused(),Q(this,J,s.Status.PAUSED)}else console.log("[statusBecomePaused]: Game is already paused")}statusBecomeOver(){if(this.status!==s.Status.PLAYING)throw new Error("Can only end a game that is currently playing.");this.players.forEach(e=>{e.__abstractNotifyThatGameStatusBecameOver()}),this.__abstractStatusBecomeOver(),Q(this,J,s.Status.OVER),console.log("game is over!")}__abstractStatusBecomePlaying(){}__abstractStatusBecomePaused(){}__abstractStatusBecomeOver(){}}$=new WeakMap,J=new WeakMap,Object.freeze(Z),Object.freeze(Z.prototype);var ee,te=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},re=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class se extends Z{constructor(e,t,r){super(e,t,r),ee.set(this,void 0),this.eventRecordBitmap=[]}reset(){const e=super.reset();return this.eventRecordBitmap.fill(!1,0,s.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH),te(this,ee,0),e}get nextUnusedEventId(){return re(this,ee)}recordEvent(e){const t=e.eventId,r=t%s.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH;if(t===v.EVENT_ID_REJECT)throw new TypeError("Do not try to record events for rejected requests.");if(t<0||t!==Math.trunc(t))throw new RangeError("Event ID's must only be assigned positive, integer values.");if(this.eventRecordBitmap[r])throw new Error("Event ID's must be assigned unique values.");this.eventRecordBitmap[r]=!0,this.eventRecordBitmap[(t+s.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH-s.K.EVENT_RECORD_FORWARD_WINDOW_LENGTH)%s.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH]=!1,te(this,ee,+re(this,ee)+1)}executeTileModEvent(e,t=!0){Object.freeze(e);const r=this.grid.tile.at(e.coord);if(r.lastKnownUpdateId>e.lastKnownUpdateId)return r;if(r.lastKnownUpdateId===e.lastKnownUpdateId)throw new Error("never.");return e.newCharSeqPair&&(r.setLangCharSeqPair(e.newCharSeqPair),t&&this.operators.filter(e=>e.tile.destsFrom().get.includes(r)).forEach(e=>e.seqBufferAcceptKey(""))),r.lastKnownUpdateId=e.lastKnownUpdateId,r.freeHealth=e.newFreeHealth,r}executePlayerMoveEvent(e){var t;const r=this.players[e.playerId],s=e.playerLastAcceptedRequestId-r.lastAcceptedRequestId;if(e.eventId===v.EVENT_ID_REJECT)return void(0===s&&(r.requestInFlight=!1));this.recordEvent(e);const i=this.executeTileModEvent(e.destModDesc,r!==this.currentOperator);if(null===(t=e.tileHealthModDescs)||void 0===t||t.forEach(e=>{this.executeTileModEvent(e)}),s>1){if(r===this.currentOperator)throw new Error("This never happens. See comment in source.")}else{if(r.requestInFlight=!1,!(r===this.currentOperator?1===s:s<=1))throw new Error("This never happens. See comment in source");r.status.health=e.newPlayerHealth.health,r.moveTo(i),r.lastAcceptedRequestId=e.playerLastAcceptedRequestId}}executePlayerBubbleEvent(e){this.players[e.playerId].requestInFlight=!1,e.eventId!==v.EVENT_ID_REJECT&&this.recordEvent(e)}}ee=new WeakMap,Object.freeze(se),Object.freeze(se.prototype);class ie{constructor(e){const t=[];for(const r of e)t[r]=new ie.Entry;this.entries=t}reset(){for(const e of this.entries)e.reset()}}!function(e){class t{constructor(){this.moveCounts={}}reset(){this.totalHealthPickedUp=0,Object.getOwnPropertyNames(n.b.MoveType).forEach(e=>{this.moveCounts[e]=0})}}e.Entry=t,Object.freeze(t),Object.freeze(t.prototype)}(ie||(ie={})),Object.freeze(ie),Object.freeze(ie.prototype);var ae,ne,oe,he,ce,le,de=function(e,t,r,s){return new(r||(r=Promise))((function(i,a){function n(e){try{h(s.next(e))}catch(e){a(e)}}function o(e){try{h(s.throw(e))}catch(e){a(e)}}function h(e){var t;e.done?i(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(n,o)}h((s=s.apply(e,t||[])).next())}))},ue=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},me=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class pe extends se{constructor(e,t,s){super(e,t,s),ae.set(this,void 0),ne.set(this,void 0),oe.set(this,void 0),this.averageFreeHealth=s.averageFreeHealthPerTile*this.grid.area,this.averageFreeHealthPerTile=s.averageFreeHealthPerTile,ue(this,ne,new Set),ue(this,oe,r(7)(`./${this.langFrontend.module}.ts`).then(e=>{this.lang=e[this.langFrontend.module][this.langFrontend.export].getInstance();const t=this.grid.static.getAmbiguityThreshold();if(this.lang.numLeavese.playerId))}reset(){const e=Object.create(null,{reset:{get:()=>super.reset}});return de(this,void 0,void 0,(function*(){yield e.reset.call(this),ue(this,ae,0),me(this,ne).clear(),yield me(this,oe),this.lang.reset(),this.grid.forEachTile(e=>{e.setLangCharSeqPair(this.dryRunShuffleLangCharSeqAt(e))}),this.teams.forEach(e=>e.reset());const t=this.grid.static.getSpawnCoords(this.teams.map(e=>e.members.length),this.grid.dimensions);return this.teams.forEach((e,r)=>{e.members.forEach((e,s)=>{e.reset(this.grid.tile.at(t[r][s]))})}),this.scoreInfo.reset(),Promise.resolve()}))}dryRunShuffleLangCharSeqAt(e){e.setLangCharSeqPair(b.a.CharSeqPair.NULL);const t=Array.from(new Set(this.grid.tile.sourcesTo(e.coord).get.flatMap(e=>this.grid.tile.destsFrom(e.coord).get)));return this.lang.getNonConflictingChar(t.map(e=>e.langSeq).filter(e=>e),this.langBalancingScheme)}get currentFreeHealth(){return me(this,ae)}get freeHealthTiles(){return me(this,ne)}dryRunSpawnFreeHealth(e){let t=this.averageFreeHealth-this.currentFreeHealth;if(t<=0)return;const r=[];for(;t>0;){let i;do{i=this.grid.tile.at(this.grid.getRandomCoord())}while(i.isOccupied||r.find(e=>i.coord.equals(e.coord)));const a=1;if(Math.random()i.coord.equals(e.coord)))?t.newFreeHealth=(t.newFreeHealth||0)+a:r.push({coord:i.coord,lastKnownUpdateId:1+i.lastKnownUpdateId,newCharSeqPair:void 0,newFreeHealth:i.freeHealth+a})}t-=a}return r}getHealthCostOfBoost(){return this.averageFreeHealthPerTile/s.K.PCT_MOVES_THAT_ARE_BOOST}executeTileModEvent(e,t=!0){Object.freeze(e);const r=this.grid.tile.at(e.coord);if(e.lastKnownUpdateId!==1+r.lastKnownUpdateId)throw new Error("this never happens. see comment in source.");return ue(this,ae,me(this,ae)+(e.newFreeHealth-r.freeHealth)),0===e.newFreeHealth?me(this,ne).delete(r):me(this,ne).add(r),super.executeTileModEvent(e,t),r}managerCheckGamePlayingRequest(e){if(this.status!==s.Status.PLAYING)return;const t=this.players[e.playerId];if(!t)throw new Error("No such player exists.");if(e.playerLastAcceptedRequestId!==t.lastAcceptedRequestId)throw new RangeError(e.playerLastAcceptedRequestIde.reset())}check(){}getRandomCoord(){return this.static.getRandomCoord(this.dimensions)}}(he=fe||(fe={})).getImplementation=e=>he.__Constructors[e],function(e){class t extends i.Abstract.Mathy{constructor(e){super(e),this.x=e.x,this.y=e.y,Object.freeze(this)}equals(e){return this.x===e.x&&this.y===e.y}round(){return new t({x:Math.round(this.x),y:Math.round(this.y)})}oneNorm(e){return this.sub(e).originOneNorm()}originOneNorm(){return Math.abs(this.x)+Math.abs(this.y)}infNorm(e){return this.sub(e).originInfNorm()}originInfNorm(){return Math.max(Math.abs(this.x),Math.abs(this.y))}axialAlignment(e){return this.sub(e).originAxialAlignment()}originAxialAlignment(){return Math.abs(Math.abs(this.x)-Math.abs(this.y))/(Math.abs(this.x)+Math.abs(this.y))}add(e){return new t({x:this.x+e.x,y:this.y+e.y})}sub(e){return new t({x:this.x-e.x,y:this.y-e.y})}mul(e){return new t({x:e*this.x,y:e*this.y})}}e.Coord=t,Object.freeze(t),Object.freeze(t.prototype);class r extends fe{constructor(e){super(e);const r=[];for(let s=0;se.coord.oneNorm(t)-r.coord.oneNorm(t)).sort((e,r)=>e.coord.infNorm(t)-r.coord.infNorm(t));for(let e=1;er[0].coord.infNorm(t)){r.splice(e);break}if(1===r.length)return r[0];if(r[0].coord.x-e.x==0||r[0].coord.y-e.y==0){if(e.axialAlignment(e.sub(t))-.5>0)return r[0];r.shift()}return r[Math.floor(r.length*Math.random())]}getUntAwayFrom(e,t){return this.getUntToward(e,e.add(e.sub(t)))}getRandomCoordAround(e,r){return new t({x:e.x+Math.trunc(2*r*(Math.random()-.5)),y:e.y+Math.trunc(2*r*(Math.random()-.5))})}__getTileAt(e){if(e.x<0||e.x>=this.dimensions.width||e.y<0||e.y>=this.dimensions.height)throw new RangeError("Out of bounds. No such tile exists.");return this.grid[e.y][e.x]}__getTileDestsFrom(e,t=1){let r=e.y-t,s=e.y+t+1,i=e.x-t,a=e.x+t+1;return r>=this.dimensions.height||s<0||i>=this.dimensions.width||a<0?[]:this.grid.slice(Math.max(0,r),Math.min(this.dimensions.height,s)).flatMap(e=>e.slice(Math.max(0,i),Math.min(this.dimensions.width,a)))}__getTileSourcesTo(e,t=1){return this.__getTileDestsFrom(e,t)}minMovesFromTo(e,t){return Math.min(Math.abs(t.x-e.x),Math.abs(t.y-e.y))}static getSpawnCoords(e,t){const s=[];return e.map(e=>{const i=[];for(;e>0;){let a;do{a=r.getRandomCoord(t)}while(s.find(e=>a.equals(e)));i.push(a),s.push(a),e--}return i})}static getArea(e){return e.height*e.width}static getRandomCoord(e){const r=Math.floor(e.width*Math.random()),s=Math.floor(e.height*Math.random());return new t({x:r,y:s})}}r.SIZE_LIMITS=Object.freeze({height:Object.freeze({min:11,max:51}),width:Object.freeze({min:11,max:51})}),e.Grid=r,function(e){class t extends e{constructor(e){super(e);const t=document.createElement("div");t.style.setProperty("--euclid2-grid-width",this.dimensions.width.toString());for(const e of this.grid)for(const r of e)r.__addToDom(t);this.__VisibleGrid_super(e,t)}}e.Visible=t,Object(n.c)(t,[w]),Object.freeze(t),Object.freeze(t.prototype)}(r=e.Grid||(e.Grid={})),Object.freeze(r),Object.freeze(r.prototype)}(ce||(ce={})),Object.freeze(ce),function(e){class t extends i.Abstract.Mathy{constructor(e){super(e),this.dash=e.dash,this.bash=e.bash,Object.freeze(this)}equals(e){return this.dash===e.dash&&this.bash===e.bash}round(){const e=Math.floor(this.dash),r=Math.floor(this.bash),s=e-this.dash,i=r-this.bash;return s>2*i?new t({dash:e+1,bash:r}):s<.5*i?new t({dash:e,bash:r+1}):Math.min(s,i)>.5?new t({dash:e+1,bash:r+1}):new t({dash:e,bash:r})}add(e){return new t({dash:this.dash+e.dash,bash:this.bash+e.bash})}sub(e){return new t({dash:this.dash-e.dash,bash:this.bash-e.bash})}mul(e){return new t({dash:e*this.dash,bash:e*this.bash})}}e.Coord=t,Object.freeze(t),Object.freeze(t.prototype);class r extends fe{constructor(e){super(e)}static getAmbiguityThreshold(){return 18}static getSizeLimits(){return this.SIZE_LIMITS}forEachTile(e,t=this){this.grid.forEach(r=>r.forEach(t=>{e(t)},t),t)}getUntToward(e,t){}getUntAwayFrom(e,t){return this.getUntToward(e,e.add(e.sub(t)))}getRandomCoordAround(e,t){}__getTileAt(e){}__getTileDestsFrom(e){}__getTileSourcesTo(e){}minMovesFromTo(e,t){}static getSpawnCoords(e,t){}static getArea(e){const t=Math.min(e.fslash,e.bslash),r=Math.max(e.fslash,e.bslash),s=-1+e.dash+t;let i=2*t*(e.dash+s);return i+=(r-t-1)*s,i}static getRandomCoord(e){return new t(void 0)}}r.SIZE_LIMITS=Object.freeze({dash:Object.freeze({min:10,max:50}),bslash:Object.freeze({min:10,max:50}),fslash:Object.freeze({min:10,max:50})}),e.Grid=r,function(e){class t extends e{constructor(e){super(e);this.__VisibleGrid_super(e,void 0)}}e.Visible=t,Object(n.c)(t,[w]),Object.freeze(t),Object.freeze(t.prototype)}(r=e.Grid||(e.Grid={})),Object.freeze(r),Object.freeze(r.prototype)}(le||(le={})),Object.freeze(le);var ge,Ee,ye=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},we=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class ve extends Y{constructor(e,t){super(e,t),ge.set(this,void 0),this.behaviour=Object.freeze(t.familyArgs),this.grid=this.game.grid}__afterAllPlayersConstruction(){super.__afterAllPlayersConstruction(),this.threatProximity=this.game.teams.filter(e=>e.id!==this.teamId).flatMap(e=>e.members),this.targetProximity=this.threatProximity.slice()}reset(e){super.reset(e),ye(this,ge,this.coord)}moveTo(e){ye(this,ge,this.coord),super.moveTo(e)}computeDesiredDestination(){this.threatProximity.sort((e,t)=>this.grid.minMovesFromTo(e.coord,this.coord)-this.grid.minMovesFromTo(t.coord,this.coord));for(const e of this.threatProximity){if(this.grid.minMovesFromTo(e.coord,this.coord)>this.behaviour.fearDistance)break;if(!e.status.isDowned&&e.status.health>this.status.health)return this.grid.getUntAwayFrom(this.coord,e.coord).coord}if(this.targetProximity.sort((e,t)=>this.grid.minMovesFromTo(this.coord,e.coord)-this.grid.minMovesFromTo(this.coord,t.coord)),this.status.isDowned)for(const e of this.targetProximity){if(this.grid.minMovesFromTo(this.coord,e.coord)>this.behaviour.bloodThirstDistance)break;if(e.status.healthe[0].length-t[0].length).forEach(e=>{r.addCharMapping(...e)}),r.finalize(),r}finalize(){this.validateConstruction(),Object.freeze(this.characters),Object.freeze(this.children),this.children.forEach(e=>e.finalize())}validateConstruction(){if(!this.sequence.startsWith(this.parent.sequence))throw new Error("Child node's sequence must start with that of its parent.")}reset(){this.children.forEach(e=>e.reset()),this.inheritingHitCount=0,this.inheritingWeightedHitCount=0,this.characters.forEach(e=>{e.reset();for(let t=0;t<10*Math.random();t++)this.incrementNumHits(e)})}addCharMapping(e,t){if(!i.a.Seq.REGEXP.test(e))throw new RangeError(`Mapping-sequence "${e}" did not match the required regular expression "${i.a.Seq.REGEXP.source}".`);if(0===t.length)throw new Error("Must not make mapping without written characters.");let r=this;{let t=this;for(;t;)r=t,t=t.children.find(t=>e.startsWith(t.sequence))}if(r.sequence===e)throw new Error(`Mappings for all written-characters with a commoncorresponding typeable-sequence should be registered together,but an existing mapping for the sequence "${e}" was found.`);r.children.push(new a(r,e,t))}chooseOnePair(e){const t=this.characters.slice(0).sort(n.CMP[e]).shift(),r={char:t.char,seq:this.sequence};return this.incrementNumHits(t),r}incrementNumHits(e){e.incrementNumHits(),this.__recursiveIncrementNumHits(e.weightInv)}__recursiveIncrementNumHits(e){this.inheritingHitCount+=1,this.inheritingWeightedHitCount+=e,this.children.forEach(t=>t.__recursiveIncrementNumHits(e))}get personalHitCount(){return this.inheritingHitCount-this.parent.inheritingHitCount}get averageCharHitCount(){return this.characters.reduce((e,t)=>e+t.hitCount,0)/this.characters.length}get personalWeightedHitCount(){return this.inheritingWeightedHitCount-this.parent.inheritingWeightedHitCount}andNonRootParents(){const e=[];let t=this;for(;t.parent;)e.push(t),t=t.parent;return e}getLeafNodes(){const e=[];return this.__recursiveGetLeafNodes(e),e}__recursiveGetLeafNodes(e){this.children.length?this.children.forEach(t=>{t.__recursiveGetLeafNodes(e)}):e.push(this)}simpleView(){let e=this.characters.map(e=>e.simpleView());return Object.assign(Object.create(null),{seq:this.sequence,chars:1===e.length?e[0]:e,hits:this.personalHitCount,kids:this.children.map(e=>e.simpleView()),__proto__:void 0})}}a.LEAF_CMP=Object.freeze({[i.a.BalancingScheme.SEQ]:(e,t)=>e.inheritingHitCount-t.inheritingHitCount,[i.a.BalancingScheme.CHAR]:(e,t)=>e.inheritingHitCount-t.inheritingHitCount,[i.a.BalancingScheme.WEIGHT]:(e,t)=>e.inheritingWeightedHitCount-t.inheritingWeightedHitCount}),a.PATH_CMP=Object.freeze({[i.a.BalancingScheme.SEQ]:(e,t)=>e.personalHitCount-t.personalHitCount,[i.a.BalancingScheme.CHAR]:(e,t)=>e.averageCharHitCount-t.averageCharHitCount,[i.a.BalancingScheme.WEIGHT]:(e,t)=>e.personalWeightedHitCount-t.personalWeightedHitCount}),(s=a||(a={})).Root=class extends s{constructor(){super(void 0,"",[])}validateConstruction(){}chooseOnePair(e){throw new TypeError("Must never hit on the root.")}get personalHitCount(){throw new TypeError("Must never hit on the root.")}get personalWeightedHitCount(){throw new TypeError("Must never hit on the root.")}andNonRootParents(){throw new TypeError}simpleView(){return this.children.map(e=>e.simpleView())}},Object.freeze(a),Object.freeze(a.prototype);class n{constructor(e,t){if(t<=0)throw new RangeError(`All weights must be positive, but we were passed the value "${t}" for the character "${e}".`);this.char=e,this.weightInv=1/t}reset(){this.hitCount=0,this.weightedHitCount=0}incrementNumHits(){this.hitCount+=1,this.weightedHitCount+=this.weightInv}simpleView(){return Object.assign(Object.create(null),{char:this.char,hits:this.hitCount})}}n.CMP=Object.freeze({[i.a.BalancingScheme.SEQ]:(e,t)=>e.hitCount-t.hitCount,[i.a.BalancingScheme.CHAR]:(e,t)=>e.hitCount-t.hitCount,[i.a.BalancingScheme.WEIGHT]:(e,t)=>e.weightedHitCount-t.weightedHitCount}),Object.freeze(n),Object.freeze(n.prototype);class o extends i.a{constructor(e,t){if(super(),this.static=e,this.treeMap=a.CREATE_TREE_MAP(t),this.leafNodes=this.treeMap.getLeafNodes(),this.leafNodes.length!==this.static.frontend.numLeaves)throw new Error(`maintenance required: the frontend constant for the language "${this.static.frontend.id}" needs to be updated to the correct, computed value, which is \`${this.leafNodes.length}\`.`)}get numLeaves(){return this.leafNodes.length}reset(){this.treeMap.reset()}getNonConflictingChar(e,t){this.leafNodes.sort(a.LEAF_CMP[t]);let r=void 0;for(const s of this.leafNodes){const i=s.andNonRootParents();for(let t=0;te.startsWith(i[t].sequence));if(r){r===i[t].sequence?i.splice(0):i.splice(t);break}}if(i.length){i.sort(a.PATH_CMP[t]),r=i[0];break}}if(!r)throw new Error("Invariants guaranteeing that a LangSeq canalways be shuffled-in were not met.");return r.chooseOnePair(t)}simpleView(){return Object.assign(Object.create(null),{id:this.static.frontend.id,displayName:this.static.frontend.display,root:this.treeMap.simpleView(),numLeaves:this.leafNodes.length})}}o||(o={}),Object.freeze(o),Object.freeze(o.prototype)},6:function(e,t,r){},7:function(e,t,r){var s={"./Emote.ts":[8,2],"./English.ts":[9,3],"./Japanese.ts":[10,4],"./Korean.ts":[11,5],"./Morse.ts":[12,6]};function i(e){if(!r.o(s,e))return Promise.resolve().then((function(){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}));var t=s[e],i=t[0];return r.e(t[1]).then((function(){return r(i)}))}i.keys=function(){return Object.keys(s)},i.id=7,e.exports=i}}]); +//# sourceMappingURL=offline.js.map \ No newline at end of file diff --git a/dist/client/chunk/lang/Emote-ts.js b/dist/client/chunk/lang/Emote-ts.js new file mode 100644 index 00000000..492988d2 --- /dev/null +++ b/dist/client/chunk/lang/Emote-ts.js @@ -0,0 +1,2 @@ +(window.webpackJsonpsnakey3=window.webpackJsonpsnakey3||[]).push([[2],{8:function(e,n,t){"use strict";t.r(n),t.d(n,"Emote",(function(){return c}));var c,o=t(5);!function(e){class n extends o.a{}e.GitHub=n,Object.freeze(n),Object.freeze(n.prototype)}(c||(c={})),Object.freeze(c)}}]); +//# sourceMappingURL=Emote-ts.js.map \ No newline at end of file diff --git a/dist/client/chunk/lang/English-ts.js b/dist/client/chunk/lang/English-ts.js new file mode 100644 index 00000000..a48cd323 --- /dev/null +++ b/dist/client/chunk/lang/English-ts.js @@ -0,0 +1,2 @@ +(window.webpackJsonpsnakey3=window.webpackJsonpsnakey3||[]).push([[3],{9:function(e,t,s){"use strict";s.r(t),s.d(t,"English",(function(){return n}));var n,r=s(5);!function(e){class t extends r.a{constructor(){super(t,Object.entries(n).reduce((e,t)=>{const s=t[0],n=t[0],r=t[1];return e[s]={seq:n,weight:r},e},{}))}static getInstance(){return this.SINGLETON||(this.SINGLETON=new t),this.SINGLETON}}t.SINGLETON=void 0,t.frontend=r.a.GET_FRONTEND_DESC_BY_ID("engl-low"),e.Lowercase=t,Object.seal(t),Object.freeze(t.prototype);class s extends r.a{constructor(){let e={};const t=t=>{e=Object.entries(n).reduce((e,s)=>{const n=t(s[0]),r=t(s[0]),c=s[1];return e[n]={seq:r,weight:c},e},e)};t(e=>e.toLowerCase()),t(e=>e.toUpperCase()),super(s,e)}static getInstance(){return this.SINGLETON||(this.SINGLETON=new s),this.SINGLETON}}s.SINGLETON=void 0,s.frontend=r.a.GET_FRONTEND_DESC_BY_ID("engl-mix"),e.MixedCase=s,Object.seal(s),Object.freeze(s.prototype);const n=Object.freeze({a:8.167,b:1.492,c:2.202,d:4.253,e:12.702,f:2.228,g:2.015,h:6.094,i:6.966,j:.153,k:1.292,l:4.025,m:2.406,n:6.749,o:7.507,p:1.929,q:.095,r:5.987,s:6.327,t:9.356,u:2.758,v:.978,w:2.56,x:.15,y:1.994,z:.077})}(n||(n={})),Object.seal(n)}}]); +//# sourceMappingURL=English-ts.js.map \ No newline at end of file diff --git a/dist/client/chunk/lang/Japanese-ts.js b/dist/client/chunk/lang/Japanese-ts.js new file mode 100644 index 00000000..b51cb11a --- /dev/null +++ b/dist/client/chunk/lang/Japanese-ts.js @@ -0,0 +1,2 @@ +(window.webpackJsonpsnakey3=window.webpackJsonpsnakey3||[]).push([[4],{10:function(e,t,i){"use strict";i.r(t),i.d(t,"Japanese",(function(){return s}));var s,h=i(5);!function(e){class t extends h.a{constructor(){super(t,t.INITIALIZER)}static getInstance(){return this.SINGLETON||(this.SINGLETON=new t,this.INITIALIZER=void 0),this.SINGLETON}}t.SINGLETON=void 0,t.frontend=h.a.GET_FRONTEND_DESC_BY_ID("japn-hir"),t.INITIALIZER=Object.freeze({"の":{seq:"no",weight:1918313},"に":{seq:"ni",weight:1108840},"た":{seq:"ta",weight:1067566},"い":{seq:"i",weight:1060284},"は":{seq:"ha",weight:937811},"を":{seq:"wo",weight:936356},"と":{seq:"to",weight:927938},"る":{seq:"ru",weight:916652},"が":{seq:"ga",weight:860742},"し":{seq:"shi",weight:848132},"で":{seq:"de",weight:764834},"て":{seq:"te",weight:758316},"な":{seq:"na",weight:720156},"か":{seq:"ka",weight:537294},"れ":{seq:"re",weight:450805},"ら":{seq:"ra",weight:42329},"も":{seq:"mo",weight:396142},"う":{seq:"u",weight:352965},"す":{seq:"su",weight:340654},"り":{seq:"ri",weight:333999},"こ":{seq:"ko",weight:312227},"だ":{seq:"da",weight:280911},"ま":{seq:"ma",weight:278599},"さ":{seq:"sa",weight:258960},"き":{seq:"ki",weight:233505},"め":{seq:"me",weight:223806},"く":{seq:"ku",weight:221960},"あ":{seq:"a",weight:204256},"け":{seq:"ke",weight:199362},"ど":{seq:"do",weight:196555},"ん":{seq:"nn",weight:190068},"え":{seq:"e",weight:163664},"よ":{seq:"yo",weight:154206},"つ":{seq:"tsu",weight:153999},"や":{seq:"ya",weight:146156},"そ":{seq:"so",weight:131611},"わ":{seq:"wa",weight:123077},"ち":{seq:"chi",weight:99183},"み":{seq:"mi",weight:89264},"せ":{seq:"se",weight:83444},"ろ":{seq:"ro",weight:73467},"ば":{seq:"ba",weight:72228},"お":{seq:"o",weight:65870},"じ":{seq:"ji",weight:56857},"べ":{seq:"be",weight:56005},"ず":{seq:"zu",weight:53256},"げ":{seq:"ge",weight:49126},"ほ":{seq:"ho",weight:48752},"へ":{seq:"he",weight:47013},"び":{seq:"bi",weight:32312},"む":{seq:"mu",weight:31212},"ご":{seq:"go",weight:26965},"ね":{seq:"ne",weight:23490},"ぶ":{seq:"bu",weight:23280},"ぐ":{seq:"gu",weight:21549},"ぎ":{seq:"gi",weight:19865},"ひ":{seq:"hi",weight:19148},"ょ":{seq:"yo",weight:14425},"づ":{seq:"du",weight:13125},"ぼ":{seq:"bo",weight:12402},"ざ":{seq:"za",weight:12108},"ふ":{seq:"fu",weight:11606},"ゃ":{seq:"ya",weight:11522},"ぞ":{seq:"zo",weight:10047},"ゆ":{seq:"yu",weight:8486},"ぜ":{seq:"ze",weight:6893},"ぬ":{seq:"nu",weight:5124},"ぱ":{seq:"pa",weight:4349},"ゅ":{seq:"yu",weight:2755},"ぴ":{seq:"pi",weight:1608},"ぽ":{seq:"po",weight:1315},"ぷ":{seq:"pu",weight:986},"ぺ":{seq:"pe",weight:477},"ぢ":{seq:"di",weight:82}}),e.Hiragana=t,Object.seal(t),Object.freeze(t.prototype);class i extends h.a{constructor(){super(i,i.INITIALIZER)}static getInstance(){return this.SINGLETON||(this.SINGLETON=new i,this.INITIALIZER=void 0),this.SINGLETON}}i.SINGLETON=void 0,i.frontend=h.a.GET_FRONTEND_DESC_BY_ID("japn-kat"),i.INITIALIZER=Object.freeze({"ン":{seq:"nn",weight:290948},"ル":{seq:"ru",weight:189442},"ス":{seq:"su",weight:178214},"ト":{seq:"to",weight:162802},"ア":{seq:"a",weight:127845},"イ":{seq:"i",weight:120807},"ラ":{seq:"ra",weight:117203},"リ":{seq:"ri",weight:106744},"ク":{seq:"ku",weight:98209},"カ":{seq:"ka",weight:82982},"シ":{seq:"shi",weight:80626},"タ":{seq:"ta",weight:75319},"ロ":{seq:"ro",weight:75301},"ド":{seq:"do",weight:74257},"ジ":{seq:"ji",weight:61171},"フ":{seq:"fu",weight:61115},"レ":{seq:"re",weight:60608},"メ":{seq:"me",weight:60230},"コ":{seq:"ko",weight:58724},"マ":{seq:"ma",weight:56123},"プ":{seq:"pu",weight:54159},"テ":{seq:"te",weight:53404},"ム":{seq:"mu",weight:50758},"チ":{seq:"chi",weight:48437},"バ":{seq:"ba",weight:44970},"ビ":{seq:"bi",weight:44462},"グ":{seq:"gu",weight:40433},"キ":{seq:"ki",weight:39608},"ウ":{seq:"u",weight:39323},"サ":{seq:"sa",weight:39202},"ニ":{seq:"ni",weight:38711},"ナ":{seq:"na",weight:38047},"エ":{seq:"e",weight:36458},"ブ":{seq:"bu",weight:35920},"パ":{seq:"pa",weight:35416},"セ":{seq:"se",weight:34883},"オ":{seq:"o",weight:34718},"ィ":{seq:"i",weight:33747},"デ":{seq:"de",weight:32665},"ュ":{seq:"yu",weight:32616},"ミ":{seq:"mi",weight:29262},"ャ":{seq:"ya",weight:28144},"ボ":{seq:"bo",weight:26651},"ダ":{seq:"da",weight:26396},"ツ":{seq:"tsu",weight:24541},"ポ":{seq:"ho",weight:23742},"ベ":{seq:"be",weight:22755},"ネ":{seq:"ne",weight:22462},"ガ":{seq:"ga",weight:22061},"ハ":{seq:"ha",weight:21839},"ワ":{seq:"wa",weight:21784},"ソ":{seq:"so",weight:20784},"ケ":{seq:"ke",weight:20633},"モ":{seq:"ho",weight:20070},"ノ":{seq:"no",weight:19572},"ズ":{seq:"zu",weight:19240},"ピ":{seq:"pi",weight:18692},"ホ":{seq:"ho",weight:18204},"ェ":{seq:"e",weight:17817},"ョ":{seq:"yo",weight:17731},"ペ":{seq:"pe",weight:14881},"ゴ":{seq:"go",weight:13931},"ヤ":{seq:"ya",weight:12526},"ギ":{seq:"gi",weight:10732},"ヨ":{seq:"yo",weight:10318},"ザ":{seq:"za",weight:10144},"ァ":{seq:"a",weight:10121},"ゼ":{seq:"ze",weight:7689},"ヒ":{seq:"hi",weight:7289},"ヘ":{seq:"he",weight:7129},"ユ":{seq:"yo",weight:6653},"ゲ":{seq:"ge",weight:6481},"ォ":{seq:"o",weight:6245},"ヌ":{seq:"nu",weight:2897},"ゾ":{seq:"zo",weight:2640},"ヴ":{seq:"vu",weight:1145},"ヂ":{seq:"di",weight:149},"ヅ":{seq:"du",weight:127},"ヲ":{seq:"wo",weight:122}}),e.Katakana=i,Object.seal(i),Object.freeze(i.prototype)}(s||(s={})),Object.freeze(s)}}]); +//# sourceMappingURL=Japanese-ts.js.map \ No newline at end of file diff --git a/dist/client/chunk/lang/Korean-ts.js b/dist/client/chunk/lang/Korean-ts.js new file mode 100644 index 00000000..0d90bb09 --- /dev/null +++ b/dist/client/chunk/lang/Korean-ts.js @@ -0,0 +1,2 @@ +(window.webpackJsonpsnakey3=window.webpackJsonpsnakey3||[]).push([[5],{11:function(a,o,e){"use strict";e.r(o),e.d(o,"Korean",(function(){return t}));var t,m=e(5);!function(a){class o extends m.a{constructor(){super(o,n((a,e,t)=>[a,e,t].flatMap(a=>a.value in o.KEYBOARD?[a.value]:a.atoms.split("")).map(a=>o.KEYBOARD[a]).join("")))}static getInstance(){return this.SINGLETON||(this.SINGLETON=new o,this.KEYBOARD=void 0),this.SINGLETON}}o.SINGLETON=void 0,o.frontend=m.a.GET_FRONTEND_DESC_BY_ID("kore-dub"),o.KEYBOARD=Object.freeze({"":"","ㅂ":"q","ㅈ":"w","ㄷ":"e","ㄱ":"r","ㅅ":"t","ㅛ":"y","ㅕ":"u","ㅑ":"i","ㅐ":"o","ㅔ":"p","ㅁ":"a","ㄴ":"s","ㅇ":"d","ㄹ":"f","ㅎ":"g","ㅗ":"h","ㅓ":"j","ㅏ":"k","ㅣ":"l","ㅋ":"z","ㅌ":"x","ㅊ":"c","ㅍ":"v","ㅠ":"b","ㅜ":"n","ㅡ":"m","ㅃ":"Q","ㅉ":"W","ㄸ":"E","ㄲ":"R","ㅆ":"T","ㅒ":"O","ㅖ":"P"}),a.Dubeolsik=o,Object.seal(o),Object.seal(o.prototype);class e extends m.a{constructor(){super(e,n((a,o,t)=>e.SEB_KEYBOARD.INITIALS[a.value]+e.SEB_KEYBOARD.MEDIALS[o.value]+e.SEB_KEYBOARD.FINALS[t.value]))}static getInstance(){return this.SINGLETON||(this.SINGLETON=new e,this.SEB_KEYBOARD=void 0),this.SINGLETON}}e.SINGLETON=void 0,e.frontend=m.a.GET_FRONTEND_DESC_BY_ID("kore-sub"),e.SEB_KEYBOARD=Object.freeze({FINALS:{"":"","ㅎ":"1","ㅆ":"2","ㅂ":"3","ㅅ":"q","ㄹ":"w","ㅇ":"a","ㄴ":"s","ㅁ":"z","ㄱ":"x","ㄲ":"!","ㄺ":"@","ㅈ":"#","ㄿ":"$","ㄾ":"%","ㅍ":"Q","ㅌ":"W","ㄵ":"E","ㅀ":"R","ㄽ":"T","ㄷ":"A","ㄶ":"S","ㄼ":"D","ㄻ":"F","ㅊ":"Z","ㅄ":"X","ㅋ":"C","ㄳ":"V"},MEDIALS:{"ㅛ":"4","ㅠ":"5","ㅑ":"6","ㅖ":"7","ㅢ":"8","ㅕ":"e","ㅐ":"r","ㅓ":"t","ㅣ":"d","ㅏ":"f","ㅡ":"g","ㅔ":"c","ㅗ":"v","ㅜ":"b","ㅒ":"G","ㅘ":"vf","ㅙ":"vr","ㅚ":"vd","ㅝ":"bt","ㅞ":"bc","ㅟ":"bd"},INITIALS:{"ㅋ":"0","ㄹ":"y","ㄷ":"u","ㅁ":"i","ㅊ":"o","ㅍ":"p","ㄴ":"h","ㅇ":"j","ㄱ":"k","ㅈ":"l","ㅂ":";","ㅌ":"'","ㅅ":"n","ㅎ":"m","ㄲ":"!","ㄸ":"uu","ㅃ":";;","ㅆ":"nn","ㅉ":"l"}}),a.Sebeolsik=e,Object.seal(e),Object.seal(e.prototype);class t extends m.a{constructor(){super(t,n((a,o,e)=>a.roman+o.roman+e.roman))}static getInstance(){return t.SINGLETON||(t.SINGLETON=new t),t.SINGLETON}static remapKey(a){return a}}t.SINGLETON=void 0,t.frontend=m.a.GET_FRONTEND_DESC_BY_ID("kore-rom"),a.Romanization=t,Object.seal(t),Object.seal(t.prototype);const n=a=>{const o={};return s.forEach((e,t)=>{r.forEach((m,n)=>{u.forEach((s,v)=>{let c=t;c=r.length*c+n,c=u.length*c+v;const E=String.fromCharCode(44032+c);o[E]={seq:a(e,m,s),weight:l[E]}})})}),o},s=Object.freeze([{value:"ㄱ",atoms:"ㄱ",roman:"g"},{value:"ㄲ",atoms:"ㄱㄱ",roman:"kk"},{value:"ㄴ",atoms:"ㄴ",roman:"n"},{value:"ㄷ",atoms:"ㄷ",roman:"d"},{value:"ㄸ",atoms:"ㄷㄷ",roman:"tt"},{value:"ㄹ",atoms:"ㄹ",roman:"r"},{value:"ㅁ",atoms:"ㅁ",roman:"m"},{value:"ㅂ",atoms:"ㅂ",roman:"b"},{value:"ㅃ",atoms:"ㅂㅂ",roman:"pp"},{value:"ㅅ",atoms:"ㅅ",roman:"s"},{value:"ㅆ",atoms:"ㅅㅅ",roman:"ss"},{value:"ㅇ",atoms:"ㅇ",roman:"-"},{value:"ㅈ",atoms:"ㅈ",roman:"j"},{value:"ㅉ",atoms:"ㅈㅈ",roman:"jj"},{value:"ㅊ",atoms:"ㅊ",roman:"ch"},{value:"ㅋ",atoms:"ㅋ",roman:"k"},{value:"ㅌ",atoms:"ㅌ",roman:"t"},{value:"ㅍ",atoms:"ㅍ",roman:"p"},{value:"ㅎ",atoms:"ㅎ",roman:"h"}]),r=Object.freeze([{value:"ㅏ",atoms:"ㅏ",roman:"a"},{value:"ㅐ",atoms:"ㅐ",roman:"ae"},{value:"ㅑ",atoms:"ㅑ",roman:"ya"},{value:"ㅒ",atoms:"ㅒ",roman:"yae"},{value:"ㅓ",atoms:"ㅓ",roman:"eo"},{value:"ㅔ",atoms:"ㅔ",roman:"e"},{value:"ㅕ",atoms:"ㅕ",roman:"yeo"},{value:"ㅖ",atoms:"ㅖ",roman:"ye"},{value:"ㅗ",atoms:"ㅗ",roman:"o"},{value:"ㅘ",atoms:"ㅗㅏ",roman:"wa"},{value:"ㅙ",atoms:"ㅗㅐ",roman:"wae"},{value:"ㅚ",atoms:"ㅗㅣ",roman:"oe"},{value:"ㅛ",atoms:"ㅛ",roman:"yo"},{value:"ㅜ",atoms:"ㅜ",roman:"u"},{value:"ㅝ",atoms:"ㅜㅓ",roman:"wo"},{value:"ㅞ",atoms:"ㅜㅔ",roman:"we"},{value:"ㅟ",atoms:"ㅜㅣ",roman:"wi"},{value:"ㅠ",atoms:"ㅠ",roman:"yu"},{value:"ㅡ",atoms:"ㅡ",roman:"eu"},{value:"ㅢ",atoms:"ㅡㅣ",roman:"ui"},{value:"ㅣ",atoms:"ㅣ",roman:"i"}]),u=Object.freeze([{value:"",atoms:"",roman:""},{value:"ㄱ",atoms:"ㄱ",roman:"k"},{value:"ㄲ",atoms:"ㄱㄱ",roman:"k"},{value:"ㄳ",atoms:"ㄱㅅ",roman:"kt"},{value:"ㄴ",atoms:"ㄴ",roman:"n"},{value:"ㄵ",atoms:"ㄴㅈ",roman:"nt"},{value:"ㄶ",atoms:"ㄴㅎ",roman:"nt"},{value:"ㄷ",atoms:"ㄷ",roman:"t"},{value:"ㄹ",atoms:"ㄹ",roman:"l"},{value:"ㄺ",atoms:"ㄹㄱ",roman:"lk"},{value:"ㄻ",atoms:"ㄹㅁ",roman:"lm"},{value:"ㄼ",atoms:"ㄹㅂ",roman:"lp"},{value:"ㄽ",atoms:"ㄹㅅ",roman:"lt"},{value:"ㄾ",atoms:"ㄹㅌ",roman:"lt"},{value:"ㄿ",atoms:"ㄹㅍ",roman:"lp"},{value:"ㅀ",atoms:"ㄹㅎ",roman:"lt"},{value:"ㅁ",atoms:"ㅁ",roman:"m"},{value:"ㅂ",atoms:"ㅂ",roman:"p"},{value:"ㅄ",atoms:"ㅂㅅ",roman:"pt"},{value:"ㅅ",atoms:"ㅅ",roman:"t"},{value:"ㅆ",atoms:"ㅅㅅ",roman:"t"},{value:"ㅇ",atoms:"ㅇ",roman:"ng"},{value:"ㅈ",atoms:"ㅈ",roman:"t"},{value:"ㅊ",atoms:"ㅊ",roman:"t"},{value:"ㅋ",atoms:"ㅋ",roman:"k"},{value:"ㅌ",atoms:"ㅌ",roman:"t"},{value:"ㅍ",atoms:"ㅍ",roman:"p"},{value:"ㅎ",atoms:"ㅎ",roman:"t"}]),l=Object.freeze({"":1})}(t||(t={})),Object.freeze(t)}}]); +//# sourceMappingURL=Korean-ts.js.map \ No newline at end of file diff --git a/dist/client/chunk/lang/Morse-ts.js b/dist/client/chunk/lang/Morse-ts.js new file mode 100644 index 00000000..c238147a --- /dev/null +++ b/dist/client/chunk/lang/Morse-ts.js @@ -0,0 +1,2 @@ +(window.webpackJsonpsnakey3=window.webpackJsonpsnakey3||[]).push([[6],{12:function(e,t,n){"use strict";n.r(t),n.d(t,"Morse",(function(){return c}));var c,o=n(5);!function(e){class t extends o.a{}e.Encode=t,Object.freeze(t),Object.freeze(t.prototype);class n extends o.a{}e.Decode=n,Object.freeze(n),Object.freeze(n.prototype)}(c||(c={})),Object.freeze(c)}}]); +//# sourceMappingURL=Morse-ts.js.map \ No newline at end of file diff --git a/dist/favicon.ico b/dist/client/favicon.ico similarity index 100% rename from dist/favicon.ico rename to dist/client/favicon.ico diff --git a/dist/client/index.css b/dist/client/index.css new file mode 100644 index 00000000..5e84fce7 --- /dev/null +++ b/dist/client/index.css @@ -0,0 +1 @@ +.fill-parent{position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden hidden}.text-select-disabled{user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.center-contents{display:inline-flex;justify-content:center;align-items:center}.stack-contents{position:relative}.stack-contents>*{position:absolute}[data-sk-colour-scheme=snakey]{--colour-mainFg:#e9eef3;--colour-mainBg:#3f5e77;--colour-tileFg:#e9eef3;--colour-tileBg:#2e3b48;--colour-tileBd:#fff;--colour-healthFg:#21352e;--colour-healthBg:#4edf8f;--colour-pFaceMe:#35e7ff;--colour-pFaceTeammate:#f8df50;--colour-pFaceImtlTeammate:#f8df50;--colour-pFaceOpponent:#ec4daf;--colour-pFaceImtlOpponent:#e23fa3}::backdrop{background-color:var(--colour-mainBg)}html{scroll-behavior:smooth}body{font-family:Trebuchet MS,Lucida Sans Unicode;font-weight:700;color:var(--colour-mainFg);text-align:center;transition:filter .5s ease-in-out}table{border-spacing:0}button{font:inherit;color:inherit;padding:0;background-color:var(--colour-tileBg)}button:disabled{opacity:.7}#all-screens-container{display:contents}.sk-screen{contain:strict;position:absolute;top:0;right:0;bottom:0;left:0;overflow:auto}.sk-screen:not([data-current]){display:none}#background{will-change:transform;z-index:-10000;display:initial;background-color:var(--colour-mainBg)}#screen-tint{pointer-events:none;z-index:10000;visibility:visible;opacity:0;transition-property:opacity,background-color;transition-timing-function:ease-in-out}.screen-home{display:flex}.screen-home--nav{--spacing:6px;--border-radius:15px;height:30em;max-height:80vmin;width:30em;max-width:80vmin;display:grid;grid-template:"pofl pofl pofl ponl" 3fr "tuto tuto tuto . " 1fr "colr colr colr . " 1fr "repo bugs . . " 1fr/1fr 1fr 1fr 3fr;background-color:var(--colour-mainBg);padding:var(--spacing);border-radius:calc(var(--border-radius) + 2*var(--spacing))}.screen-home--nav>*{position:relative;overflow:hidden;border-radius:var(--border-radius);margin:var(--spacing);border:0 double var(--colour-tileBg);color:var(--colour-mainFg);background-color:var(--colour-tileBg);transition:all .15s ease-in}.screen-home--nav>:focus{outline:none;border-color:var(--colour-tileBd);box-shadow:inset 0 0 20px rgba(0,0,0,.25);text-shadow:0 0 8px #000;animation:nav-button-focus .4s ease-in-out -.5s infinite alternate}.screen-home--nav:focus-within>:not(:focus){filter:brightness(.9);opacity:.6}.screen-home--nav--play-offline{grid-area:pofl}.screen-home--nav--play-online{grid-area:ponl}.screen-home--nav--tutorial{grid-area:tuto}.screen-home--nav--colour-scheme{grid-area:colr}.screen-home--nav--goto-repo{grid-area:repo}.screen-home--nav--report-issue{grid-area:bugs}@keyframes nav-button-focus{0%{margin:calc(var(--spacing)/3);border-width:calc(1.2*var(--spacing))}to{margin:calc(var(--spacing)*2/3);border-width:calc(1.4*var(--spacing))}}.screen-play{display:grid;grid-template:"controls main scoreboard" 1fr/minmax(5em,1fr) minmax(24em,auto) minmax(12em,1fr)}.screen-play--grid-container{grid-area:main;flex-flow:column nowrap;min-width:max-content}.screen-play--controls-bar{grid-area:controls;display:flex;flex-direction:column;align-items:stretch}.screen-play--scoreboard-bar{grid-area:scoreboard}@media screen and (max-aspect-ratio:1/1){.screen-play{grid-template:"main" 1fr "controls scoreboard" 1fr/minmax(5em,1fr) minmax(24em,auto) minmax(12em,1fr)}}.sk-pick-one{display:flex;flex-direction:column;align-items:stretch;overflow-x:hidden;overflow-y:auto}.sk-pick-one--opt{overflow:hidden hidden}[data-sk-colour-scheme=smooth-stone]{--colour-mainFg:#c3c5ce;--colour-mainBg:#72888d;--colour-tileFg:#a2b6bb;--colour-tileBg:#546164;--colour-tileBd:#fff;--colour-healthFg:#21352e;--colour-healthBg:#398f5a;--colour-pFaceMe:#43aec9;--colour-pFaceTeammate:#bdae58;--colour-pFaceImtlTeammate:#f8df50;--colour-pFaceOpponent:#a34e59;--colour-pFaceImtlOpponent:#e23fa3} \ No newline at end of file diff --git a/dist/client/index.js b/dist/client/index.js index 4047fd55..dfaca2ac 100644 --- a/dist/client/index.js +++ b/dist/client/index.js @@ -1,2 +1,2 @@ -var snakey3=function(e){function t(t){for(var r,n,o=t[0],i=t[1],s=0,c=[];se,TO_LOWER:e=>e.toLowerCase()}),e.__RemapTemplates,e.RemappingFunctions=Object.freeze({[e.Names.ENGLISH__LOWERCASE.id]:e.__RemapTemplates.TO_LOWER,[e.Names.ENGLISH__MIXEDCASE.id]:e.__RemapTemplates.IDENTITY,[e.Names.JAPANESE__HIRAGANA.id]:e.__RemapTemplates.TO_LOWER,[e.Names.JAPANESE__KATAKANA.id]:e.__RemapTemplates.TO_LOWER,[e.Names.KOREAN__DUBEOLSIK.id]:e.__RemapTemplates.IDENTITY,[e.Names.KOREAN__SEBEOLSIK.id]:e.__RemapTemplates.IDENTITY,[e.Names.KOREAN__ROMANIZATION.id]:e.__RemapTemplates.TO_LOWER})}(o||(o={})),Object.freeze(o),Object.freeze(o.prototype)},function(e,t,r){"use strict";var n;r.d(t,"a",(function(){return n})),function(e){e.General=Object.freeze({Class:Object.freeze({TEXT_SELECT_DISABLED:"text-select-disabled",FILL_PARENT:"fill-parent",CENTER_CONTENTS:"center-contents",STACK_CONTENTS:"stack-contents"})}),e.Tile=Object.freeze({Class:Object.freeze({BASE:"tile",POINTER_HB:"tile__pointer-hitbox",LANG_CHAR:"tile__char",LANG_SEQ:"tile__seq"}),Dataset:Object.freeze({HEALTH:"health"})}),e.Grid=Object.freeze({Id:Object.freeze({GRID:"game-grid"}),Class:Object.freeze({GRID:"game-grid",IMPL_BODY:"game-grid-impl-body",KBD_DC_BASE:"game-grid-kbd-dc",KBD_DC_ICON:"game-grid-kbd-dc__icon"}),Dataset:Object.freeze({COORD_SYS:"coordSys"})}),e.Player=Object.freeze({Class:Object.freeze({BASE:"player",FACE:"player__face",DOWNED_OVERLAY:"player__downed-overlay",SHORT_SPOTLIGHT:"player__spotlight-short",LONG_SPOTLIGHT:"player__spotlight-long"}),Dataset:Object.freeze({DOWNED:"downed",FACE_SWATCH:"face"})}),e.Screen=Object.freeze({Class:Object.freeze({BASE:"screen"}),Dataset:Object.freeze({CURRENT:"current"})})}(n||(n={})),Object.freeze(n)},function(e,t,r){},function(e,t,r){"use strict";r.r(t);var n,a=r(1),o=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},i=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class s{constructor(e,t){n.set(this,void 0);const r=document.createElement("div");r.classList.add(a.a.Screen.Class.BASE),e.appendChild(r),o(this,n,!1),this.requestGoToScreen=t}enter(){i(this,n)||(this.__lazyLoad(),o(this,n,!0)),this.__abstractOnBeforeEnter()}leave(){return this.__abstractOnBeforeLeave()}__abstractOnBeforeLeave(){return!0}__abstractOnBeforeEnter(){}}n=new WeakMap,function(e){let t;!function(e){e[e.HOME=0]="HOME",e[e.GAME_SETUP=1]="GAME_SETUP",e[e.SESH_JOINER=2]="SESH_JOINER",e[e.HOW_TO_PLAY=3]="HOW_TO_PLAY",e[e.HOW_TO_HOST=4]="HOW_TO_HOST",e[e.PLAY_GAME=5]="PLAY_GAME"}(t=e.Id||(e.Id={}))}(s||(s={})),Object.freeze(s),Object.freeze(s.prototype);class c extends s{__lazyLoad(){}}Object.freeze(c),Object.freeze(c.prototype);var l=r(0);class d extends s{__lazyLoad(){{const e=document.createElement("select");for(const t of Object.values(l.a.Names)){const r=document.createElement("option");r.value=t.id,r.innerText=t.display,e.add(r)}e.onselect=()=>{e.blur()},this.langSel=e}}}Object.freeze(d),Object.freeze(d.prototype);class f extends s{__lazyLoad(){}}Object.freeze(f),Object.freeze(f.prototype);class u extends s{__lazyLoad(){}}Object.freeze(u),Object.freeze(u.prototype);class _ extends s{__lazyLoad(){}}(_||(_={})).INSTRUCTIONS_STEPS=Object.freeze(["$ npm install 'https://github.com/david-fong/SnaKey-NTS#gh-pages'","$ npm run start","send the url to your friends"]),Object.freeze(_),Object.freeze(_.prototype);class p extends s{__lazyLoad(){}}Object.freeze(p),Object.freeze(p.prototype);var O,E=function(e,t,r){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,r),r},m=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class S{constructor(e){O.set(this,void 0);const t=e,r=this.goToScreen;this.dict=Object.freeze({[s.Id.HOME]:new c(t,r),[s.Id.GAME_SETUP]:new d(t,r),[s.Id.SESH_JOINER]:new f(t,r),[s.Id.HOW_TO_PLAY]:new u(t,r),[s.Id.HOW_TO_HOST]:new _(t,r),[s.Id.PLAY_GAME]:new p(t,r)}),this.goToScreen(s.Id.HOME)}goToScreen(e){var t;const r=this.dict[e];if(this.currentScreen===r)throw new Error("never happens. see comment in source.");(null===(t=this.currentScreen)||void 0===t?void 0:t.leave())&&(r.enter(),E(this,O,r))}get currentScreen(){return m(this,O)}}O=new WeakMap,Object.freeze(S),Object.freeze(S.prototype),r(2),window.origin&&"null"!==window.origin&&"serviceWorker"in navigator&&navigator.serviceWorker.register("./src/client/ServiceWorker.js");new S(document.createElement("div"));r.e(1).then(r.bind(null,5)).then(e=>{e.game})}]); +var snakey3=function(e){function t(t){for(var n,a,s=t[0],o=t[1],i=0,c=[];i{Object.getOwnPropertyNames(t.prototype).forEach(n=>{Object.defineProperty(e.prototype,n,Object.getOwnPropertyDescriptor(t.prototype,n))})})}n.d(t,"c",(function(){return s})),n.d(t,"b",(function(){return o})),n.d(t,"a",(function(){return i})),function(e){e.NEVER="Never happens. See comment in source."}(a||(a={})),function(e){e.HOST_REGISTRATION="/host-reg",e.GROUP_PREFIX="/group"}(r||(r={}));class o{}!function(e){let t;e.Family=Object.freeze({HUMAN:"HUMAN",CHASER:"CHASER"}),e.Family,function(e){e.NULL=void 0}(t=e.Id||(e.Id={})),e.MoveType=Object.freeze({NORMAL:"NORMAL",BOOST:"BOOST"}),e.MoveType}(o||(o={})),Object.freeze(o),Object.freeze(o.prototype);class i{}!function(e){let t,n,a;!function(e){e.REGEXP=new RegExp("^[a-zA-Z0-9!@#$%^&*()-_=+;:'\"\\|,.<>/?]+$")}(t=e.Seq||(e.Seq={})),function(e){e.NULL=Object.freeze({char:"",seq:""})}(n=e.CharSeqPair||(e.CharSeqPair={})),function(e){e.SEQ="SEQ",e.CHAR="CHAR",e.WEIGHT="WEIGHT"}(a=e.BalancingScheme||(e.BalancingScheme={})),e.__RemapTemplates=Object.freeze({IDENTITY:e=>e,TO_LOWER:e=>e.toLowerCase()}),e.__RemapTemplates,e.FrontendDescs=Object.freeze([{id:"engl-low",module:"English",export:"Lowercase",numLeaves:26,remapFunc:e.__RemapTemplates.TO_LOWER,display:"English Lowercase (QWERTY)",blurb:""},{id:"engl-mix",module:"English",export:"MixedCase",numLeaves:52,remapFunc:e.__RemapTemplates.IDENTITY,display:"English Mixed-Case (QWERTY)",blurb:""},{id:"japn-hir",module:"Japanese",export:"Hiragana",numLeaves:71,remapFunc:e.__RemapTemplates.TO_LOWER,display:"Japanese Hiragana",blurb:""},{id:"japn-kat",module:"Japanese",export:"Katakana",numLeaves:70,remapFunc:e.__RemapTemplates.TO_LOWER,display:"Japanese Katakana",blurb:""},{id:"kore-dub",module:"Korean",export:"Dubeolsik",numLeaves:9177,remapFunc:e.__RemapTemplates.IDENTITY,display:"Korean Dubeolsik (두벌식 키보드)",blurb:"The most common keyboard layout, and South Korea's only Hangul standard since 1969. Consonants are on the left, and vowels on the right."},{id:"kore-sub",module:"Korean",export:"Sebeolsik",numLeaves:10206,remapFunc:e.__RemapTemplates.IDENTITY,display:"Korean Sebeolsik (세벌식 최종 키보드)",blurb:"Another Hangul keyboard layout used in South Korea, and the final Sebeolsik layout designed by Dr. Kong Byung Woo, hence the name. Syllable-initial consonants are on the right, final consonants on the left, and vowels in the middle. It is more ergonomic than the dubeolsik, but not widely used."},{id:"kore-rom",module:"Korean",export:"Romanization",numLeaves:3990,remapFunc:e.__RemapTemplates.TO_LOWER,display:"Korean Revised Romanization",blurb:"The Revised Romanization of Korean (국어의 로마자 표기법; 國語의 로마字 表記法) is the official South Korean language romanization system. It was developed by the National Academy of the Korean Language from 1995, and was released on 7 July 2000 by South Korea's Ministry of Culture and Tourism"}].map(e=>Object.freeze(e))),e.FrontendDescs,e.GET_FRONTEND_DESC_BY_ID=function(t){const n=e.FrontendDescs.find(e=>e.id===t);if(!n)throw new Error(`Frontend descriptor of language with id "${t}" not found.`);return n}}(i||(i={})),Object.freeze(i),Object.freeze(i.prototype)},function(e,t,n){},function(e,t,n){},function(e,t,n){"use strict";n.r(t),n.d(t,"OmHooks",(function(){return o.a})),n.d(t,"top",(function(){return W}));var a,r,s,o=n(0),i=function(e,t,n,a){return new(n||(n=Promise))((function(r,s){function o(e){try{c(a.next(e))}catch(e){s(e)}}function i(e){try{c(a.throw(e))}catch(e){s(e)}}function c(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,i)}c((a=a.apply(e,t||[])).next())}))},c=function(e,t,n){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,n),n},l=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class d{constructor(e,t){a.set(this,void 0),r.set(this,void 0),c(this,r,!1),this.requestGoToScreen=t,c(this,a,e)}enter(e){return i(this,void 0,void 0,(function*(){if(!l(this,r)){const e=this.baseElem=document.createElement("div");e.classList.add(o.a.Screen.Class.BASE),this.__lazyLoad(),l(this,a).appendChild(e),c(this,r,!0)}yield this.__abstractOnBeforeEnter(e),window.requestAnimationFrame(e=>{this.baseElem.dataset[o.a.Screen.Dataset.CURRENT]="",this.baseElem.setAttribute("aria-hidden","false")})}))}leave(){return!!this.__abstractOnBeforeLeave()&&(delete this.baseElem.dataset[o.a.Screen.Dataset.CURRENT],this.baseElem.setAttribute("aria-hidden","true"),!0)}__abstractOnBeforeEnter(e){return i(this,void 0,void 0,(function*(){}))}__abstractOnBeforeLeave(){return!0}}a=new WeakMap,r=new WeakMap,function(e){let t;!function(e){e.HOME="home",e.HOW_TO_PLAY="howToPlay",e.HOW_TO_HOST="howToHost",e.COLOUR_CTRL="colourControl",e.GAME_SETUP="gameSetup",e.PLAY_OFFLINE="playOffline",e.PLAY_ONLINE="playOnline",e.SESH_JOINER="sessionJoiner"}(t=e.Id||(e.Id={}))}(d||(d={})),Object.freeze(d),Object.freeze(d.prototype);class u extends d{__lazyLoad(){const e=o.a.Screen.Impl.Home.Class;this.baseElem.classList.add(o.a.General.Class.CENTER_CONTENTS,e.SCREEN),this.baseElem.setAttribute("aria-label","Home Page Screen");const t=this.navElem=document.createElement("div");function n(e,n){e.classList.add(o.a.General.Class.CENTER_CONTENTS,o.a.General.Class.TEXT_SELECT_DISABLED,n.cssClass),e.innerText=n.text,e.addEventListener("pointerenter",()=>{e.focus()}),t.appendChild(e)}t.classList.add(e.NAV),t.setAttribute("role","navigation"),t.addEventListener("pointerleave",()=>{var e;(null===(e=document.activeElement)||void 0===e?void 0:e.parentElement)===t&&document.activeElement.blur()}),[{text:"Offline Single-player",cssClass:e.NAV_PLAY_OFFLINE,screenId:d.Id.PLAY_OFFLINE},{text:"Online Multi-player",cssClass:e.NAV_PLAY_ONLINE,screenId:e=>{}},{text:"Tutorial",cssClass:e.NAV_TUTORIAL,screenId:d.Id.HOW_TO_PLAY},{text:"Colour Schemes",cssClass:e.NAV_COLOURS,screenId:d.Id.COLOUR_CTRL}].map(e=>Object.freeze(e)).forEach(e=>{const t=document.createElement("button");t.onclick=e.screenId instanceof Function?e.screenId:()=>{this.requestGoToScreen(e.screenId,{})},n(t,e)}),[{text:"Visit Repo",cssClass:e.NAV_VIEW_REPO,href:new window.URL("https://github.com/david-fong/SnaKey-NTS")},{text:"Report Issue",cssClass:e.NAV_RPT_ISSUE,href:new window.URL("https://github.com/david-fong/SnaKey-NTS/issues")}].map(e=>Object.freeze(e)).forEach(e=>{const t=document.createElement("a");t.href=e.href.toString(),t.referrerPolicy="strict-origin-when-cross-origin",t.target="_blank",n(t,e)}),this.baseElem.appendChild(t)}}Object.freeze(u),Object.freeze(u.prototype);class h extends d{__lazyLoad(){}}Object.freeze(h),Object.freeze(h.prototype);class p extends d{__lazyLoad(){}}(p||(p={})).INSTRUCTIONS_STEPS=Object.freeze(["$ npm install 'https://github.com/david-fong/SnaKey-NTS#gh-pages'","$ npm run start","send the url to your friends"]),Object.freeze(p),Object.freeze(p.prototype),function(e){let t;e.LocalKeys=Object.freeze({MUSIC_VOLUME:"musicVolume",SFX_VOLUME:"sfxVolume",COLOUR_ID:"colourSchemeId",COLOUR_LITERAL:"colourSchemeStyleLiteral",GAME_PRESET:"gamePresetId",USERNAME:"username",AVATAR:"avatarId"}),e.SessionKeys=Object.freeze({}),function(e){let t;e.DB_NAME="snakeyDB",function(e){e.STORE_NAME="userGamePresets"}(t=e.UserGamePresetStore||(e.UserGamePresetStore={})),Object.freeze(t)}(t=e.IDB||(e.IDB={})),Object.freeze(t)}(s||(s={})),Object.freeze(s);var m,f,E,O,_,b,y,g=function(e,t,n){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,n),n},v=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class S{constructor(){m.set(this,void 0),f.set(this,void 0),E.set(this,void 0);const e=document.createElement("div");e.tabIndex=0,e.classList.add(o.a.SkPickOne.Class.BASE),e.addEventListener("keydown",this.onKeyDown.bind(this)),e.setAttribute("role","listbox"),this.baseElem=e,this.options=[]}addOption(e){this.options.push(e),this.baseElem.appendChild(e.baseElem),e.__registerParent(this.onOptDisabledChange.bind(this))}hoverOpt(e){var t;this.hoveredOpt!==e&&(null===(t=this.hoveredOpt)||void 0===t||t.baseElem.setAttribute("aria-active-descendant","false"),g(this,f,e),this.hoveredOpt.baseElem.setAttribute("aria-active-descendant","true"))}selectOpt(e,t=!0){var n;if(!e)throw new Error("opt must be defined");this.hoverOpt(e),this.confirmedOpt!==e&&(null===(n=this.confirmedOpt)||void 0===n||n.baseElem.setAttribute("aria-selected","false"),g(this,m,e),this.confirmedOpt.baseElem.setAttribute("aria-selected","true"),t&&this.__onSelectOpt(e))}get confirmedOpt(){return v(this,m)}get hoveredOpt(){return v(this,f)}onOptDisabledChange(e){this.confirmedOpt===e&&(this.validity=!e.disabled)}set validity(e){this.validity!==e&&(this.baseElem.setAttribute("aria-invalid",e?"false":"true"),g(this,E,e))}onKeyDown(e){if(" "===e.key||"Enter"===e.key)return this.selectOpt(this.hoveredOpt),e.preventDefault(),!1;{const t=this.options.indexOf(this.hoveredOpt);if("ArrowDown"===e.key||"Down"===e.key){if(t0)return this.hoverOpt(this.options[t-1]),e.preventDefault(),!1}return!0}}m=new WeakMap,f=new WeakMap,E=new WeakMap,O=S||(S={}),_=new WeakMap,b=new WeakMap,O.__Option=class{constructor(){_.set(this,void 0),b.set(this,void 0);const e=this.baseElem=document.createElement("div");e.classList.add(o.a.SkPickOne.Class.OPT_BASE),e.setAttribute("role","option"),g(this,_,!1)}__registerParent(e){g(this,b,e)}get disabled(){return v(this,_)}set disabled(e){this.disabled!==e&&(this.baseElem.setAttribute("aria-disabled",e?"true":"false"),g(this,_,e),v(this,b).call(this,this))}},Object.freeze(O),Object.freeze(O.prototype),Object.freeze(S),Object.freeze(S.prototype),n(3);class T extends d{__lazyLoad(){const e=new T.PickOne;this.baseElem.appendChild(e.baseElem),this.sel=e;const t=localStorage.getItem(s.LocalKeys.COLOUR_ID);t&&this.sel.selectOpt(this.sel.getOptById(t),!1)}}!function(e){class t extends S{constructor(){if(super(),this.garageDoorElem=document.getElementById(o.a.Screen.Id.SCREEN_TINT),!this.garageDoorElem)throw new Error;this.garageDoorElem.style.transitionDuration=y.SMOOTH_CHANGE_DURATION/3+"ms";for(const e of y.Schemes)this.addOption(new t.Option(e));this.selectOpt(this.getOptById("snakey"),!1)}__onHoverOpt(e){}__onSelectOpt(e){localStorage.setItem(s.LocalKeys.COLOUR_ID,e.desc.id),localStorage.setItem(s.LocalKeys.COLOUR_LITERAL,e.cssLiteral);const t=y.SMOOTH_CHANGE_DURATION/3+80;setTimeout(()=>{this.garageDoorElem.style.opacity=1..toString(),setTimeout(()=>{document.documentElement.dataset[o.a.General.Dataset.COLOUR_SCHEME]=e.desc.id,setTimeout(()=>{this.garageDoorElem.style.opacity=(0).toString()},t)},t)},t)}getOptById(e){return this.options.find(t=>t.desc.id===e)}}e.PickOne=t,function(e){class t extends S.__Option{constructor(e){super(),this.desc=e}}e.Option=t,Object.freeze(t),Object.freeze(t.prototype)}(t=e.PickOne||(e.PickOne={})),Object.freeze(t),Object.freeze(t.prototype)}(T||(T={})),Object.freeze(T),Object.freeze(T.prototype),function(e){e.Swatch=Object.freeze(["mainFg","mainBg","tileFg","tileBg","tileBd","healthFg","healthBg","pFaceMe","pFaceTeammate","pFaceImtlTeammate","pFaceOpponent","pFaceImtlOpponent"]),e.Schemes=Object.freeze([{id:"snakey",displayName:"Snakey",author:"N.W."},{id:"smooth-stone",displayName:"Smooth Stone",author:"Dav"}].map(e=>Object.freeze(e))),e.SMOOTH_CHANGE_DURATION=2e3}(y||(y={})),Object.freeze(y);var L=n(1);class C extends d{__lazyLoad(){this.createLangSelector()}createLangSelector(){const e=document.createElement("select");for(const t of Object.values(L.a.FrontendDescs)){const n=document.createElement("option");n.value=t.id,n.innerText=t.display,e.add(n)}e.onchange=()=>{e.blur()},this.langSel=e}}Object.freeze(C),Object.freeze(C.prototype);class A extends d{__lazyLoad(){}}Object.freeze(A),Object.freeze(A.prototype);var w,I,N,R,P=function(e,t,n,a){return new(n||(n=Promise))((function(r,s){function o(e){try{c(a.next(e))}catch(e){s(e)}}function i(e){try{c(a.throw(e))}catch(e){s(e)}}function c(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,i)}c((a=a.apply(e,t||[])).next())}))},k=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)},D=function(e,t,n){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,n),n};class G extends d{constructor(){super(...arguments),w.set(this,void 0),I.set(this,void 0),N.set(this,void 0),R.set(this,void 0)}__lazyLoad(){this.baseElem.classList.add(o.a.Screen.Impl.PlayGame.Class.SCREEN),this.baseElem.setAttribute("aria-label","Play Game Screen");const e=G.createCenterColElem();this.gridElem=e.gridElem,this.baseElem.appendChild(e.baseElem),this.initializeControlsBar(),this.initializeScoresBar(),D(this,I,()=>{this.wantsAutoPause&&(document.hidden?void 0===k(this,R)&&this.statusBecomePaused():"page-hide"===k(this,R)&&this.statusBecomePlaying())}),D(this,N,this.__gridKeyDownCallback.bind(this))}__abstractOnBeforeEnter(){return(()=>P(this,void 0,void 0,(function*(){document.addEventListener("visibilitychange",k(this,I)),this.pauseButton.disabled=!0,this.statusBecomePaused(),D(this,w,yield this.__createNewGame()),this.gridElem.addEventListener("keydown",k(this,N));const e=this.currentGame.reset();yield e,this.gridElem.insertAdjacentElement("afterbegin",this.currentGame.htmlElements.gridImplElem),this.pauseButton.onclick=this.statusBecomePlaying.bind(this),this.pauseButton.disabled=!1,this.wantsAutoPause&&setTimeout(()=>{document.hidden||this.statusBecomePlaying()},1e3)})))()}__abstractOnBeforeLeave(){if(!window.confirm("Are you sure you would like to leave?"))return!1;document.removeEventListener("visibilitychange",k(this,I));for(const e of Object.values(this.currentGame.htmlElements))e.remove();return this.gridElem.removeEventListener("keydown",k(this,N)),D(this,w,void 0),!0}get currentGame(){return k(this,w)}__gridKeyDownCallback(e){if(e.ctrlKey&&" "===e.key){const e=this.currentGame.operators;this.currentGame.currentOperator=e[(e.indexOf(this.currentGame.currentOperator)+1)%e.length]}else this.currentGame.currentOperator.processKeyboardInput(e);return" "!==e.key||(e.preventDefault(),!1)}statusBecomePlaying(){var e;const t=o.a.Grid.Dataset.GAME_STATE;null===(e=this.currentGame)||void 0===e||e.statusBecomePlaying(),this.pauseButton.innerText="Pause",D(this,R,void 0),this.gridElem.dataset[t.KEY]=t.VALUES.PLAYING,window.requestAnimationFrame(e=>{this.pauseButton.onclick=this.statusBecomePaused.bind(this),this.resetButton.disabled=!0,this.gridElem.focus()})}statusBecomePaused(){var e;const t=o.a.Grid.Dataset.GAME_STATE;null===(e=this.currentGame)||void 0===e||e.statusBecomePaused(),this.pauseButton.innerText="Unpause",D(this,R,document.hidden?"page-hide":"other"),this.gridElem.dataset[t.KEY]=t.VALUES.PAUSED,this.pauseButton.onclick=this.statusBecomePlaying.bind(this),this.resetButton.disabled=!1}__onGameBecomeOver(){const e=o.a.Grid.Dataset.GAME_STATE;this.pauseButton.disabled=!0,this.gridElem.dataset[e.KEY]=e.VALUES.OVER}__resetGame(){this.currentGame.reset(),this.pauseButton.disabled=!1,this.wantsAutoPause&&this.statusBecomePlaying()}initializeControlsBar(){const e=document.createElement("div");e.classList.add(o.a.Screen.Impl.PlayGame.Class.CONTROLS_BAR),e.setAttribute("role","menu");{const t=this.backToHomeButton=document.createElement("button");t.innerText="Return To Homepage",t.onclick=this.requestGoToScreen.bind(this,d.Id.HOME),e.appendChild(t)}{const t=this.pauseButton=document.createElement("button");e.appendChild(t)}{const t=this.resetButton=document.createElement("button");t.innerText="Reset",t.onclick=this.__resetGame.bind(this),e.appendChild(t)}this.baseElem.appendChild(e)}initializeScoresBar(){}}w=new WeakMap,I=new WeakMap,N=new WeakMap,R=new WeakMap,(G||(G={})).createCenterColElem=function(){const e=o.a.General.Class,t=document.createElement("div");t.classList.add(e.CENTER_CONTENTS,o.a.Screen.Impl.PlayGame.Class.GRID_CONTAINER);const n=document.createElement("div");n.tabIndex=0,n.setAttribute("role","textbox"),n.setAttribute("aria-label","Game Grid"),n.classList.add(e.CENTER_CONTENTS,e.STACK_CONTENTS,e.TEXT_SELECT_DISABLED,o.a.Grid.Class.GRID);{const t=document.createElement("div");t.classList.add(e.CENTER_CONTENTS,o.a.Grid.Class.KBD_DC);{const e=document.createElement("div");e.classList.add(o.a.Grid.Class.KBD_DC_ICON),e.innerText="(click here to continue typing)",t.appendChild(e)}n.appendChild(t)}{const t=document.createElement("div");t.classList.add(e.CENTER_CONTENTS,o.a.Grid.Class.PAUSE_OL);{const e=document.createElement("div");e.classList.add(o.a.Grid.Class.PAUSE_OL_ICON),e.innerText="(The Game is Paused)",t.appendChild(e)}n.appendChild(t)}return t.appendChild(n),Object.freeze({baseElem:t,gridElem:n})},Object.freeze(G),Object.freeze(G.prototype);var B=function(e,t,n,a){return new(n||(n=Promise))((function(r,s){function o(e){try{c(a.next(e))}catch(e){s(e)}}function i(e){try{c(a.throw(e))}catch(e){s(e)}}function c(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,i)}c((a=a.apply(e,t||[])).next())}))};class j extends G{constructor(){super(...arguments),this.wantsAutoPause=!0}__lazyLoad(){super.__lazyLoad()}__createNewGame(){return B(this,void 0,void 0,(function*(){return new((yield n.e(0).then(n.bind(null,13))).OfflineGame)({coordSys:"EUCLID2",gridDimensions:{height:21,width:21},averageFreeHealthPerTile:1/45,langBalancingScheme:L.a.BalancingScheme.WEIGHT,langId:"engl-low",playerDescs:[{isALocalOperator:!0,familyId:"HUMAN",teamId:0,socketId:void 0,username:"hello world 1",noCheckGameOver:!1,familyArgs:{}},{isALocalOperator:!0,familyId:"HUMAN",teamId:0,socketId:void 0,username:"hello world 2",noCheckGameOver:!1,familyArgs:{}},{isALocalOperator:!1,familyId:"CHASER",teamId:1,socketId:void 0,username:"chaser test 1",noCheckGameOver:!0,familyArgs:{fearDistance:5,bloodThirstDistance:7,healthReserve:3,movesPerSecond:2}},{isALocalOperator:!1,familyId:"CHASER",teamId:1,socketId:void 0,username:"chaser test 2",noCheckGameOver:!0,familyArgs:{fearDistance:5,bloodThirstDistance:7,healthReserve:3,movesPerSecond:2}}]})}))}}Object.freeze(j),Object.freeze(j.prototype);var z=function(e,t,n,a){return new(n||(n=Promise))((function(r,s){function o(e){try{c(a.next(e))}catch(e){s(e)}}function i(e){try{c(a.throw(e))}catch(e){s(e)}}function c(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,i)}c((a=a.apply(e,t||[])).next())}))};class x extends G{constructor(){super(...arguments),this.wantsAutoPause=!1}__lazyLoad(){super.__lazyLoad()}__createNewGame(){return z(this,void 0,void 0,(function*(){}))}}Object.freeze(x),Object.freeze(x.prototype);var H,M=function(e,t,n){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,n),n},U=function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};class F{constructor(e){H.set(this,void 0),e.setAttribute("role","presentation");const t=e,n=this.goToScreen.bind(this);this.dict=Object.freeze({[d.Id.HOME]:new u(t,n),[d.Id.HOW_TO_PLAY]:new h(t,n),[d.Id.HOW_TO_HOST]:new p(t,n),[d.Id.COLOUR_CTRL]:new T(t,n),[d.Id.GAME_SETUP]:new C(t,n),[d.Id.PLAY_OFFLINE]:new j(t,n),[d.Id.PLAY_ONLINE]:new x(t,n),[d.Id.SESH_JOINER]:new A(t,n)}),this.goToScreen(d.Id.HOME,{})}goToScreen(e,t){const n=this.dict[e];if(this.currentScreen===n)throw new Error("never happens. see comment in source.");this.currentScreen&&!this.currentScreen.leave()||(n.enter(t),M(this,H,n))}get currentScreen(){return U(this,H)}}H=new WeakMap,Object.freeze(F),Object.freeze(F.prototype);class K{constructor(){const e=document.getElementById(o.a.Screen.Id.ALL_SCREENS);if(!e)throw new Error;this.allScreens=new F(e)}}Object.freeze(K),Object.freeze(K.prototype),n(2),window.origin&&"null"!==window.origin&&"serviceWorker"in navigator&&window.addEventListener("load",(function(){navigator.serviceWorker.register("/ServiceWorker.js").then(e=>{console.log("ServiceWorker registration successful with scope: ",e.scope)},e=>{console.log("ServiceWorker registration failed: ",e)})}));const W=new K}]); //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/client/index.js.map b/dist/client/index.js.map deleted file mode 100644 index a15d4f32..00000000 --- a/dist/client/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack://snakey3/webpack/bootstrap","webpack://snakey3/./src/base/utils/TypeDefs.ts","webpack://snakey3/./src/base/browser/OmHooks.ts","webpack://snakey3/./src/base/browser/screen/SkScreen.ts","webpack://snakey3/./src/base/browser/screen/impl/Home.ts","webpack://snakey3/./src/base/browser/screen/impl/GameSetup.ts","webpack://snakey3/./src/base/browser/screen/impl/SeshJoiner.ts","webpack://snakey3/./src/base/browser/screen/impl/HowToPlay.ts","webpack://snakey3/./src/base/browser/screen/impl/HowToHost.ts","webpack://snakey3/./src/base/browser/screen/impl/PlayGame.ts","webpack://snakey3/./src/base/browser/screen/AllSkScreens.ts","webpack://snakey3/./src/client/index.ts"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","i","resolves","length","Object","prototype","hasOwnProperty","call","installedChunks","push","modules","parentJsonpFunction","shift","installedModules","installedCssChunks","0","__webpack_require__","exports","module","l","e","promises","Promise","resolve","reject","href","fullhref","p","existingLinkTags","document","getElementsByTagName","dataHref","tag","getAttribute","rel","existingStyleTags","linkTag","createElement","type","onload","onerror","event","request","target","src","err","Error","code","parentNode","removeChild","appendChild","then","installedChunkData","promise","onScriptComplete","script","charset","timeout","nc","setAttribute","jsonpScriptSrc","error","clearTimeout","chunk","errorType","realSrc","message","name","undefined","setTimeout","head","all","m","c","d","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","oe","console","jsonpArray","window","oldJsonpFunction","slice","s","SkErrors","NEVER","Player","Id","Family","freeze","HUMAN","CHASER","NULL","MoveType","NORMAL","BOOST","Lang","Seq","CharSeqPair","BalancingScheme","REGEXP","RegExp","char","seq","Names","ENGLISH__LOWERCASE","display","id","ENGLISH__MIXEDCASE","JAPANESE__HIRAGANA","JAPANESE__KATAKANA","KOREAN__DUBEOLSIK","KOREAN__SEBEOLSIK","KOREAN__ROMANIZATION","__RemapTemplates","IDENTITY","input","TO_LOWER","toLowerCase","RemappingFunctions","OmHooks","General","Class","TEXT_SELECT_DISABLED","FILL_PARENT","CENTER_CONTENTS","STACK_CONTENTS","Tile","BASE","POINTER_HB","LANG_CHAR","LANG_SEQ","Dataset","HEALTH","Grid","GRID","IMPL_BODY","KBD_DC_BASE","KBD_DC_ICON","COORD_SYS","FACE","DOWNED_OVERLAY","SHORT_SPOTLIGHT","LONG_SPOTLIGHT","DOWNED","FACE_SWATCH","Screen","CURRENT","parentElem","requestGoToDisplay","baseElem","classList","add","this","requestGoToScreen","__lazyLoad","__abstractOnBeforeEnter","__abstractOnBeforeLeave","SkScreen","langSel","langName","values","opt","innerText","onselect","blur","INSTRUCTIONS_STEPS","f","goToScreen","dict","HOME","GAME_SETUP","SESH_JOINER","HOW_TO_PLAY","HOW_TO_HOST","PLAY_GAME","destId","destScreen","currentScreen","leave","enter","origin","navigator","serviceWorker","register","mod","game"],"mappings":"wBACE,SAASA,EAAqBC,GAQ7B,IAPA,IAMIC,EAAUC,EANVC,EAAWH,EAAK,GAChBI,EAAcJ,EAAK,GAKAK,EAAI,EAAGC,EAAW,GACpCD,EAAIF,EAASI,OAAQF,IACzBH,EAAUC,EAASE,GAChBG,OAAOC,UAAUC,eAAeC,KAAKC,EAAiBV,IAAYU,EAAgBV,IACpFI,EAASO,KAAKD,EAAgBV,GAAS,IAExCU,EAAgBV,GAAW,EAE5B,IAAID,KAAYG,EACZI,OAAOC,UAAUC,eAAeC,KAAKP,EAAaH,KACpDa,EAAQb,GAAYG,EAAYH,IAKlC,IAFGc,GAAqBA,EAAoBf,GAEtCM,EAASC,QACdD,EAASU,OAATV,GAOF,IAAIW,EAAmB,GAGnBC,EAAqB,CACxBC,EAAG,GAMAP,EAAkB,CACrBO,EAAG,GAWJ,SAASC,EAAoBnB,GAG5B,GAAGgB,EAAiBhB,GACnB,OAAOgB,EAAiBhB,GAAUoB,QAGnC,IAAIC,EAASL,EAAiBhB,GAAY,CACzCI,EAAGJ,EACHsB,GAAG,EACHF,QAAS,IAUV,OANAP,EAAQb,GAAUU,KAAKW,EAAOD,QAASC,EAAQA,EAAOD,QAASD,GAG/DE,EAAOC,GAAI,EAGJD,EAAOD,QAKfD,EAAoBI,EAAI,SAAuBtB,GAC9C,IAAIuB,EAAW,GAKZP,EAAmBhB,GAAUuB,EAASZ,KAAKK,EAAmBhB,IACzB,IAAhCgB,EAAmBhB,IAFX,CAAC,EAAI,GAEkCA,IACtDuB,EAASZ,KAAKK,EAAmBhB,GAAW,IAAIwB,SAAQ,SAASC,EAASC,GAIzE,IAHA,IAAIC,EAAY3B,EAAU,aACtB4B,EAAWV,EAAoBW,EAAIF,EACnCG,EAAmBC,SAASC,qBAAqB,QAC7C7B,EAAI,EAAGA,EAAI2B,EAAiBzB,OAAQF,IAAK,CAChD,IACI8B,GADAC,EAAMJ,EAAiB3B,IACRgC,aAAa,cAAgBD,EAAIC,aAAa,QACjE,GAAe,eAAZD,EAAIE,MAAyBH,IAAaN,GAAQM,IAAaL,GAAW,OAAOH,IAErF,IAAIY,EAAoBN,SAASC,qBAAqB,SACtD,IAAQ7B,EAAI,EAAGA,EAAIkC,EAAkBhC,OAAQF,IAAK,CACjD,IAAI+B,EAEJ,IADID,GADAC,EAAMG,EAAkBlC,IACTgC,aAAa,gBAChBR,GAAQM,IAAaL,EAAU,OAAOH,IAEvD,IAAIa,EAAUP,SAASQ,cAAc,QACrCD,EAAQF,IAAM,aACdE,EAAQE,KAAO,WACfF,EAAQG,OAAShB,EACjBa,EAAQI,QAAU,SAASC,GAC1B,IAAIC,EAAUD,GAASA,EAAME,QAAUF,EAAME,OAAOC,KAAOlB,EACvDmB,EAAM,IAAIC,MAAM,qBAAuBhD,EAAU,cAAgB4C,EAAU,KAC/EG,EAAIE,KAAO,wBACXF,EAAIH,QAAUA,SACP5B,EAAmBhB,GAC1BsC,EAAQY,WAAWC,YAAYb,GAC/BZ,EAAOqB,IAERT,EAAQX,KAAOC,EAEJG,SAASC,qBAAqB,QAAQ,GAC5CoB,YAAYd,MACfe,MAAK,WACPrC,EAAmBhB,GAAW,MAMhC,IAAIsD,EAAqB5C,EAAgBV,GACzC,GAA0B,IAAvBsD,EAGF,GAAGA,EACF/B,EAASZ,KAAK2C,EAAmB,QAC3B,CAEN,IAAIC,EAAU,IAAI/B,SAAQ,SAASC,EAASC,GAC3C4B,EAAqB5C,EAAgBV,GAAW,CAACyB,EAASC,MAE3DH,EAASZ,KAAK2C,EAAmB,GAAKC,GAGtC,IACIC,EADAC,EAAS1B,SAASQ,cAAc,UAGpCkB,EAAOC,QAAU,QACjBD,EAAOE,QAAU,IACbzC,EAAoB0C,IACvBH,EAAOI,aAAa,QAAS3C,EAAoB0C,IAElDH,EAAOX,IAnGV,SAAwB9C,GACvB,OAAOkB,EAAoBW,EAAI,IAAM,GAAG7B,IAAUA,GAAW,YAkG9C8D,CAAe9D,GAG5B,IAAI+D,EAAQ,IAAIf,MAChBQ,EAAmB,SAAUb,GAE5Bc,EAAOf,QAAUe,EAAOhB,OAAS,KACjCuB,aAAaL,GACb,IAAIM,EAAQvD,EAAgBV,GAC5B,GAAa,IAAViE,EAAa,CACf,GAAGA,EAAO,CACT,IAAIC,EAAYvB,IAAyB,SAAfA,EAAMH,KAAkB,UAAYG,EAAMH,MAChE2B,EAAUxB,GAASA,EAAME,QAAUF,EAAME,OAAOC,IACpDiB,EAAMK,QAAU,iBAAmBpE,EAAU,cAAgBkE,EAAY,KAAOC,EAAU,IAC1FJ,EAAMM,KAAO,iBACbN,EAAMvB,KAAO0B,EACbH,EAAMnB,QAAUuB,EAChBF,EAAM,GAAGF,GAEVrD,EAAgBV,QAAWsE,IAG7B,IAAIX,EAAUY,YAAW,WACxBf,EAAiB,CAAEhB,KAAM,UAAWK,OAAQY,MAC1C,MACHA,EAAOf,QAAUe,EAAOhB,OAASe,EACjCzB,SAASyC,KAAKpB,YAAYK,GAG5B,OAAOjC,QAAQiD,IAAIlD,IAIpBL,EAAoBwD,EAAI9D,EAGxBM,EAAoByD,EAAI5D,EAGxBG,EAAoB0D,EAAI,SAASzD,EAASkD,EAAMQ,GAC3C3D,EAAoB4D,EAAE3D,EAASkD,IAClC/D,OAAOyE,eAAe5D,EAASkD,EAAM,CAAEW,YAAY,EAAMC,IAAKJ,KAKhE3D,EAAoBgE,EAAI,SAAS/D,GACX,oBAAXgE,QAA0BA,OAAOC,aAC1C9E,OAAOyE,eAAe5D,EAASgE,OAAOC,YAAa,CAAEC,MAAO,WAE7D/E,OAAOyE,eAAe5D,EAAS,aAAc,CAAEkE,OAAO,KAQvDnE,EAAoBoE,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQnE,EAAoBmE,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,iBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAKnF,OAAOoF,OAAO,MAGvB,GAFAxE,EAAoBgE,EAAEO,GACtBnF,OAAOyE,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOnE,EAAoB0D,EAAEa,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIRvE,EAAoB2E,EAAI,SAASzE,GAChC,IAAIyD,EAASzD,GAAUA,EAAOoE,WAC7B,WAAwB,OAAOpE,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAF,EAAoB0D,EAAEC,EAAQ,IAAKA,GAC5BA,GAIR3D,EAAoB4D,EAAI,SAASgB,EAAQC,GAAY,OAAOzF,OAAOC,UAAUC,eAAeC,KAAKqF,EAAQC,IAGzG7E,EAAoBW,EAAI,QAGxBX,EAAoB8E,GAAK,SAASjD,GAA2B,MAApBkD,QAAQlC,MAAMhB,GAAYA,GAEnE,IAAImD,EAAaC,OAA4B,oBAAIA,OAA4B,qBAAK,GAC9EC,EAAmBF,EAAWvF,KAAKiF,KAAKM,GAC5CA,EAAWvF,KAAOd,EAClBqG,EAAaA,EAAWG,QACxB,IAAI,IAAIlG,EAAI,EAAGA,EAAI+F,EAAW7F,OAAQF,IAAKN,EAAqBqG,EAAW/F,IAC3E,IAAIU,EAAsBuF,EAI1B,OAAOlF,EAAoBA,EAAoBoF,EAAI,G,+BClP9C,IAAUC,EAAjB,6EAAiBA,GAEA,EAAAC,MAAe,wCAFhC,CAAiBD,MAAQ,KAMlB,MAAME,IACb,SAAiBA,GAmBb,IAAiBC,EAXJ,EAAAC,OAASrG,OAAOsG,OAAc,CACvCC,MAAQ,QACRC,OAAQ,WAEZ,EAAAH,OAOA,SAAiBD,GAIA,EAAAK,UAAOzC,EAJxB,CAAiBoC,EAAA,EAAAA,KAAA,EAAAA,GAAE,KAcN,EAAAM,SAAW1G,OAAOsG,OAAc,CACzCK,OAAQ,SACRC,MAAQ,UAEZ,EAAAF,SArCJ,CAAiBP,MAAM,KAuCvBnG,OAAOsG,OAAOH,GACdnG,OAAOsG,OAAOH,EAAOlG,WAGd,MAAM4G,IACb,SAAiBA,GASb,IAAiBC,EAmBAC,EAiBCC,GApClB,SAAiBF,GAUA,EAAAG,OAAS,IAAIC,OAAO,iBAVrC,CAAiBJ,EAAA,EAAAA,MAAA,EAAAA,IAAG,KAmBpB,SAAiBC,GAMA,EAAAN,KAAOzG,OAAOsG,OAAc,CACrCa,KAAM,GACNC,IAAM,KARd,CAAiBL,EAAA,EAAAA,cAAA,EAAAA,YAAW,KAiB5B,SAAkBC,GACd,YACA,cACA,kBAHJ,CAAkBA,EAAA,EAAAA,kBAAA,EAAAA,gBAAe,KAYpB,EAAAK,MAAQrH,OAAOsG,OAAc,CACtCgB,mBAAoB,CAChBC,QAAS,6BACTC,GAAI,YAERC,mBAAoB,CAChBF,QAAS,8BACTC,GAAI,YAERE,mBAAoB,CAChBH,QAAS,oBACTC,GAAI,YAERG,mBAAoB,CAChBJ,QAAS,oBACTC,GAAI,YAERI,kBAAmB,CACfL,QAAS,6BACTC,GAAI,YAERK,kBAAmB,CACfN,QAAS,gCACTC,GAAI,YAERM,qBAAsB,CAClBP,QAAS,8BACTC,GAAI,cAGZ,EAAAH,MAOa,EAAAU,iBAAmB/H,OAAOsG,OAAc,CACjD0B,SAAWC,GAA0BA,EACrCC,SAAWD,GAA0BA,EAAME,gBAE/C,EAAAJ,iBAwBa,EAAAK,mBAEXpI,OAAOsG,OAAc,CACnB,CAAE,EAAAe,MAAMC,mBAAmBE,IAAM,EAAAO,iBAAiBG,SAClD,CAAE,EAAAb,MAAMI,mBAAmBD,IAAM,EAAAO,iBAAiBC,SAClD,CAAE,EAAAX,MAAMK,mBAAmBF,IAAM,EAAAO,iBAAiBG,SAClD,CAAE,EAAAb,MAAMM,mBAAmBH,IAAM,EAAAO,iBAAiBG,SAClD,CAAE,EAAAb,MAAMO,kBAAkBJ,IAAO,EAAAO,iBAAiBC,SAClD,CAAE,EAAAX,MAAMQ,kBAAkBL,IAAO,EAAAO,iBAAiBC,SAClD,CAAE,EAAAX,MAAMS,qBAAqBN,IAAI,EAAAO,iBAAiBG,WAnI1D,CAAiBrB,MAAI,KAsIrB7G,OAAOsG,OAAOO,GACd7G,OAAOsG,OAAOO,EAAK5G,Y,6BClLZ,IAAUoI,EAAjB,2CAAiBA,GAKA,EAAAC,QAAUtI,OAAOsG,OAAc,CACxCiC,MAAOvI,OAAOsG,OAAc,CACxBkC,qBAAsB,uBACtBC,YAAa,cACbC,gBAAiB,kBACjBC,eAAgB,qBAKX,EAAAC,KAAO5I,OAAOsG,OAAc,CACrCiC,MAAOvI,OAAOsG,OAAc,CACxBuC,KAAgB,OAMhBC,WAAgB,uBAChBC,UAAgB,aAChBC,SAAgB,cAEpBC,QAASjJ,OAAOsG,OAAc,CAC1B4C,OAAY,aAKP,EAAAC,KAAOnJ,OAAOsG,OAAc,CACrCF,GAAIpG,OAAOsG,OAAc,CAMrB8C,KAAY,cAEhBb,MAAOvI,OAAOsG,OAAc,CACxB8C,KAAgB,YAUhBC,UAAgB,sBAChBC,YAAgB,mBAChBC,YAAgB,2BAEpBN,QAASjJ,OAAOsG,OAAc,CAK1BkD,UAAY,eAKP,EAAArD,OAASnG,OAAOsG,OAAc,CACvCiC,MAAOvI,OAAOsG,OAAc,CACxBuC,KAAgB,SAChBY,KAAgB,eAChBC,eAAgB,yBAChBC,gBAAgB,0BAChBC,eAAgB,2BAEpBX,QAASjJ,OAAOsG,OAAc,CAC1BuD,OAAY,SACZC,YAAY,WAKP,EAAAC,OAAS/J,OAAOsG,OAAc,CACvCiC,MAAOvI,OAAOsG,OAAc,CACxBuC,KAAY,WAEhBI,QAASjJ,OAAOsG,OAAc,CAC1B0D,QAAY,cAvFxB,CAAiB3B,MAAO,KA2FxBrI,OAAOsG,OAAO+B,I,iTC1FP,MAAe,EAYlB,YAAmB4B,EAAyBC,GAR5C,mBASI,MAAMC,EAAW1I,SAASQ,cAAc,OACxCkI,EAASC,UAAUC,IAAIhC,EAAA,EAAQ0B,OAAOxB,MAAMM,MAC5CoB,EAAWnH,YAAYqH,GACvB,EAAAG,KAAI,GAAkB,GACtBA,KAAKC,kBAAoBL,EAGtB,QACE,EAAD,UACAI,KAAKE,aACL,EAAAF,KAAI,GAAkB,IAE1BA,KAAKG,0BAGF,QACH,OAAOH,KAAKI,0BAaN,0BACN,OAAO,EAOD,4B,cAGd,SAAiBC,GAEb,IAAkBvE,GAAlB,SAAkBA,GACd,mBACA,+BACA,iCACA,iCACA,iCACA,6BANJ,CAAkBA,EAAA,EAAAA,KAAA,EAAAA,GAAE,KAFxB,CAAiB,MAAQ,KAWzBpG,OAAOsG,OAAO,GACdtG,OAAOsG,OAAO,EAASrG,WCxEhB,MAAM,UAAmB,EAElB,eAKdD,OAAOsG,OAAO,GACdtG,OAAOsG,OAAO,EAAWrG,W,WCJlB,MAAM,UAAwB,EAIvB,aAEN,CAEA,MAAM2K,EAAUnJ,SAASQ,cAAc,UACvC,IAAK,MAAM4I,KAAY7K,OAAO8K,OAAO,IAAKzD,OAAQ,CAC9C,MAAM0D,EAAMtJ,SAASQ,cAAc,UACnC8I,EAAIhG,MAAQ8F,EAASrD,GACrBuD,EAAIC,UAAYH,EAAStD,QACzBqD,EAAQP,IAAIU,GAEhBH,EAAQK,SAAW,KACfL,EAAQM,QAIXZ,KAAKM,QAAgCA,IAM9C5K,OAAOsG,OAAO,GACdtG,OAAOsG,OAAO,EAAgBrG,WC5BvB,MAAM,UAAyB,EAExB,eAKdD,OAAOsG,OAAO,GACdtG,OAAOsG,OAAO,EAAiBrG,WCXxB,MAAM,UAAwB,EAEvB,eAKdD,OAAOsG,OAAO,GACdtG,OAAOsG,OAAO,EAAgBrG,WCRvB,MAAM,UAAwB,EAEvB,gBAKG,MAAe,KAKfkL,mBAAqBnL,OAAOsG,OAAc,CACnD,oEACA,kBACA,iCAGRtG,OAAOsG,OAAO,GACdtG,OAAOsG,OAAO,EAAgBrG,WChBvB,MAAM,UAAuB,EAEtB,eAKdD,OAAOsG,OAAO,GACdtG,OAAOsG,OAAO,EAAerG,W,oPCJtB,MAAM,EAMT,YAAmBkK,GAFnB,mBAGI,MAAM5I,EAAI4I,EACJiB,EAAId,KAAKe,WACff,KAAKgB,KAAOtL,OAAOsG,OAAO,CACtB,CAAE,EAASF,GAAGmF,MAAe,IAAU,EAAWhK,EAAE6J,GACpD,CAAE,EAAShF,GAAGoF,YAAe,IAAK,EAAgBjK,EAAE6J,GACpD,CAAE,EAAShF,GAAGqF,aAAe,IAAI,EAAiBlK,EAAE6J,GACpD,CAAE,EAAShF,GAAGsF,aAAe,IAAK,EAAgBnK,EAAE6J,GACpD,CAAE,EAAShF,GAAGuF,aAAe,IAAK,EAAgBpK,EAAE6J,GACpD,CAAE,EAAShF,GAAGwF,WAAe,IAAM,EAAerK,EAAE6J,KAExDd,KAAKe,WAAW,EAASjF,GAAGmF,MAGzB,WAAWM,G,MACd,MAAMC,EAAaxB,KAAKgB,KAAKO,GAC7B,GAAIvB,KAAKyB,gBAAkBD,EAEvB,MAAM,IAAIpJ,MAAO,0CAEC,QAAtB,EAAI4H,KAAKyB,qBAAa,eAAEC,WAIpBF,EAAWG,QACX,EAAA3B,KAAI,EAAkBwB,IAI9B,oBACI,OAAO,EAAAxB,KAAA,I,cAGftK,OAAOsG,OAAO,GACdtG,OAAOsG,OAAO,EAAarG,WCjD3B,EAAQ,GAKA4F,OAAOqG,QAA4B,SAAlBrG,OAAOqG,QAAqB,kBAAmBC,WAEhEA,UAAUC,cAAcC,SAAS,iCAWrB,IAAI,EAAa5K,SAASQ,cAAc,QAE5D,4BAAgEc,KAAMuJ,IAClEA,EAAIC","file":"client/index.js","sourceRoot":""} \ No newline at end of file diff --git a/dist/index.css b/dist/index.css deleted file mode 100644 index 1b854162..00000000 --- a/dist/index.css +++ /dev/null @@ -1 +0,0 @@ -.fill-parent{position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden hidden}.text-select-disabled{user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.center-contents{display:inline-flex;align-items:center;justify-content:center}.stack-contents{position:relative}.stack-contents>*{position:absolute}:root{--colour-snakey-tileFg:#aebde2;--colour-snakey-tileBg:#2e3b48;--colour-snakey-tileBd:#fff;--colour-snakey-healthFg:#21352e;--colour-snakey-healthBg:#4edf8f;--colour-snakey-mainFg:#e9eef3;--colour-snakey-mainBg:#3f5e77;--colour-snakey-pFaceMe:#2fdef5;--colour-snakey-pFaceTeammate:#f8df50;--colour-snakey-pFaceImmortalTeammate:#f8df50;--colour-snakey-pFaceOpponent:#ec4daf;--colour-snakey-pFaceImmortalOpponent:#e23fa3;--colour-tileFg:var(--colour-selected-tileFg,var(--colour-snakey-tileFg));--colour-tileBg:var(--colour-selected-tileBg,var(--colour-snakey-tileBg));--colour-tileBd:var(--colour-selected-tileBd,var(--colour-snakey-tileBd));--colour-healthFg:var(--colour-selected-healthFg,var(--colour-snakey-healthFg));--colour-healthBg:var(--colour-selected-healthBg,var(--colour-snakey-healthBg));--colour-mainFg:var(--colour-selected-mainFg,var(--colour-snakey-mainFg));--colour-mainBg:var(--colour-selected-mainBg,var(--colour-snakey-mainBg));--colour-pFaceMe:var(--colour-selected-pFaceMe,var(--colour-snakey-pFaceMe));--colour-pFaceTeammate:var(--colour-selected-pFaceTeammate,var(--colour-snakey-pFaceTeammate));--colour-pFaceOpponent:var(--colour-selected-pFaceOpponent,var(--colour-snakey-pFaceOpponent));--colour-pFaceImmortalTeammate:var(--colour-selected-pFaceImmortalTeammate,var(--colour-snakey-pFaceImmortalTeammate));--colour-pFaceImmortalOpponent:var(--colour-selected-pFaceImmortalOpponent,var(--colour-snakey-pFaceImmortalOpponent))}::backdrop{background-color:var(--colour-mainBg)}html{scroll-behavior:smooth}body{font-family:Trebuchet MS,Lucida Sans Unicode;font-weight:700;color:var(--colour-mainFg);text-align:center;transition:filter .5s ease-in-out}table{border-spacing:0}.screen{z-index:0;position:fixed;top:0;right:0;bottom:0;left:0}#background{z-index:-1000;background-color:var(--colour-mainBg)} \ No newline at end of file diff --git a/dist/server/index.js b/dist/server/index.js new file mode 100644 index 00000000..7ec91117 --- /dev/null +++ b/dist/server/index.js @@ -0,0 +1,2 @@ +var snakey3=function(e){var t={};function s(r){if(t[r])return t[r].exports;var a=t[r]={i:r,l:!1,exports:{}};return e[r].call(a.exports,a,a.exports,s),a.l=!0,a.exports}return s.m=e,s.c=t,s.d=function(e,t,r){s.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},s.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},s.t=function(e,t){if(1&t&&(e=s(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(s.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)s.d(r,a,function(t){return e[t]}.bind(null,a));return r},s.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return s.d(t,"a",t),t},s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},s.p="dist/server/",s(s.s=9)}([function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),function(e){e.NEVER="Never happens. See comment in source."}(t.SkErrors||(t.SkErrors={})),t.applyMixins=function(e,t){t.forEach(t=>{Object.getOwnPropertyNames(t.prototype).forEach(s=>{Object.defineProperty(e.prototype,s,Object.getOwnPropertyDescriptor(t.prototype,s))})})},function(e){e.HOST_REGISTRATION="/host-reg",e.GROUP_PREFIX="/group"}(t.SnakeyNsps||(t.SnakeyNsps={}));class r{}t.Player=r,function(e){let t;e.Family=Object.freeze({HUMAN:"HUMAN",CHASER:"CHASER"}),e.Family,function(e){e.NULL=void 0}(t=e.Id||(e.Id={})),e.MoveType=Object.freeze({NORMAL:"NORMAL",BOOST:"BOOST"}),e.MoveType}(r=t.Player||(t.Player={})),Object.freeze(r),Object.freeze(r.prototype);class a{}t.Lang=a,function(e){let t,s,r;!function(e){e.REGEXP=new RegExp("^[a-zA-Z0-9!@#$%^&*()-_=+;:'\"\\|,.<>/?]+$")}(t=e.Seq||(e.Seq={})),function(e){e.NULL=Object.freeze({char:"",seq:""})}(s=e.CharSeqPair||(e.CharSeqPair={})),function(e){e.SEQ="SEQ",e.CHAR="CHAR",e.WEIGHT="WEIGHT"}(r=e.BalancingScheme||(e.BalancingScheme={})),e.__RemapTemplates=Object.freeze({IDENTITY:e=>e,TO_LOWER:e=>e.toLowerCase()}),e.__RemapTemplates,e.FrontendDescs=Object.freeze([{id:"engl-low",module:"English",export:"Lowercase",numLeaves:26,remapFunc:e.__RemapTemplates.TO_LOWER,display:"English Lowercase (QWERTY)",blurb:""},{id:"engl-mix",module:"English",export:"MixedCase",numLeaves:52,remapFunc:e.__RemapTemplates.IDENTITY,display:"English Mixed-Case (QWERTY)",blurb:""},{id:"japn-hir",module:"Japanese",export:"Hiragana",numLeaves:71,remapFunc:e.__RemapTemplates.TO_LOWER,display:"Japanese Hiragana",blurb:""},{id:"japn-kat",module:"Japanese",export:"Katakana",numLeaves:70,remapFunc:e.__RemapTemplates.TO_LOWER,display:"Japanese Katakana",blurb:""},{id:"kore-dub",module:"Korean",export:"Dubeolsik",numLeaves:9177,remapFunc:e.__RemapTemplates.IDENTITY,display:"Korean Dubeolsik (두벌식 키보드)",blurb:"The most common keyboard layout, and South Korea's only Hangul standard since 1969. Consonants are on the left, and vowels on the right."},{id:"kore-sub",module:"Korean",export:"Sebeolsik",numLeaves:10206,remapFunc:e.__RemapTemplates.IDENTITY,display:"Korean Sebeolsik (세벌식 최종 키보드)",blurb:"Another Hangul keyboard layout used in South Korea, and the final Sebeolsik layout designed by Dr. Kong Byung Woo, hence the name. Syllable-initial consonants are on the right, final consonants on the left, and vowels in the middle. It is more ergonomic than the dubeolsik, but not widely used."},{id:"kore-rom",module:"Korean",export:"Romanization",numLeaves:3990,remapFunc:e.__RemapTemplates.TO_LOWER,display:"Korean Revised Romanization",blurb:"The Revised Romanization of Korean (국어의 로마자 표기법; 國語의 로마字 表記法) is the official South Korean language romanization system. It was developed by the National Academy of the Korean Language from 1995, and was released on 7 July 2000 by South Korea's Ministry of Culture and Tourism"}].map(e=>Object.freeze(e))),e.FrontendDescs,e.GET_FRONTEND_DESC_BY_ID=function(t){const s=e.FrontendDescs.find(e=>e.id===t);if(!s)throw new Error(`Frontend descriptor of language with id "${t}" not found.`);return s}}(a=t.Lang||(t.Lang={})),Object.freeze(a),Object.freeze(a.prototype)},function(e,t,s){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),function(e){let t,s,r,a;!function(e){e.SERVER="SERVER",e.ONLINE="ONLINE",e.OFFLINE="OFFLINE"}(t=e.Type||(e.Type={})),function(e){e.EVENT_NAME="game-create"}(s=e.CtorArgs||(e.CtorArgs={})),function(e){e.EVENT_NAME="game-reset"}(r=e.Serialization||(e.Serialization={})),function(e){e.PLAYING="PLAYING",e.PAUSED="PAUSED",e.OVER="OVER"}(a=e.Status||(e.Status={})),e.K=Object.freeze({HEALTH_UPDATE_CHANCE:.1,PCT_MOVES_THAT_ARE_BOOST:.05,HEALTH_EFFECT_FOR_DOWNED_PLAYER:.6,EVENT_RECORD_WRAPPING_BUFFER_LENGTH:128,EVENT_RECORD_FORWARD_WINDOW_LENGTH:64})}(r=t.Game||(t.Game={})),Object.freeze(r)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(0),a=s(25);class n extends r.Lang{constructor(e,t){if(super(),this.static=e,this.treeMap=a.LangSeqTreeNode.CREATE_TREE_MAP(t),this.leafNodes=this.treeMap.getLeafNodes(),this.leafNodes.length!==this.static.frontend.numLeaves)throw new Error(`maintenance required: the frontend constant for the language "${this.static.frontend.id}" needs to be updated to the correct, computed value, which is \`${this.leafNodes.length}\`.`)}get numLeaves(){return this.leafNodes.length}reset(){this.treeMap.reset()}getNonConflictingChar(e,t){this.leafNodes.sort(a.LangSeqTreeNode.LEAF_CMP[t]);let s=void 0;for(const r of this.leafNodes){const n=r.andNonRootParents();for(let t=0;te.startsWith(n[t].sequence));if(s){s===n[t].sequence?n.splice(0):n.splice(t);break}}if(n.length){n.sort(a.LangSeqTreeNode.PATH_CMP[t]),s=n[0];break}}if(!s)throw new Error("Invariants guaranteeing that a LangSeq canalways be shuffled-in were not met.");return s.chooseOnePair(t)}simpleView(){return Object.assign(Object.create(null),{id:this.static.frontend.id,displayName:this.static.frontend.display,root:this.treeMap.simpleView(),numLeaves:this.leafNodes.length})}}t.Lang=n,n=t.Lang||(t.Lang={}),Object.freeze(n),Object.freeze(n.prototype)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(1),a=s(7),n=s(21);t.PlayerSkeleton=n.PlayerSkeleton;const i=s(22);t.PlayerStatus=i.PlayerStatus;const o=s(8);t.Team=o.Team;class c extends n.PlayerSkeleton{constructor(e,t){if(super(e,t),!c.Username.REGEXP.test(t.username))throw new RangeError(`Username "${t.username}" does not match the required regular expression, "${c.Username.REGEXP.source}".`);this.familyId=t.familyId,this.teamId=t.teamId,this.username=t.username}reset(e){super.reset(e),this.status.reset(),this.lastAcceptedRequestId=a.PlayerActionEvent.INITIAL_REQUEST_ID,this.requestInFlight=!1}__abstractNotifyThatGameStatusBecamePlaying(){}__abstractNotifyThatGameStatusBecamePaused(){}__abstractNotifyThatGameStatusBecameOver(){}makeMovementRequest(e,t){if(this.game.status!==r.Game.Status.PLAYING)throw new Error("This is not a necessary precondition, but we're doing it anyway.");if(this.requestInFlight)throw new Error("Only one request should ever be in flight at a time.");this.requestInFlight=!0,this.game.processMoveRequest(new a.PlayerActionEvent.Movement(this.playerId,this.lastAcceptedRequestId,e,t))}get team(){return this.game.teams[this.teamId]}isTeamedWith(e){return this.team.members.includes(e)}}t.Player=c,function(e){let t,s;!function(e){e.REGEXP=/[a-zA-Z](?:[ ]?[a-zA-Z0-9:-]+?){4,}/}(t=e.Username||(e.Username={})),function(e){e.finalize=e=>{const t=Array.from(new Set(e.map(e=>e.teamId))).sort((e,t)=>e-t).reduce((e,t,s)=>(e[t]=s,e),[]);return e.slice().sort((e,s)=>t[e.teamId]-t[s.teamId]).map((e,s)=>Object.assign(e,{playerId:s,teamId:t[e.teamId]}))}}(s=e.CtorArgs||(e.CtorArgs={})),Object.freeze(s)}(c=t.Player||(t.Player={})),Object.freeze(c),Object.freeze(c.prototype)},function(e,t,s){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),function(e){e.EVENT_ID_REJECT=-1}(r=t.EventRecordEntry||(t.EventRecordEntry={})),Object.freeze(r)},function(e,t,s){"use strict";var r=this&&this.__awaiter||function(e,t,s,r){return new(s||(s=Promise))((function(a,n){function i(e){try{c(r.next(e))}catch(e){n(e)}}function o(e){try{c(r.throw(e))}catch(e){n(e)}}function c(e){var t;e.done?a(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,o)}c((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0});const a=s(17),n=s(1),i=s(18),o=s(20),c=s(3),h=s(23),l=s(4),u=s(7),d=s(24);class m extends d.GameManager{constructor(e,t){super(n.Game.Type.SERVER,{tileClass:i.Tile,playerStatusCtor:c.PlayerStatus},t),this.namespace=e;{const e={};t.playerDescs.forEach(t=>{if(t.familyId===c.Player.Family.HUMAN&&!t.socketId)throw new Error;e[t.playerId]=this.namespace.sockets[t.socketId]}),this.playerSockets=e}const s=this.players.filter(e=>e.familyId===c.Player.Family.HUMAN);s.map(e=>this.playerSockets[e.playerId]).forEach(e=>{e.removeAllListeners(u.PlayerActionEvent.EVENT_NAME.Movement),e.on(u.PlayerActionEvent.EVENT_NAME.Movement,this.processMoveRequest),e.removeAllListeners(u.PlayerActionEvent.EVENT_NAME.Bubble),e.on(u.PlayerActionEvent.EVENT_NAME.Bubble,this.processBubbleRequest)}),s.forEach(e=>{t.playerDescs.forEach(t=>{t.isALocalOperator=t.socketId===this.playerSockets[e.playerId].id}),this.playerSockets[e.playerId].emit(n.Game.CtorArgs.EVENT_NAME,t)},this)}__getGridImplementation(e){return o.Grid.getImplementation(e)}reset(){const e=Object.create(null,{reset:{get:()=>super.reset}});return r(this,void 0,void 0,(function*(){const t=e.reset.call(this);return this.namespace.emit(n.Game.Serialization.EVENT_NAME,this.serializeResetState()),t}))}__createOperatorPlayer(e){throw new TypeError("This should never be called for a ServerGame.")}__createArtifPlayer(e){return h.ArtificialPlayer.of(this,e)}setTimeout(e,t,...s){return a.setTimeout(e,t,s).unref()}cancelTimeout(e){clearTimeout(e)}executePlayerMoveEvent(e){super.executePlayerMoveEvent(e),e.eventId===l.EventRecordEntry.EVENT_ID_REJECT?this.playerSockets[e.playerId].emit(u.PlayerActionEvent.EVENT_NAME.Movement,e):this.namespace.emit(u.PlayerActionEvent.EVENT_NAME.Movement,e)}executePlayerBubbleEvent(e){super.executePlayerBubbleEvent(e),e.eventId===l.EventRecordEntry.EVENT_ID_REJECT?this.playerSockets[e.playerId].emit(u.PlayerActionEvent.EVENT_NAME.Bubble,e):this.namespace.emit(u.PlayerActionEvent.EVENT_NAME.Bubble,e)}}t.ServerGame=m,Object.freeze(m),Object.freeze(m.prototype)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});class r{constructor(e){this.source=e}at(...e){return this.source.__getTileAt(...e)}destsFrom(...e){return new a(this.source.__getTileDestsFrom(...e))}sourcesTo(...e){return new a(this.source.__getTileSourcesTo(...e))}}t.TileGetter=r,Object.freeze(r),Object.freeze(r.prototype);class a{constructor(e){this.contents=e}get occupied(){return this.contents=this.contents.filter(e=>e.isOccupied),this}get unoccupied(){return this.contents=this.contents.filter(e=>!e.isOccupied),this}get get(){return this.contents}}Object.freeze(a),Object.freeze(a.prototype)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(4);var a;!function(e){e.INITIAL_REQUEST_ID=-1,e.EVENT_NAME=Object.freeze({Bubble:"player-bubble",Movement:"player-movement"});class t{constructor(e,t){this.eventId=r.EventRecordEntry.EVENT_ID_REJECT,this.affectedNeighbours=void 0,this.playerId=e,this.playerLastAcceptedRequestId=t}}e.Bubble=t;e.Movement=class extends t{constructor(e,t,s,r){super(e,t),this.newPlayerHealth=void 0,this.tileHealthModDescs=void 0,this.destModDesc={coord:s.coord,lastKnownUpdateId:s.lastKnownUpdateId,newCharSeqPair:void 0,newFreeHealth:void 0},this.moveType=r}}}(a=t.PlayerActionEvent||(t.PlayerActionEvent={})),Object.freeze(a)},function(e,t,s){"use strict";var r,a=this&&this.__classPrivateFieldSet||function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},n=this&&this.__classPrivateFieldGet||function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};Object.defineProperty(t,"__esModule",{value:!0});class i{constructor(e,t){if(r.set(this,void 0),0===t.length)throw new Error("teams must have at least one member.");this.id=e,this.members=t,a(this,r,this.members.every(e=>e.status.noCheckGameOver)?i.ElimOrder.IMMORTAL:i.ElimOrder.STANDING)}reset(){this.elimOrder!==i.ElimOrder.IMMORTAL&&(this.elimOrder=i.ElimOrder.STANDING)}get elimOrder(){return n(this,r)}set elimOrder(e){if(this.elimOrder===i.ElimOrder.IMMORTAL)throw new Error("Cannot change the elimination status of an immortal team.");a(this,r,e)}}t.Team=i,r=new WeakMap,function(e){let t;!function(e){e.IMMORTAL=-1,e.STANDING=0}(t=e.ElimOrder||(e.ElimOrder={}))}(i=t.Team||(t.Team={})),Object.freeze(i),Object.freeze(i.prototype)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(10);t.server=new r.SnakeyServer},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(11),a=s(12),n=s(13),i=s(14),o=s(15),c=s(0),h=s(16);class l{constructor(e=l.DEFAULT_PORT,t){this.app=i(),this.http=n.createServer({},this.app),this.io=o(this.http);const s=a.resolve(__dirname,"../..");this.app.disable("x-powered-by"),this.app.get("/",(e,t)=>{t.sendFile(a.resolve(s,"index.html"))}),this.app.use("/dist",i.static(a.resolve(s,"dist"))),this.app.use("/assets",i.static(a.resolve(s,"assets"))),this.http.listen({port:e,host:t},()=>{const e=this.http.address();console.log(`Server mounted to: \`${e.family}${e.address}${e.port}\`.`)}),this.io.of(c.SnakeyNsps.HOST_REGISTRATION).on("connection",this.onHostsConnection.bind(this))}onHostsConnection(e){e.on(h.GroupSession.CtorArgs.EVENT_NAME,t=>{const s=this.createUniqueSessionName(t.groupName);if(!s)return void e.emit(h.GroupSession.CtorArgs.EVENT_NAME,new h.GroupSession.CtorArgs(""));t.groupName=s;const r=this.io.of(s);this.allGroupSessions.set(r.name,new h.GroupSession(r,()=>{this.allGroupSessions.delete(r.name)},t.initialTtl)),e.emit(h.GroupSession.CtorArgs.EVENT_NAME,t)})}createUniqueSessionName(e){if(!h.GroupSession.SessionName.REGEXP.test(e))return;const t=`${c.SnakeyNsps.GROUP_PREFIX}/${e}`;return this.allGroupSessions.has(t)?void 0:t}}t.SnakeyServer=l,function(e){e.DEFAULT_PORT=8080,e.chooseIPv4Address=()=>Object.values(r.networkInterfaces()).flat().filter(e=>!e.internal&&"IPv4"===e.family).map(e=>e.address)}(l=t.SnakeyServer||(t.SnakeyServer={})),Object.freeze(l),Object.freeze(l.prototype)},function(e,t){e.exports=require("os")},function(e,t){e.exports=require("path")},function(e,t){e.exports=require("http")},function(e,t){e.exports=require("express")},function(e,t){e.exports=require("socket.io")},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(5);var a=s(5);t.ServerGame=a.ServerGame;class n{constructor(e,t,s){this.namespace=e,this.currentGame=void 0,this.initialTtlTimeout=setTimeout(()=>{0===Object.keys(this.namespace.connected).length&&this.terminate()},1e3*s).unref(),this.deleteExternalRefs=t,this.namespace.on("connection",this.onConnection)}onConnection(e){console.log("A user has connected."),e.join(n.RoomNames.MAIN),e.teamId=void 0,e.updateId=0,0===Object.keys(this.namespace.connected).length&&(clearTimeout(this.initialTtlTimeout),this.sessionHost=e),e.on("disconnect",(...t)=>{e===this.sessionHost&&this.terminate()})}terminate(){delete this.currentGame;const e=this.namespace;e.removeAllListeners("connect"),e.removeAllListeners("connection"),Object.values(e.connected).forEach(e=>{e.disconnect()}),e.removeAllListeners(),delete e.server.nsps[e.name],this.deleteExternalRefs()}createGameInstance(e,t){const s={undefinedUsername:Object.values(this.sockets).filter(e=>!e.username).map(e=>e.id),undefinedTeamId:Object.values(this.sockets).filter(e=>!e.teamId).map(e=>e.id)};if(s.undefinedUsername.length||s.undefinedTeamId.length)return s;this.currentGame=new r.ServerGame(this.namespace,{coordSys:e,gridDimensions:t,averageFreeHealthPerTile:void 0,langId:void 0,langBalancingScheme:void 0,playerDescs:Object.assign({},Object.values(this.sockets).map(e=>({isALocalOperator:!1,familyId:"HUMAN",teamId:e.teamId,socketId:e.id,username:e.username,noCheckGameOver:!1,familyArgs:{}})))})}get sockets(){return this.namespace.sockets}}t.GroupSession=n,function(e){let t,s;!function(e){e.REGEXP=/[a-zA-Z](?:[a-zA-Z0-9:-]+?){4,}/}(t=e.SessionName||(e.SessionName={})),function(e){e.MAIN="main"}(s=e.RoomNames||(e.RoomNames={}));class r{constructor(e,t=r.DEFAULT_INITIAL_TTL){this.groupName=e,this.initialTtl=t}}r.EVENT_NAME="group-session-create",r.DEFAULT_INITIAL_TTL=60,e.CtorArgs=r}(n=t.GroupSession||(t.GroupSession={})),Object.freeze(n),Object.freeze(n.prototype)},function(e,t){e.exports=require("timers")},function(e,t,s){"use strict";var r,a,n,i,o=this&&this.__classPrivateFieldSet||function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},c=this&&this.__classPrivateFieldGet||function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};Object.defineProperty(t,"__esModule",{value:!0});const h=s(0),l=s(19);t.Coord=l.Coord;class u{constructor(e){r.set(this,void 0),a.set(this,void 0),n.set(this,void 0),i.set(this,void 0),this.coord=e,o(this,r,h.Player.Id.NULL)}reset(){this.evictOccupant(),this.lastKnownUpdateId=0,this.freeHealth=0,this.setLangCharSeqPair(h.Lang.CharSeqPair.NULL)}__setOccupant(e,t){o(this,r,e)}get isOccupied(){return this.occupantId!==h.Player.Id.NULL}evictOccupant(){o(this,r,h.Player.Id.NULL)}get occupantId(){return c(this,r)}get freeHealth(){return c(this,a)}set freeHealth(e){o(this,a,e)}setLangCharSeqPair(e){o(this,n,e.char),o(this,i,e.seq)}get langChar(){return c(this,n)}get langSeq(){return c(this,i)}}t.Tile=u,r=new WeakMap,a=new WeakMap,n=new WeakMap,i=new WeakMap,Object.freeze(u),Object.freeze(u.prototype)},function(e,t,s){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),function(e){let t;!function(e){e.EUCLID2="EUCLID2",e.BEEHIVE="BEEHIVE"}(t=e.System||(e.System={}));class s{constructor(e){}}e.Abstract=s,function(t){class s extends e.Abstract{}t.Mathy=s}(s=e.Abstract||(e.Abstract={})),Object.freeze(s),Object.freeze(s.prototype)}(r=t.Coord||(t.Coord={})),Object.freeze(r)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(6);class a{constructor(e){this.static=e.gridClass,this.dimensions=e.dimensions,this.tile=new r.TileGetter(this)}get area(){return this.static.getArea(this.dimensions)}reset(){this.forEachTile(e=>e.reset())}check(){}getRandomCoord(){return this.static.getRandomCoord(this.dimensions)}}t.Grid=a,function(e){e.getImplementation=t=>e.__Constructors[t]}(a=t.Grid||(t.Grid={}))},function(e,t,s){"use strict";var r,a=this&&this.__classPrivateFieldSet||function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},n=this&&this.__classPrivateFieldGet||function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};Object.defineProperty(t,"__esModule",{value:!0});const i=s(0),o=s(1),c=s(6);class h extends i.Player{constructor(e,t){if(super(),r.set(this,void 0),Math.trunc(t.playerId)!==t.playerId)throw new RangeError("Player ID's must be integer values.");this.playerId=t.playerId,this.isALocalOperator=t.isALocalOperator,this.game=e,this.status=new this.game.__playerStatusCtor(this,t.noCheckGameOver),this.tile=new c.TileGetter(new h.TileGetterSource(this))}__afterAllPlayersConstruction(){this.status.__afterAllPlayersConstruction()}reset(e){a(this,r,e),this.hostTile.__setOccupant(this.playerId,this.status.immigrantInfo)}get coord(){return this.hostTile.coord}get hostTile(){return n(this,r)}onGoBesideOtherPlayer(){}moveTo(e){if(this.hostTile.occupantId!==this.playerId){if(this.game.gameType!==o.Game.Type.ONLINE)throw new Error("Linkage between player and occupied tile disagrees.")}else this.hostTile.evictOccupant();if(e.isOccupied){if(this.game.gameType!==o.Game.Type.ONLINE)throw new Error("Only one player can occupy a tile at a time.")}else a(this,r,e),e.__setOccupant(this.playerId,this.status.immigrantInfo)}}t.PlayerSkeleton=h,r=new WeakMap,function(e){class t{constructor(e){this.player=e}__getTileAt(){return this.player.game.grid.tile.at(this.player.coord)}__getTileDestsFrom(){return this.player.game.grid.tile.destsFrom(this.player.coord).get}__getTileSourcesTo(){return this.player.game.grid.tile.sourcesTo(this.player.coord).get}}e.TileGetterSource=t,Object.freeze(t),Object.freeze(t.prototype)}(h=t.PlayerSkeleton||(t.PlayerSkeleton={})),Object.freeze(h),Object.freeze(h.prototype)},function(e,t,s){"use strict";var r,a=this&&this.__classPrivateFieldGet||function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)},n=this&&this.__classPrivateFieldSet||function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s};Object.defineProperty(t,"__esModule",{value:!0});const i=s(8);class o{constructor(e,t){r.set(this,void 0),this.player=e,this.noCheckGameOver=t}reset(){this.health=0}__afterAllPlayersConstruction(){}get immigrantInfo(){}get health(){return a(this,r)}set health(e){const t=this.isDowned;if(n(this,r,e),t||!this.isDowned||this.noCheckGameOver)return;const s=this.player.team,a=this.player.game.teams;if(s.elimOrder===i.Team.ElimOrder.STANDING&&s.members.every(e=>e.status.noCheckGameOver||e.status.isDowned)){const e=1+a.filter(e=>e.elimOrder!==i.Team.ElimOrder.STANDING).length;s.elimOrder=1+a.filter(e=>e.elimOrder!==i.Team.ElimOrder.STANDING&&e.elimOrder!==i.Team.ElimOrder.IMMORTAL).length,e===a.length&&this.player.game.statusBecomeOver()}}get isDowned(){return this.health<0}}t.PlayerStatus=o,r=new WeakMap,Object.freeze(o),Object.freeze(o.prototype)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(1),a=s(3);class n extends a.Player{constructor(e,t){if(super(e,t),e.gameType===r.Game.Type.ONLINE)throw new TypeError("OnlineGames should be using regular Players instead.")}__abstractNotifyThatGameStatusBecamePlaying(){this.movementContinueWithInitialDelay()}__abstractNotifyThatGameStatusBecamePaused(){this.game.cancelTimeout(this.scheduledMovementCallbackId),this.scheduledMovementCallbackId=void 0}__abstractNotifyThatGameStatusBecameOver(){this.game.cancelTimeout(this.scheduledMovementCallbackId),this.scheduledMovementCallbackId=void 0}movementContinue(){this.makeMovementRequest(this.game.grid.getUntToward(this.coord,this.computeDesiredDestination()),this.getNextMoveType()),this.movementContinueWithInitialDelay()}movementContinueWithInitialDelay(){this.scheduledMovementCallbackId=this.game.setTimeout(()=>this.movementContinue(),this.computeNextMovementTimer())}}t.ArtificialPlayer=n,function(e){e.of=(t,s)=>{const r=s.familyId;return new e.__Constructors[r](t,s)}}(n=t.ArtificialPlayer||(t.ArtificialPlayer={}))},function(e,t,s){"use strict";var r,a,n,i=this&&this.__awaiter||function(e,t,s,r){return new(s||(s=Promise))((function(a,n){function i(e){try{c(r.next(e))}catch(e){n(e)}}function o(e){try{c(r.throw(e))}catch(e){n(e)}}function c(e){var t;e.done?a(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,o)}c((r=r.apply(e,t||[])).next())}))},o=this&&this.__classPrivateFieldSet||function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},c=this&&this.__classPrivateFieldGet||function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};Object.defineProperty(t,"__esModule",{value:!0});const h=s(2),l=s(1),u=s(3),d=s(26),m=s(28);class p extends d.GameEvents{constructor(e,t,i){super(e,t,i),r.set(this,void 0),a.set(this,void 0),n.set(this,void 0),this.averageFreeHealth=i.averageFreeHealthPerTile*this.grid.area,this.averageFreeHealthPerTile=i.averageFreeHealthPerTile,o(this,a,new Set),o(this,n,Promise.resolve().then(()=>s(29)(`./${this.langFrontend.module}.ts`)).then(e=>{this.lang=e[this.langFrontend.module][this.langFrontend.export].getInstance();const t=this.grid.static.getAmbiguityThreshold();if(this.lang.numLeavese.playerId))}reset(){const e=Object.create(null,{reset:{get:()=>super.reset}});return i(this,void 0,void 0,(function*(){yield e.reset.call(this),o(this,r,0),c(this,a).clear(),yield c(this,n),this.lang.reset(),this.grid.forEachTile(e=>{e.setLangCharSeqPair(this.dryRunShuffleLangCharSeqAt(e))}),this.teams.forEach(e=>e.reset());const t=this.grid.static.getSpawnCoords(this.teams.map(e=>e.members.length),this.grid.dimensions);return this.teams.forEach((e,s)=>{e.members.forEach((e,r)=>{e.reset(this.grid.tile.at(t[s][r]))})}),this.scoreInfo.reset(),Promise.resolve()}))}dryRunShuffleLangCharSeqAt(e){e.setLangCharSeqPair(h.Lang.CharSeqPair.NULL);const t=Array.from(new Set(this.grid.tile.sourcesTo(e.coord).get.flatMap(e=>this.grid.tile.destsFrom(e.coord).get)));return this.lang.getNonConflictingChar(t.map(e=>e.langSeq).filter(e=>e),this.langBalancingScheme)}get currentFreeHealth(){return c(this,r)}get freeHealthTiles(){return c(this,a)}dryRunSpawnFreeHealth(e){let t=this.averageFreeHealth-this.currentFreeHealth;if(t<=0)return;const s=[];for(;t>0;){let r;do{r=this.grid.tile.at(this.grid.getRandomCoord())}while(r.isOccupied||s.find(e=>r.coord.equals(e.coord)));const a=1;if(Math.random()r.coord.equals(e.coord)))?t.newFreeHealth=(t.newFreeHealth||0)+a:s.push({coord:r.coord,lastKnownUpdateId:1+r.lastKnownUpdateId,newCharSeqPair:void 0,newFreeHealth:r.freeHealth+a})}t-=a}return s}getHealthCostOfBoost(){return this.averageFreeHealthPerTile/l.Game.K.PCT_MOVES_THAT_ARE_BOOST}executeTileModEvent(e,t=!0){Object.freeze(e);const s=this.grid.tile.at(e.coord);if(e.lastKnownUpdateId!==1+s.lastKnownUpdateId)throw new Error("this never happens. see comment in source.");return o(this,r,c(this,r)+(e.newFreeHealth-s.freeHealth)),0===e.newFreeHealth?c(this,a).delete(s):c(this,a).add(s),super.executeTileModEvent(e,t),s}managerCheckGamePlayingRequest(e){if(this.status!==l.Game.Status.PLAYING)return;const t=this.players[e.playerId];if(!t)throw new Error("No such player exists.");if(e.playerLastAcceptedRequestId!==t.lastAcceptedRequestId)throw new RangeError(e.playerLastAcceptedRequestIde[0].length-t[0].length).forEach(e=>{s.addCharMapping(...e)}),s.finalize(),s}finalize(){this.validateConstruction(),Object.freeze(this.characters),Object.freeze(this.children),this.children.forEach(e=>e.finalize())}validateConstruction(){if(!this.sequence.startsWith(this.parent.sequence))throw new Error("Child node's sequence must start with that of its parent.")}reset(){this.children.forEach(e=>e.reset()),this.inheritingHitCount=0,this.inheritingWeightedHitCount=0,this.characters.forEach(e=>{e.reset();for(let t=0;t<10*Math.random();t++)this.incrementNumHits(e)})}addCharMapping(e,t){if(!r.Lang.Seq.REGEXP.test(e))throw new RangeError(`Mapping-sequence "${e}" did not match the required regular expression "${r.Lang.Seq.REGEXP.source}".`);if(0===t.length)throw new Error("Must not make mapping without written characters.");let s=this;{let t=this;for(;t;)s=t,t=t.children.find(t=>e.startsWith(t.sequence))}if(s.sequence===e)throw new Error(`Mappings for all written-characters with a commoncorresponding typeable-sequence should be registered together,but an existing mapping for the sequence "${e}" was found.`);s.children.push(new a(s,e,t))}chooseOnePair(e){const t=this.characters.slice(0).sort(n.CMP[e]).shift(),s={char:t.char,seq:this.sequence};return this.incrementNumHits(t),s}incrementNumHits(e){e.incrementNumHits(),this.__recursiveIncrementNumHits(e.weightInv)}__recursiveIncrementNumHits(e){this.inheritingHitCount+=1,this.inheritingWeightedHitCount+=e,this.children.forEach(t=>t.__recursiveIncrementNumHits(e))}get personalHitCount(){return this.inheritingHitCount-this.parent.inheritingHitCount}get averageCharHitCount(){return this.characters.reduce((e,t)=>e+t.hitCount,0)/this.characters.length}get personalWeightedHitCount(){return this.inheritingWeightedHitCount-this.parent.inheritingWeightedHitCount}andNonRootParents(){const e=[];let t=this;for(;t.parent;)e.push(t),t=t.parent;return e}getLeafNodes(){const e=[];return this.__recursiveGetLeafNodes(e),e}__recursiveGetLeafNodes(e){this.children.length?this.children.forEach(t=>{t.__recursiveGetLeafNodes(e)}):e.push(this)}simpleView(){let e=this.characters.map(e=>e.simpleView());return Object.assign(Object.create(null),{seq:this.sequence,chars:1===e.length?e[0]:e,hits:this.personalHitCount,kids:this.children.map(e=>e.simpleView()),__proto__:void 0})}}t.LangSeqTreeNode=a,a.LEAF_CMP=Object.freeze({[r.Lang.BalancingScheme.SEQ]:(e,t)=>e.inheritingHitCount-t.inheritingHitCount,[r.Lang.BalancingScheme.CHAR]:(e,t)=>e.inheritingHitCount-t.inheritingHitCount,[r.Lang.BalancingScheme.WEIGHT]:(e,t)=>e.inheritingWeightedHitCount-t.inheritingWeightedHitCount}),a.PATH_CMP=Object.freeze({[r.Lang.BalancingScheme.SEQ]:(e,t)=>e.personalHitCount-t.personalHitCount,[r.Lang.BalancingScheme.CHAR]:(e,t)=>e.averageCharHitCount-t.averageCharHitCount,[r.Lang.BalancingScheme.WEIGHT]:(e,t)=>e.personalWeightedHitCount-t.personalWeightedHitCount}),function(e){e.Root=class extends e{constructor(){super(void 0,"",[])}validateConstruction(){}chooseOnePair(e){throw new TypeError("Must never hit on the root.")}get personalHitCount(){throw new TypeError("Must never hit on the root.")}get personalWeightedHitCount(){throw new TypeError("Must never hit on the root.")}andNonRootParents(){throw new TypeError}simpleView(){return this.children.map(e=>e.simpleView())}}}(a=t.LangSeqTreeNode||(t.LangSeqTreeNode={})),Object.freeze(a),Object.freeze(a.prototype);class n{constructor(e,t){if(t<=0)throw new RangeError(`All weights must be positive, but we were passed the value "${t}" for the character "${e}".`);this.char=e,this.weightInv=1/t}reset(){this.hitCount=0,this.weightedHitCount=0}incrementNumHits(){this.hitCount+=1,this.weightedHitCount+=this.weightInv}simpleView(){return Object.assign(Object.create(null),{char:this.char,hits:this.hitCount})}}n.CMP=Object.freeze({[r.Lang.BalancingScheme.SEQ]:(e,t)=>e.hitCount-t.hitCount,[r.Lang.BalancingScheme.CHAR]:(e,t)=>e.hitCount-t.hitCount,[r.Lang.BalancingScheme.WEIGHT]:(e,t)=>e.weightedHitCount-t.weightedHitCount}),Object.freeze(n),Object.freeze(n.prototype)},function(e,t,s){"use strict";var r,a=this&&this.__classPrivateFieldSet||function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},n=this&&this.__classPrivateFieldGet||function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};Object.defineProperty(t,"__esModule",{value:!0});const i=s(1),o=s(4),c=s(27);class h extends c.GameBase{constructor(e,t,s){super(e,t,s),r.set(this,void 0),this.eventRecordBitmap=[]}reset(){const e=super.reset();return this.eventRecordBitmap.fill(!1,0,i.Game.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH),a(this,r,0),e}get nextUnusedEventId(){return n(this,r)}recordEvent(e){const t=e.eventId,s=t%i.Game.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH;if(t===o.EventRecordEntry.EVENT_ID_REJECT)throw new TypeError("Do not try to record events for rejected requests.");if(t<0||t!==Math.trunc(t))throw new RangeError("Event ID's must only be assigned positive, integer values.");if(this.eventRecordBitmap[s])throw new Error("Event ID's must be assigned unique values.");this.eventRecordBitmap[s]=!0,this.eventRecordBitmap[(t+i.Game.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH-i.Game.K.EVENT_RECORD_FORWARD_WINDOW_LENGTH)%i.Game.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH]=!1,a(this,r,+n(this,r)+1)}executeTileModEvent(e,t=!0){Object.freeze(e);const s=this.grid.tile.at(e.coord);if(s.lastKnownUpdateId>e.lastKnownUpdateId)return s;if(s.lastKnownUpdateId===e.lastKnownUpdateId)throw new Error("never.");return e.newCharSeqPair&&(s.setLangCharSeqPair(e.newCharSeqPair),t&&this.operators.filter(e=>e.tile.destsFrom().get.includes(s)).forEach(e=>e.seqBufferAcceptKey(""))),s.lastKnownUpdateId=e.lastKnownUpdateId,s.freeHealth=e.newFreeHealth,s}executePlayerMoveEvent(e){var t;const s=this.players[e.playerId],r=e.playerLastAcceptedRequestId-s.lastAcceptedRequestId;if(e.eventId===o.EventRecordEntry.EVENT_ID_REJECT)return void(0===r&&(s.requestInFlight=!1));this.recordEvent(e);const a=this.executeTileModEvent(e.destModDesc,s!==this.currentOperator);if(null===(t=e.tileHealthModDescs)||void 0===t||t.forEach(e=>{this.executeTileModEvent(e)}),r>1){if(s===this.currentOperator)throw new Error("This never happens. See comment in source.")}else{if(s.requestInFlight=!1,!(s===this.currentOperator?1===r:r<=1))throw new Error("This never happens. See comment in source");s.status.health=e.newPlayerHealth.health,s.moveTo(a),s.lastAcceptedRequestId=e.playerLastAcceptedRequestId}}executePlayerBubbleEvent(e){this.players[e.playerId].requestInFlight=!1,e.eventId!==o.EventRecordEntry.EVENT_ID_REJECT&&this.recordEvent(e)}}t.GameEvents=h,r=new WeakMap,Object.freeze(h),Object.freeze(h.prototype)},function(e,t,s){"use strict";var r,a,n=this&&this.__classPrivateFieldSet||function(e,t,s){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,s),s},i=this&&this.__classPrivateFieldGet||function(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)};Object.defineProperty(t,"__esModule",{value:!0});const o=s(1),c=s(0),h=s(3);class l{constructor(e,t,s){r.set(this,void 0),a.set(this,void 0),this.gameType=e;const n=this.__getGridImplementation(s.coordSys);if(this.grid=new n({gridClass:n,tileClass:t.tileClass,coordSys:s.coordSys,dimensions:s.gridDimensions}),this.langFrontend=c.Lang.GET_FRONTEND_DESC_BY_ID(s.langId),this.__playerStatusCtor=t.playerStatusCtor,this.players=this.createPlayers(s),this.operators=this.players.filter(e=>e.isALocalOperator),this.currentOperator=this.operators[0],this.operators.some(e=>e.teamId!==this.operators[0].teamId))throw new Error("All local operators must be on the same team.");{const e=[];if(this.players.forEach(t=>{e[t.teamId]||(e[t.teamId]=[]),e[t.teamId].push(t)}),this.teams=e.map((e,t)=>new h.Team(t,e)),this.teams.every(e=>e.id===h.Team.ElimOrder.IMMORTAL))throw new Error("All teams are immortal. The game will never end.")}this.players.forEach(e=>e.__afterAllPlayersConstruction())}reset(){return this.grid.reset(),n(this,a,o.Game.Status.PAUSED),Promise.resolve()}createPlayers(e){const t=e.playerDescs=this.gameType===o.Game.Type.ONLINE?e.playerDescs:h.Player.CtorArgs.finalize(e.playerDescs);return Object.freeze(t.map(e=>e.familyId===h.Player.Family.HUMAN?e.isALocalOperator?this.__createOperatorPlayer(e):new h.Player(this,e):this.__createArtifPlayer(e)))}serializeResetState(){const e=[],t=this.players.map(e=>e.coord),s=[];return this.grid.forEachTile(t=>{e.push({char:t.langChar,seq:t.langSeq}),t.freeHealth&&s.push({coord:t.coord,health:t.freeHealth})}),{csps:e,playerCoords:t,healthCoords:s}}deserializeResetState(e){{let t=0;this.grid.forEachTile(s=>{s.setLangCharSeqPair(e.csps[t++]),s.lastKnownUpdateId=1})}e.playerCoords.forEach((e,t)=>{this.players[t].moveTo(this.grid.tile.at(e))}),e.healthCoords.forEach(e=>{this.grid.tile.at(e.coord).freeHealth=e.health})}get currentOperator(){return i(this,r)}set currentOperator(e){e&&this.currentOperator!==e&&this.operators.includes(e)&&(n(this,r,e),e.__abstractNotifyBecomeCurrent())}get status(){return i(this,a)}statusBecomePlaying(){if(this.status!==o.Game.Status.PLAYING){if(this.status!==o.Game.Status.PAUSED)throw new Error("Can only resume a game that is currently paused.");this.players.forEach(e=>{e.__abstractNotifyThatGameStatusBecamePlaying()}),this.__abstractStatusBecomePlaying(),n(this,a,o.Game.Status.PLAYING)}else console.log("[statusBecomePlaying]: Game is already playing")}statusBecomePaused(){if(this.status!==o.Game.Status.PAUSED){if(this.status!==o.Game.Status.PLAYING)throw new Error("Can only pause a game that is currently playing.");this.players.forEach(e=>{e.__abstractNotifyThatGameStatusBecamePaused()}),this.__abstractStatusBecomePaused(),n(this,a,o.Game.Status.PAUSED)}else console.log("[statusBecomePaused]: Game is already paused")}statusBecomeOver(){if(this.status!==o.Game.Status.PLAYING)throw new Error("Can only end a game that is currently playing.");this.players.forEach(e=>{e.__abstractNotifyThatGameStatusBecameOver()}),this.__abstractStatusBecomeOver(),n(this,a,o.Game.Status.OVER),console.log("game is over!")}__abstractStatusBecomePlaying(){}__abstractStatusBecomePaused(){}__abstractStatusBecomeOver(){}}t.GameBase=l,r=new WeakMap,a=new WeakMap,Object.freeze(l),Object.freeze(l.prototype)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(0);class a{constructor(e){const t=[];for(const s of e)t[s]=new a.Entry;this.entries=t}reset(){for(const e of this.entries)e.reset()}}t.ScoreInfo=a,function(e){class t{constructor(){this.moveCounts={}}reset(){this.totalHealthPickedUp=0,Object.getOwnPropertyNames(r.Player.MoveType).forEach(e=>{this.moveCounts[e]=0})}}e.Entry=t,Object.freeze(t),Object.freeze(t.prototype)}(a=t.ScoreInfo||(t.ScoreInfo={})),Object.freeze(a),Object.freeze(a.prototype)},function(e,t,s){var r={"./Emote.ts":30,"./English.ts":31,"./Japanese.ts":32,"./Korean.ts":33,"./Morse.ts":34};function a(e){var t=n(e);return s(t)}function n(e){if(!s.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}a.keys=function(){return Object.keys(r)},a.resolve=n,e.exports=a,a.id=29},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(2);var a;!function(e){class t extends r.Lang{}e.GitHub=t,Object.freeze(t),Object.freeze(t.prototype)}(a=t.Emote||(t.Emote={})),Object.freeze(a)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(2);var a;!function(e){class t extends r.Lang{constructor(){super(t,Object.entries(a).reduce((e,t)=>{const s=t[0],r=t[0],a=t[1];return e[s]={seq:r,weight:a},e},{}))}static getInstance(){return this.SINGLETON||(this.SINGLETON=new t),this.SINGLETON}}t.SINGLETON=void 0,t.frontend=r.Lang.GET_FRONTEND_DESC_BY_ID("engl-low"),e.Lowercase=t,Object.seal(t),Object.freeze(t.prototype);class s extends r.Lang{constructor(){let e={};const t=t=>{e=Object.entries(a).reduce((e,s)=>{const r=t(s[0]),a=t(s[0]),n=s[1];return e[r]={seq:a,weight:n},e},e)};t(e=>e.toLowerCase()),t(e=>e.toUpperCase()),super(s,e)}static getInstance(){return this.SINGLETON||(this.SINGLETON=new s),this.SINGLETON}}s.SINGLETON=void 0,s.frontend=r.Lang.GET_FRONTEND_DESC_BY_ID("engl-mix"),e.MixedCase=s,Object.seal(s),Object.freeze(s.prototype);const a=Object.freeze({a:8.167,b:1.492,c:2.202,d:4.253,e:12.702,f:2.228,g:2.015,h:6.094,i:6.966,j:.153,k:1.292,l:4.025,m:2.406,n:6.749,o:7.507,p:1.929,q:.095,r:5.987,s:6.327,t:9.356,u:2.758,v:.978,w:2.56,x:.15,y:1.994,z:.077})}(a=t.English||(t.English={})),Object.seal(a)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(2);var a;!function(e){class t extends r.Lang{constructor(){super(t,t.INITIALIZER)}static getInstance(){return this.SINGLETON||(this.SINGLETON=new t,this.INITIALIZER=void 0),this.SINGLETON}}t.SINGLETON=void 0,t.frontend=r.Lang.GET_FRONTEND_DESC_BY_ID("japn-hir"),t.INITIALIZER=Object.freeze({"の":{seq:"no",weight:1918313},"に":{seq:"ni",weight:1108840},"た":{seq:"ta",weight:1067566},"い":{seq:"i",weight:1060284},"は":{seq:"ha",weight:937811},"を":{seq:"wo",weight:936356},"と":{seq:"to",weight:927938},"る":{seq:"ru",weight:916652},"が":{seq:"ga",weight:860742},"し":{seq:"shi",weight:848132},"で":{seq:"de",weight:764834},"て":{seq:"te",weight:758316},"な":{seq:"na",weight:720156},"か":{seq:"ka",weight:537294},"れ":{seq:"re",weight:450805},"ら":{seq:"ra",weight:42329},"も":{seq:"mo",weight:396142},"う":{seq:"u",weight:352965},"す":{seq:"su",weight:340654},"り":{seq:"ri",weight:333999},"こ":{seq:"ko",weight:312227},"だ":{seq:"da",weight:280911},"ま":{seq:"ma",weight:278599},"さ":{seq:"sa",weight:258960},"き":{seq:"ki",weight:233505},"め":{seq:"me",weight:223806},"く":{seq:"ku",weight:221960},"あ":{seq:"a",weight:204256},"け":{seq:"ke",weight:199362},"ど":{seq:"do",weight:196555},"ん":{seq:"nn",weight:190068},"え":{seq:"e",weight:163664},"よ":{seq:"yo",weight:154206},"つ":{seq:"tsu",weight:153999},"や":{seq:"ya",weight:146156},"そ":{seq:"so",weight:131611},"わ":{seq:"wa",weight:123077},"ち":{seq:"chi",weight:99183},"み":{seq:"mi",weight:89264},"せ":{seq:"se",weight:83444},"ろ":{seq:"ro",weight:73467},"ば":{seq:"ba",weight:72228},"お":{seq:"o",weight:65870},"じ":{seq:"ji",weight:56857},"べ":{seq:"be",weight:56005},"ず":{seq:"zu",weight:53256},"げ":{seq:"ge",weight:49126},"ほ":{seq:"ho",weight:48752},"へ":{seq:"he",weight:47013},"び":{seq:"bi",weight:32312},"む":{seq:"mu",weight:31212},"ご":{seq:"go",weight:26965},"ね":{seq:"ne",weight:23490},"ぶ":{seq:"bu",weight:23280},"ぐ":{seq:"gu",weight:21549},"ぎ":{seq:"gi",weight:19865},"ひ":{seq:"hi",weight:19148},"ょ":{seq:"yo",weight:14425},"づ":{seq:"du",weight:13125},"ぼ":{seq:"bo",weight:12402},"ざ":{seq:"za",weight:12108},"ふ":{seq:"fu",weight:11606},"ゃ":{seq:"ya",weight:11522},"ぞ":{seq:"zo",weight:10047},"ゆ":{seq:"yu",weight:8486},"ぜ":{seq:"ze",weight:6893},"ぬ":{seq:"nu",weight:5124},"ぱ":{seq:"pa",weight:4349},"ゅ":{seq:"yu",weight:2755},"ぴ":{seq:"pi",weight:1608},"ぽ":{seq:"po",weight:1315},"ぷ":{seq:"pu",weight:986},"ぺ":{seq:"pe",weight:477},"ぢ":{seq:"di",weight:82}}),e.Hiragana=t,Object.seal(t),Object.freeze(t.prototype);class s extends r.Lang{constructor(){super(s,s.INITIALIZER)}static getInstance(){return this.SINGLETON||(this.SINGLETON=new s,this.INITIALIZER=void 0),this.SINGLETON}}s.SINGLETON=void 0,s.frontend=r.Lang.GET_FRONTEND_DESC_BY_ID("japn-kat"),s.INITIALIZER=Object.freeze({"ン":{seq:"nn",weight:290948},"ル":{seq:"ru",weight:189442},"ス":{seq:"su",weight:178214},"ト":{seq:"to",weight:162802},"ア":{seq:"a",weight:127845},"イ":{seq:"i",weight:120807},"ラ":{seq:"ra",weight:117203},"リ":{seq:"ri",weight:106744},"ク":{seq:"ku",weight:98209},"カ":{seq:"ka",weight:82982},"シ":{seq:"shi",weight:80626},"タ":{seq:"ta",weight:75319},"ロ":{seq:"ro",weight:75301},"ド":{seq:"do",weight:74257},"ジ":{seq:"ji",weight:61171},"フ":{seq:"fu",weight:61115},"レ":{seq:"re",weight:60608},"メ":{seq:"me",weight:60230},"コ":{seq:"ko",weight:58724},"マ":{seq:"ma",weight:56123},"プ":{seq:"pu",weight:54159},"テ":{seq:"te",weight:53404},"ム":{seq:"mu",weight:50758},"チ":{seq:"chi",weight:48437},"バ":{seq:"ba",weight:44970},"ビ":{seq:"bi",weight:44462},"グ":{seq:"gu",weight:40433},"キ":{seq:"ki",weight:39608},"ウ":{seq:"u",weight:39323},"サ":{seq:"sa",weight:39202},"ニ":{seq:"ni",weight:38711},"ナ":{seq:"na",weight:38047},"エ":{seq:"e",weight:36458},"ブ":{seq:"bu",weight:35920},"パ":{seq:"pa",weight:35416},"セ":{seq:"se",weight:34883},"オ":{seq:"o",weight:34718},"ィ":{seq:"i",weight:33747},"デ":{seq:"de",weight:32665},"ュ":{seq:"yu",weight:32616},"ミ":{seq:"mi",weight:29262},"ャ":{seq:"ya",weight:28144},"ボ":{seq:"bo",weight:26651},"ダ":{seq:"da",weight:26396},"ツ":{seq:"tsu",weight:24541},"ポ":{seq:"ho",weight:23742},"ベ":{seq:"be",weight:22755},"ネ":{seq:"ne",weight:22462},"ガ":{seq:"ga",weight:22061},"ハ":{seq:"ha",weight:21839},"ワ":{seq:"wa",weight:21784},"ソ":{seq:"so",weight:20784},"ケ":{seq:"ke",weight:20633},"モ":{seq:"ho",weight:20070},"ノ":{seq:"no",weight:19572},"ズ":{seq:"zu",weight:19240},"ピ":{seq:"pi",weight:18692},"ホ":{seq:"ho",weight:18204},"ェ":{seq:"e",weight:17817},"ョ":{seq:"yo",weight:17731},"ペ":{seq:"pe",weight:14881},"ゴ":{seq:"go",weight:13931},"ヤ":{seq:"ya",weight:12526},"ギ":{seq:"gi",weight:10732},"ヨ":{seq:"yo",weight:10318},"ザ":{seq:"za",weight:10144},"ァ":{seq:"a",weight:10121},"ゼ":{seq:"ze",weight:7689},"ヒ":{seq:"hi",weight:7289},"ヘ":{seq:"he",weight:7129},"ユ":{seq:"yo",weight:6653},"ゲ":{seq:"ge",weight:6481},"ォ":{seq:"o",weight:6245},"ヌ":{seq:"nu",weight:2897},"ゾ":{seq:"zo",weight:2640},"ヴ":{seq:"vu",weight:1145},"ヂ":{seq:"di",weight:149},"ヅ":{seq:"du",weight:127},"ヲ":{seq:"wo",weight:122}}),e.Katakana=s,Object.seal(s),Object.freeze(s.prototype)}(a=t.Japanese||(t.Japanese={})),Object.freeze(a)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(2);var a;!function(e){class t extends r.Lang{constructor(){super(t,n((e,s,r)=>[e,s,r].flatMap(e=>e.value in t.KEYBOARD?[e.value]:e.atoms.split("")).map(e=>t.KEYBOARD[e]).join("")))}static getInstance(){return this.SINGLETON||(this.SINGLETON=new t,this.KEYBOARD=void 0),this.SINGLETON}}t.SINGLETON=void 0,t.frontend=r.Lang.GET_FRONTEND_DESC_BY_ID("kore-dub"),t.KEYBOARD=Object.freeze({"":"","ㅂ":"q","ㅈ":"w","ㄷ":"e","ㄱ":"r","ㅅ":"t","ㅛ":"y","ㅕ":"u","ㅑ":"i","ㅐ":"o","ㅔ":"p","ㅁ":"a","ㄴ":"s","ㅇ":"d","ㄹ":"f","ㅎ":"g","ㅗ":"h","ㅓ":"j","ㅏ":"k","ㅣ":"l","ㅋ":"z","ㅌ":"x","ㅊ":"c","ㅍ":"v","ㅠ":"b","ㅜ":"n","ㅡ":"m","ㅃ":"Q","ㅉ":"W","ㄸ":"E","ㄲ":"R","ㅆ":"T","ㅒ":"O","ㅖ":"P"}),e.Dubeolsik=t,Object.seal(t),Object.seal(t.prototype);class s extends r.Lang{constructor(){super(s,n((e,t,r)=>s.SEB_KEYBOARD.INITIALS[e.value]+s.SEB_KEYBOARD.MEDIALS[t.value]+s.SEB_KEYBOARD.FINALS[r.value]))}static getInstance(){return this.SINGLETON||(this.SINGLETON=new s,this.SEB_KEYBOARD=void 0),this.SINGLETON}}s.SINGLETON=void 0,s.frontend=r.Lang.GET_FRONTEND_DESC_BY_ID("kore-sub"),s.SEB_KEYBOARD=Object.freeze({FINALS:{"":"","ㅎ":"1","ㅆ":"2","ㅂ":"3","ㅅ":"q","ㄹ":"w","ㅇ":"a","ㄴ":"s","ㅁ":"z","ㄱ":"x","ㄲ":"!","ㄺ":"@","ㅈ":"#","ㄿ":"$","ㄾ":"%","ㅍ":"Q","ㅌ":"W","ㄵ":"E","ㅀ":"R","ㄽ":"T","ㄷ":"A","ㄶ":"S","ㄼ":"D","ㄻ":"F","ㅊ":"Z","ㅄ":"X","ㅋ":"C","ㄳ":"V"},MEDIALS:{"ㅛ":"4","ㅠ":"5","ㅑ":"6","ㅖ":"7","ㅢ":"8","ㅕ":"e","ㅐ":"r","ㅓ":"t","ㅣ":"d","ㅏ":"f","ㅡ":"g","ㅔ":"c","ㅗ":"v","ㅜ":"b","ㅒ":"G","ㅘ":"vf","ㅙ":"vr","ㅚ":"vd","ㅝ":"bt","ㅞ":"bc","ㅟ":"bd"},INITIALS:{"ㅋ":"0","ㄹ":"y","ㄷ":"u","ㅁ":"i","ㅊ":"o","ㅍ":"p","ㄴ":"h","ㅇ":"j","ㄱ":"k","ㅈ":"l","ㅂ":";","ㅌ":"'","ㅅ":"n","ㅎ":"m","ㄲ":"!","ㄸ":"uu","ㅃ":";;","ㅆ":"nn","ㅉ":"l"}}),e.Sebeolsik=s,Object.seal(s),Object.seal(s.prototype);class a extends r.Lang{constructor(){super(a,n((e,t,s)=>e.roman+t.roman+s.roman))}static getInstance(){return a.SINGLETON||(a.SINGLETON=new a),a.SINGLETON}static remapKey(e){return e}}a.SINGLETON=void 0,a.frontend=r.Lang.GET_FRONTEND_DESC_BY_ID("kore-rom"),e.Romanization=a,Object.seal(a),Object.seal(a.prototype);const n=e=>{const t={};return i.forEach((s,r)=>{o.forEach((a,n)=>{c.forEach((i,l)=>{let u=r;u=o.length*u+n,u=c.length*u+l;const d=String.fromCharCode(44032+u);t[d]={seq:e(s,a,i),weight:h[d]}})})}),t},i=Object.freeze([{value:"ㄱ",atoms:"ㄱ",roman:"g"},{value:"ㄲ",atoms:"ㄱㄱ",roman:"kk"},{value:"ㄴ",atoms:"ㄴ",roman:"n"},{value:"ㄷ",atoms:"ㄷ",roman:"d"},{value:"ㄸ",atoms:"ㄷㄷ",roman:"tt"},{value:"ㄹ",atoms:"ㄹ",roman:"r"},{value:"ㅁ",atoms:"ㅁ",roman:"m"},{value:"ㅂ",atoms:"ㅂ",roman:"b"},{value:"ㅃ",atoms:"ㅂㅂ",roman:"pp"},{value:"ㅅ",atoms:"ㅅ",roman:"s"},{value:"ㅆ",atoms:"ㅅㅅ",roman:"ss"},{value:"ㅇ",atoms:"ㅇ",roman:"-"},{value:"ㅈ",atoms:"ㅈ",roman:"j"},{value:"ㅉ",atoms:"ㅈㅈ",roman:"jj"},{value:"ㅊ",atoms:"ㅊ",roman:"ch"},{value:"ㅋ",atoms:"ㅋ",roman:"k"},{value:"ㅌ",atoms:"ㅌ",roman:"t"},{value:"ㅍ",atoms:"ㅍ",roman:"p"},{value:"ㅎ",atoms:"ㅎ",roman:"h"}]),o=Object.freeze([{value:"ㅏ",atoms:"ㅏ",roman:"a"},{value:"ㅐ",atoms:"ㅐ",roman:"ae"},{value:"ㅑ",atoms:"ㅑ",roman:"ya"},{value:"ㅒ",atoms:"ㅒ",roman:"yae"},{value:"ㅓ",atoms:"ㅓ",roman:"eo"},{value:"ㅔ",atoms:"ㅔ",roman:"e"},{value:"ㅕ",atoms:"ㅕ",roman:"yeo"},{value:"ㅖ",atoms:"ㅖ",roman:"ye"},{value:"ㅗ",atoms:"ㅗ",roman:"o"},{value:"ㅘ",atoms:"ㅗㅏ",roman:"wa"},{value:"ㅙ",atoms:"ㅗㅐ",roman:"wae"},{value:"ㅚ",atoms:"ㅗㅣ",roman:"oe"},{value:"ㅛ",atoms:"ㅛ",roman:"yo"},{value:"ㅜ",atoms:"ㅜ",roman:"u"},{value:"ㅝ",atoms:"ㅜㅓ",roman:"wo"},{value:"ㅞ",atoms:"ㅜㅔ",roman:"we"},{value:"ㅟ",atoms:"ㅜㅣ",roman:"wi"},{value:"ㅠ",atoms:"ㅠ",roman:"yu"},{value:"ㅡ",atoms:"ㅡ",roman:"eu"},{value:"ㅢ",atoms:"ㅡㅣ",roman:"ui"},{value:"ㅣ",atoms:"ㅣ",roman:"i"}]),c=Object.freeze([{value:"",atoms:"",roman:""},{value:"ㄱ",atoms:"ㄱ",roman:"k"},{value:"ㄲ",atoms:"ㄱㄱ",roman:"k"},{value:"ㄳ",atoms:"ㄱㅅ",roman:"kt"},{value:"ㄴ",atoms:"ㄴ",roman:"n"},{value:"ㄵ",atoms:"ㄴㅈ",roman:"nt"},{value:"ㄶ",atoms:"ㄴㅎ",roman:"nt"},{value:"ㄷ",atoms:"ㄷ",roman:"t"},{value:"ㄹ",atoms:"ㄹ",roman:"l"},{value:"ㄺ",atoms:"ㄹㄱ",roman:"lk"},{value:"ㄻ",atoms:"ㄹㅁ",roman:"lm"},{value:"ㄼ",atoms:"ㄹㅂ",roman:"lp"},{value:"ㄽ",atoms:"ㄹㅅ",roman:"lt"},{value:"ㄾ",atoms:"ㄹㅌ",roman:"lt"},{value:"ㄿ",atoms:"ㄹㅍ",roman:"lp"},{value:"ㅀ",atoms:"ㄹㅎ",roman:"lt"},{value:"ㅁ",atoms:"ㅁ",roman:"m"},{value:"ㅂ",atoms:"ㅂ",roman:"p"},{value:"ㅄ",atoms:"ㅂㅅ",roman:"pt"},{value:"ㅅ",atoms:"ㅅ",roman:"t"},{value:"ㅆ",atoms:"ㅅㅅ",roman:"t"},{value:"ㅇ",atoms:"ㅇ",roman:"ng"},{value:"ㅈ",atoms:"ㅈ",roman:"t"},{value:"ㅊ",atoms:"ㅊ",roman:"t"},{value:"ㅋ",atoms:"ㅋ",roman:"k"},{value:"ㅌ",atoms:"ㅌ",roman:"t"},{value:"ㅍ",atoms:"ㅍ",roman:"p"},{value:"ㅎ",atoms:"ㅎ",roman:"t"}]),h=Object.freeze({"":1})}(a=t.Korean||(t.Korean={})),Object.freeze(a)},function(e,t,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=s(2);var a;!function(e){class t extends r.Lang{}e.Encode=t,Object.freeze(t),Object.freeze(t.prototype);class s extends r.Lang{}e.Decode=s,Object.freeze(s),Object.freeze(s.prototype)}(a=t.Morse||(t.Morse={})),Object.freeze(a)}]); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/index.ejs b/index.ejs index 9651c0d5..c78ab6ca 100644 --- a/index.ejs +++ b/index.ejs @@ -1,25 +1,31 @@ - + SnaKey v3 + <%= htmlWebpackPlugin.tags.headTags.join("\n ") %> - -
-
-
-
+ +
+
+
<%= htmlWebpackPlugin.tags.bodyTags.join("\n ") %> \ No newline at end of file diff --git a/index.html b/index.html index e86870e8..ddb88329 100644 --- a/index.html +++ b/index.html @@ -1,27 +1,6 @@ - - - - SnaKey v3 - - - - - - - - - - - - - - - - -
-
-
-
- - - \ No newline at end of file +SnaKey v3
\ No newline at end of file diff --git a/manifest.webmanifest b/manifest.webmanifest index 6eeb1488..c8b3d932 100644 --- a/manifest.webmanifest +++ b/manifest.webmanifest @@ -7,7 +7,10 @@ "entertainment", "games" ], - "icons": [{ "src": "dist/favicon.ico" }], + "icons": [{ + "src": "dist/favicon.ico", + "sizes": "302x302" + }], "screenshots": [{ "src": "assets/images/screenshots/snakey_version3_blanktitle.png" }], diff --git a/package-lock.json b/package-lock.json index d00f3b74..c82ce1d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "snakey-mp", - "version": "0.0.0", + "version": "0.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -8206,6 +8206,12 @@ } } }, + "webpack-node-externals": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-1.7.2.tgz", + "integrity": "sha512-ajerHZ+BJKeCLviLUUmnyd5B4RavLF76uv3cs6KNuO8W+HuQaEs0y0L7o40NQxdPy5w0pcv8Ew7yPUAQG0UdCg==", + "dev": true + }, "webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", diff --git a/package.json b/package.json index 0087c3f2..e7644b16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "snakey-mp", - "version": "0.2.1", + "version": "0.3.0", "description": "A multiplayer typing game for learning written languages.", "keywords": [ "typing", @@ -12,12 +12,14 @@ "type": "module", "files": [ "dist/", - "!dist/{src,test}/", + "!dist/ts/", "index.html", "manifest.webmanifest" ], "exports": {}, - "scripts": {}, + "scripts": { + "start": "node ./dist/server/index.js" + }, "dependencies": { "express": "^4.17.1", "socket.io": "^2.2.0", @@ -45,6 +47,7 @@ "ts-loader": "^6.2.2", "typescript": "^3.8.3", "webpack": "^4.43.0", - "webpack-cli": "^3.3.11" + "webpack-cli": "^3.3.11", + "webpack-node-externals": "^1.7.2" } } diff --git a/scripts/Sandbox.js b/scripts/Sandbox.js index 52995652..51689974 100644 --- a/scripts/Sandbox.js +++ b/scripts/Sandbox.js @@ -29,10 +29,13 @@ log("" == null) log("" + null) // what is the length of an array with leading holes? +// and what does Object.getOwnPropertyNames return on an array? a = [] a[5] = "hi this is five" log(a[5], a.length) -// one plus the index of the last defined entry. +log(Object.getOwnPropertyNames(a)); +log(Object.keys(a)); +// the length is one plus the index of the last defined entry. // what is in the `undefined` key of a plain object? e = [1, 2, 3,] @@ -52,9 +55,11 @@ const f = () => { } log(f()); e.push("4") +f().addedtoreturnvalue = "hello?" log(f()); -// darn. it does it by value... good to know. -// well, I guess that's the whole basis of Javascript OOP :/ +// The returned object literal is a new object each time. +// Of course, the property `e` only _refers_ to an array. +// The arrow-function does not make a deep copy of `e` unless I tell it to. // What is the truthiness of an empty array? if ([]) { diff --git a/scripts/pack.sh b/scripts/pack.sh index 417115f7..59a2a242 100644 --- a/scripts/pack.sh +++ b/scripts/pack.sh @@ -7,7 +7,7 @@ declare -r cwd="$(dirname "${BASH_SOURCE[0]}")" # convert the Typescript version of the config to Javascript. if [[ "$@" =~ '-t' ]] then - time npx tsc --project "${cwd}/webpack/webpack.tsconfig.json" + time npx tsc --project "${cwd}/webpack/tsconfig.json" echo 'built the webpack config' echo else diff --git a/scripts/webpack/tsconfig.json b/scripts/webpack/tsconfig.json new file mode 100644 index 00000000..72c45c7e --- /dev/null +++ b/scripts/webpack/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "CommonJS", + "rootDir": ".", + "outDir": ".", + "listEmittedFiles": true, + "incremental": true, + + "removeComments": true, + "alwaysStrict": true, + "skipLibCheck": true, // time-saving measure. + + // Grammar Checking: + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitReturns": true, + "strictBindCallApply": true, + "noImplicitThis": true, + }, + "files": [ + "webpack.config.ts" + ] +} \ No newline at end of file diff --git a/scripts/webpack/webpack.config.ts b/scripts/webpack/webpack.config.ts index fb4152ab..d3e00e8b 100644 --- a/scripts/webpack/webpack.config.ts +++ b/scripts/webpack/webpack.config.ts @@ -1,25 +1,17 @@ -import path = require("path"); -import fs = require("fs"); +import path = require("path"); +import webpack = require("webpack"); -import webpack = require("webpack"); -import HtmlPlugin = require("html-webpack-plugin"); +// https://github.com/TypeStrong/ts-loader#loader-options +import type * as tsloader from "ts-loader/dist/interfaces"; -// https://webpack.js.org/plugins/mini-css-extract-plugin/ +import nodeExternals = require("webpack-node-externals") +import HtmlPlugin = require("html-webpack-plugin"); import MiniCssExtractPlugin = require("mini-css-extract-plugin"); import OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); -// Note: if I ever add this back, I'll need to look into how to make -// sure it doesn't clean away things from separate configs (See notes -// below on why I export multiple configurations). -// import clean = require("clean-webpack-plugin"); - -// https://github.com/TypeStrong/ts-loader#loader-options -import type * as tsloader from "ts-loader/dist/interfaces"; - type Require = T & Pick, K>; - /** * Externalized definition (for convenience of toggling). */ @@ -27,7 +19,7 @@ const PACK_MODE = (process.env.NODE_ENV) as webpack.Configuration["mode"]; export const PROJECT_ROOT = path.resolve(__dirname, "../.."); -const BASE_PLUGINS: ReadonlyArray> = [ +const BASE_PLUGINS = (): ReadonlyArray> => { return [ // new webpack.ProgressPlugin((pct, msg, moduleProgress?, activeModules?, moduleName?) => { // console.log( // `[${Math.floor(pct * 100).toString().padStart(3)}% ]`, @@ -43,12 +35,12 @@ const BASE_PLUGINS: ReadonlyArray> = [ // /\.js$/, // /\.d\.ts$/, // ]), -]; +]}; /** * https://webpack.js.org/loaders/ */ -const MODULE_RULES: Array = [{ +const MODULE_RULES = (): Array => { return [{ // With ts-loader@7.0.0, you need to set: // options.compilerOptions.emitDeclarationsOnly: false // options.transpileOnly: false @@ -58,7 +50,14 @@ const MODULE_RULES: Array = [{ options: { projectReferences: true, compilerOptions: { - emitDeclarationOnly: true, + // We need to preserve comments in transpiled output + // so that magic comments in dynamic imports can be + // seen by webpack. + removeComments: false, + importHelpers: false, // :'( + // TODO.build get rid of the above line when + // https://github.com/microsoft/TypeScript/issues/36841 + // is fixed. What an absolute tragedy T^T //noEmit: true, }, // https://github.com/TypeStrong/ts-loader#faster-builds @@ -71,14 +70,12 @@ const MODULE_RULES: Array = [{ test: /\.css$/, use: ((): webpack.RuleSetUseItem[] => { const retval: webpack.RuleSetUse = [ "css-loader", ]; - //if (PACK_MODE !== "development") { - retval.unshift({ - loader: MiniCssExtractPlugin.loader, - }); - //} + retval.unshift({ + loader: MiniCssExtractPlugin.loader, + }); return retval; })(), -}, ]; +}, ]}; /** * # Base Config @@ -90,9 +87,7 @@ const MODULE_RULES: Array = [{ * * Everything that builds off of this will need to add the `entry` field. * - * **Important**: Make sure all referenced objects are only accessible - * via pure producers. Otherwise, mutations in one bundle's config will - * propagate to all the following config definitions. + * Important implementation note: make sure helpers such as * * ## Help Links * @@ -102,43 +97,46 @@ const MODULE_RULES: Array = [{ * * @returns A standalone ("deep-copy") basic configuration. */ -const BaseConfig: () => Require = () => { return { +const __BaseConfig = (distSubFolder: string): Require => { return { mode: PACK_MODE, - // https://webpack.js.org/guides/caching/ - // https://webpack.js.org/configuration/other-options/#cache - cache: true, - stats: { - // https://webpack.js.org/configuration/stats/ - //warningsFilter: [ /export .* was not found in/, ], - }, + name: `\n\n${"=".repeat(32)} ${distSubFolder.toUpperCase()} ${"=".repeat(32)}\n`, + stats: { /* https://webpack.js.org/configuration/stats/ */ }, context: PROJECT_ROOT, // https://webpack.js.org/configuration/entry-context/#context entry: { /* Left to each branch config */ }, - devtool: (PACK_MODE === "production") - ? "nosources-source-map" : "eval-source-map", - plugins: [ ...BASE_PLUGINS, ], + plugins: [ ...BASE_PLUGINS(), ], resolve: { - extensions: [ ".ts", ], // ".json", ".tsx", - modules: [ path.resolve(PROJECT_ROOT, "src", "base"), ], // match tsconfig.baseUrl + extensions: [ ".ts", ".css", ".js", ], + modules: [ + path.resolve(PROJECT_ROOT, "src", "base"), + "node_modules", + ], // match tsconfig.baseUrl }, - watchOptions: { - ignored: [ "node_modules", ], + module: { rules: MODULE_RULES(), }, + devtool: (PACK_MODE === "production") + ? "nosources-source-map" + : "eval-source-map", + output: { + path: path.resolve(PROJECT_ROOT, "dist", distSubFolder), + publicPath: `dist/${distSubFolder}/`, // need trailing "/". + filename: "[name].js", + chunkFilename: "chunk/[name].js", + library: "snakey3", + pathinfo: false, // unneeded. minor performance gain. }, - module: { rules: MODULE_RULES, }, + + // https://webpack.js.org/guides/caching/ + // https://webpack.js.org/configuration/other-options/#cache + cache: true, optimization: { // runtimeChunk: { // name: entrypoint => `${entrypoint.name}/runtime`, // } as webpack.Options.RuntimeChunkOptions, //mergeDuplicateChunks: true, }, - output: { - path: path.resolve(PROJECT_ROOT, "dist"), - publicPath: "dist/", // lol webpack fails without the trailing slash. - filename: "[name]/index.js", - chunkFilename: "[name]/index.js", - library: "snakey3", - pathinfo: false, // don't need it. suppression yields small performance gain. + watchOptions: { + ignored: [ "node_modules", ], }, }; }; @@ -155,15 +153,13 @@ const BaseConfig: () => Require): void => { +const __applyCommonNodeConfigSettings = (config: ReturnType): void => { config.target = "node"; - config.resolve.modules!.push("node_modules"); - config.resolve.extensions!.push(".js"); // alternative to below: https://www.npmjs.com/package/webpack-node-externals - config.externals = fs.readdirSync(path.resolve(PROJECT_ROOT, "node_modules")); + config.externals = [ nodeExternals(), ], + config.resolve.extensions!.push(".js"); + config.node = { + __filename: false, + __dirname: false, + global: false, + }; }; /** * ## Node Bundles */ -const nodeBundleConfig = BaseConfig(); { - const config = nodeBundleConfig; - config.name = "src-node"; - NODE_CONFIG(config); - ([ "server", ]).forEach((name) => { - config.entry[name] = `./src/${name}/index.ts`; - }); +const SERVER_CONFIG = __BaseConfig("server"); { + const config = SERVER_CONFIG; + __applyCommonNodeConfigSettings(config); + config.entry["index"] = `./src/server/index.ts`; } /** * ## Test Bundles - * - * See the node settings. - * - * Emit all test bundles under a single folder. */ -const testBundleConfig = BaseConfig(); { - const config = testBundleConfig; - config.name = "test"; - config.resolve.modules = [ - path.resolve(PROJECT_ROOT, "src", "base"), - path.resolve(PROJECT_ROOT, "src"), - PROJECT_ROOT, - ]; - NODE_CONFIG(config); - config.output.path = path.resolve(PROJECT_ROOT, "dist", "test"); +const TEST_CONFIG = __BaseConfig("test"); { + const config = TEST_CONFIG; + config.resolve.modules!.push(path.resolve(PROJECT_ROOT, "src")); + __applyCommonNodeConfigSettings(config); ([ "lang", ]).forEach((name) => { config.entry[name] = `./test/${name}/index.ts`; }); @@ -246,8 +235,8 @@ const testBundleConfig = BaseConfig(); { module.exports = [ - webBundleConfig, + CLIENT_CONFIG, // TODO.build Uncomment these pack configs when we get to using them. - //nodeBundleConfig, - //testBundleConfig, + SERVER_CONFIG, + //TEST_CONFIG, ]; diff --git a/src/.templates/tsconfig.json b/src/.templates/tsconfig.json index d900ecf2..3cd2a9d6 100644 --- a/src/.templates/tsconfig.json +++ b/src/.templates/tsconfig.json @@ -23,6 +23,6 @@ "compilerOptions": { "baseUrl": "../base", "rootDir": "../..", - "outDir": "../../dist", + "outDir": "../../dist/ts", }, } \ No newline at end of file diff --git a/src/base/.templates/tsconfig.json b/src/base/.templates/tsconfig.json index 459edb62..2282b00b 100644 --- a/src/base/.templates/tsconfig.json +++ b/src/base/.templates/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "baseUrl": "..", "rootDir": "../../..", - "outDir": "../../../dist", + "outDir": "../../../dist/ts", }, } \ No newline at end of file diff --git a/src/base/browser/ColourScheme.ts b/src/base/browser/ColourScheme.ts deleted file mode 100644 index ac33b98b..00000000 --- a/src/base/browser/ColourScheme.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { StorageHooks } from 'browser/StorageHooks'; - -/** - * - * CSS variables declared for each color scheme are in the - * form: `--colour-schemeId-swatchName`. js/ts should not need to - * interface with these values directly. Instead, it should use - * provided class names and dataset attributes as to minimize its - * effort when colour schemes are swapped. - */ -// TODO.design I want each option to show the swatches. To make the -// transition smooth without making every element with the CSS have -// transition behaviour for colour-related properties, use a garage- -// door with -export class Colour { - - public readonly sel: HTMLSelectElement & {value: Colour.Scheme.Id}; - - public constructor(hostElement: HTMLElement) { - const sel = document.createElement("select"); - //sel.name = - for (const scheme of Colour.Scheme) { - const opt = document.createElement("option"); - opt.innerText = Colour.Scheme[scheme.id].displayName; - opt.value = scheme.id; - sel.add(opt); - } - sel.onchange = () => { - sel.blur(); - this.switchToScheme(this.sel.value); - localStorage.setItem( - StorageHooks.Keys.COLOUR, - this.sel.value, - ); - }; - hostElement.appendChild(sel); - this.sel = sel as Colour["sel"]; - - // Initialize to the user's last selected colour scheme (if it exists). - const lastUsedSchemeId = localStorage.getItem(StorageHooks.Keys.COLOUR); - if (lastUsedSchemeId) { - for (let i = 0; i < sel.length; i++) { - if ((sel.item(i) as HTMLOptionElement).value === lastUsedSchemeId) { - sel.selectedIndex = i; - sel.dispatchEvent(new Event("change")); - } - } - } - } - - /** - * See `:/assets/style/colour/index.css`. - * - * @param schemeId - - */ - public switchToScheme(schemeId: Colour.Scheme.Id): void { - for (const swatchName of Colour.Swatch) { - document.body.style.setProperty( - `--colour-selected-${swatchName}`, - `--colour-${schemeId}-${swatchName}`, - ); - } - } -} -export namespace Colour { - export const Swatch = Object.freeze([ - "mainFg", "mainBg", - "tileFg", "tileBg", "tileBd", - "healthFg", "healthBg", - "pFaceMe", - "pFaceTeammate", "pFaceImmortalTeammate", - "pFaceOpponent", "pFaceImmortalOpponent", - ]); - /** - * The scheme id `selected` is a special value and should not - * be used. - */ - export const Scheme = Object.freeze([ - { id: "snakey", displayName: "Snakey by N.W.", }, - ].map((scheme) => Object.freeze(scheme))); - export namespace Scheme { - export type Id = (typeof Scheme)[number]["id"]; - } -} -Object.freeze(Colour); -Object.freeze(Colour.prototype); diff --git a/src/base/browser/OmHooks.ts b/src/base/browser/OmHooks.ts deleted file mode 100644 index e9671496..00000000 --- a/src/base/browser/OmHooks.ts +++ /dev/null @@ -1,101 +0,0 @@ - -/** - * # DOM / CSSOM Hook Strings - * - * Must be matched exactly in the html, css, and javascript. - * - * Dataset values are defined for the javascript domain. The CSS should - * use the CSS-cased version with dash-separators. - */ -export namespace OmHooks { - - /** - * See `:/assets/style/utils.css`. - */ - export const General = Object.freeze({ - Class: Object.freeze({ - TEXT_SELECT_DISABLED: "text-select-disabled", - FILL_PARENT: "fill-parent", - CENTER_CONTENTS: "center-contents", - STACK_CONTENTS: "stack-contents", - }), - }); - - - export const Tile = Object.freeze({ - Class: Object.freeze({ - BASE: "tile", - /** - * Must precede the char and seq elements to allow CSS - * to use the variable-distance same-parent precede - * selection operator ("~"). - */ - POINTER_HB: "tile__pointer-hitbox", - LANG_CHAR: "tile__char", - LANG_SEQ: "tile__seq", - }), - Dataset: Object.freeze({ - HEALTH: "health", - }), - }); - - - export const Grid = Object.freeze({ - Id: Object.freeze({ - /** - * js/ts and html are allowed to prepend or append to this - * string. The specified element should _only_ contain the - * grid element. Anything else will be removed by js/ts. - */ - GRID: "game-grid", - }), - Class: Object.freeze({ - GRID: "game-grid", - /** - * Must be the first child because it is the element that - * can take focus (tabIndex = 0). It needs to be that way - * so that its onkeydown handler (which references a Game - * object) will get GC'd with it when removed from the DOM. - * The keyboard-disconnected overlay is a sibling whose - * CSS visibility depends on whether this element has focus - * or not. - */ - IMPL_BODY: "game-grid-impl-body", - KBD_DC_BASE: "game-grid-kbd-dc", - KBD_DC_ICON: "game-grid-kbd-dc__icon", - }), - Dataset: Object.freeze({ - /** - * Used as a part of CSS selector queries to specify coord- - * system-specific styling. - */ - COORD_SYS: "coordSys", - }), - }); - - - export const Player = Object.freeze({ - Class: Object.freeze({ - BASE: "player", - FACE: "player__face", - DOWNED_OVERLAY: "player__downed-overlay", - SHORT_SPOTLIGHT:"player__spotlight-short", - LONG_SPOTLIGHT: "player__spotlight-long", - }), - Dataset: Object.freeze({ - DOWNED: "downed", - FACE_SWATCH:"face", - }), - }); - - - export const Screen = Object.freeze({ - Class: Object.freeze({ - BASE: "screen", - }), - Dataset: Object.freeze({ - CURRENT: "current", - }), - }); -} -Object.freeze(OmHooks); diff --git a/src/base/browser/Storage.ts b/src/base/browser/Storage.ts deleted file mode 100644 index 2c1897da..00000000 --- a/src/base/browser/Storage.ts +++ /dev/null @@ -1,15 +0,0 @@ - - -/** - * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API - * https://www.w3schools.com/html/html5_webstorage.asp - */ -export namespace Storage { - export const Keys = Object.freeze({ - MUSIC_VOLUME: "musicVolume", - SFX_VOLUME: "sfxVolume", - SAVED_GAME_SETUPS: "savedGameSetups", - USERNAME: "username", - AVATAR: "avatarId", - }); -} diff --git a/src/base/browser/StorageHooks.ts b/src/base/browser/StorageHooks.ts deleted file mode 100644 index ac6c8b4f..00000000 --- a/src/base/browser/StorageHooks.ts +++ /dev/null @@ -1,16 +0,0 @@ - - -/** - * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API - * https://www.w3schools.com/html/html5_webstorage.asp - */ -export namespace StorageHooks { - export const Keys = Object.freeze({ - MUSIC_VOLUME: "musicVolume", - SFX_VOLUME: "sfxVolume", - SAVED_GAME_SETUPS: "savedGameSetups", - USERNAME: "username", - AVATAR: "avatarId", - COLOUR: "colourSchemeId", - }); -} diff --git a/src/base/browser/TopLevel.ts b/src/base/browser/TopLevel.ts deleted file mode 100644 index 1ef6c939..00000000 --- a/src/base/browser/TopLevel.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { OmHooks } from "./OmHooks"; - -// TODO.impl Allow users to change the spotlight radius via slider. -export class TopLevel { - -} \ No newline at end of file diff --git a/src/base/browser/screen/AllSkScreens.ts b/src/base/browser/screen/AllSkScreens.ts deleted file mode 100644 index b1db43bf..00000000 --- a/src/base/browser/screen/AllSkScreens.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { SkScreen } from "./SkScreen"; - -import { HomeScreen } from "./impl/Home"; -import { GameSetupScreen } from "./impl/GameSetup"; -import { SeshJoinerScreen } from "./impl/SeshJoiner"; -import { HowToPlayScreen } from "./impl/HowToPlay"; -import { HowToHostScreen } from "./impl/HowToHost"; -import { PlayGameScreen } from "./impl/PlayGame"; - - -export class AllSkScreens { - - private readonly dict: Readonly>; - - #currentScreen: SkScreen; - - public constructor(baseElem: HTMLElement) { - const p = baseElem; - const f = this.goToScreen; - this.dict = Object.freeze({ - [ SkScreen.Id.HOME ]: new HomeScreen(p,f), - [ SkScreen.Id.GAME_SETUP ]: new GameSetupScreen(p,f), - [ SkScreen.Id.SESH_JOINER ]: new SeshJoinerScreen(p,f), - [ SkScreen.Id.HOW_TO_PLAY ]: new HowToPlayScreen(p,f), - [ SkScreen.Id.HOW_TO_HOST ]: new HowToHostScreen(p,f), - [ SkScreen.Id.PLAY_GAME ]: new PlayGameScreen(p,f), - }); - this.goToScreen(SkScreen.Id.HOME); - } - - public goToScreen(destId: SkScreen.Id): void { - const destScreen = this.dict[destId]; - if (this.currentScreen === destScreen) { - // I don't see why this would ever need to happen. - throw new Error ("never happens. see comment in source."); - } - if (this.currentScreen?.leave()) { - // Note on above nullish coalesce: Special case entered - // during construction when there is no currentScreen yet. - // Any confirm-leave prompts made to the user were OK-ed. - destScreen.enter(); - this.#currentScreen = destScreen; - } - } - - public get currentScreen(): SkScreen { - return this.#currentScreen; - } -} -Object.freeze(AllSkScreens); -Object.freeze(AllSkScreens.prototype); diff --git a/src/base/browser/screen/SkScreen.ts b/src/base/browser/screen/SkScreen.ts deleted file mode 100644 index 6fe95b49..00000000 --- a/src/base/browser/screen/SkScreen.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { OmHooks } from "../OmHooks"; -import type { AllSkScreens } from 'browser/screen/AllSkScreens'; - -/** - * - * - * NOTE: Design decision: Isolate from the rest of the architecture. - * Ie. Do not give it circular / upward references to anything that - * references it. - */ -export abstract class SkScreen { - - protected readonly baseElem: HTMLElement; - - #hasLazyLoaded: boolean; - - /** - * Implementations can use this as part of navigation button - * handlers. - */ - protected readonly requestGoToScreen: AllSkScreens["goToScreen"]; - - public constructor(parentElem: HTMLElement, requestGoToDisplay: SkScreen["requestGoToScreen"]) { - const baseElem = document.createElement("div"); - baseElem.classList.add(OmHooks.Screen.Class.BASE); - parentElem.appendChild(baseElem); - this.#hasLazyLoaded = false; - this.requestGoToScreen = requestGoToDisplay; - } - - public enter(): void { - if (!this.#hasLazyLoaded) { - this.__lazyLoad(); - this.#hasLazyLoaded = true; - } - this.__abstractOnBeforeEnter(); - } - - public leave(): boolean { - return this.__abstractOnBeforeLeave(); - } - - protected abstract __lazyLoad(): void; - - /** - * Return false if the leave should be cancelled. This functionality - * allows an implementation to provide a prompt to the user such as - * a confirmation modal warning that unsaved changes would be lost. - * - * This is a good place, for example, to stop any non-essential - * `setInterval` schedules. - */ - protected __abstractOnBeforeLeave(): boolean { - return true; - } - - /** - * This is a good place to start any `setInterval` schedules, and - * to bring focus to a starting HTML element if appropriate. - */ - protected __abstractOnBeforeEnter(): void {} - -} -export namespace SkScreen { - - export const enum Id { - HOME, - GAME_SETUP, - SESH_JOINER, - HOW_TO_PLAY, - HOW_TO_HOST, - PLAY_GAME, - } -} -Object.freeze(SkScreen); -Object.freeze(SkScreen.prototype); diff --git a/src/base/browser/screen/impl/Home.ts b/src/base/browser/screen/impl/Home.ts deleted file mode 100644 index 977f1b0a..00000000 --- a/src/base/browser/screen/impl/Home.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { SkScreen } from "../SkScreen"; - - -export class HomeScreen extends SkScreen { - - protected __lazyLoad(): void { - ; - } - -} -Object.freeze(HomeScreen); -Object.freeze(HomeScreen.prototype); diff --git a/src/base/browser/screen/impl/PlayGame.ts b/src/base/browser/screen/impl/PlayGame.ts deleted file mode 100644 index 79ad9b61..00000000 --- a/src/base/browser/screen/impl/PlayGame.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SkScreen } from "../SkScreen"; - - -/** - * - */ -export class PlayGameScreen extends SkScreen { - - protected __lazyLoad(): void { - ; - } - -} -Object.freeze(PlayGameScreen); -Object.freeze(PlayGameScreen.prototype); diff --git a/src/base/browser/tsconfig.json b/src/base/browser/tsconfig.json deleted file mode 100644 index a5a210cf..00000000 --- a/src/base/browser/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../.templates/tsconfig.json", - "references": [ - { "path": "../utils", }, - ], -} \ No newline at end of file diff --git a/src/base/defs/OmHooks.ts b/src/base/defs/OmHooks.ts new file mode 100644 index 00000000..9219928a --- /dev/null +++ b/src/base/defs/OmHooks.ts @@ -0,0 +1,136 @@ + +/** + * # DOM / CSSOM Hook Strings + * + * Must be matched exactly in the html, css, and javascript. + * + * Dataset values are defined for the javascript domain. The CSS should + * use the CSS-cased version with dash-separators. + */ +export namespace OmHooks { + + /** + * See `:/assets/style/utils.css`. + */ + export namespace General { + export const Class = { + TEXT_SELECT_DISABLED: "text-select-disabled", + FILL_PARENT: "fill-parent", + CENTER_CONTENTS: "center-contents", + STACK_CONTENTS: "stack-contents", + }; + export const Dataset = { + COLOUR_SCHEME: "skColourScheme", + }; + } + + export namespace Tile { + export const Class = { + BASE: "tile", + /** + * Must precede the char and seq elements to allow CSS + * to use the variable-distance same-parent precede + * selection operator ("~"). + */ + POINTER_HB: "tile__pointer-hitbox", + LANG_CHAR: "tile__char", + LANG_SEQ: "tile__seq", + }; + export const Dataset = { + HEALTH: "health", + }; + } + + export namespace Grid { + export const Class = { + GRID: "game-grid", + IMPL_BODY: "game-grid-impl-body", + KBD_DC: "game-grid-kbd-dc", + KBD_DC_ICON: "game-grid-kbd-dc__icon", + PAUSE_OL: "game-grid-pause-overlay", + PAUSE_OL_ICON: "game-grid-pause-overlay__icon", + }; + export const Dataset = { + /** + * Used as a part of CSS selector queries to specify coord- + * system-specific styling. This is set on the `impl-body` + * element. See `VisibleGridMixin`. + */ + IMPL_COORD_SYS: "coordSys", + /** + * This is set on the `game-grid` element by `__PlayScreen`. + */ + GAME_STATE: { KEY: "gameState", VALUES: { + PLAYING: "playing", PAUSED: "paused", OVER: "over", + },}, + }; + } + + export namespace Player { + export const Class = { + BASE: "player", + FACE: "player__face", + DOWNED_OVERLAY: "player__downed-overlay", + SHORT_SPOTLIGHT:"player__spotlight-short", + LONG_SPOTLIGHT: "player__spotlight-long", + }; + export const Dataset = { + DOWNED: { KEY: "downed", VALUES: { + TEAM: "team", SELF: "self", NO: "no", + },}, + FACE_SWATCH: "face", + }; + } + + export namespace Screen { + export const Id = { + ALL_SCREENS: "all-screens-container", + SCREEN_TINT: "screen-tint", + }; + export const Class = { + BASE: "sk-screen", + }; + export const Dataset = { + CURRENT: "current", + }; + export namespace Impl { + export namespace Home { + export const Class = { + SCREEN: "screen-home", + NAV: "screen-home--nav", + NAV_PLAY_OFFLINE:"screen-home--nav--play-offline", + NAV_PLAY_ONLINE:"screen-home--nav--play-online", + NAV_TUTORIAL: "screen-home--nav--tutorial", + NAV_COLOURS: "screen-home--nav--colour-scheme", + NAV_VIEW_REPO: "screen-home--nav--goto-repo", + NAV_RPT_ISSUE: "screen-home--nav--report-issue", + }; + } + export namespace PlayGame { + export const Class = { + SCREEN: "screen-play", + GRID_CONTAINER: "screen-play--grid-container", + CONTROLS_BAR: "screen-play--controls-bar", + }; + } + } + } + + export namespace SkPickOne { + export const Class = { + BASE: "sk-pick-one", + OPT_BASE: "sk-pick-one--opt" + }; + } +} +Object.freeze(OmHooks.Player.Dataset.DOWNED); // String with properties. +function deepFreeze(obj: any): void { + for (const key of Object.getOwnPropertyNames(obj)) { + const val = obj[key]; + if (typeof val === "object") { + deepFreeze(val); + } + } + return Object.freeze(obj); +} +deepFreeze(OmHooks); diff --git a/src/base/defs/StorageHooks.ts b/src/base/defs/StorageHooks.ts new file mode 100644 index 00000000..3160d5fe --- /dev/null +++ b/src/base/defs/StorageHooks.ts @@ -0,0 +1,58 @@ +/** + * + */ + +/** + * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API + * https://www.w3schools.com/html/html5_webstorage.asp + */ +export namespace StorageHooks { + /** + * Keys for this origin's local storage records. + * + * These are used to store identifiers for "last-used" settings to + * be restored on startup. + */ + export const LocalKeys = Object.freeze({ + MUSIC_VOLUME: "musicVolume", + SFX_VOLUME: "sfxVolume", + /** + * Only used to highlight the last-used colour scheme when + * cold-initializing the colour selection screen. + */ + COLOUR_ID: "colourSchemeId", + /** + * Stores a css rule string for quick recovery on page load + * without even needing any colour scheme CSS files loaded. + */ + COLOUR_LITERAL: "colourSchemeStyleLiteral", + + GAME_PRESET: "gamePresetId", + + USERNAME: "username", + AVATAR: "avatarId", + }); + + /** + * Keys for this origin's session storage records. + */ + export const SessionKeys = Object.freeze({ + }); + + export namespace IDB { + /** + * + */ + export const DB_NAME = "snakeyDB"; + + /** + * + */ + export namespace UserGamePresetStore { + export const STORE_NAME = "userGamePresets"; + } + Object.freeze(UserGamePresetStore); + } + Object.freeze(IDB); +} +Object.freeze(StorageHooks); diff --git a/src/base/utils/TypeDefs.ts b/src/base/defs/TypeDefs.ts similarity index 52% rename from src/base/utils/TypeDefs.ts rename to src/base/defs/TypeDefs.ts index 2a517380..bf23b1ba 100644 --- a/src/base/utils/TypeDefs.ts +++ b/src/base/defs/TypeDefs.ts @@ -5,6 +5,35 @@ export namespace SkErrors { } +/** + * Copied from TypeScript official docs. + * + * @param derivedCtor - + * @param baseCtors - + */ +export function applyMixins(derivedCtor: any, baseCtors: any[]) { + baseCtors.forEach((baseCtor) => { + Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { + Object.defineProperty(derivedCtor.prototype, name, + Object.getOwnPropertyDescriptor(baseCtor.prototype, name)! + ); + }); + }); +} + + +/** + * + */ +export const enum SnakeyNsps { + HOST_REGISTRATION = "/host-reg", + GROUP_PREFIX = "/group", +} + + +/** + * + */ export class Player { } export namespace Player { @@ -49,6 +78,9 @@ Object.freeze(Player); Object.freeze(Player.prototype); +/** + * + */ export class Lang {} export namespace Lang { /** @@ -69,7 +101,7 @@ export namespace Lang { * Characters that must never be unmarked as reserved (state reason): * (currently none. update as needed) */ - export const REGEXP = new RegExp("^[a-zA-Z\-.]+$"); + export const REGEXP = new RegExp("^[a-zA-Z0-9!@#$%^&*()\-_=+;:'\"\\|,.<>/?]+$"); } /** * See the main documentation in game/lang/Lang @@ -101,55 +133,6 @@ export namespace Lang { WEIGHT = "WEIGHT", } - /*** - * There are three string for each language! - * - The object key string is for use in the ts/js code, hence the casing. - * - The display name is for display purposes. Special characters are OK. - * - The id name is a shorter string for web-storage keys and a URL query. - */ - export const Names = Object.freeze({ - ENGLISH__LOWERCASE: { - display: "English Lowercase (QWERTY)", - id: "engl-low", - }, - ENGLISH__MIXEDCASE: { - display: "English Mixed-Case (QWERTY)", - id: "engl-mix", - }, - JAPANESE__HIRAGANA: { - display: "Japanese Hiragana", - id: "japn-hir", - }, - JAPANESE__KATAKANA: { - display: "Japanese Katakana", - id: "japn-kat", - }, - KOREAN__DUBEOLSIK: { - display: "Korean Dubeolsik (두벌식 키보드)", - id: "kore-dub", - }, - KOREAN__SEBEOLSIK: { - display: "Korean Sebeolsik (세벌식 최종 키보드)", - id: "kore-sub", - }, - KOREAN__ROMANIZATION: { - display: "Korean Revised Romanization", - id: "kore-rom", - }, - }); - Names as Record; - export namespace Names { - export type Key = keyof typeof Names; - export type Value = typeof Names[keyof typeof Names]; - } - - // Common remapping functions. - export const __RemapTemplates = Object.freeze({ - IDENTITY: (input: string): string => input, - TO_LOWER: (input: string): string => input.toLowerCase(), - }); - __RemapTemplates as Readonly>; - /** * * This can be used, for example, for basic practical purposes like @@ -172,17 +155,91 @@ export namespace Lang { * @param input - * @returns */ - export const RemappingFunctions - : Readonly> - = Object.freeze({ - [ Names.ENGLISH__LOWERCASE.id ]: __RemapTemplates.TO_LOWER, - [ Names.ENGLISH__MIXEDCASE.id ]: __RemapTemplates.IDENTITY, - [ Names.JAPANESE__HIRAGANA.id ]: __RemapTemplates.TO_LOWER, - [ Names.JAPANESE__KATAKANA.id ]: __RemapTemplates.TO_LOWER, - [ Names.KOREAN__DUBEOLSIK.id ]: __RemapTemplates.IDENTITY, - [ Names.KOREAN__SEBEOLSIK.id ]: __RemapTemplates.IDENTITY, - [ Names.KOREAN__ROMANIZATION.id]:__RemapTemplates.TO_LOWER, + export const __RemapTemplates = Object.freeze({ + IDENTITY: (input: string): string => input, + TO_LOWER: (input: string): string => input.toLowerCase(), }); + __RemapTemplates as Readonly>; + + /** + * + */ + export const FrontendDescs = Object.freeze([ + { + id: "engl-low", + module: "English", export: "Lowercase", numLeaves: 26, + remapFunc: __RemapTemplates.TO_LOWER, + display: "English Lowercase (QWERTY)", + blurb: "", + }, { + id: "engl-mix", + module: "English", export: "MixedCase", numLeaves: 52, + remapFunc: __RemapTemplates.IDENTITY, + display: "English Mixed-Case (QWERTY)", + blurb: "", + }, { + id: "japn-hir", + module: "Japanese", export: "Hiragana", numLeaves: 71, + remapFunc: __RemapTemplates.TO_LOWER, + display: "Japanese Hiragana", + blurb: "", + }, { + id: "japn-kat", + module: "Japanese", export: "Katakana", numLeaves: 70, + remapFunc: __RemapTemplates.TO_LOWER, + display: "Japanese Katakana", + blurb: "", + }, { + id: "kore-dub", + module: "Korean", export: "Dubeolsik", numLeaves: 9177, + remapFunc: __RemapTemplates.IDENTITY, + display: "Korean Dubeolsik (두벌식 키보드)", + blurb: "The most common keyboard layout, and South Korea's only Hangul" + + " standard since 1969. Consonants are on the left, and vowels on" + + " the right.", + }, { + id: "kore-sub", + module: "Korean", export: "Sebeolsik", numLeaves: 10206, + remapFunc: __RemapTemplates.IDENTITY, + display: "Korean Sebeolsik (세벌식 최종 키보드)", + blurb: "Another Hangul keyboard layout used in South Korea, and the" + + " final Sebeolsik layout designed by Dr. Kong Byung Woo, hence" + + " the name. Syllable-initial consonants are on the right, final" + + " consonants on the left, and vowels in the middle. It is more" + + " ergonomic than the dubeolsik, but not widely used.", + }, { + id: "kore-rom", + module: "Korean", export: "Romanization", numLeaves: 3990, + remapFunc: __RemapTemplates.TO_LOWER, + display: "Korean Revised Romanization", + blurb: "The Revised Romanization of Korean (국어의 로마자 표기법; 國語의 로마字" + + " 表記法) is the official South Korean language romanization system. It" + + " was developed by the National Academy of the Korean Language from 1995," + + " and was released on 7 July 2000 by South Korea's Ministry of Culture" + + " and Tourism", + }, + ].map((desc) => Object.freeze(desc)),); + FrontendDescs as TU.RoArr>>; + export type FrontendDesc = typeof FrontendDescs[number]; + + /** + * + * @param langId - + */ + export function GET_FRONTEND_DESC_BY_ID(langId: FrontendDesc["id"]): FrontendDesc { + const desc = FrontendDescs.find((desc) => desc.id === langId); + if (!desc) throw new Error(`Frontend descriptor of language with id` + + ` \"${langId}\" not found.`); + return desc!; + } } Object.freeze(Lang); Object.freeze(Lang.prototype); diff --git a/src/base/utils/readme.md b/src/base/defs/readme.md similarity index 100% rename from src/base/utils/readme.md rename to src/base/defs/readme.md diff --git a/src/base/utils/tsconfig.json b/src/base/defs/tsconfig.json similarity index 100% rename from src/base/utils/tsconfig.json rename to src/base/defs/tsconfig.json diff --git a/src/base/floor/Grid.ts b/src/base/floor/Grid.ts index bd448129..fb1cf287 100644 --- a/src/base/floor/Grid.ts +++ b/src/base/floor/Grid.ts @@ -1,10 +1,8 @@ -import { OmHooks } from "browser/OmHooks"; import { Coord, Tile } from "./Tile"; import { TileGetter } from "./TileGetter"; import type { Euclid2 } from "./impl/Euclid2"; import type { Beehive } from "./impl/Beehive"; -import { VisibleGrid } from "floor/VisibleGrid"; /** @@ -30,8 +28,6 @@ export abstract class Grid implements TileGetter.Source< * Protected. See `Grid.getImplementation` for how to access class * literals for construction. * - * _Does not call reset._ - * * @param desc - */ protected constructor(desc: Grid.CtorArgs) { @@ -139,58 +135,6 @@ export abstract class Grid implements TileGetter.Source< * @param dest - */ public abstract minMovesFromTo(source: Coord.Bare, dest: Coord.Bare): number; - - /** - * Note: I would rather have this implementation go under the - * `VisibleGrid` class, but I don't want to get into mixins as of - * now to get around no-multiple-inheritance. - * - * @param desc - - * @param gridImplElem - - */ - public __VisibleGrid_super(desc: Grid.CtorArgs, gridImplElem: HTMLElement): void { - const OHG = OmHooks.Grid; - gridImplElem.tabIndex = 0; - gridImplElem.classList.add(OHG.Class.IMPL_BODY); - const parentElem = document.getElementById(desc.domParentHtmlIdHook); - if (!parentElem) { - throw new RangeError(`The ID \"${desc.domParentHtmlIdHook}\"` - + ` did not refer to an existing html element.`); - } - parentElem.dataset[OHG.Dataset.COORD_SYS] = desc.coordSys; - parentElem.classList.add( - OHG.Class.GRID, - OmHooks.General.Class.TEXT_SELECT_DISABLED, - OmHooks.General.Class.CENTER_CONTENTS, - OmHooks.General.Class.STACK_CONTENTS, - ); - // Remove all child elements from host and then append the new grid: - parentElem.querySelectorAll(`.${OHG.Class.IMPL_BODY}`).forEach((node) => node.remove()); - parentElem.insertAdjacentElement("afterbegin", gridImplElem); - (this as TU.NoRo> as TU.NoRo>).baseElem = gridImplElem; - { - // Add a "keyboard-disconnected" icon if not added already: - // This needs to be a _later_ sibling of gridImplElem. - let kbdDcBase: HTMLElement | null = parentElem - .querySelector(`:scope .${OHG.Class.KBD_DC_BASE}`); - if (!kbdDcBase) { - const kbdDcBase = document.createElement("div"); - kbdDcBase.classList.add( - OHG.Class.KBD_DC_BASE, - OmHooks.General.Class.CENTER_CONTENTS, - ); - // TODO.impl Add an with icon instead please. - { - const kbdDcIcon = document.createElement("div"); - kbdDcIcon.classList.add(OHG.Class.KBD_DC_ICON); - kbdDcIcon.innerText = "(click grid to continue typing)"; - kbdDcBase.appendChild(kbdDcIcon); - } - parentElem.appendChild(kbdDcBase); - } - } - } - } export namespace Grid { @@ -207,13 +151,12 @@ export namespace Grid { // the additions of new coordinate systems. // ============================================================== - export type CtorArgs = { + export type CtorArgs = Readonly<{ gridClass: Grid.ClassIf; tileClass: Tile.ClassIf; coordSys: S; dimensions: Dimensions; - domParentHtmlIdHook: string; - }; + }>; /** * Used to simulate abstract static methods. diff --git a/src/base/floor/Tile.ts b/src/base/floor/Tile.ts index 6fd508b3..497134bb 100644 --- a/src/base/floor/Tile.ts +++ b/src/base/floor/Tile.ts @@ -1,4 +1,4 @@ -import { Lang, Player } from "utils/TypeDefs"; +import { Lang, Player } from "defs/TypeDefs"; import { Coord } from "./Coord"; export { Coord }; @@ -37,8 +37,6 @@ export class Tile { public lastKnownUpdateId: number; /** - * _Does not call reset._ - * * @param coord - */ public constructor(coord: Coord) { @@ -57,15 +55,6 @@ export class Tile { this.setLangCharSeqPair(Lang.CharSeqPair.NULL); } - /** - * Called, for example, when a {@link Player} on this `Tile` provides - * input that did not work to complete their {@link Player#seqBuffer} - * against any neighbouring `Tile`s. - */ - public visualBell(): void { - // does nothing by default. - } - /** diff --git a/src/base/floor/VisibleGrid.ts b/src/base/floor/VisibleGrid.ts index 5cd3774f..9d85de5f 100644 --- a/src/base/floor/VisibleGrid.ts +++ b/src/base/floor/VisibleGrid.ts @@ -1,5 +1,6 @@ import type { Coord, Tile } from "floor/Tile"; import { Grid } from "floor/Grid"; +import { OmHooks } from 'defs/OmHooks'; /** @@ -15,22 +16,8 @@ import { Grid } from "floor/Grid"; * _server_ related code will benefit from this choice since it will * not use */ -export interface VisibleGrid extends Grid { - - /** - * Contains the implementation-dependant HTML representation of - * the grid. - */ - readonly baseElem: HTMLElement; - - // This is just a reminder to the developer that such a function - // exists and is an important part of the architecture. Since - // VisibleGrid can't be a class (no multiple inheritance), this - // part of a VisibleGrid's constructor sequence is implemented - // in Grid. - __VisibleGrid_super(desc: Grid.CtorArgs, domGrid: HTMLElement): void; -} - +export interface VisibleGrid +extends Grid, VisibleGridMixin { } export namespace VisibleGrid { @@ -51,3 +38,40 @@ export namespace VisibleGrid { return ctor as unknown as ClassIf; }; } + + +/** + * + */ +export class VisibleGridMixin { + /** + * Contains the implementation-dependant HTML representation of + * the grid. + */ + public readonly baseElem: HTMLElement; + public readonly spotlightElems: TU.RoArr; + + /** + * Note: I would rather have this implementation go under the + * `VisibleGrid` class, but I don't want to get into mixins as of + * now to get around no-multiple-inheritance. + * + * @param desc - + * @param gridImplElem - + */ + public __VisibleGrid_super(desc: Grid.CtorArgs, gridImplElem: HTMLElement): void { + const OHG = OmHooks.Grid; + gridImplElem.classList.add(OHG.Class.IMPL_BODY); + gridImplElem.dataset[OHG.Dataset.IMPL_COORD_SYS] = desc.coordSys; + (this.baseElem as HTMLElement) = gridImplElem; + + // Initialize spotlight elements: + const sslElem = document.createElement("div"); + sslElem.classList.add(OmHooks.Player.Class.SHORT_SPOTLIGHT); + const lslElem = document.createElement("div"); + lslElem.classList.add(OmHooks.Player.Class.LONG_SPOTLIGHT); + (this.spotlightElems as TU.RoArr) = Object.freeze([ sslElem, lslElem, ]); + } +} +Object.freeze(VisibleGridMixin); +Object.freeze(VisibleGridMixin.prototype); diff --git a/src/base/floor/VisibleTile.ts b/src/base/floor/VisibleTile.ts index 0c6e284b..ca641e0f 100644 --- a/src/base/floor/VisibleTile.ts +++ b/src/base/floor/VisibleTile.ts @@ -1,5 +1,5 @@ -import { OmHooks } from "browser/OmHooks"; -import type { Lang, Player } from "utils/TypeDefs"; +import { OmHooks } from "defs/OmHooks"; +import type { Lang, Player } from "defs/TypeDefs"; import { Coord, Tile } from "./Tile"; @@ -8,56 +8,47 @@ export { Coord } from "./Tile"; /** * Implicitly handles visuals with help from CSS. - * - * Layers: - * 0. Invisible cell layer (opaque on visual bell) - * 1. Empty layer for spotlight mask - * 2. Player face layer - * 3. Language Written Character - * 4. Language Type-able Sequence - * - * https://developer.mozilla.org/en-US/docs/Web/CSS/z-index - * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index - * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context - * - * Dataset: - * Top-level layer has property "scoreValue" - * - * @extends Tile */ export class VisibleTile extends Tile { - readonly #baseElem: HTMLElement; + readonly #baseElem: HTMLDivElement; private readonly langCharElem: HTMLDivElement; private readonly langSeqElem: HTMLDivElement; public constructor(coordDesc: Tile["coord"]) { super(coordDesc); { - const baseElem = document.createElement("div"); + const baseElem + = this.#baseElem + = document.createElement("div"); baseElem.classList.add( - OmHooks.Tile.Class.BASE, OmHooks.General.Class.CENTER_CONTENTS, OmHooks.General.Class.STACK_CONTENTS, + OmHooks.Tile.Class.BASE, ); - this.#baseElem = baseElem; + baseElem.setAttribute("aria-label", "Tile"); } { + // Pointer hitbox element. // Must be the first child. See note in CSS class hook. - const pthbElem = document.createElement("div"); - pthbElem.classList.add(OmHooks.Tile.Class.POINTER_HB); - this.#baseElem.appendChild(pthbElem); + const pthb = document.createElement("div"); + pthb.classList.add(OmHooks.Tile.Class.POINTER_HB); + pthb.setAttribute("aria-hidden", "true"); + this.#baseElem.appendChild(pthb); } { - const charElem = document.createElement("div"); + const charElem + = this.langCharElem + = document.createElement("div"); charElem.classList.add( OmHooks.Tile.Class.LANG_CHAR, ); this.#baseElem.appendChild(charElem); - this.langCharElem = charElem; } { - const seqElem = document.createElement("div"); + const seqElem + = this.langSeqElem + = document.createElement("div"); seqElem.classList.add(OmHooks.Tile.Class.LANG_SEQ); + seqElem.setAttribute("role", "tooltip"); this.#baseElem.appendChild(seqElem); - this.langSeqElem = seqElem; } } @@ -74,26 +65,20 @@ export class VisibleTile extends Tile { ): void { super.__setOccupant(playerId, immigrantInfo); // It must go at least before the langChar element so that the - // CSS can create a fading trail effect. + // CSS can create a fading trail effect. It must go after the + // hitbox so that it can be hidden to avoid covering the tooltip. this.#baseElem.insertBefore(immigrantInfo.playerElem, this.langCharElem); this.langSeqElem.innerText = immigrantInfo.username; } - /** - * @override - */ - public visualBell(): void { - this.#baseElem; // TODO.impl Use an animation to flash tile element? - } - - /** * @override */ public evictOccupant(): void { super.evictOccupant(); - // Undo setting mouseover text to occupant username: + // Undo setting mouseover text to something player-related + // (See `__setOccupant` for what we did and now need to undo): this.langSeqElem.innerText = this.langSeq; } diff --git a/src/base/floor/impl/Beehive.ts b/src/base/floor/impl/Beehive.ts index 04dee7c6..08f3ca5c 100644 --- a/src/base/floor/impl/Beehive.ts +++ b/src/base/floor/impl/Beehive.ts @@ -1,7 +1,7 @@ +import { applyMixins } from 'defs/TypeDefs'; import { Coord as BaseCoord, Tile } from "../Tile"; -import type { VisibleTile } from "floor/VisibleTile"; import { Grid as AbstractGrid } from "../Grid"; -import { VisibleGrid } from "../VisibleGrid"; +import { VisibleGrid, VisibleGridMixin } from "../VisibleGrid"; type S = BaseCoord.System.BEEHIVE; @@ -204,7 +204,6 @@ export namespace Beehive { }; export class Visible extends Grid implements VisibleGrid { - public readonly baseElem: HTMLElement; public constructor(desc: AbstractGrid.CtorArgs) { super(desc); const domGrid: HTMLElement = undefined!; @@ -212,6 +211,10 @@ export namespace Beehive { this.__VisibleGrid_super(desc, domGrid); } } + export interface Visible extends VisibleGridMixin { }; + applyMixins(Visible, [VisibleGridMixin,]); + Object.freeze(Visible); + Object.freeze(Visible.prototype); } Object.freeze(Grid); Object.freeze(Grid.prototype); diff --git a/src/base/floor/impl/Euclid2.ts b/src/base/floor/impl/Euclid2.ts index a9a1c52a..22628f65 100644 --- a/src/base/floor/impl/Euclid2.ts +++ b/src/base/floor/impl/Euclid2.ts @@ -1,7 +1,8 @@ +import { applyMixins } from "defs/TypeDefs"; import { Coord as BaseCoord, Tile } from "../Tile"; import type { VisibleTile } from "floor/VisibleTile"; import { Grid as AbstractGrid } from "../Grid"; -import { VisibleGrid } from "../VisibleGrid"; +import { VisibleGrid, VisibleGridMixin } from "../VisibleGrid"; type S = BaseCoord.System.EUCLID2; @@ -119,7 +120,6 @@ export namespace Euclid2 { }); } } - export namespace Coord { export type Bare = Readonly<{ x: number; @@ -169,9 +169,11 @@ export namespace Euclid2 { } public forEachTile(consumer: (tile: Tile) => void, thisArg: object = this): void { - this.grid.forEach((row) => row.forEach((tile) => { - consumer(tile); - }, thisArg), thisArg); + for (const row of this.grid) { + for (const tile of row) { + consumer(tile); + } + } } public getUntToward(sourceCoord: Coord, intendedDest: Coord.Bare): Tile { @@ -316,7 +318,6 @@ export namespace Euclid2 { return new Coord({x,y,}); } } - export namespace Grid { /** * If `width` is not specified, `height` is taken as its default value. @@ -327,8 +328,6 @@ export namespace Euclid2 { }; export class Visible extends Grid implements VisibleGrid { - public readonly baseElem: HTMLElement; - /** * @override */ @@ -347,9 +346,12 @@ export namespace Euclid2 { this.__VisibleGrid_super(desc, gridElem); } } + export interface Visible extends VisibleGridMixin { }; + applyMixins(Visible, [VisibleGridMixin,]); + Object.freeze(Visible); + Object.freeze(Visible.prototype); } Object.freeze(Grid); Object.freeze(Grid.prototype); - } Object.freeze(Euclid2); diff --git a/src/base/floor/impl/readme.md b/src/base/floor/impl/readme.md index c6b84e51..6c7b8e1f 100644 --- a/src/base/floor/impl/readme.md +++ b/src/base/floor/impl/readme.md @@ -34,6 +34,8 @@ export namespace SysName { // typed field definitions }>; } + Object.freeze(Coord); + Object.freeze(Coord.prototype); ... } ``` @@ -48,10 +50,11 @@ Import your system namespace, and add entries to `Coord` and `Coord.Bare` for th ```typescript // coord-related imports +import { applyMixins } from "defs/TypeDefs"; import { Coord as BaseCoord, Tile } from "../Tile"; import type { VisibleTile } from "floor/VisibleTile"; import { Grid as AbstractGrid } from "../Grid"; -import { VisibleGrid } from "../VisibleGrid"; +import { VisibleGrid, VisibleGridMixin } from "../VisibleGrid"; // documentation export namespace SysName { @@ -99,8 +102,15 @@ export namespace SysName { // Set up DOM fields for rendering the grid. } } + export interface Visible extends VisibleGridMixin { }; + applyMixins(Visible, [VisibleGridMixin]); + Object.freeze(Visible); + Object.freeze(Visible.prototype); } + Object.freeze(Grid); + Object.freeze(Grid.prototype); } +Object.freeze(SysName); ``` **In [`Grid.ts`](../Grid.ts)** diff --git a/src/base/floor/tsconfig.json b/src/base/floor/tsconfig.json index 328ce64b..7a500e57 100644 --- a/src/base/floor/tsconfig.json +++ b/src/base/floor/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../.templates/tsconfig.json", "references": [ - { "path": "../utils", }, - { "path": "../browser", }, + { "path": "../defs", }, ], } \ No newline at end of file diff --git a/src/base/game/Game.ts b/src/base/game/Game.ts index a214b7ce..03a66828 100644 --- a/src/base/game/Game.ts +++ b/src/base/game/Game.ts @@ -37,7 +37,10 @@ export namespace Game { * These are abstract handles to game-implementation-dependant * components. */ - export type ImplArgs = { + export type ImplArgs< + G extends Game.Type, + S extends Coord.System, + > = { tileClass: Tile.ClassIf, playerStatusCtor: typeof PlayerStatus, }; @@ -55,24 +58,16 @@ export namespace Game { > = Readonly<{ coordSys: S; gridDimensions: Grid.Dimensions; - gridHtmlIdHook: G extends Game.Type.SERVER ? undefined : string; + averageFreeHealthPerTile: Player.Health; - languageName: Lang.Names.Value["id"]; + langId: Lang.FrontendDesc["id"]; langBalancingScheme: Lang.BalancingScheme; - /** - * The index in `playerDescs` of the operator's ctor args. - */ - operatorIndex: G extends Game.Type.SERVER - ? undefined - : Player.Id; playerDescs: TU.RoArr<( G extends Game.Type.Manager ? Player.CtorArgs.PreIdAssignment : Player.CtorArgs )>; - - averageFreeHealthPerTile: Player.Health; }>; export namespace CtorArgs { diff --git a/src/base/game/ScoreInfo.ts b/src/base/game/ScoreInfo.ts new file mode 100644 index 00000000..36521326 --- /dev/null +++ b/src/base/game/ScoreInfo.ts @@ -0,0 +1,56 @@ +import type { Player } from "./player/Player"; +import { Player as __Player } from "defs/TypeDefs"; + + +/** + * + */ +export class ScoreInfo { + + public readonly entries: TU.RoArr; + + public constructor(playerIds: TU.RoArr) { + const entries: Array = []; + for (const id of playerIds) { + entries[id] = new ScoreInfo.Entry(); + } + this.entries = entries; + } + + public reset(): void { + for (const entry of this.entries) { + entry.reset(); + } + } +} +export namespace ScoreInfo { + /** + * + */ + export class Entry { + + public totalHealthPickedUp: Player.Health; + + // TODO.design how to send this imfo to the game manager? + // It is currently checked on the client side. + //public invalidKeyPresses + + public readonly moveCounts: {[M in Player.MoveType]: number}; + + public constructor() { + this.moveCounts = {} as any; // This will be initialized during reset. + } + + public reset(): void { + this.totalHealthPickedUp = 0.0; + (Object.getOwnPropertyNames(__Player.MoveType) as + Array).forEach((key) => { + this.moveCounts[key] = 0; + }); + } + } + Object.freeze(Entry); + Object.freeze(Entry.prototype); +} +Object.freeze(ScoreInfo); +Object.freeze(ScoreInfo.prototype); diff --git a/src/base/game/__gameparts/Base.ts b/src/base/game/__gameparts/Base.ts index 43f38525..82bedcc4 100644 --- a/src/base/game/__gameparts/Base.ts +++ b/src/base/game/__gameparts/Base.ts @@ -1,5 +1,6 @@ import { Game } from "../Game"; -import type { Lang } from 'utils/TypeDefs'; +import { Lang } from "defs/TypeDefs"; + import type { Coord, Tile } from "floor/Tile"; import type { Grid } from "floor/Grid"; import type { VisibleGrid } from "floor/VisibleGrid"; @@ -19,11 +20,12 @@ export abstract class GameBase { public readonly grid: G extends Game.Type.SERVER ? Grid : VisibleGrid; - public readonly langName: Lang.Names.Value["id"]; + public readonly langFrontend: Lang.FrontendDesc; public readonly players: TU.RoArr>; - public readonly operator: G extends Game.Type.SERVER ? undefined : OperatorPlayer; + public readonly operators: TU.RoArr>; + #currentOperator: OperatorPlayer | undefined; /** * Indexable by team ID's. @@ -36,8 +38,6 @@ export abstract class GameBase { /** - * _Does not call reset._ - * * Performs the "no invincible player" check (See {@link Player#teamSet}). * * @param gameType - @@ -46,7 +46,7 @@ export abstract class GameBase { */ public constructor( gameType: G, - impl: Game.ImplArgs, + impl: Game.ImplArgs, desc: Game.CtorArgs, ) { this.gameType = gameType; @@ -56,46 +56,58 @@ export abstract class GameBase { tileClass: impl.tileClass, coordSys: desc.coordSys, dimensions: desc.gridDimensions, - domParentHtmlIdHook: (desc.gridHtmlIdHook || "n/a")!, }) as GameBase["grid"]; - this.langName = desc.languageName; + + this.langFrontend = Lang.GET_FRONTEND_DESC_BY_ID(desc.langId); // Construct players: this.__playerStatusCtor = impl.playerStatusCtor; this.players = this.createPlayers(desc); - if (desc.operatorIndex !== undefined) { - // Note at above comparison: we must be explicit - // since zero is a valid, _falsy_ operatorIndex. - (this.operator as Player) = this.players[desc.operatorIndex!]; - } - const teams: Array>> = []; - this.players.forEach((player) => { - if (!teams[player.teamId]) { - teams[player.teamId] = []; + + this.operators = this.players.filter((player) => player.isALocalOperator) as OperatorPlayer[]; + this.currentOperator = this.operators[0]; + if (this.operators.some((op) => op.teamId !== this.operators[0].teamId)) { + // Currently requiring this because the current visual colouring + // is initialized based on whether a player is on the operator's + // team. Otherwise, we'd have to re-colour when rotating operator. + throw new Error("All local operators must be on the same team."); + } { + const teams: Array>> = []; + this.players.forEach((player) => { + if (!teams[player.teamId]) { + teams[player.teamId] = []; + } + teams[player.teamId].push(player); + }); + this.teams = teams.map((teammateArray, teamId) => { + return new Team(teamId, teammateArray); + }); + if (this.teams.every((team) => team.id === Team.ElimOrder.IMMORTAL)) { + // TODO.design put a check inside the UI code to prevent this. + // The purpose of this restriction is to prevent DoS attacks on + // a hosting server by creating games that can never end and + // leaving them open forever, thus leaking the server's resources. + throw new Error("All teams are immortal. The game will never end."); } - teams[player.teamId].push(player); - }); - this.teams = teams.map((teammateArray, teamId) => { - return new Team(teamId, teammateArray); - }); - this.players.forEach((player) => player.__afterAllPlayersConstruction()); - if (this.teams.every((team) => team.id === Team.ElimOrder.IMMORTAL)) { - // TODO.design put a check inside the UI code to prevent this. - // The purpose of this restriction is to prevent DoS attacks on - // a hosting server by creating games that can never end and - // leaving them open forever, thus leaking the server's resources. - throw new Error("All teams are immortal. The game will never end."); } + this.players.forEach((player) => player.__afterAllPlayersConstruction()); } /** * Reset the grid. + * + * Overrides should not use the return value. They should return + * the result of calling `ctorAsync`. */ - public reset(): void { + public reset(): Promise { this.grid.reset(); // We must reset status to PAUSED to pass a state-transition // assertion when changing status later to PLAYING. this.#status = Game.Status.PAUSED; + + // Important: Since there is nothing to do in this game-part's + // ctorAsync getter, we don't need to use `await`. + return Promise.resolve(); } protected abstract __getGridImplementation(coordSys: S): @@ -119,18 +131,18 @@ export abstract class GameBase { ? (gameDesc.playerDescs as pCtorArgs) : Player.CtorArgs.finalize(gameDesc.playerDescs); - return playerDescs.map((playerDesc, playerIndex) => { + return Object.freeze(playerDescs.map((playerDesc) => { if (playerDesc.familyId === Player.Family.HUMAN) { - return (playerIndex === gameDesc.operatorIndex) + return (playerDesc.isALocalOperator) ? this.__createOperatorPlayer(playerDesc) : new Player(this, playerDesc); } else { - return this.__createArtifPlayer(playerDesc); + return this.__createArtifPlayer(playerDesc) as Player; } - }); + })); } protected abstract __createOperatorPlayer(desc: Player.__CtorArgs<"HUMAN">): OperatorPlayer; - protected abstract __createArtifPlayer(desc: Player.CtorArgs): + protected abstract __createArtifPlayer(desc: Player.__CtorArgs): (G extends Game.Type.Manager ? ArtificialPlayer : Player); public serializeResetState(): Game.ResetSer { @@ -146,7 +158,7 @@ export abstract class GameBase { healthCoords.push({ coord: tile.coord, health: tile.freeHealth, - }) + }); } }); return { csps, playerCoords, healthCoords, }; @@ -168,11 +180,37 @@ export abstract class GameBase { }); } + public get currentOperator(): OperatorPlayer | undefined { + return this.#currentOperator; + } + /** + * Passing `undefined` or the current operator has no effect. + */ + public set currentOperator(nextOperator: OperatorPlayer | undefined) { + if (nextOperator + && this.currentOperator !== nextOperator + && this.operators.includes(nextOperator)) + { + this.#currentOperator = nextOperator; + nextOperator.__abstractNotifyBecomeCurrent(); + } + } + public get status(): Game.Status { return this.#status; } + /** + * On the client side, this should only be accessed through a + * wrapper function that also makes UI-related changes. + * + * If the game is already playing, this does nothing. + */ public statusBecomePlaying(): void { + if (this.status === Game.Status.PLAYING) { + console.log("[statusBecomePlaying]: Game is already playing"); + return; + } if (this.status !== Game.Status.PAUSED) { throw new Error("Can only resume a game that is currently paused."); } @@ -181,13 +219,18 @@ export abstract class GameBase { }); this.__abstractStatusBecomePlaying(); this.#status = Game.Status.PLAYING; - // Make sure focus goes back to the grid element so that it - // can pick up user input as keydown events: - if ((this.grid as VisibleGrid).baseElem) { - (this.grid as VisibleGrid).baseElem.focus(); - } } + /** + * On the client side, this should only be accessed through a + * wrapper function that also makes UI-related changes. + * + * If the game is already paused, this does nothing. + */ public statusBecomePaused(): void { + if (this.status === Game.Status.PAUSED) { + console.log("[statusBecomePaused]: Game is already paused"); + return; + } if (this.status !== Game.Status.PLAYING) { throw new Error("Can only pause a game that is currently playing."); } @@ -203,6 +246,8 @@ export abstract class GameBase { * `noCheckGameOver` flag set to `true`. A mortal team becomes * (and subsequently, unconditionally stays) eliminated when all * their members are in a downed state at the same time. + * + * This should not be controllable by UI input elements. */ public statusBecomeOver(): void { if (this.status !== Game.Status.PLAYING) { diff --git a/src/base/game/__gameparts/Events.ts b/src/base/game/__gameparts/Events.ts index 0cd3ae7e..6b5bd1b9 100644 --- a/src/base/game/__gameparts/Events.ts +++ b/src/base/game/__gameparts/Events.ts @@ -1,4 +1,4 @@ -import type { Coord } from "floor/Tile"; +import type { Coord, Tile } from "floor/Tile"; import { Game } from "../Game"; import { PlayerActionEvent, TileModEvent } from "../events/PlayerActionEvent"; @@ -52,19 +52,22 @@ export abstract class GameEvents ex public constructor( gameType: G, - impl: Game.ImplArgs, + impl: Game.ImplArgs, gameDesc: Game.CtorArgs, ) { super(gameType, impl, gameDesc); this.eventRecordBitmap = []; } - public reset(): void { - super.reset(); + public reset(): Promise { + const superPromise = super.reset(); // Clear the event record: this.eventRecordBitmap.fill(false, 0, Game.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH); this.#nextUnusedEventId = 0; + + // Since we didn't wait for the superPromise, return it. + return superPromise; } protected get nextUnusedEventId(): EventRecordEntry["eventId"] { @@ -93,6 +96,9 @@ export abstract class GameEvents ex } else if (this.eventRecordBitmap[wrappedId]) { throw new Error("Event ID's must be assigned unique values."); } + // TODO.impl Check for an OnlineGame that it is not far behind the Server. + // also design what should be done to handle that... Do we really need to + // recover from that? this.eventRecordBitmap[wrappedId] = true; this.eventRecordBitmap[(id + Game.K.EVENT_RECORD_WRAPPING_BUFFER_LENGTH @@ -103,31 +109,30 @@ export abstract class GameEvents ex protected executeTileModEvent( - desc: TileModEvent, + desc: Readonly>, doCheckOperatorSeqBuffer: boolean = true, - ): void { + ): Tile { + Object.freeze(desc); const dest = this.grid.tile.at(desc.coord); - if (dest.lastKnownUpdateId < desc.lastKnownUpdateId) { - if (desc.newCharSeqPair) { - dest.setLangCharSeqPair(desc.newCharSeqPair); - // Refresh the operator's `seqBuffer` (maintain invariant) for new CSP: - if (doCheckOperatorSeqBuffer && this.operator !== undefined - && !(this.operator.tile.destsFrom().get.includes(dest))) { - // ^Do this when non-operator moves into the the operator's vicinity. - this.operator.seqBufferAcceptKey(""); - } + if (dest.lastKnownUpdateId > desc.lastKnownUpdateId) return dest; + if (dest.lastKnownUpdateId === desc.lastKnownUpdateId) throw new Error("never."); + + if (desc.newCharSeqPair) { + dest.setLangCharSeqPair(desc.newCharSeqPair); + // Refresh the operator's `seqBuffer` (maintain invariant) for new CSP: + if (doCheckOperatorSeqBuffer) { + // ^Do this when non-operator moves into the the operator's vicinity. + this.operators.filter((op) => { + return op.tile.destsFrom().get.includes(dest); + }).forEach((op) => op.seqBufferAcceptKey("")); } - dest.lastKnownUpdateId = desc.lastKnownUpdateId; - dest.freeHealth = desc.newFreeHealth!; } + dest.lastKnownUpdateId = desc.lastKnownUpdateId; + dest.freeHealth = desc.newFreeHealth!; + return dest; } /** - * Update the {@link Game#grid}. Call either at the end of - * {@link Game#processMoveRequest} if I am a {@link ServerGame} or - * {@link OfflineGame}, or as an event callback if I am a - * {@link OnlineGame}. - * * Automatically lowers the {@link Player#requestInFlight} field * for the requesting `Player` if the arriving event description * is the newest one for the specified `Player`. @@ -140,9 +145,8 @@ export abstract class GameEvents ex * @param desc * A descriptor for all changes mandated by the player-movement event. */ - protected processMoveExecute(desc: Readonly>): void { + protected executePlayerMoveEvent(desc: Readonly>): void { const player = this.players[desc.playerId]; - const dest = this.grid.tile.at(desc.dest.coord); const clientEventLag = desc.playerLastAcceptedRequestId - player.lastAcceptedRequestId; if (desc.eventId === EventRecordEntry.EVENT_ID_REJECT) { @@ -154,15 +158,15 @@ export abstract class GameEvents ex return; // Short-circuit! } this.recordEvent(desc); - this.executeTileModEvent(desc.dest, player !== this.operator); - desc.tilesWithHealthUpdates?.forEach((desc) => { + const dest = this.executeTileModEvent(desc.destModDesc, player !== this.currentOperator); + desc.tileHealthModDescs?.forEach((desc) => { this.executeTileModEvent(desc); }); if (clientEventLag > 1) { // ===== Out of order receipt (client-side) ===== // Already received more recent request responses. - if (player === this.operator) { + if (player === this.currentOperator) { // Operator never receives their own updates out of // order because they only have one unacknowledged // in-flight request at a time. @@ -173,10 +177,9 @@ export abstract class GameEvents ex // Okay- the response is an acceptance of the specified player's most // recent request pending this acknowledgement. player.requestInFlight = false; - if ((player === this.operator) + if ((player === this.currentOperator) ? (clientEventLag === 1) : (clientEventLag <= 1)) { - player.status.score = desc.newPlayerHealth!.score; player.status.health = desc.newPlayerHealth!.health; player.moveTo(dest); @@ -198,8 +201,7 @@ export abstract class GameEvents ex * * @param desc - */ - protected processBubbleExecute(desc: Readonly): void { - // TODO.impl Visually highlight the affected tiles for the specified estimate-duration. + protected executePlayerBubbleEvent(desc: Readonly): void { const bubbler = this.players[desc.playerId]; bubbler.requestInFlight = false; diff --git a/src/base/game/__gameparts/Manager.ts b/src/base/game/__gameparts/Manager.ts index 995f2138..455df360 100644 --- a/src/base/game/__gameparts/Manager.ts +++ b/src/base/game/__gameparts/Manager.ts @@ -7,8 +7,8 @@ import { Player } from "../player/Player"; import { PlayerGeneratedRequest } from "../events/EventRecordEntry"; import { PlayerActionEvent, TileModEvent } from "../events/PlayerActionEvent"; -import { English } from "lang/impl/English"; // NOTE: temporary placeholder. -import { GameEvents } from "game/__gameparts/Events"; +import { GameEvents } from "./Events"; +import { ScoreInfo } from "game/ScoreInfo"; /** @@ -22,6 +22,7 @@ export abstract class GameManager e readonly #freeHealthTiles: Set>; public readonly lang: Lang; + readonly #langImportPromise: Promise; /** * NOTE: Shuffling operations and the @@ -33,9 +34,9 @@ export abstract class GameManager e */ protected readonly langBalancingScheme: Lang.BalancingScheme; + private readonly scoreInfo: ScoreInfo; + /** - * _Does not call reset._ - * * Performs the "no invincible player" check (See {@link Player#teamSet}). * * @param gameType - @@ -44,7 +45,7 @@ export abstract class GameManager e */ public constructor( gameType: G, - impl: Game.ImplArgs, + impl: Game.ImplArgs, desc: Game.CtorArgs, ) { super(gameType, impl, desc); @@ -52,30 +53,41 @@ export abstract class GameManager e this.averageFreeHealthPerTile = desc.averageFreeHealthPerTile; this.#freeHealthTiles = new Set(); - // TODO.impl Change this to use a dynamic import for a Lang registry dict. - // We need to make that registry dict first! - this.lang = English.Lowercase.getInstance(); - - // TODO.impl Enforce this in the UI code by greying out unusable combos of lang and coord-sys. - const minLangLeaves = this.grid.static.getAmbiguityThreshold(); - if (this.lang.numLeaves < minLangLeaves) { - throw new Error(`Found ${this.lang.numLeaves} leaves, but at` - + ` least ${minLangLeaves} were required. The provided mappings` - + ` composing the current Lang-under-construction are not` - + ` sufficient to ensure that a shuffling operation will always` - + ` be able to find a safe candidate to use as a replacement.` - + ` Please see the spec for Lang.getNonConflictingChar.` - ); - } + // https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import + this.#langImportPromise = (import( + /* webpackChunkName: "lang/[request]" */ + `lang/impl/${this.langFrontend.module}.ts` + ) /* as Promise<{ readonly [K in Lang.FrontendDesc["export"]]: Lang; }> */) + .then((module) => { + (this.lang as Lang) = (module + [this.langFrontend.module] + [this.langFrontend.export] + ).getInstance(); + + // TODO.impl Enforce this in the UI code by greying out unusable combos of lang and coord-sys. + const minLangLeaves = this.grid.static.getAmbiguityThreshold(); + if (this.lang.numLeaves < minLangLeaves) { + throw new Error(`Found ${this.lang.numLeaves} leaves, but at` + + ` least ${minLangLeaves} were required. The provided mappings` + + ` composing the current Lang-under-construction are not` + + ` sufficient to ensure that a shuffling operation will always` + + ` be able to find a safe candidate to use as a replacement.` + + ` Please see the spec for Lang.getNonConflictingChar.` + ); + } + return this.lang; + }); this.langBalancingScheme = desc.langBalancingScheme; + + this.scoreInfo = new ScoreInfo(this.players.map((player) => player.playerId)); } /** * */ - public reset(): void { + public async reset(): Promise { // Reset the grid and event record: - super.reset(); + await super.reset(); this.#currentFreeHealth = 0.0; this.#freeHealthTiles.clear(); @@ -83,6 +95,7 @@ export abstract class GameManager e // Reset hit-counters in the current language: // This must be done before shuffling so that the previous // history of shuffle-ins has no effects on the new pairs. + await this.#langImportPromise; this.lang.reset(); // Shuffle everything: this.grid.forEachTile((tile) => { @@ -99,7 +112,8 @@ export abstract class GameManager e team.members.forEach((member, memberIndex) => { member.reset(this.grid.tile.at(spawnPoints[teamIndex][memberIndex])); }); - }) + }); + this.scoreInfo.reset(); // NOTE: This is currently commented out because they'll just // spawn as the players start moving. It's not necessary. @@ -108,6 +122,7 @@ export abstract class GameManager e // this.dryRunSpawnFreeHealth([])?.forEach((tileModDesc) => { // this.executeTileModEvent(tileModDesc); // }) + return Promise.resolve(); } @@ -196,7 +211,7 @@ export abstract class GameManager e lastKnownUpdateId: 1 + tile.lastKnownUpdateId, newCharSeqPair: undefined, // "do not change". newFreeHealth: tile.freeHealth + tileHealthToAdd, - }) + }); } } healthToSpawn -= tileHealthToAdd; @@ -218,10 +233,14 @@ export abstract class GameManager e * @override */ protected executeTileModEvent( - desc: TileModEvent, + desc: Readonly>, doCheckOperatorSeqBuffer: boolean = true, - ): void { + ): Tile { + Object.freeze(desc); const tile = this.grid.tile.at(desc.coord); + // NOTE: This assertion must be performed before executing + // changes by making a supercall or else the previous state + // will be gone. if (desc.lastKnownUpdateId !== (1 + tile.lastKnownUpdateId)) { // We literally just specified this in processMoveRequest. throw new Error("this never happens. see comment in source."); @@ -233,6 +252,7 @@ export abstract class GameManager e this.#freeHealthTiles.add(tile); } super.executeTileModEvent(desc, doCheckOperatorSeqBuffer); + return tile; } /** @@ -288,17 +308,17 @@ export abstract class GameManager e const player = this.managerCheckGamePlayingRequest(desc); if (!player) { // Reject the request: - this.processMoveExecute(desc); + this.executePlayerMoveEvent(desc); return; } - const dest = this.grid.tile.at(desc.dest.coord); + const dest = this.grid.tile.at(desc.destModDesc.coord); if (dest.isOccupied || - dest.lastKnownUpdateId !== desc.dest.lastKnownUpdateId) { + dest.lastKnownUpdateId !== desc.destModDesc.lastKnownUpdateId) { // The update ID check is not essential, but it helps // enforce stronger client-experience consistency: they cannot // move somewhere where they have not realized the `LangSeq` has // changed. - this.processMoveExecute(desc); // Reject the request. + this.executePlayerMoveEvent(desc); // Reject the request. return; } const newPlayerHealthValue @@ -308,25 +328,33 @@ export abstract class GameManager e if (newPlayerHealthValue < 0) { // Reject a boost-type movement request if it would make // the player become downed (or if they are already downed): - this.processMoveExecute(desc); + this.executePlayerMoveEvent(desc); return; } + // Update stats records: + const playerScoreInfo = this.scoreInfo.entries[player.playerId]; + playerScoreInfo.totalHealthPickedUp += dest.freeHealth; + playerScoreInfo.moveCounts[desc.moveType] += 1; + // Set response fields according to spec in `PlayerMovementEvent`: desc.playerLastAcceptedRequestId = (1 + player.lastAcceptedRequestId); desc.newPlayerHealth = { - score: player.status.score + dest.freeHealth, health: newPlayerHealthValue, }; - desc.dest.lastKnownUpdateId = (1 + dest.lastKnownUpdateId); - desc.dest.newFreeHealth = 0; - desc.dest.newCharSeqPair = this.dryRunShuffleLangCharSeqAt(dest); - desc.tilesWithHealthUpdates = this.dryRunSpawnFreeHealth([desc.dest,]); + desc.destModDesc.lastKnownUpdateId = (1 + dest.lastKnownUpdateId); + desc.destModDesc.newFreeHealth = 0; + desc.destModDesc.newCharSeqPair = this.dryRunShuffleLangCharSeqAt(dest); + desc.tileHealthModDescs = this.dryRunSpawnFreeHealth([desc.destModDesc,]); // Accept the request, and trigger calculation // and enactment of the requested changes: desc.eventId = this.nextUnusedEventId; - this.processMoveExecute(desc); + this.executePlayerMoveEvent(desc); + } + + private processPlayerContact(sourceP: Player): PlayerActionEvent.Movement["playerHealthModDescs"] { + return undefined!; } @@ -342,14 +370,14 @@ export abstract class GameManager e const bubbler = this.managerCheckGamePlayingRequest(desc); if (!bubbler) { // Reject the request: - this.processBubbleExecute(desc); + this.executePlayerBubbleEvent(desc); return; } desc.playerLastAcceptedRequestId = (1 + bubbler.lastAcceptedRequestId); // We are all go! Do it. desc.eventId = this.nextUnusedEventId; - this.processBubbleExecute(desc); + this.executePlayerBubbleEvent(desc); } } diff --git a/src/base/game/__gameparts/pause.md b/src/base/game/__gameparts/pause.md index 5660da9f..31bd7b46 100644 --- a/src/base/game/__gameparts/pause.md +++ b/src/base/game/__gameparts/pause.md @@ -13,7 +13,7 @@ In order to play nice with any behaviours dependent on how long the game has las ## Responsibility Overview -Behaviours dependent on game-state: A game should be automatically un-paused upon reset. Requests to pause the game when it is over should be immediately rejected. +Behaviours dependent on game-state: A game should _not_ be automatically un-paused upon reset. Requests to pause the game when it is over should be immediately rejected. (per implementation): @@ -42,7 +42,9 @@ Behaviours dependent on game-state: A game should be automatically un-paused upo ### Request Vectors -These listener functions must either be bound to an object that gets garbage collected with a dead Game instance, or they must be manually de-registered when a Game object will no longer be used. An example of this would be [`document.onvisibilitychange`](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API). +Make sure that any registered listener callbacks do not prevent garbage-collection of a Game instance that is no longer intended to be used. + +Take a look at [`document.onvisibilitychange`](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API). ### Web UI Changes diff --git a/src/base/game/events/PlayerActionEvent.ts b/src/base/game/events/PlayerActionEvent.ts index b4e5d68d..9a08138b 100644 --- a/src/base/game/events/PlayerActionEvent.ts +++ b/src/base/game/events/PlayerActionEvent.ts @@ -122,18 +122,22 @@ export namespace PlayerActionEvent { * values taken on by the player for these fields. */ public newPlayerHealth?: { - score: Player.Health; health: Player.Health; } = undefined; - public readonly dest: TileModEvent; + public readonly destModDesc: TileModEvent; public readonly moveType: Player.MoveType; /** * Undefined is equivalent to an empty array. */ - public tilesWithHealthUpdates?: TU.RoArr> = undefined; + public tileHealthModDescs?: TU.RoArr> = undefined; + + public playerHealthModDescs?: TU.RoArr<{ + playerId: Player.Id; + newHealth: Player.Health; + }>; public constructor( playerId: Player.Id, @@ -142,7 +146,7 @@ export namespace PlayerActionEvent { moveType: Player.MoveType ) { super(playerId, lastAcceptedRequestId); - this.dest = { + this.destModDesc = { coord: destTile.coord, lastKnownUpdateId: destTile.lastKnownUpdateId, newCharSeqPair: undefined, diff --git a/src/base/game/player/ArtificialPlayer.ts b/src/base/game/player/ArtificialPlayer.ts index 7997ef9d..a78ccea5 100644 --- a/src/base/game/player/ArtificialPlayer.ts +++ b/src/base/game/player/ArtificialPlayer.ts @@ -86,8 +86,11 @@ export abstract class ArtificialPlayer extends Player export namespace ArtificialPlayer { export declare const __Constructors: Readonly<{ - [ F in Exclude ]: - typeof ArtificialPlayer + [ F in Player.FamilyArtificial ]: { + new( + game: GameManager, desc: Player.__CtorArgs + ): ArtificialPlayer; + }; }>; export type FamilySpecificPart = @@ -96,10 +99,11 @@ export namespace ArtificialPlayer { ); export const of = ( - game: Readonly>, - playerDesc: Player.CtorArgs, + game: GameManager, + playerDesc: Player.__CtorArgs, ): ArtificialPlayer => { - return new (__Constructors[playerDesc.familyId])(game, playerDesc); + const familyId = playerDesc.familyId as Player.FamilyArtificial; + return new (__Constructors[familyId])(game, playerDesc); }; } // ArtificialPlayer gets frozen in PostInit after __Constructors get initialized. diff --git a/src/base/game/player/OperatorPlayer.ts b/src/base/game/player/OperatorPlayer.ts index 285960c4..1c536dc8 100644 --- a/src/base/game/player/OperatorPlayer.ts +++ b/src/base/game/player/OperatorPlayer.ts @@ -10,8 +10,8 @@ import { Player } from "./Player"; /** - * - * @extends Player + * There is at least one in online-client-side and offline games. + * There are none for online-server-side games. */ export class OperatorPlayer extends Player { @@ -43,7 +43,7 @@ export class OperatorPlayer extends Player { public constructor(game: GameBase, desc: Player.__CtorArgs<"HUMAN">) { super(game, desc); - this.langRemappingFunc = Lang.RemappingFunctions[this.game.langName]; + this.langRemappingFunc = this.game.langFrontend.remapFunc; } public reset(spawnTile: Tile): void { @@ -142,7 +142,7 @@ export class OperatorPlayer extends Player { } // Operator's new `seqBuffer` didn't match anything. this.#seqBuffer = ""; - this.hostTile.visualBell(); + this.status.visualBell(); } /** @@ -157,6 +157,10 @@ export class OperatorPlayer extends Player { super.moveTo(dest); } + public __abstractNotifyBecomeCurrent(): void { + this.status.__notifyBecomeCurrent(this.game.grid.spotlightElems); + } + public get seqBuffer(): Lang.Seq { return this.#seqBuffer; diff --git a/src/base/game/player/Player.ts b/src/base/game/player/Player.ts index 59c43497..0dc4d12f 100644 --- a/src/base/game/player/Player.ts +++ b/src/base/game/player/Player.ts @@ -1,7 +1,7 @@ import { Game } from "game/Game"; import type { Coord, Tile } from "floor/Tile"; -import type { Player as __Player } from "utils/TypeDefs"; +import type { Player as __Player } from "defs/TypeDefs"; import type { ArtificialPlayer } from "./ArtificialPlayer"; import type { GameBase } from "game/__gameparts/Base"; @@ -99,6 +99,7 @@ export class Player extends PlayerSkeleton { export namespace Player { export type Family = __Player.Family; + export type FamilyArtificial = Exclude; export type Id = __Player.Id; @@ -133,32 +134,30 @@ export namespace Player { * # Player Constructor Arguments */ export type CtorArgs = __CtorArgs; - export type __CtorArgs = any extends F ? never - : { [atomic_F in F]: atomic_F extends Player.Family - ? (CtorArgs.__PreIdAssignment & Readonly<{ + export type __CtorArgs = any extends F_group ? never + : { [F in F_group]: F extends Player.Family + ? (CtorArgs.__PreIdAssignment & Readonly<{ playerId: Player.Id; }>) : never - }[F]; + }[F_group]; export namespace CtorArgs { export type PreIdAssignment = __PreIdAssignment; - export type __PreIdAssignment = any extends F ? never - : { [atomic_F in F]: atomic_F extends Player.Family + export type __PreIdAssignment = any extends F_group ? never + : { [F in F_group]: F extends Player.Family ? (Readonly<{ - /** - * This determines which constructor function to use. - */ - familyId: atomic_F; + isALocalOperator: F extends typeof Player.Family.HUMAN ? boolean : false; + familyId: F; teamId: Team.Id; - socketId: SocketId | undefined; // Must exist for operated players. + socketId: F extends typeof Player.Family.HUMAN ? (SocketId | undefined) : undefined; username: Username; noCheckGameOver: boolean; - familyArgs: FamilySpecificPart; + familyArgs: FamilySpecificPart; }>) : never; - }[F]; + }[F_group]; export type FamilySpecificPart = ( F extends typeof Player.Family.HUMAN ? {} @@ -179,27 +178,20 @@ export namespace Player { // (to play nice with array representations): const teamIdCleaner: TU.RoArr = Array.from(new Set(playerDescs.map((player) => player.teamId))) - .sort((a, b) => a - b) + .sort((a, b) => a - b) // This is not a representation requirement. .reduce((prev, originalId, squashedId) => { prev[originalId] = squashedId; return prev; }, [] as Array); return playerDescs.slice() .sort((pda, pdb) => teamIdCleaner[pda.teamId] - teamIdCleaner[pdb.teamId]) - .map((playerDesc, index) => { return { + .map((playerDesc, index) => Object.assign(playerDesc, { playerId: index, - familyId: playerDesc.familyId, teamId: teamIdCleaner[playerDesc.teamId], - socketId: playerDesc.socketId, - username: playerDesc.username, - noCheckGameOver: playerDesc.noCheckGameOver, - familyArgs: playerDesc.familyArgs, - } as CtorArgs; }); + }, ), ); }; - } Object.freeze(CtorArgs); - } Object.freeze(Player); Object.freeze(Player.prototype); diff --git a/src/base/game/player/PlayerSkeleton.ts b/src/base/game/player/PlayerSkeleton.ts index 6e0c7d3e..a110928c 100644 --- a/src/base/game/player/PlayerSkeleton.ts +++ b/src/base/game/player/PlayerSkeleton.ts @@ -1,4 +1,4 @@ -import { Player as __Player } from "utils/TypeDefs"; +import { Player as __Player } from "defs/TypeDefs"; import { Game } from "game/Game"; import type { Coord, Tile } from "floor/Tile"; @@ -20,6 +20,8 @@ export abstract class PlayerSkeleton extends __Player public readonly playerId: Player.Id; + readonly isALocalOperator: boolean; + /** * The game object that this player belongs to. */ @@ -39,6 +41,7 @@ export abstract class PlayerSkeleton extends __Player throw new RangeError("Player ID's must be integer values."); } this.playerId = desc.playerId; + this.isALocalOperator = desc.isALocalOperator; this.game = game; this.status = new (this.game.__playerStatusCtor)( this as PlayerSkeleton as Player, diff --git a/src/base/game/player/PlayerStatus.ts b/src/base/game/player/PlayerStatus.ts index 47e8a6dd..79dc4540 100644 --- a/src/base/game/player/PlayerStatus.ts +++ b/src/base/game/player/PlayerStatus.ts @@ -13,7 +13,6 @@ export class PlayerStatus { protected readonly player: Readonly>; // Circular field reference. public readonly noCheckGameOver: boolean; - #score: Player.Health; #health: Player.Health; public constructor(player: Readonly>, noCheckGameOver: boolean) { @@ -22,8 +21,7 @@ export class PlayerStatus { } public reset(): void { - this.score = 0; - this.health = 0; + this.health = 0; } public __afterAllPlayersConstruction(): void { } @@ -33,13 +31,6 @@ export class PlayerStatus { } - public get score(): Player.Health { - return this.#score; - } - public set score(newValue: Player.Health) { - this.#score = newValue; - } - public get health(): Player.Health { return this.#health; } diff --git a/src/base/game/player/VisiblePlayerStatus.ts b/src/base/game/player/VisiblePlayerStatus.ts index 1c5cffa4..19abeceb 100644 --- a/src/base/game/player/VisiblePlayerStatus.ts +++ b/src/base/game/player/VisiblePlayerStatus.ts @@ -1,13 +1,17 @@ -import { OmHooks } from "browser/OmHooks"; +import { OmHooks } from "defs/OmHooks"; import type { Coord, Tile } from "floor/Tile"; import type { Player } from "./Player"; import { PlayerStatus } from "./PlayerStatus"; +/** + * + */ export class VisiblePlayerStatus extends PlayerStatus { readonly #baseElem: HTMLElement; + readonly #visualBellAnimations: Animation[]; private readonly __immigrantInfoCache: Tile.VisibleImmigrantInfo; @@ -15,23 +19,34 @@ export class VisiblePlayerStatus extends PlayerStatus public constructor(player: Player, noCheckGameOver: boolean) { super(player, noCheckGameOver); { - const baseElem = document.createElement("div"); + const baseElem + = this.#baseElem + = document.createElement("div"); baseElem.classList.add( - OmHooks.Player.Class.BASE, OmHooks.General.Class.CENTER_CONTENTS, OmHooks.General.Class.STACK_CONTENTS, + OmHooks.Player.Class.BASE, ); - this.#baseElem = baseElem; } { // Setup face element: const faceElem = document.createElement("div"); faceElem.classList.add(OmHooks.Player.Class.FACE); + const anims = this.#visualBellAnimations = (this.player.isALocalOperator) ? [ + faceElem.animate({ + filter: ["brightness(0.7)", "brightness(1.0)",], + },{ duration: 300, easing: "ease-in", }), + faceElem.animate({ + transform: VisiblePlayerStatus.makeWiggleAnimation(10, 2), + },{ duration: 270, easing: "ease-out", }), + ] : []; + // anims.forEach((anim) => anim.pause()); + { + // Setup downedOverlay element: + const dOverlayElem = document.createElement("div"); + dOverlayElem.classList.add(OmHooks.Player.Class.DOWNED_OVERLAY); + faceElem.appendChild(dOverlayElem); + } this.#baseElem.appendChild(faceElem); - } { - // Setup downedOverlay element: - const dOverlayElem = document.createElement("div"); - dOverlayElem.classList.add(OmHooks.Player.Class.DOWNED_OVERLAY); - this.#baseElem.appendChild(dOverlayElem); } } @@ -40,44 +55,43 @@ export class VisiblePlayerStatus extends PlayerStatus */ public __afterAllPlayersConstruction(): void { const player = this.player; - const operator = this.player.game.operator!; - if (!operator) { - // This _would_ be the case if we did it in the constructor - // where `this.player.game.operator` has not been defined yet. - throw new Error("this never happens. see comment in source."); - } + const operatorTeamId = this.player.game.operators[0].teamId; this.#baseElem.dataset[OmHooks.Player.Dataset.FACE_SWATCH] - = (player === operator) ? "me" - : (player.teamId === operator.teamId) ? "teammate" : "opponent"; - - // Setup spotlight element: - if (player === operator) { - const sslElem = document.createElement("div"); - sslElem.classList.add(OmHooks.Player.Class.SHORT_SPOTLIGHT); - this.#baseElem.appendChild(sslElem); - - const lslElem = document.createElement("div"); - lslElem.classList.add(OmHooks.Player.Class.LONG_SPOTLIGHT); - this.#baseElem.appendChild(lslElem); - } + = (player.isALocalOperator) ? "me" + : (player.teamId === operatorTeamId) ? "teammate" : "opponent"; + (this.__immigrantInfoCache as Tile.VisibleImmigrantInfo) = Object.freeze({ playerElem: this.#baseElem, username: player.username, }); } + public reset(): void { + super.reset(); + const DDH = OmHooks.Player.Dataset.DOWNED + this.#baseElem.dataset[DDH.KEY] = DDH.VALUES.NO; + // ^We need to do this explicitly. It won't be done + // automatically when setting `health` because of the short- + // circuit=optimization made when `isDowned` hasn't changed. + } + public get immigrantInfo(): Tile.VisibleImmigrantInfo { return this.__immigrantInfoCache; } - - public get score(): Player.Health { - return super.score; + public __notifyBecomeCurrent(spotlightElems: TU.RoArr): void { + spotlightElems.forEach((elem) => { + this.#baseElem.appendChild(elem); + }); } - public set score(newValue: Player.Health) { - super.score = newValue; + + public visualBell(): void { + window.requestAnimationFrame((time) => { + this.#visualBellAnimations.forEach((anim) => anim.play()); + }); } + public get health(): Player.Health { return super.health; } @@ -87,10 +101,21 @@ export class VisiblePlayerStatus extends PlayerStatus if (oldIsDowned !== this.isDowned) { // CSS integration for Player.isDowned rendering. - this.#baseElem[OmHooks.Player.Dataset.DOWNED] = (this.isDowned) - ? ((this.player.team.elimOrder) ? "team" : "self") : "no"; + const DDH = OmHooks.Player.Dataset.DOWNED; + this.#baseElem.dataset[DDH.KEY] = (this.isDowned) + ? ((this.player.team.elimOrder) + ? DDH.VALUES.TEAM + : DDH.VALUES.SELF + ) : DDH.VALUES.NO; } } } +export namespace VisiblePlayerStatus { + export function makeWiggleAnimation(pctX: number, numWiggles: number): string[] { + const arr = Array(numWiggles * 2).fill(pctX); + arr.unshift(0); arr.push(0); + return arr.map((n,i) => `translate(${(i%2)?n:-n}%)`); + } +} Object.freeze(VisiblePlayerStatus); Object.freeze(VisiblePlayerStatus.prototype); diff --git a/src/base/game/player/artificials/Chaser.ts b/src/base/game/player/artificials/Chaser.ts index ba81b764..c1d8d173 100644 --- a/src/base/game/player/artificials/Chaser.ts +++ b/src/base/game/player/artificials/Chaser.ts @@ -20,7 +20,7 @@ export class Chaser extends ArtificialPlayer { private readonly grid: Chaser["game"]["grid"]; #prevCoord: Coord; - protected constructor(game: GameManager, desc: Player.__CtorArgs<"CHASER">) { + public constructor(game: GameManager, desc: Player.__CtorArgs<"CHASER">) { super(game, desc); this.behaviour = Object.freeze(desc.familyArgs); this.grid = this.game.grid; @@ -90,7 +90,7 @@ export class Chaser extends ArtificialPlayer { this.#prevCoord).coord, 1); } } - let closestFht: Tile = this.game.freeHealthTiles[0]; + let closestFht: Tile = undefined!; let closestFhtDistance = Infinity; for (const fht of this.game.freeHealthTiles) { const distance = this.grid.minMovesFromTo(this.coord, fht.coord); diff --git a/src/base/game/tsconfig.json b/src/base/game/tsconfig.json index ebbcc6fc..de04475b 100644 --- a/src/base/game/tsconfig.json +++ b/src/base/game/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../.templates/tsconfig.json", "references": [ - { "path": "../utils", }, - { "path": "../browser", }, + { "path": "../defs", }, { "path": "../lang", }, { "path": "../floor", }, ], diff --git a/src/base/lang/Lang.ts b/src/base/lang/Lang.ts index 567c314c..878bcd00 100644 --- a/src/base/lang/Lang.ts +++ b/src/base/lang/Lang.ts @@ -1,4 +1,4 @@ -import { Lang as __Lang } from "utils/TypeDefs"; +import { Lang as __Lang } from "defs/TypeDefs"; import { LangSeqTreeNode } from "lang/LangSeqTreeNode"; @@ -43,19 +43,26 @@ export abstract class Lang extends __Lang { /** - * _Does not call reset._ - * * @param classIf - * @param forwardDict - Weights are _relative_ values handled by * {@link LangSeqTreeNode}, which requires the provided values * to all be strictly positive values. They do not all need * to sum to a specific value such as 100. */ - protected constructor(classIf: Lang.ClassIf, forwardDict: Lang.CharSeqPair.WeightedForwardMap) { + protected constructor( + classIf: Lang.ClassIf, + forwardDict: Lang.CharSeqPair.WeightedForwardMap, + ) { super(); - this.static = classIf; - this.treeMap = LangSeqTreeNode.CREATE_TREE_MAP(forwardDict); - this.leafNodes = this.treeMap.getLeafNodes(); + this.static = classIf; + this.treeMap = LangSeqTreeNode.CREATE_TREE_MAP(forwardDict); + this.leafNodes = this.treeMap.getLeafNodes(); + if (this.leafNodes.length !== this.static.frontend.numLeaves) { + throw new Error(`maintenance required: the frontend constant` + + ` for the language \"${this.static.frontend.id}\" needs to` + + ` be updated to the correct, computed value, which is` + + ` \`${this.leafNodes.length}\`.`); + } } public reset(): void { @@ -151,27 +158,23 @@ export abstract class Lang extends __Lang { public simpleView(): object { return Object.assign(Object.create(null), { - name: this.static.getName(), - desc: this.static.getBlurb(), + id: this.static.frontend.id, + displayName: this.static.frontend.display, root: this.treeMap.simpleView(), + numLeaves: this.leafNodes.length, }); } } - - - export namespace Lang { - /** * Every constructor function (class literal) implementing the * `Lang` class must implement this interface. Ie. These will be * implemented as static methods. */ export interface ClassIf { - getName(): Lang.Names.Value; - getBlurb(): string; getInstance(): Lang; + readonly frontend: Lang.FrontendDesc; }; /** @@ -208,11 +211,7 @@ export namespace Lang { export type BalancingScheme = __Lang.BalancingScheme; - export namespace Names { - export type Key = __Lang.Names.Key; - export type Value = __Lang.Names.Value; - } - + export type FrontendDesc = __Lang.FrontendDesc; } Object.freeze(Lang); Object.freeze(Lang.prototype); diff --git a/src/base/lang/LangSeqTreeNode.ts b/src/base/lang/LangSeqTreeNode.ts index 7774b7ad..8fe81bb6 100644 --- a/src/base/lang/LangSeqTreeNode.ts +++ b/src/base/lang/LangSeqTreeNode.ts @@ -1,4 +1,4 @@ -import { Lang as __Lang } from "utils/TypeDefs"; +import { Lang as __Lang } from "defs/TypeDefs"; import type { Lang } from "./Lang"; @@ -38,8 +38,6 @@ export class LangSeqTreeNode { /** - * _Does not call reset._ - * * @param forwardDict - * @returns The root node of a new tree map. */ @@ -125,6 +123,9 @@ export class LangSeqTreeNode { */ protected addCharMapping(seq: Lang.Seq, chars: TU.RoArr): void { if (!(__Lang.Seq.REGEXP.test(seq))) { + // If this errs, and the offending character is one that can + // be easily entered on a generic keyboard, don't be afraid + // to just add it to the regexp. throw new RangeError(`Mapping-sequence \"${seq}\" did not match the` + ` required regular expression \"${__Lang.Seq.REGEXP.source}\".` ); diff --git a/src/base/lang/impl/Emote.ts b/src/base/lang/impl/Emote.ts new file mode 100644 index 00000000..88412828 --- /dev/null +++ b/src/base/lang/impl/Emote.ts @@ -0,0 +1,17 @@ +import { Lang } from "../Lang"; + + +/** + * + */ +export namespace Emote { + + /** + * + */ + export class GitHub extends Lang { + } + Object.freeze(GitHub); + Object.freeze(GitHub.prototype); +} +Object.freeze(Emote); diff --git a/src/base/lang/impl/English.ts b/src/base/lang/impl/English.ts index 6761bc85..27f60103 100644 --- a/src/base/lang/impl/English.ts +++ b/src/base/lang/impl/English.ts @@ -15,14 +15,6 @@ export namespace English { private static SINGLETON?: Lowercase = undefined; - public static getName(): Lang.Names.Value { - return Lang.Names.ENGLISH__LOWERCASE; - } - - public static getBlurb(): string { - return ""; // TODO.doc - } - public static getInstance(): Lowercase { if (!this.SINGLETON) { this.SINGLETON = new Lowercase(); @@ -30,6 +22,8 @@ export namespace English { return this.SINGLETON; } + public static readonly frontend = Lang.GET_FRONTEND_DESC_BY_ID("engl-low"); + // TODO.learn see https://wikipedia.org/wiki/Keyboard_layout#Dvorak // and https://wikipedia.org/wiki/Keyboard_layout#Colemak @@ -61,24 +55,18 @@ export namespace English { private static SINGLETON?: MixedCase = undefined; - public static getName(): Lang.Names.Value { - return Lang.Names.ENGLISH__MIXEDCASE; - } - - public static getBlurb(): string { - return ""; // TODO.doc - } - public static getInstance(): MixedCase { if (!this.SINGLETON) { - this.SINGLETON = new MixedCase(); + this.SINGLETON = new MixedCase(); } return this.SINGLETON; } + public static readonly frontend = Lang.GET_FRONTEND_DESC_BY_ID("engl-mix"); + private constructor() { let initializer: Lang.CharSeqPair.WeightedForwardMap = {}; - const addMappings = (charSeqTransform: (charOrSeq: string) => string): void => { + const addMappings = (charSeqTransform: (cs: string) => string): void => { initializer = Object.entries(LETTER_FREQUENCY).reduce( (accumulator, current) => { const char: Lang.Char = charSeqTransform(current[0]); diff --git a/src/base/lang/impl/Japanese.ts b/src/base/lang/impl/Japanese.ts index 36e53cc6..4f51baa0 100644 --- a/src/base/lang/impl/Japanese.ts +++ b/src/base/lang/impl/Japanese.ts @@ -19,22 +19,16 @@ export namespace Japanese { private static SINGLETON?: Hiragana = undefined; - public static getName(): Lang.Names.Value { - return Lang.Names.JAPANESE__HIRAGANA; - } - - public static getBlurb(): string { - return ""; // TODO.doc - } - public static getInstance(): Hiragana { if (!this.SINGLETON) { this.SINGLETON = new Hiragana(); - delete this.INITIALIZER; + (this.INITIALIZER as any) = undefined; } return this.SINGLETON; } + public static readonly frontend = Lang.GET_FRONTEND_DESC_BY_ID("japn-hir"); + /** * Values obtained from page 18 of the below pdf (p.499 of text) * https://link.springer.com/content/pdf/10.3758/BF03200819.pdf @@ -143,22 +137,16 @@ export namespace Japanese { private static SINGLETON?: Katakana = undefined; - public static getName(): Lang.Names.Value { - return Lang.Names.JAPANESE__KATAKANA; - } - - public static getBlurb(): string { - return ""; // TODO.doc - } - public static getInstance(): Katakana { if (!this.SINGLETON) { this.SINGLETON = new Katakana(); - delete this.INITIALIZER; + (this.INITIALIZER as any) = undefined; } return this.SINGLETON; } + public static readonly frontend = Lang.GET_FRONTEND_DESC_BY_ID("japn-kat"); + /** * Values obtained from page 19 of the below pdf (p.500 of text) * https://link.springer.com/content/pdf/10.3758/BF03200819.pdf diff --git a/src/base/lang/impl/Korean.ts b/src/base/lang/impl/Korean.ts index 392f7ad1..9ffb3cc8 100644 --- a/src/base/lang/impl/Korean.ts +++ b/src/base/lang/impl/Korean.ts @@ -20,25 +20,16 @@ export namespace Korean { private static SINGLETON?: Dubeolsik = undefined; - public static getName(): Lang.Names.Value { - return Lang.Names.KOREAN__DUBEOLSIK; - } - - public static getBlurb(): string { - // A paraphrase of a segment from Wikipedia: - return "The most common keyboard layout, and South Korea's only Hangul" - + " standard since 1969. Consonants are on the left, and vowels on" - + " the right."; - } - public static getInstance(): Dubeolsik { if (!this.SINGLETON) { this.SINGLETON = new Dubeolsik(); - delete this.KEYBOARD; + (this.KEYBOARD as any) = undefined; } return this.SINGLETON; } + public static readonly frontend = Lang.GET_FRONTEND_DESC_BY_ID("kore-dub"); + private static KEYBOARD = Object.freeze({ "": "", "ㅂ": "q", "ㅈ": "w", "ㄷ": "e", "ㄱ": "r", "ㅅ": "t", @@ -54,10 +45,13 @@ export namespace Korean { private constructor() { super( Dubeolsik, - INITIALIZE((ij, mj, fj) => { - const atoms = [ij, mj, fj,].flatMap((jamos) => jamos.atoms.split("")); + INITIALIZE(((ij, mj, fj) => { + const atoms = [ij, mj, fj,].flatMap((jamos) => { + return (jamos.value in Dubeolsik.KEYBOARD) + ? [jamos.value,] : jamos.atoms.split(""); + }) as Array; return atoms.map((atom) => Dubeolsik.KEYBOARD[atom]).join(""); - }), + }) as SeqBuilder), ); } } @@ -79,32 +73,22 @@ export namespace Korean { private static SINGLETON?: Sebeolsik = undefined; - public static getName(): Lang.Names.Value { - return Lang.Names.KOREAN__SEBEOLSIK; - } - - public static getBlurb(): string { - // A paraphrase of a segment from Wikipedia: - return "Another Hangul keyboard layout used in South Korea, and the" - + " final Sebeolsik layout designed by Dr. Kong Byung Woo, hence" - + " the name. Syllable-initial consonants are on the right, final" - + " consonants on the left, and vowels in the middle. It is more" - + " ergonomic than the dubeolsik, but not widely used."; - } - public static getInstance(): Sebeolsik { if (!this.SINGLETON) { this.SINGLETON = new Sebeolsik(); - delete this.KEYBOARD; + (this.SEB_KEYBOARD as any) = undefined; } return this.SINGLETON; } + public static readonly frontend = Lang.GET_FRONTEND_DESC_BY_ID("kore-sub"); + /** * */ - private static KEYBOARD = Object.freeze({ - "FINALS": { + private static SEB_KEYBOARD = Object.freeze({ + // Finals and consonant clusters are found on the left. + FINALS: { "": "", "ㅎ": "1", "ㅆ": "2", "ㅂ": "3", // 1-row "ㅅ": "q", "ㄹ": "w", // q-row @@ -115,29 +99,36 @@ export namespace Korean { "ㄷ": "A", "ㄶ": "S", "ㄼ": "D", "ㄻ": "F", // A-row "ㅊ": "Z", "ㅄ": "X", "ㅋ": "C", "ㄳ": "V", // Z-row }, - "MEDIALS": { - "ㅛ": "4", "ㅠ": "5", "ㅑ": "6", "ㅖ": "7", "ᅴ": "8", // "ㅜ": "9", + // Medials are found in the middle. + MEDIALS: { + "ㅛ": "4", "ㅠ": "5", "ㅑ": "6", "ㅖ": "7", "ㅢ": "8", // "ㅜ": "9", "ㅕ": "e", "ㅐ": "r", "ㅓ": "t", // q-row "ㅣ": "d", "ㅏ": "f", "ㅡ": "g", // a-row "ㅔ": "c", "ㅗ": "v", "ㅜ": "b", // z-row "ㅒ": "G", + // Things that don't have dedicated keys: + "ㅘ": "vf", "ㅙ": "vr", "ㅚ": "vd", "ㅝ": "bt", "ㅞ": "bc", "ㅟ": "bd" }, - "INITIALS": { + // Initials are found on the right. + INITIALS: { "ㅋ": "0", // 1-row "ㄹ": "y", "ㄷ": "u", "ㅁ": "i", "ㅊ": "o", "ㅍ": "p", // q-row "ㄴ": "h", "ㅇ": "j", "ㄱ": "k", "ㅈ": "l", "ㅂ": ";", "ㅌ": "'", // a-row "ㅅ": "n", "ㅎ": "m", // z-row + "ㄲ": "!", // !-row // NOTE: If we included numbers, this is where they would go. + // Things that don't have dedicated keys: + "ㄸ": "uu", "ㅃ": ";;", "ㅆ": "nn", "ㅉ": "l", }, }); private constructor() { super( Sebeolsik, - INITIALIZE((ij, mj, fj) => { - return Sebeolsik.KEYBOARD.INITIALS[ij.value] - + Sebeolsik.KEYBOARD.MEDIALS[mj.value] - + Sebeolsik.KEYBOARD.FINALS[fj.value]; - }), + INITIALIZE(((ij, mj, fj) => { + return Sebeolsik.SEB_KEYBOARD.INITIALS[ij.value] + + Sebeolsik.SEB_KEYBOARD.MEDIALS[mj.value] + + Sebeolsik.SEB_KEYBOARD.FINALS[fj.value]; + }) as SeqBuilder), ); } } Sebeolsik as Lang.ClassIf; @@ -154,20 +145,6 @@ export namespace Korean { export class Romanization extends Lang { private static SINGLETON?: Romanization = undefined; - - public static getName(): Lang.Names.Value { - return Lang.Names.KOREAN__ROMANIZATION; - } - - public static getBlurb(): string { - // A paraphrase of a segment from Wikipedia: - return "The Revised Romanization of Korean (국어의 로마자 표기법; 國語의 로마字" - + " 表記法) is the official South Korean language romanization system. It" - + " was developed by the National Academy of the Korean Language from 1995," - + " and was released on 7 July 2000 by South Korea's Ministry of Culture" - + " and Tourism"; - } - public static getInstance(): Romanization { if (!Romanization.SINGLETON) { Romanization.SINGLETON = new Romanization(); @@ -175,6 +152,8 @@ export namespace Korean { return Romanization.SINGLETON; } + public static readonly frontend = Lang.GET_FRONTEND_DESC_BY_ID("kore-rom"); + /** * Does nothing. * @@ -186,9 +165,9 @@ export namespace Korean { private constructor() { super( Romanization, - INITIALIZE((ij, mj, fj) => { + INITIALIZE(((ij, mj, fj) => { return ij.roman + mj.roman + fj.roman; - }), + }) as SeqBuilder), ); } } Romanization as Lang.ClassIf; @@ -213,11 +192,7 @@ export namespace Korean { * suitable for consumption by the {@link Lang} constructor. */ const INITIALIZE = ( - seqBuilder: { ( - ij: typeof INITIALS[number], - mj: typeof MEDIALS[number], - fj: typeof FINALS[number], - ): Lang.Seq, } + seqBuilder: SeqBuilder, ): Lang.CharSeqPair.WeightedForwardMap => { const forwardDict: Lang.CharSeqPair.WeightedForwardMap = {}; INITIALS.forEach((initialJamo, initialIdx) => { @@ -237,6 +212,11 @@ export namespace Korean { }); return forwardDict; }; + type SeqBuilder = { ( + ij: typeof INITIALS[number], + mj: typeof MEDIALS[number], + fj: typeof FINALS[number], + ): Lang.Seq, }; /** * # Initial Jamo (Choseong) @@ -331,9 +311,9 @@ export namespace Korean { * */ // TODO.learn - const WEIGHTS = { + const WEIGHTS = Object.freeze({ "": 1, - }; + }) as Record; } Object.freeze(Korean); diff --git a/src/base/lang/impl/Morse.ts b/src/base/lang/impl/Morse.ts new file mode 100644 index 00000000..0a7344f6 --- /dev/null +++ b/src/base/lang/impl/Morse.ts @@ -0,0 +1,30 @@ +import { Lang } from "../Lang"; + + +/** + * # Morse + * + * `beep beep boop` + */ +export namespace Morse { + + /** + * + * You see letters and numbers and you type sequences of dots and dashes. + */ + export class Encode extends Lang { + } + Object.freeze(Encode); + Object.freeze(Encode.prototype); + + + /** + * + * You see dots and dashes and you type alphanumeric characters. + */ + export class Decode extends Lang { + } + Object.freeze(Decode); + Object.freeze(Decode.prototype); +} +Object.freeze(Morse); diff --git a/src/base/lang/impl/readme.md b/src/base/lang/impl/readme.md index 21ae9cca..7a6179f3 100644 --- a/src/base/lang/impl/readme.md +++ b/src/base/lang/impl/readme.md @@ -4,8 +4,8 @@ Each implementation must: - Go under the [`./impl/`](./impl/) folder. -- Implement the [`Lang`](./Lang.ts) interface. +- Implement the [`Lang` and `Lang.ClassIf`](./Lang.ts) interfaces. - Assert its own class literal as a [`Lang.Info`](./Lang.ts) instance. - - Expose static getters for a name string and a blurb string. - Follow a singleton pattern exposed through a static method "`getInstance`". -- TODO: ~Register their _file_ name in the [`Lang.Modules.NAMES`](Lang.ts) array for dynamic importing.~ +- Register their filename (without the extension) and exports in the [`Lang.FrontendDescs`](../../defs/TypeDefs.ts) array for dynamic importing. +- Call `Object.seal` on its constructor and prototype objects. If you can use `Object.freeze`, that's even better. diff --git a/src/base/lang/tsconfig.json b/src/base/lang/tsconfig.json index a5a210cf..7a500e57 100644 --- a/src/base/lang/tsconfig.json +++ b/src/base/lang/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../.templates/tsconfig.json", "references": [ - { "path": "../utils", }, + { "path": "../defs", }, ], } \ No newline at end of file diff --git a/src/base/tsconfig.json b/src/base/tsconfig.json index 77ae531e..660aa6f1 100644 --- a/src/base/tsconfig.json +++ b/src/base/tsconfig.json @@ -2,8 +2,7 @@ "extends": "../.templates/tsconfig.json", "files": [], "references": [ - { "path": "utils", }, - { "path": "browser",}, + { "path": "defs", }, { "path": "lang", }, { "path": "floor", }, { "path": "game", }, diff --git a/src/client/OfflineGame.ts b/src/client/OfflineGame.ts deleted file mode 100644 index 3fec562b..00000000 --- a/src/client/OfflineGame.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Game } from "game/Game"; - -import type { Coord } from "floor/Tile"; -import { VisibleTile } from "floor/VisibleTile"; -import { VisibleGrid } from "floor/VisibleGrid"; - -import type { Player } from "game/player/Player"; -import { OperatorPlayer } from "game/player/OperatorPlayer"; -import { VisiblePlayerStatus } from "game/player/VisiblePlayerStatus"; -import { ArtificialPlayer } from "game/player/ArtificialPlayer"; - -import { GameManager } from "game/__gameparts/Manager"; - - -type G = Game.Type.OFFLINE; - -/** - * - * - * @extends Game - */ -export class OfflineGame extends GameManager { - - /** - * @override - */ - protected __getGridImplementation(coordSys: S): VisibleGrid.ClassIf { - return VisibleGrid.getImplementation(coordSys); - } - - /** - * _Does not call reset._ - * - * @param gameDesc - - */ - public constructor(gameDesc: Game.CtorArgs) { - super( - Game.Type.OFFLINE, { - tileClass: VisibleTile, - playerStatusCtor: VisiblePlayerStatus, - }, gameDesc, - ); - if (!this.operator) { - throw new Error("The Operator for an OfflineGame should be defined."); - } - - // ===================================== - // CALL TO RESET - this.reset(); - // ===================================== - - /* TODO.test This should be safe in a garbage-collection and - event-handler sense: Since the event handler is added to the - event-handler-list of the `.game-grid-impl-body` element as an - anonymous function, which makes it impossible to remove from - the list without a reference to that function. Luckily for us, - Grid.__VisibleGrid_super will automatically remove the old game- - grid-impl-body child, thus allowing it to be GC-ed, and its - event-handler function with it. */ - this.grid.baseElem.addEventListener("keydown", (ev): boolean => { - // console.log(`key: ${ev.key}, code: ${ev.code},` - // + ` keyCode: ${ev.keyCode}, char: ${ev.char},` - // + ` charCode: ${ev.charCode}`); - this.operator.processKeyboardInput(ev); - // Disable scroll-down via spacebar: - if (ev.keyCode === 32) { - ev.preventDefault(); - return false; - } - return true; - }); - } - - /** - * @override - */ - protected __createOperatorPlayer(desc: Player.__CtorArgs<"HUMAN">): OperatorPlayer { - return new OperatorPlayer(this, desc); - } - - /** - * @override - */ - protected __createArtifPlayer(desc: Player.CtorArgs): ArtificialPlayer { - return ArtificialPlayer.of(this, desc); - } - - /** - * @override - */ - public setTimeout(callback: TimerHandler, millis: number, ...args: any[]): number { - return setTimeout(callback, millis, args); - } - - /** - * @override - */ - public cancelTimeout(handle: number): void { - clearTimeout(handle); - } -} -Object.freeze(OfflineGame); -Object.freeze(OfflineGame.prototype); diff --git a/src/client/ScratchMakeGame.ts b/src/client/ScratchMakeGame.ts deleted file mode 100644 index 225bf06c..00000000 --- a/src/client/ScratchMakeGame.ts +++ /dev/null @@ -1,53 +0,0 @@ -require("../../assets/style/game/index.css"); - -import { OmHooks } from "browser/OmHooks"; -import type { Coord } from "floor/Tile"; -import { Lang } from "utils/TypeDefs"; -import { Player } from "game/player/Player"; -import { OfflineGame } from "./OfflineGame"; -import { IndexTasks } from "game/IndexTasks"; - -IndexTasks.INIT_CLASS_REGISTRIES(); - -// TODO.design override ctor args for each impl, and make it so they adapt input to pass to super ctor. -// TODO.build this has been set to `var` for testing purposes. It should be `const` in production. -export const game = new OfflineGame({ - coordSys: "EUCLID2" as Coord.System.EUCLID2, - gridDimensions: { - height: 21, - width: 21, - }, - gridHtmlIdHook: OmHooks.Grid.Id.GRID, - averageFreeHealthPerTile: 1.0 / 45.0, - langBalancingScheme: Lang.BalancingScheme.WEIGHT, - languageName: "engl-low", - operatorIndex: 0, - playerDescs: [ - { - familyId: "HUMAN", - teamId: 0, - socketId: undefined, - username: "hello world", - noCheckGameOver: false, - familyArgs: {}, - }, { - familyId: "CHASER", - teamId: 1, - socketId: undefined, - username: "chaser test", - noCheckGameOver: true, - familyArgs: { - fearDistance: 5, - bloodThirstDistance: 7, - healthReserve: 3.0, - movesPerSecond: 2.0, - } - } - ], -}); - -// Print some things: -// console.log(game); -// console.log(game.lang.simpleView()); - -game.statusBecomePlaying(); diff --git a/src/base/browser/Sound.ts b/src/client/Sound.ts similarity index 100% rename from src/base/browser/Sound.ts rename to src/client/Sound.ts diff --git a/src/client/TopLevel.ts b/src/client/TopLevel.ts new file mode 100644 index 00000000..dd521072 --- /dev/null +++ b/src/client/TopLevel.ts @@ -0,0 +1,19 @@ +import { OmHooks } from "defs/OmHooks"; +import { AllSkScreens } from "./screen/AllSkScreens"; + +// TODO.impl Allow users to change the spotlight radius via slider. +export class TopLevel { + + private readonly allScreens: AllSkScreens; + + + public constructor() { + const allScreensElem = document.getElementById(OmHooks.Screen.Id.ALL_SCREENS); + if (!allScreensElem) { throw new Error; } + this.allScreens = new AllSkScreens(allScreensElem); + } +} +export namespace TopLevel { +} +Object.freeze(TopLevel); +Object.freeze(TopLevel.prototype); diff --git a/src/client/game/GamePreset.ts b/src/client/game/GamePreset.ts new file mode 100644 index 00000000..3a33421d --- /dev/null +++ b/src/client/game/GamePreset.ts @@ -0,0 +1,9 @@ + +/** + * + */ +export class GamePreset { + ; +} +Object.freeze(GamePreset); +Object.freeze(GamePreset.prototype); diff --git a/src/client/game/OfflineGame.ts b/src/client/game/OfflineGame.ts new file mode 100644 index 00000000..8307e7b5 --- /dev/null +++ b/src/client/game/OfflineGame.ts @@ -0,0 +1,74 @@ +// Tell WebPack about the CSS chunk we want: +require("assets/style/game/index.css"); + +import { Game } from "game/Game"; +import type { Coord } from "floor/Tile"; +import { VisibleTile } from "floor/VisibleTile"; +import { VisibleGrid } from "floor/VisibleGrid"; +import { VisibleGame } from "./VisibleGame"; + +import type { Player } from "game/player/Player"; +import { OperatorPlayer } from "game/player/OperatorPlayer"; +import { VisiblePlayerStatus } from "game/player/VisiblePlayerStatus"; +import { ArtificialPlayer } from "game/player/ArtificialPlayer"; + +import { GameManager } from "game/__gameparts/Manager"; + +import { IndexTasks } from "game/IndexTasks"; +IndexTasks.INIT_CLASS_REGISTRIES(); + + +type G = Game.Type.OFFLINE; + +/** + * + */ +export class OfflineGame +extends GameManager implements VisibleGame { + + declare public currentOperator: NonNullable["currentOperator"]>; + + public htmlElements: VisibleGame["htmlElements"]; + + /** + * @override + */ + protected __getGridImplementation(coordSys: S): VisibleGrid.ClassIf { + return VisibleGrid.getImplementation(coordSys); + } + + /** + * @param gameDesc - + */ + public constructor( + gameDesc: Game.CtorArgs, + ) { + super( + Game.Type.OFFLINE, { + tileClass: VisibleTile, + playerStatusCtor: VisiblePlayerStatus, + }, gameDesc, + ); + this.htmlElements = Object.freeze({ + gridImplElem: this.grid.baseElem, + }); + } + + protected __createOperatorPlayer(desc: Player.__CtorArgs<"HUMAN">): OperatorPlayer { + return new OperatorPlayer(this, desc); + } + + protected __createArtifPlayer(desc: Player.__CtorArgs): ArtificialPlayer { + return ArtificialPlayer.of(this, desc); + } + + public setTimeout(callback: TimerHandler, millis: number, ...args: any[]): number { + return setTimeout(callback, millis, args); + } + + public cancelTimeout(handle: number): void { + clearTimeout(handle); + } +} +Object.freeze(OfflineGame); +Object.freeze(OfflineGame.prototype); diff --git a/src/client/OnlineGame.ts b/src/client/game/OnlineGame.ts similarity index 77% rename from src/client/OnlineGame.ts rename to src/client/game/OnlineGame.ts index 14de95d2..ad310ae8 100644 --- a/src/client/OnlineGame.ts +++ b/src/client/game/OnlineGame.ts @@ -1,9 +1,12 @@ -import * as io from "socket.io-client"; +// Tell WebPack about the CSS chunk we want: +require("assets/style/game/index.css"); -import { Game } from "game/Game"; +import * as io from "socket.io-client"; +import { Game } from "game/Game"; import { Coord, VisibleTile } from "floor/VisibleTile"; import { VisibleGrid } from "floor/VisibleGrid"; +import { VisibleGame } from "./VisibleGame"; import { Player } from "game/player/Player"; import { OperatorPlayer } from "game/player/OperatorPlayer"; @@ -12,15 +15,21 @@ import { VisiblePlayerStatus } from "game/player/VisiblePlayerStatus"; import { PlayerActionEvent } from "game/events/PlayerActionEvent"; import { GameEvents } from "game/__gameparts/Events"; +import { IndexTasks } from "game/IndexTasks"; +IndexTasks.INIT_CLASS_REGISTRIES(); + type G = Game.Type.ONLINE; /** * - * - * @extends Game */ -export class OnlineGame extends GameEvents { +export class OnlineGame +extends GameEvents implements VisibleGame { + + declare public currentOperator: NonNullable["currentOperator"]>; + + public htmlElements: VisibleGame["htmlElements"]; public readonly socket: SocketIOClient.Socket; @@ -32,10 +41,7 @@ export class OnlineGame extends GameEvents { } - /** - * _Calls recursively for this entire composition._ - * * Note that this class does not extend `GameManager`. * * @param socket - @@ -52,38 +58,27 @@ export class OnlineGame extends GameEvents { playerStatusCtor: VisiblePlayerStatus, }, gameDesc, ); - if (!this.operator) { - throw new Error("The Operator for an OnlineGame should be defined."); - } this.socket = socket; + this.htmlElements = Object.freeze({ + gridImplElem: this.grid.baseElem, + }); this.socket.off(PlayerActionEvent.EVENT_NAME.Movement); this.socket.on( PlayerActionEvent.EVENT_NAME.Movement, - this.processMoveExecute + this.executePlayerMoveEvent.bind(this), ); this.socket.off(PlayerActionEvent.EVENT_NAME.Bubble); this.socket.on( PlayerActionEvent.EVENT_NAME.Bubble, - this.processBubbleExecute, + this.executePlayerBubbleEvent.bind(this), ); - // ===================================== - // CALL TO RESET - this.reset(); - // ===================================== - } - - /** - * @override - */ - public reset(): void { - super.reset(); - // TODO.impl Send ack? - this.socket.once( + this.socket.on( Game.Serialization.EVENT_NAME, (ser: Game.ResetSer) => { + this.reset(); this.deserializeResetState(ser); }, ); @@ -92,13 +87,14 @@ export class OnlineGame extends GameEvents { /** * @override */ + public async reset(): Promise { + return super.reset(); + } + protected __createOperatorPlayer(desc: Player.__CtorArgs<"HUMAN">): OperatorPlayer { return new OperatorPlayer(this, desc); } - /** - * @override - */ protected __createArtifPlayer(desc: Player.CtorArgs): Player { return new Player(this, desc); } diff --git a/src/client/game/VisibleGame.ts b/src/client/game/VisibleGame.ts new file mode 100644 index 00000000..7d043150 --- /dev/null +++ b/src/client/game/VisibleGame.ts @@ -0,0 +1,10 @@ + + +/** + * + */ +export interface VisibleGame { + readonly htmlElements: Readonly<{ + gridImplElem: HTMLElement; + }>; +} diff --git a/src/client/index.ts b/src/client/index.ts index a05908a2..7018841f 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,15 +1,29 @@ // Tell WebPack we want CSS: -require("../../assets/style/initial/index.css"); +require("assets/style/initial/index.css"); + +import { TopLevel } from '../client/TopLevel'; + +export { OmHooks } from "defs/OmHooks"; -import { AllSkScreens } from "browser/screen/AllSkScreens"; ((): void => { - if (window.origin && window.origin !== "null" && "serviceWorker" in navigator) { +/** + * https://developers.google.com/web/fundamentals/primers/service-workers + */ +if (window.origin && window.origin !== "null" && "serviceWorker" in navigator) { + window.addEventListener('load', function() { // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register - navigator.serviceWorker.register("./src/client/ServiceWorker.js"); + navigator.serviceWorker.register("/ServiceWorker.js").then( + (registration) => { + console.log('ServiceWorker registration successful with scope: ', registration.scope); + }, + (err) => { + console.log('ServiceWorker registration failed: ', err); + },); // TODO.learn Using Service Workers to make an offline-friendly PWA. // https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Offline_Service_workers - } + }); +} })(); // window.onerror = (msg, url, lineNum) => { @@ -17,8 +31,4 @@ import { AllSkScreens } from "browser/screen/AllSkScreens"; // return true; // } -const allDisplays = new AllSkScreens(document.createElement("div")); - -import(/* webpackChunkName: "[request]" */ "./ScratchMakeGame").then((mod) => { - mod.game; -}); +export const top = new TopLevel(); diff --git a/src/base/browser/readme.md b/src/client/readme.md similarity index 85% rename from src/base/browser/readme.md rename to src/client/readme.md index 55b8130b..322bf18f 100644 --- a/src/base/browser/readme.md +++ b/src/client/readme.md @@ -1,7 +1,7 @@ # Web User Interface -**`IMPORTANT`**: This TypeScript project-reference does not depend on anything except the utils project. This is important. In the WebPack configuration, we split this project into its own chunk so that it is all that is required for the initial page load. The game code can then be lazily loaded. This will speed up the initial page load of the home screen. +**`IMPORTANT`**: This TypeScript project-reference does not depend on anything except the defs project. This is important. In the WebPack configuration, we split this project into its own chunk so that it is all that is required for the initial page load. The game code can then be lazily loaded. This will speed up the initial page load of the home screen. As a TypeScript project, this will have minimal dependencies. It will present a bunch of hooks for the rest of the game code to connect itself to. It will mainly be the case that other higher-level projects depend on this one. diff --git a/src/client/screen/AllSkScreens.ts b/src/client/screen/AllSkScreens.ts new file mode 100644 index 00000000..9a158a0f --- /dev/null +++ b/src/client/screen/AllSkScreens.ts @@ -0,0 +1,64 @@ +import { SkScreen } from "./SkScreen"; + +import { HomeScreen } from "./impl/Home"; +import { HowToPlayScreen } from "./impl/HowToPlay"; +import { HowToHostScreen } from "./impl/HowToHost"; +import { ColourCtrlScreen } from "./impl/ColourCtrl"; +import { GameSetupScreen } from "./impl/GameSetup"; +import { SeshJoinerScreen } from "./impl/SeshJoiner"; +import { PlayOfflineScreen } from "./impl/PlayOffline"; +import { PlayOnlineScreen } from "./impl/PlayOnline"; + + +export class AllSkScreens { + + private readonly dict: { + readonly [SID in SkScreen.Id]: SkScreen; + }; + + #currentScreen: SkScreen; + + public constructor(baseElem: HTMLElement) { + baseElem.setAttribute("role", "presentation"); + // Setting role="presentation" is similar to setting "display: content" + // Setting aria-hidden="true" is similar to setting "visibility: hidden" + const p = baseElem; + const f = this.goToScreen.bind(this); + this.dict = Object.freeze({ + [ SkScreen.Id.HOME ]: new HomeScreen(p,f), + [ SkScreen.Id.HOW_TO_PLAY ]: new HowToPlayScreen(p,f), + [ SkScreen.Id.HOW_TO_HOST ]: new HowToHostScreen(p,f), + [ SkScreen.Id.COLOUR_CTRL ]: new ColourCtrlScreen(p,f), + [ SkScreen.Id.GAME_SETUP ]: new GameSetupScreen(p,f), + [ SkScreen.Id.PLAY_OFFLINE ]: new PlayOfflineScreen(p,f), + [ SkScreen.Id.PLAY_ONLINE ]: new PlayOnlineScreen(p,f), + [ SkScreen.Id.SESH_JOINER ]: new SeshJoinerScreen(p,f), + }); + this.goToScreen(SkScreen.Id.HOME, {}); + } + + public goToScreen( + // NOTE: use a tuple wrapper to expand bundled type. + destId: SID[0], + ctorArgs: SkScreen.CtorArgs, + ): void { + const destScreen = this.dict[destId] as SkScreen; + if (this.currentScreen === destScreen) { + // I don't see why this would ever need to happen. + throw new Error ("never happens. see comment in source."); + } + if ((!this.currentScreen) || this.currentScreen.leave()) { + // Note on above nullish coalesce: Special case entered + // during construction when there is no currentScreen yet. + // Any confirm-leave prompts made to the user were OK-ed. + destScreen.enter(ctorArgs); + this.#currentScreen = destScreen; + } + } + + public get currentScreen(): SkScreen { + return this.#currentScreen; + } +} +Object.freeze(AllSkScreens); +Object.freeze(AllSkScreens.prototype); diff --git a/src/client/screen/SkScreen.ts b/src/client/screen/SkScreen.ts new file mode 100644 index 00000000..3f1030a1 --- /dev/null +++ b/src/client/screen/SkScreen.ts @@ -0,0 +1,132 @@ +import { OmHooks } from "defs/OmHooks"; +import type { AllSkScreens } from "./AllSkScreens"; + +import type { HomeScreen } from "./impl/Home"; +import type { HowToPlayScreen } from "./impl/HowToPlay"; +import type { HowToHostScreen } from "./impl/HowToHost"; +import type { ColourCtrlScreen } from "./impl/ColourCtrl"; +import type { GameSetupScreen } from "./impl/GameSetup"; +import type { SeshJoinerScreen } from "./impl/SeshJoiner"; +import type { PlayOfflineScreen } from "./impl/PlayOffline"; +import type { PlayOnlineScreen } from "./impl/PlayOnline"; + + +/** + * + * + * NOTE: Design decision: Isolate from the rest of the architecture. + * Ie. Do not give it circular / upward references to anything that + * references it. + */ +export abstract class SkScreen { + + readonly #parentElem: HTMLElement; + + protected readonly baseElem: HTMLElement; + + #hasLazyLoaded: boolean; + + /** + * Implementations can use this as part of navigation button + * handlers. + */ + protected readonly requestGoToScreen: AllSkScreens["goToScreen"]; + + /** + * + * @param parentElem - + * @param requestGoToDisplay - + */ + public constructor( + parentElem: HTMLElement, + requestGoToDisplay: AllSkScreens["goToScreen"], + ) { + this.#hasLazyLoaded = false; + this.requestGoToScreen = requestGoToDisplay; + this.#parentElem = parentElem; + } + + /** + * **Do not override.** + */ + public async enter(ctorArgs: SkScreen.CtorArgs): Promise { + if (!this.#hasLazyLoaded) { + const baseElem + = (this.baseElem as HTMLElement) + = document.createElement("div"); + baseElem.classList.add(OmHooks.Screen.Class.BASE); + this.__lazyLoad(); + this.#parentElem.appendChild(baseElem); + this.#hasLazyLoaded = true; + } + await this.__abstractOnBeforeEnter(ctorArgs); + // ^Wait until the screen has finished setting itself up + // before entering it. + window.requestAnimationFrame((time) => { + this.baseElem.dataset[OmHooks.Screen.Dataset.CURRENT] = ""; // exists. + this.baseElem.setAttribute("aria-hidden", "false"); + }); + } + + /** + * **Do not override.** + */ + public leave(): boolean { + if (this.__abstractOnBeforeLeave()) { + delete this.baseElem.dataset[OmHooks.Screen.Dataset.CURRENT]; // non-existant. + this.baseElem.setAttribute("aria-hidden", "true"); + return true; + } + return false; + } + + /** + * Implementations should set the CSS class for the base element, + * and also set its aria label to an appropriate string. + */ + protected abstract __lazyLoad(): void; + + /** + * This is a good place to start any `setInterval` schedules, and + * to bring focus to a starting HTML element if appropriate. + * + * The default implementation does nothing. Overriding implementations + * from direct subclasses can safely skip making a supercall. + */ + protected async __abstractOnBeforeEnter(ctorArgs: SkScreen.CtorArgs): Promise { } + + /** + * Return false if the leave should be cancelled. This functionality + * allows an implementation to provide a prompt to the user such as + * a confirmation modal warning that unsaved changes would be lost. + * + * This is a good place, for example, to stop any non-essential + * `setInterval` schedules. + */ + protected __abstractOnBeforeLeave(): boolean { + return true; + } + +} +export namespace SkScreen { + + export const enum Id { + HOME = "home", + HOW_TO_PLAY = "howToPlay", + HOW_TO_HOST = "howToHost", + COLOUR_CTRL = "colourControl", + GAME_SETUP = "gameSetup", + PLAY_OFFLINE = "playOffline", + PLAY_ONLINE = "playOnline", + SESH_JOINER = "sessionJoiner", + } + + export type CtorArgs = any extends SID_group ? never + : { [SID in SID_group]: + SID extends SkScreen.Id.PLAY_ONLINE ? PlayOnlineScreen.CtorArgs + : SID extends SkScreen.Id ? {} // Placeholder for screens that haven't defined their ctor arg types yet. + : never + }[SID_group]; +} +Object.freeze(SkScreen); +Object.freeze(SkScreen.prototype); diff --git a/src/client/screen/impl/ColourCtrl.ts b/src/client/screen/impl/ColourCtrl.ts new file mode 100644 index 00000000..81f9c064 --- /dev/null +++ b/src/client/screen/impl/ColourCtrl.ts @@ -0,0 +1,145 @@ +// Tell WebPack about the css we want: +require("assets/style/colour/index.css"); + +import { StorageHooks } from "defs/StorageHooks"; +import { OmHooks } from "defs/OmHooks"; +import { SkPickOne } from "../../../client/utils/SkPickOne"; + +import { SkScreen } from "../SkScreen"; + + +/** + * + */ +export class ColourCtrlScreen extends SkScreen { + + public readonly sel: ColourCtrlScreen.PickOne; + + protected __lazyLoad(): void { + const sel = new ColourCtrlScreen.PickOne(); + this.baseElem.appendChild(sel.baseElem); + (this.sel as ColourCtrlScreen.PickOne) = sel; + + // Highlight the user's last selected colour scheme (if it exists). + // This will already have been loaded up during page load, hence + // passing `false` to the `noCallback` argument + const lastUsedSchemeId = localStorage.getItem(StorageHooks.LocalKeys.COLOUR_ID); + if (lastUsedSchemeId) { + this.sel.selectOpt(this.sel.getOptById(lastUsedSchemeId)!, false); + } + } +} +export namespace ColourCtrlScreen { + type O = PickOne.Option; + /** + * + */ + export class PickOne extends SkPickOne { + + public readonly garageDoorElem: HTMLElement; + + public constructor() { + super(); + this.garageDoorElem = document.getElementById(OmHooks.Screen.Id.SCREEN_TINT)!; + if (!this.garageDoorElem) throw new Error; + this.garageDoorElem.style.transitionDuration = `${Colour.SMOOTH_CHANGE_DURATION/3.0}ms`; + + for (const schemeDesc of Colour.Schemes) { + this.addOption(new PickOne.Option(schemeDesc)) + } + this.selectOpt(this.getOptById("snakey")!, false); + } + + public __onHoverOpt(opt: O): void { + ; + } + + public __onSelectOpt(opt: O): void { + localStorage.setItem( + StorageHooks.LocalKeys.COLOUR_ID, + opt.desc.id, + ); + localStorage.setItem( + StorageHooks.LocalKeys.COLOUR_LITERAL, + opt.cssLiteral, + ); + const duration = (Colour.SMOOTH_CHANGE_DURATION / 3.0) + 80; + setTimeout(() => { + this.garageDoorElem.style.opacity = 1.0.toString(); + setTimeout(() => { + document.documentElement.dataset[OmHooks.General.Dataset.COLOUR_SCHEME] = opt.desc.id; + setTimeout(() => { + this.garageDoorElem.style.opacity = 0.0.toString(); + }, duration); + }, duration); + }, duration); + } + + public getOptById(searchId: Colour.Scheme["id"]): O | undefined { + return this.options.find((opt) => opt.desc.id === searchId); + } + } + export namespace PickOne { + /** + * + */ + export class Option extends SkPickOne.__Option { + + public readonly desc: Colour.Scheme; + + public readonly cssLiteral: string; + + public constructor(desc: Colour.Scheme) { + super(); + this.desc = desc; + // TODO.impl `this.cssLiteral`; + } + } + Object.freeze(Option); + Object.freeze(Option.prototype); + } + Object.freeze(PickOne); + Object.freeze(PickOne.prototype); +} +Object.freeze(ColourCtrlScreen); +Object.freeze(ColourCtrlScreen.prototype); + + +/** + * + */ +export namespace Colour { + export const Swatch = Object.freeze([ + "mainFg", "mainBg", + "tileFg", "tileBg", "tileBd", + "healthFg", "healthBg", + "pFaceMe", + "pFaceTeammate", "pFaceImtlTeammate", + "pFaceOpponent", "pFaceImtlOpponent", + ]); + export const Schemes = Object.freeze(([{ + id: "snakey", + displayName: "Snakey", + author: "N.W.", + }, { + id: "smooth-stone", + displayName: "Smooth Stone", + author: "Dav", + }, + ] as Array).map((scheme) => Object.freeze(scheme))); + export type Scheme = Readonly<{ + /** + * Must be matched in the CSS as an attribute value. + */ + id: string; + displayName: string; + author: string; + }>; + + /** + * How long the entire smooth transition between colour schemes + * should last in units of milliseconds. + */ + export const SMOOTH_CHANGE_DURATION = 2_000.0; +} +Object.freeze(Colour); diff --git a/src/base/browser/screen/impl/GameSetup.ts b/src/client/screen/impl/GameSetup.ts similarity index 55% rename from src/base/browser/screen/impl/GameSetup.ts rename to src/client/screen/impl/GameSetup.ts index b1ed96c6..00cd952f 100644 --- a/src/base/browser/screen/impl/GameSetup.ts +++ b/src/client/screen/impl/GameSetup.ts @@ -1,35 +1,38 @@ +import { Lang } from "defs/TypeDefs"; + import { SkScreen } from "../SkScreen"; -import { Lang } from "utils/TypeDefs"; /** - * + * What coordinate systems are available will depend on what language + * the user chooses. */ -export class GameSetupScreen extends SkScreen { +// TODO.learn how to use the IndexDB web API. +export class GameSetupScreen extends SkScreen { private readonly langSel: HTMLSelectElement; protected __lazyLoad(): void { - // TODO.impl Read last used setup from localStorage. - { - // LANGUAGE SELECTION: + this.createLangSelector(); + } + + private createLangSelector(): void { const langSel = document.createElement("select"); - for (const langName of Object.values(Lang.Names)) { + for (const langName of Object.values(Lang.FrontendDescs)) { const opt = document.createElement("option"); opt.value = langName.id; opt.innerText = langName.display; langSel.add(opt); } - langSel.onselect = (): void => { + langSel.onchange = (): void => { langSel.blur(); // TODO - } + }; // TODO.impl set defaults from last used setup. (this.langSel as HTMLSelectElement) = langSel; - } { - // TODO.impl - } } - + } +} +export namespace GameSetupScreen { } Object.freeze(GameSetupScreen); Object.freeze(GameSetupScreen.prototype); diff --git a/src/client/screen/impl/Home.ts b/src/client/screen/impl/Home.ts new file mode 100644 index 00000000..9c580842 --- /dev/null +++ b/src/client/screen/impl/Home.ts @@ -0,0 +1,105 @@ +import { OmHooks } from "defs/OmHooks"; + +import { SkScreen } from "../SkScreen"; + + +/** + * + */ +export class HomeScreen extends SkScreen { + + private readonly navElem: HTMLElement; + + protected __lazyLoad(): void { + const OMHC = OmHooks.Screen.Impl.Home.Class; + type OMHC = typeof OMHC; + this.baseElem.classList.add( + OmHooks.General.Class.CENTER_CONTENTS, + OMHC.SCREEN, + ); + this.baseElem.setAttribute("aria-label", "Home Page Screen"); + + const nav + = (this.navElem as HTMLElement) + = document.createElement("div"); + nav.classList.add(OMHC.NAV); + nav.setAttribute("role", "navigation"); + nav.addEventListener("pointerleave", () => { + if (document.activeElement?.parentElement === nav) { + (document.activeElement as HTMLElement).blur(); + } + }); + + // NOTE: Define array entries in order that their + // buttons should be tabbed through via keyboard. + ([{ + text: "Offline Single-player", // TODO.impl this should go to the game setup screen. + cssClass: OMHC.NAV_PLAY_OFFLINE, + screenId: SkScreen.Id.PLAY_OFFLINE, + },{ + text: "Online Multi-player", + cssClass: OMHC.NAV_PLAY_ONLINE, + screenId: (ev: MouseEvent) => {}, + },{ + text: "Tutorial", + cssClass: OMHC.NAV_TUTORIAL, + screenId: SkScreen.Id.HOW_TO_PLAY, + },{ + text: "Colour Schemes", + cssClass: OMHC.NAV_COLOURS, + screenId: SkScreen.Id.COLOUR_CTRL, + },]) + .map void); + }>>((desc) => Object.freeze(desc)) + .forEach((desc) => { + const button = document.createElement("button"); + button.onclick = (desc.screenId instanceof Function) ? desc.screenId : () => { + // TODO.impl play a health-up sound. + this.requestGoToScreen(desc.screenId as SkScreen.Id, {}); + }; + addToNav(button, desc); + }); + + ([{ + text: "Visit Repo", + cssClass: OMHC.NAV_VIEW_REPO, + href: new window.URL("https://github.com/david-fong/SnaKey-NTS"), + },{ + text: "Report Issue", + cssClass: OMHC.NAV_RPT_ISSUE, + href: new window.URL("https://github.com/david-fong/SnaKey-NTS/issues"), + },]) + .map>((desc) => Object.freeze(desc)) + .forEach((desc) => { + const a = document.createElement("a"); + a.href = (desc.href).toString(); + a.referrerPolicy = "strict-origin-when-cross-origin"; + a.target = "_blank"; + addToNav(a, desc); + }); + + function addToNav(elem: HTMLElement, desc: { text: string, cssClass: string; }): void { + elem.classList.add( + OmHooks.General.Class.CENTER_CONTENTS, + OmHooks.General.Class.TEXT_SELECT_DISABLED, + desc.cssClass, + ); + elem.innerText = desc.text; + elem.addEventListener("pointerenter", () => { + elem.focus(); + // TODO.impl play a keyboard click sound. + }); + nav.appendChild(elem); + } + this.baseElem.appendChild(nav); + } +} +Object.freeze(HomeScreen); +Object.freeze(HomeScreen.prototype); diff --git a/src/base/browser/screen/impl/HowToHost.ts b/src/client/screen/impl/HowToHost.ts similarity index 57% rename from src/base/browser/screen/impl/HowToHost.ts rename to src/client/screen/impl/HowToHost.ts index 174f4af9..14f329ba 100644 --- a/src/base/browser/screen/impl/HowToHost.ts +++ b/src/client/screen/impl/HowToHost.ts @@ -1,7 +1,10 @@ -import { SkScreen } from '../SkScreen'; +import { SkScreen } from "../SkScreen"; -export class HowToHostScreen extends SkScreen { +/** + * + */ +export class HowToHostScreen extends SkScreen { protected __lazyLoad(): void { ; @@ -10,9 +13,6 @@ export class HowToHostScreen extends SkScreen { } export namespace HowToHostScreen { // TODO.doc - // This will not work. They would need to run webpack, which would - // require installing developer dependencies... Should we eventually - // publish this npm package containing only the built output? export const INSTRUCTIONS_STEPS = Object.freeze([ "$ npm install \'https://github.com/david-fong/SnaKey-NTS#gh-pages\'", "$ npm run start", diff --git a/src/base/browser/screen/impl/HowToPlay.ts b/src/client/screen/impl/HowToPlay.ts similarity index 67% rename from src/base/browser/screen/impl/HowToPlay.ts rename to src/client/screen/impl/HowToPlay.ts index 82c18cca..70a7a848 100644 --- a/src/base/browser/screen/impl/HowToPlay.ts +++ b/src/client/screen/impl/HowToPlay.ts @@ -1,7 +1,10 @@ import { SkScreen } from '../SkScreen'; -export class HowToPlayScreen extends SkScreen { +/** + * + */ +export class HowToPlayScreen extends SkScreen { protected __lazyLoad(): void { ; diff --git a/src/client/screen/impl/PlayOffline.ts b/src/client/screen/impl/PlayOffline.ts new file mode 100644 index 00000000..09335f98 --- /dev/null +++ b/src/client/screen/impl/PlayOffline.ts @@ -0,0 +1,83 @@ +import type { Coord } from "floor/Tile"; +import type { OfflineGame } from "../../game/OfflineGame"; +import { Lang } from "defs/TypeDefs"; + +import type { SkScreen } from '../SkScreen'; +import { __PlayScreen } from "./__Play"; + + +/** + * + */ +export class PlayOfflineScreen extends __PlayScreen { + + declare public readonly currentGame: OfflineGame | undefined; + + protected readonly wantsAutoPause = true; + + protected __lazyLoad(): void { + super.__lazyLoad(); + } + + protected async __createNewGame(): Promise> { + // TODO.impl fetch special game preset "forNextGame". + return new (await import( + /* webpackChunkName: "game/offline" */ + "../../game/OfflineGame" + )).OfflineGame({ + coordSys: "EUCLID2" as Coord.System.EUCLID2, + gridDimensions: { + height: 21, + width: 21, + }, + averageFreeHealthPerTile: 1.0 / 45.0, + langBalancingScheme: Lang.BalancingScheme.WEIGHT, + langId: "engl-low", + playerDescs: [{ + isALocalOperator: true, + familyId: "HUMAN", + teamId: 0, + socketId: undefined, + username: "hello world 1", + noCheckGameOver: false, + familyArgs: { }, + }, { + isALocalOperator: true, + familyId: "HUMAN", + teamId: 0, + socketId: undefined, + username: "hello world 2", + noCheckGameOver: false, + familyArgs: { }, + }, { + isALocalOperator: false, + familyId: "CHASER", + teamId: 1, + socketId: undefined, + username: "chaser test 1", + noCheckGameOver: true, + familyArgs: { + fearDistance: 5, + bloodThirstDistance: 7, + healthReserve: 3.0, + movesPerSecond: 2.0, + } + }, { + isALocalOperator: false, + familyId: "CHASER", + teamId: 1, + socketId: undefined, + username: "chaser test 2", + noCheckGameOver: true, + familyArgs: { + fearDistance: 5, + bloodThirstDistance: 7, + healthReserve: 3.0, + movesPerSecond: 2.0, + } + }], + }); + } +} +Object.freeze(PlayOfflineScreen); +Object.freeze(PlayOfflineScreen.prototype); diff --git a/src/client/screen/impl/PlayOnline.ts b/src/client/screen/impl/PlayOnline.ts new file mode 100644 index 00000000..86266abb --- /dev/null +++ b/src/client/screen/impl/PlayOnline.ts @@ -0,0 +1,35 @@ +// TODO dynamically import socket.io-client. +import type * as SocketIOClient from "socket.io-client"; + +import { OmHooks } from "defs/OmHooks"; +import type { Coord } from "floor/Tile"; +import type { OnlineGame } from "../../game/OnlineGame"; + +import type { SkScreen } from "../SkScreen"; +import { __PlayScreen } from "./__Play"; + + +/** + * + */ +export class PlayOnlineScreen extends __PlayScreen { + + declare public readonly currentGame: OnlineGame | undefined; + + protected readonly wantsAutoPause = false; + + protected __lazyLoad(): void { + super.__lazyLoad(); + } + + protected async __createNewGame(): Promise> { + return undefined!; + } +} +export namespace PlayOnlineScreen { + export type CtorArgs = Readonly<{ + socket: typeof SocketIOClient.Socket.id; + }>; +} +Object.freeze(PlayOnlineScreen); +Object.freeze(PlayOnlineScreen.prototype); diff --git a/src/base/browser/screen/impl/SeshJoiner.ts b/src/client/screen/impl/SeshJoiner.ts similarity index 71% rename from src/base/browser/screen/impl/SeshJoiner.ts rename to src/client/screen/impl/SeshJoiner.ts index 1b34fff8..eb4a47e7 100644 --- a/src/base/browser/screen/impl/SeshJoiner.ts +++ b/src/client/screen/impl/SeshJoiner.ts @@ -4,7 +4,7 @@ import { SkScreen } from "../SkScreen"; /** * */ -export class SeshJoinerScreen extends SkScreen { +export class SeshJoinerScreen extends SkScreen { protected __lazyLoad(): void { ; diff --git a/src/client/screen/impl/__Play.ts b/src/client/screen/impl/__Play.ts new file mode 100644 index 00000000..6b44c051 --- /dev/null +++ b/src/client/screen/impl/__Play.ts @@ -0,0 +1,340 @@ +import { OmHooks } from "defs/OmHooks"; +import type { OfflineGame } from "../../game/OfflineGame"; +import type { OnlineGame } from "../../game/OnlineGame"; + +import { SkScreen } from "../SkScreen"; + + +type Game = (OfflineGame | OnlineGame); +type SID_options = SkScreen.Id.PLAY_OFFLINE | SkScreen.Id.PLAY_ONLINE; + +/** + * If and only if this screen is the current screen, then its + * `currentGame` property is defined, although it is not recommended + * to manage its state except through this class' wrapper methods, + * which are bound to buttons and maintain other invariants between + * the game's state and the UI's state. + */ +// TODO.impl change the document title base on game state. +export abstract class __PlayScreen extends SkScreen { + + /** + * Hosts the implementation-specific grid element, as well as some + * other overlays. + */ + protected readonly gridElem: HTMLElement; + + private readonly backToHomeButton: HTMLButtonElement; + + /** + * Must be disabled when + * - The game does not exist. + * - The game is over. + */ + private readonly pauseButton: HTMLButtonElement; + /** + * Must be disabled when + * - The game does not exist. + * - The game is playing. + */ + private readonly resetButton: HTMLButtonElement; + + /** + * This field is defined when this is the current screen, and next + * to all code here will only run when this screen is the current + * screen, but I have annotated the type as possibly undefined just + * so there's no question that it can, under certain conditions be + * undefined. + */ + #currentGame: Game | undefined; + + protected abstract readonly wantsAutoPause: boolean; + /** + * Automatically added and removed from listeners when entering + * and leaving this screen. + */ + readonly #onVisibilityChange: VoidFunction; + /** + * Automatically added and removed from listeners when entering + * and leaving this screen. + */ + readonly #gridOnKeyDown: (ev: KeyboardEvent) => boolean; + /** + * Undefined when the game is playing. + */ + #pauseReason: "page-hide" | "other" | undefined; + + + /** + * @override + */ + protected __lazyLoad(): void { + this.baseElem.classList.add( + OmHooks.Screen.Impl.PlayGame.Class.SCREEN, + ); + this.baseElem.setAttribute("aria-label", "Play Game Screen"); + + const centerColItems = __PlayScreen.createCenterColElem(); + (this.gridElem as HTMLElement) = centerColItems.gridElem; + this.baseElem.appendChild(centerColItems.baseElem); + // ^Purposely make the grid the first child so it gets tabbed to first. + + this.initializeControlsBar(); + this.initializeScoresBar(); + + // @ts-ignore Assignment to readonly property. + // We can't use a type assertion to cast off the readonly-ness + // because it messed up the transpilation for #private fields. + this.#onVisibilityChange = () => { + if (!this.wantsAutoPause) return; + if (document.hidden) { + if (this.#pauseReason === undefined) this.statusBecomePaused(); + } else { + if (this.#pauseReason === "page-hide") this.statusBecomePlaying(); + } + } + // @ts-ignore Assignment to readonly property. + // See above note. + this.#gridOnKeyDown = this.__gridKeyDownCallback.bind(this); + } + + /** + * @override + */ + protected __abstractOnBeforeEnter(): Promise { + return (async () => { + document.addEventListener("visibilitychange", this.#onVisibilityChange); + this.pauseButton.disabled = true; + this.statusBecomePaused(); // <-- Leverage some state initialization. + + // TODO.design Are there ways we can share more code between + // implementations by passing common arguments? + this.#currentGame = await this.__createNewGame(); + this.gridElem.addEventListener("keydown", this.#gridOnKeyDown); + const resetPromise = this.currentGame!.reset(); + + // Wait until resetting has finished before attaching the + // grid element to the screen so that the DOM changes made + // by populating tiles with CSP's can be done all at once. + await resetPromise; + this.gridElem.insertAdjacentElement("afterbegin", + this.currentGame!.htmlElements.gridImplElem, + ); // ^The order of insertion does not matter (it used to). + + this.pauseButton.onclick = this.statusBecomePlaying.bind(this); + this.pauseButton.disabled = false; + if (this.wantsAutoPause) { + setTimeout(() => { + if (!document.hidden) this.statusBecomePlaying(); + }, 1000); + } + return; + })(); + } + + /** + * @override + */ + protected __abstractOnBeforeLeave(): boolean { + if (!window.confirm("Are you sure you would like to leave?")) { + return false; + } + document.removeEventListener("visibilitychange", this.#onVisibilityChange); + + // Release the game: + for (const elem of Object.values(this.currentGame!.htmlElements)) { + elem.remove(); + } + this.gridElem.removeEventListener("keydown", this.#gridOnKeyDown); + this.#currentGame = undefined; + return true; + } + + + public get currentGame(): Game | undefined { + return this.#currentGame; + } + + protected abstract async __createNewGame(): Promise; + + + /** + * Do not use this directly. See `this.#gridOnKeyDown`. + * + * Note the uses of typescript `!` assertion instead of the nullish + * coalescing operator for `this.currentGame`. This is safe because + * this callback is managed by the screen-enter and leave hooks to + * only be registered when the current game is defined. + * + * @param ev - + */ + private __gridKeyDownCallback(ev: KeyboardEvent): boolean { + // console.log(`key: ${ev.key}, code: ${ev.code},` + // + ` keyCode: ${ev.keyCode}, char: ${ev.char},` + // + ` charCode: ${ev.charCode}`); + if (ev.ctrlKey && ev.key === " ") { + // If switching operator: + const operators = this.currentGame!.operators; + this.currentGame!.currentOperator = operators[ + (operators.indexOf(this.currentGame!.currentOperator) + 1) + % operators.length + ]; + } else { + // Process event as regular typing: + this.currentGame!.currentOperator.processKeyboardInput(ev); + } + // Disable scroll-down via spacebar: + if (ev.key === " ") { + ev.preventDefault(); + return false; + } + return true; + } + + + private statusBecomePlaying(): void { + const OHGD = OmHooks.Grid.Dataset.GAME_STATE; + this.currentGame?.statusBecomePlaying(); + this.pauseButton.innerText = "Pause"; + this.#pauseReason = undefined; + this.gridElem.dataset[OHGD.KEY] = OHGD.VALUES.PLAYING; + + window.requestAnimationFrame((time) => { + this.pauseButton.onclick = this.statusBecomePaused.bind(this); + this.resetButton.disabled = true; + this.gridElem.focus(); + }); + } + + private statusBecomePaused(): void { + const OHGD = OmHooks.Grid.Dataset.GAME_STATE; + this.currentGame?.statusBecomePaused(); + this.pauseButton.innerText = "Unpause"; + this.#pauseReason = document.hidden ? "page-hide" : "other"; + this.gridElem.dataset[OHGD.KEY] = OHGD.VALUES.PAUSED; + + this.pauseButton.onclick = this.statusBecomePlaying.bind(this); + this.resetButton.disabled = false; + } + + // TODO.impl pass this to the created game. + public __onGameBecomeOver(): void { + const OHGD = OmHooks.Grid.Dataset.GAME_STATE; + this.pauseButton.disabled = true; + this.gridElem.dataset[OHGD.KEY] = OHGD.VALUES.OVER; + } + + /** + * Should only be called if all the following conditions are met: + * - The current game exists. + * - The current game is paused or it is over. + */ + private __resetGame(): void { + this.currentGame!.reset(); + this.pauseButton.disabled = false; + if (this.wantsAutoPause) { + this.statusBecomePlaying(); + } + } + + + /** + * + */ + protected initializeControlsBar(): void { + const controlsBar = document.createElement("div"); + controlsBar.classList.add( + OmHooks.Screen.Impl.PlayGame.Class.CONTROLS_BAR, + ); + controlsBar.setAttribute("role", "menu"); + + { const bth + = (this.backToHomeButton as HTMLButtonElement) + = document.createElement("button"); + bth.innerText = "Return To Homepage"; + bth.onclick = this.requestGoToScreen.bind(this, SkScreen.Id.HOME); + controlsBar.appendChild(bth); } + + { const pause + = (this.pauseButton as HTMLButtonElement) + = document.createElement("button"); + controlsBar.appendChild(pause); } + + { const reset + = (this.resetButton as HTMLButtonElement) + = document.createElement("button"); + reset.innerText = "Reset"; + reset.onclick = this.__resetGame.bind(this); + controlsBar.appendChild(reset); } + + this.baseElem.appendChild(controlsBar); + } + + protected initializeScoresBar(): void { + ; + } +} +export namespace __PlayScreen { + /** + * + */ + export function createCenterColElem(): + { baseElem: HTMLElement, gridElem: HTMLElement, } + { + const CssFx = OmHooks.General.Class; + const centerColElem = document.createElement("div"); + centerColElem.classList.add( + CssFx.CENTER_CONTENTS, + OmHooks.Screen.Impl.PlayGame.Class.GRID_CONTAINER, + ); + const gridElem = document.createElement("div"); + gridElem.tabIndex = 0; // <-- allow focusing this element. + gridElem.setAttribute("role", "textbox"); + gridElem.setAttribute("aria-label", "Game Grid"); + gridElem.classList.add( + CssFx.CENTER_CONTENTS, + CssFx.STACK_CONTENTS, + CssFx.TEXT_SELECT_DISABLED, + OmHooks.Grid.Class.GRID, + ); + { + // Add a "keyboard-disconnected" overlay if not added already: + const kbdDcBase = document.createElement("div"); + kbdDcBase.classList.add( + CssFx.CENTER_CONTENTS, + OmHooks.Grid.Class.KBD_DC, + ); + // TODO.impl Add an with icon instead please. + { + const kbdDcIcon = document.createElement("div"); + kbdDcIcon.classList.add(OmHooks.Grid.Class.KBD_DC_ICON); + kbdDcIcon.innerText = "(click here to continue typing)"; + kbdDcBase.appendChild(kbdDcIcon); + } + gridElem.appendChild(kbdDcBase); + } { + // Add a "keyboard-disconnected" overlay if not added already: + const pauseOl = document.createElement("div"); + pauseOl.classList.add( + CssFx.CENTER_CONTENTS, + OmHooks.Grid.Class.PAUSE_OL, + ); + // TODO.impl Add an with icon instead please. + { + const pauseIcon = document.createElement("div"); + pauseIcon.classList.add(OmHooks.Grid.Class.PAUSE_OL_ICON); + pauseIcon.innerText = "(The Game is Paused)"; + pauseOl.appendChild(pauseIcon); + } + gridElem.appendChild(pauseOl); + } + centerColElem.appendChild(gridElem); + + return Object.freeze({ + baseElem: centerColElem, + gridElem, + }); + } +} +Object.freeze(__PlayScreen); +Object.freeze(__PlayScreen.prototype); diff --git a/src/base/browser/screen/readme.md b/src/client/screen/readme.md similarity index 90% rename from src/base/browser/screen/readme.md rename to src/client/screen/readme.md index f55eec31..528c3979 100644 --- a/src/base/browser/screen/readme.md +++ b/src/client/screen/readme.md @@ -1,4 +1,4 @@ -# Displays +# Screens These are wrappers around `HTMLElement`s that will fill the page, and that the user navigates between. diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index 69f1a29b..069fc107 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../.templates/tsconfig.json", "references": [ - { "path": "../base/browser", }, + { "path": "../base/defs", }, { "path": "../base/game", }, ], } \ No newline at end of file diff --git a/src/client/utils/SkPickOne.ts b/src/client/utils/SkPickOne.ts new file mode 100644 index 00000000..f4e92a47 --- /dev/null +++ b/src/client/utils/SkPickOne.ts @@ -0,0 +1,152 @@ +import { OmHooks } from "defs/OmHooks"; + + +/** + * Consumers and implementers of this utility widget must ensure that + * a call to `selectOpt` is made with valid arguments before it becomes + * interfaceable to a browser user. + * + * https://www.w3.org/TR/wai-aria-1.1/#listbox + */ +// TODO.learn https://www.w3.org/TR/wai-aria-1.1/#managingfocus +export abstract class SkPickOne { + + public readonly baseElem: HTMLElement; + + protected readonly options: Array; + + #confirmedOpt: O; + #hoveredOpt: O; + #validity: boolean; + + public constructor() { + const baseElem = document.createElement("div"); + baseElem.tabIndex = 0; + baseElem.classList.add(OmHooks.SkPickOne.Class.BASE); + baseElem.addEventListener("keydown", this.onKeyDown.bind(this)); + baseElem.setAttribute("role", "listbox"); + this.baseElem = baseElem; + + this.options = []; + } + + public addOption(opt: O): void { + this.options.push(opt); + this.baseElem.appendChild(opt.baseElem); + opt.__registerParent(this.onOptDisabledChange.bind(this)); + } + + public hoverOpt(opt: O): void { + if (this.hoveredOpt !== opt) { + // nullish coalesce is for edge-case on adding first option. + this.hoveredOpt?.baseElem.setAttribute("aria-active-descendant", "false"); + this.#hoveredOpt = opt; + this.hoveredOpt.baseElem.setAttribute("aria-active-descendant", "true"); + } + } + protected abstract __onHoverOpt(opt: O): void; + + public selectOpt(opt: O, doCallback: boolean = true): void { + if (!opt) throw new Error("opt must be defined"); + // We must ensure hovering before executing confirmation: + this.hoverOpt(opt); + // Now that hovering is done, execute confirmation of selection: + if (this.confirmedOpt !== opt) { + // nullish coalesce is for edge-case on adding first option. + this.confirmedOpt?.baseElem.setAttribute("aria-selected", "false"); + this.#confirmedOpt = opt; + this.confirmedOpt.baseElem.setAttribute("aria-selected", "true"); + if (doCallback) { + this.__onSelectOpt(opt); + } + } + } + /** + * When selecting an option makes changes to other elements, + * implementations should see the aria attributes `aria-controls` + * and `aria-describedby`. Ex. Each option may encapsulate a + * description element (use described-by) that is made visible + * in a separate pane (use aria-controls) when selected. + */ + protected abstract __onSelectOpt(opt: O): void; + + public get confirmedOpt(): O { + return this.#confirmedOpt; + } + public get hoveredOpt(): O { + return this.#hoveredOpt; + } + + private onOptDisabledChange(opt: O): void { + if (this.confirmedOpt === opt) { + this.validity = !opt.disabled; + } + } + + private set validity(newValidity: boolean) { + if (this.validity !== newValidity) { + this.baseElem.setAttribute("aria-invalid", (newValidity ? "false": "true")); + this.#validity = newValidity; + // TODO.impl styling. + } + } + + // TODO.impl skip disabled options. + private onKeyDown(ev: KeyboardEvent): boolean { + if (ev.key === " " || ev.key === "Enter") { + this.selectOpt(this.hoveredOpt); + ev.preventDefault(); + return false; + } else { + const hoverOptIndex = this.options.indexOf(this.hoveredOpt); + if (ev.key === "ArrowDown" || ev.key === "Down") { + if (hoverOptIndex < (this.options.length - 1)) { + this.hoverOpt(this.options[hoverOptIndex + 1]); + ev.preventDefault(); + return false; + } + } else if (ev.key === "ArrowUp" || ev.key === "Up") { + if (hoverOptIndex > 0) { + this.hoverOpt(this.options[hoverOptIndex - 1]); + ev.preventDefault(); + return false; + } + } + } + return true; + } +} +export namespace SkPickOne { + /** + * + * https://www.w3.org/TR/wai-aria-1.1/#option + */ + export abstract class __Option { + public readonly baseElem: HTMLElement; + #disabled: boolean; + #notifyParentOfDisabledChange: (me: __Option) => void; + public constructor() { + const base = this.baseElem = document.createElement("div"); + base.classList.add(OmHooks.SkPickOne.Class.OPT_BASE); + base.setAttribute("role", "option"); + this.#disabled = false; + } + public __registerParent<__O extends __Option>(onDisabledChange: (me: __O) => void): void { + this.#notifyParentOfDisabledChange = onDisabledChange as (me: __Option) => void; + } + public get disabled(): boolean { + return this.#disabled; + } + public set disabled(newDisabled: boolean) { + if (this.disabled !== newDisabled) { + this.baseElem.setAttribute("aria-disabled", (newDisabled ? "true" : "false")); + this.#disabled = newDisabled; + this.#notifyParentOfDisabledChange(this); + } + } + } + Object.freeze(SkPickOne); + Object.freeze(SkPickOne.prototype); +} +Object.freeze(SkPickOne); +Object.freeze(SkPickOne.prototype); diff --git a/src/server/GroupSession.ts b/src/server/GroupSession.ts index b62e6370..18eb6e12 100644 --- a/src/server/GroupSession.ts +++ b/src/server/GroupSession.ts @@ -143,20 +143,19 @@ export class GroupSession { this.currentGame = new ServerGame(this.namespace, { coordSys, gridDimensions, - gridHtmlIdHook: undefined, averageFreeHealthPerTile: undefined!, - languageName: undefined!, + langId: undefined!, langBalancingScheme: undefined!, - operatorIndex: undefined, playerDescs: { ...Object.values(this.sockets).map((socket) => { - return { + return { + isALocalOperator: false, familyId: "HUMAN", teamId: socket.teamId!, socketId: socket.id, username: socket.username!, // checked above. noCheckGameOver: false, - familyArgs: {} + familyArgs: {}, }; }), }, diff --git a/src/server/ServerGame.ts b/src/server/ServerGame.ts index 4382703f..bd5a085d 100644 --- a/src/server/ServerGame.ts +++ b/src/server/ServerGame.ts @@ -5,10 +5,10 @@ import { Game } from "game/Game"; import { Coord, Tile } from "floor/Tile"; import { Grid } from "floor/Grid"; import { Player, PlayerStatus } from "game/player/Player"; +import { ArtificialPlayer } from "game/player/ArtificialPlayer"; import { EventRecordEntry } from "game/events/EventRecordEntry"; import { PlayerActionEvent } from "game/events/PlayerActionEvent"; -import { ArtificialPlayer } from "game/player/ArtificialPlayer"; import { GameManager } from "game/__gameparts/Manager"; @@ -18,8 +18,6 @@ type G = Game.Type.SERVER; /** * Handles game-related events and attaches listeners to each client * socket. - * - * @extends Game */ export class ServerGame extends GameManager { @@ -42,8 +40,6 @@ export class ServerGame extends GameManager { /** - * _Calls reset recursively for this entire composition._ - * * Attach listeners for requests to each socket. * * Broadcasts constructor arguments to all clients. @@ -103,27 +99,29 @@ export class ServerGame extends GameManager { // Pass on Game constructor arguments to each client: humanPlayers.forEach((player) => { - (gameDesc.operatorIndex! as Player.Id) = player.playerId; + gameDesc.playerDescs.forEach((playerDesc) => { + (playerDesc.isALocalOperator as boolean) = + (playerDesc.socketId === this.playerSockets[player.playerId].id); + }) this.playerSockets[player.playerId].emit( Game.CtorArgs.EVENT_NAME, gameDesc, ); }, this); - - this.reset(); } /** * @override */ - public reset(): void { - super.reset(); + public async reset(): Promise { + const superPromise = super.reset(); // TODO.design Should we wait for ACK's from all clients before - // enabling `stateBecomePlayer` + // enabling the privileged users' `stateBecomePlaying` buttons? this.namespace.emit( Game.Serialization.EVENT_NAME, this.serializeResetState(), ); + return superPromise; } /** @@ -136,7 +134,7 @@ export class ServerGame extends GameManager { /** * @override */ - protected __createArtifPlayer(desc: Player.CtorArgs): ArtificialPlayer { + protected __createArtifPlayer(desc: Player.__CtorArgs): ArtificialPlayer { return ArtificialPlayer.of(this, desc); } @@ -152,8 +150,8 @@ export class ServerGame extends GameManager { /** * @override */ - public processMoveExecute(desc: Readonly>): void { - super.processMoveExecute(desc); + public executePlayerMoveEvent(desc: Readonly>): void { + super.executePlayerMoveEvent(desc); if (desc.eventId === EventRecordEntry.EVENT_ID_REJECT) { // The request was rejected- Notify the requester. @@ -171,8 +169,8 @@ export class ServerGame extends GameManager { } } - public processBubbleExecute(desc: Readonly): void { - super.processBubbleExecute(desc); + public executePlayerBubbleEvent(desc: Readonly): void { + super.executePlayerBubbleEvent(desc); if (desc.eventId === EventRecordEntry.EVENT_ID_REJECT) { // The request was rejected- Notify the requester. diff --git a/src/server/SnakeyServer.ts b/src/server/SnakeyServer.ts index b739deb9..824deaeb 100644 --- a/src/server/SnakeyServer.ts +++ b/src/server/SnakeyServer.ts @@ -1,7 +1,11 @@ -import * as os from "os"; -import * as http from "http"; -import * as app from "express"; -import * as io from "socket.io"; +import os = require("os"); +import path = require("path"); +import http = require("http"); +import express = require("express"); +import io = require("socket.io"); +import type * as net from "net"; + +import { SnakeyNsps } from "defs/TypeDefs"; import { GroupSession } from "./GroupSession"; @@ -12,7 +16,7 @@ import { GroupSession } from "./GroupSession"; export class SnakeyServer { protected readonly http: http.Server; - protected readonly app: app.Application; + protected readonly app: express.Application; protected readonly io: io.Server; /** @@ -28,21 +32,30 @@ export class SnakeyServer { * @param port - The port number on which to host the Server. * Defaults to {@link Defs.SERVER_PORT}. */ - public constructor(host: string, port: number = SnakeyServer.DEFAULT_PORT) { - this.app = app(); + public constructor( + port: number = SnakeyServer.DEFAULT_PORT, + host: string | undefined = undefined, + ) { + this.app = express(); this.http = http.createServer({}, this.app); this.io = io(this.http); - this.http.listen({ host, port, }, (): void => { - console.log(`Server mounted to: ${this.http.address}.`); + // At runtime, __dirname resolves to ":/dist/server/" + const PROJECT_ROOT = path.resolve(__dirname, "../.."); + this.app.disable("x-powered-by"); + this.app.get("/", (req, res) => { + res.sendFile(path.resolve(PROJECT_ROOT, "index.html")); }); + this.app.use("/dist", express.static(path.resolve(PROJECT_ROOT, "dist"))); + this.app.use("/assets", express.static(path.resolve(PROJECT_ROOT, "assets"))); - this.app.get("/", (req, res) => { - res.sendFile(`${__dirname}/../../index.html`); + this.http.listen({ port, host, }, (): void => { + const info = this.http.address(); + console.log(`Server mounted to: \`${info.family}${info.address}${info.port}\`.`); }); - this.io.of(SnakeyServer.SocketIoNamespaces.GROUP_JOINER) - .on("connection", this.onGameHostsConnection); + this.io.of(SnakeyNsps.HOST_REGISTRATION) + .on("connection", this.onHostsConnection.bind(this)); } /** @@ -51,7 +64,7 @@ export class SnakeyServer { * * @param socket - The socket from the game host. */ - protected onGameHostsConnection(socket: io.Socket): void { + protected onHostsConnection(socket: io.Socket): void { socket.on(GroupSession.CtorArgs.EVENT_NAME, (desc: GroupSession.CtorArgs): void => { // Create a new group session: const groupName = this.createUniqueSessionName(desc.groupName); @@ -94,43 +107,21 @@ export class SnakeyServer { * which happens if `groupName` is already taken, or if it * does not match the required regular expression. */ - protected createUniqueSessionName(groupName: GroupSession.SessionName): string | null { + protected createUniqueSessionName(groupName: GroupSession.SessionName): string | undefined { if (!(GroupSession.SessionName.REGEXP.test(groupName))) { - return null; + return undefined; } - const sessionName: string = `${SnakeyServer.SocketIoNamespaces.GROUP_LOBBY}/${groupName}`; + const sessionName: string = `${SnakeyNsps.GROUP_PREFIX}/${groupName}`; if (this.allGroupSessions.has(sessionName)) { - return null; + return undefined; } return sessionName; } - } - - export namespace SnakeyServer { export const DEFAULT_PORT = 8080; - /** - * Paths to pages on the site: - */ - export const PATHS = Object.freeze({ - - /** - * A global hub where clients can join or create groups - */ - GROUP_SESSIONS: "groups", - }); - - /** - * - */ - export const enum SocketIoNamespaces { - GROUP_JOINER = "/groups", - GROUP_LOBBY = "/groups", - } - /** * @returns An array of non-internal IPv4 addresses from any of the * local hosts' network interfaces. diff --git a/src/server/index.ts b/src/server/index.ts index e711c7e5..4be94309 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,4 +1,3 @@ import { SnakeyServer } from "./SnakeyServer"; -const server = new SnakeyServer("localhost"); -console.log(server); +export const server = new SnakeyServer(); diff --git a/src/tsconfig.json b/src/tsconfig.json index d5f25727..c4a8217a 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "baseUrl": "./base", // not used "rootDir": "..", - "outDir": "../dist", + "outDir": "../dist/ts", }, "files": [], "references": [ diff --git a/test/.templates/tsconfig.json b/test/.templates/tsconfig.json index 17e9cd63..e09996b2 100644 --- a/test/.templates/tsconfig.json +++ b/test/.templates/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "baseUrl": "../../src", "rootDir": "../..", - "outDir": "../../dist", + "outDir": "../../dist/ts", }, } \ No newline at end of file diff --git a/test/lang/Lang.ts b/test/lang/Lang.ts index 52246c0c..ab6de453 100644 --- a/test/lang/Lang.ts +++ b/test/lang/Lang.ts @@ -4,19 +4,20 @@ import { Korean } from "base/lang/impl/Korean"; export namespace Lang { - - // const eng = English.getInstance(); - // console.log(eng); - - // const jpnH = Japanese.Hiragana.getInstance(); - // console.log(jpnH); - - // const jpnK = Japanese.Katakana.getInstance(); - // console.log(jpnK); - - const korD = Korean.Dubeolsik.getInstance(); - korD.reset(); - console.log(korD.simpleView()); - debugger; - + // PRINT ALL THE LANGS !!! + [ + English.Lowercase, + English.MixedCase, + Japanese.Hiragana, + Japanese.Katakana, + Korean.Dubeolsik, + Korean.Sebeolsik, + Korean.Romanization, + ] + .forEach((langImpl) => { + const inst = langImpl.getInstance(); + inst.reset(); + console.log(inst.simpleView()); + debugger; + }); } diff --git a/test/tsconfig.json b/test/tsconfig.json index 7df57c4d..c8ae5303 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -4,7 +4,7 @@ "compilerOptions": { "baseUrl": "..", // not used "rootDir": "..", - "outDir": "../dist", + "outDir": "../dist/ts", }, "files": [], "references": [ diff --git a/tsconfig.json b/tsconfig.json index 2bfab858..866897a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "extends": "./.templates/tsconfig.json", "compilerOptions": { "baseUrl": ".", - "outDir": "./dist", + "outDir": "./dist/ts", }, "files": [],