Merge pull request #75 from AlecM33/develop
Initial merging of new codebase
28
.eslintrc.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"standard"
|
||||
],
|
||||
"ignorePatterns": ["/client/dist/*", "client/certs/*", "client/favicon_package/*", "client/webpack/*", "node_modules/*"],
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"requireConfigFile": false,
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"semi": [2, "always"],
|
||||
"operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before", "&&": "before", "||": "before", "+": "after" } }],
|
||||
"no-void": ["error", { "allowAsStatement": true }],
|
||||
"no-prototype-builtins": "off",
|
||||
"no-undef": "off",
|
||||
"no-return-assign": "warn",
|
||||
"prefer-promise-reject-errors": "warn",
|
||||
"no-trailing-spaces": "off"
|
||||
}
|
||||
}
|
||||
17
.gcloudignore
Normal file
@@ -0,0 +1,17 @@
|
||||
# This file specifies files that are *not* uploaded to Google Cloud
|
||||
# using gcloud. It follows the same syntax as .gitignore, with the addition of
|
||||
# "#!include" directives (which insert the entries of the given .gitignore-style
|
||||
# file at that point).
|
||||
#
|
||||
# For more information, run:
|
||||
# $ gcloud topic gcloudignore
|
||||
#
|
||||
.gcloudignore
|
||||
# If you would like to upload your .git directory, .gitignore file or files
|
||||
# from your .gitignore file, remove the corresponding line
|
||||
# below:
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Node.js dependencies:
|
||||
node_modules/
|
||||
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
.idea
|
||||
node_modules/*
|
||||
client/certs/
|
||||
client/dist/
|
||||
app.yaml
|
||||
.vscode/launch.json
|
||||
|
||||
100
README.md
@@ -1,49 +1,83 @@
|
||||
<img alt="Werewolf" src="/assets/images/roles-small/wolf_logo.png" />
|
||||
An application to run games of <a href="https://en.wikipedia.org/wiki/Mafia_(party_game)">Werewolf (Mafia)</a>
|
||||
smoothly when you don't have a deck, or when you and your friends are together virtually. Inspired by my time playing
|
||||
<a href="https://boardgamegeek.com/boardgame/152242/ultimate-werewolf-deluxe-edition">Ultimate Werewolf</a> and by
|
||||
2020's quarantine. The app is free to use and anonymous.
|
||||
|
||||
This app is still in active development. The latest deployment can be found <a href="https://play-werewolf.herokuapp.com">here</a>
|
||||
After a long hiatus from maintaining the application, I have come back and undertaken a large-scale redesign, rewriting
|
||||
most of the code and producing a result that I believe is more stable and has much more sensible client-server interaction.
|
||||
|
||||
A Werewolf utility that provides the tools to run games smoothly in the absence of a deck, or in any context in which traditional moderation is hindered.
|
||||

|
||||
|
||||
This is a Javascript application running on a node express server. I am using the socket.io package as a wrapper for Javascript Websocket. This was built from scratch as a learning project; I do not claim it as a shining example of socket programming or web app design in general. I welcome collaboration and suggestions for improvements.
|
||||
## Features
|
||||
|
||||
All pixel art is my own (for better or for worse).
|
||||
This is meant to facilitate a game through a shared game state and various utilities - not to control
|
||||
every aspect of the game flow. The app provides a host the ability to construct a deck with a custom distribution
|
||||
of roles. Players can join a game with one click and are then dealt a role to their device. The app features a concealable
|
||||
role card, an optional shared timer (which the moderator can play/pause), a reference for roles in the game, and status
|
||||
information for players including who is alive/dead and who has had their role revealed. The app also provides the
|
||||
option for a "dedicated moderator" or a "temporary moderator." Dedicated moderators will never be dealt in, and will
|
||||
have all controls and information from the beginning of the game. Temporary moderators _will_ be dealt a role and will
|
||||
have some moderator powers, but will only exist until the first player is out, at which point that player will be made
|
||||
the game's dedicated moderator. A dedicated moderator can transfer their powers to another player that is out of the
|
||||
game at any time.
|
||||
|
||||
This is meant to facilitate the game in a face-to-face social setting and provide utility/convenience - not control all aspects of the game flow. The app allows players to create or join a game lobby where state is synchronized. The creator of the game can build a deck from either the standard set of provided cards, or from any number of custom cards the user creates. Once the game begins, this deck will be randomly dealt to all participants.
|
||||
The application prioritizes responsiveness. A key scenario would be when a group is hanging out with only their phones.
|
||||
|
||||
Players will see their card (which can be flipped up and down), an optional timer, and a button to say that they have been killed off. If a player presses the button, they will be removed from the game, and their role revealed to other players. The game will continue until the end of the game is detected, or the timer expires.
|
||||
## Tech Stack
|
||||
|
||||
To learn more about this type of game, see the Wikipedia entry on the game's ancestor, <a href="https://en.wikipedia.org/wiki/Mafia_(party_game)">Mafia</a>.
|
||||
This is a Node.js application. It is written purely using JavaScript/HTML/CSS. The main dependencies are
|
||||
<a href="https://expressjs.com/">Express.js</a> and <a href="https://socket.io/">Socket.io</a>. It is fully open-source
|
||||
and under the MIT license. This was (and still is) fundamentally a learning project, and thus I welcome collaboration
|
||||
and feedback of any kind.
|
||||
|
||||
<br>
|
||||
<div>
|
||||
<img alt="home" width="200" src="/assets/images/screenshots/home.PNG" />
|
||||
<img alt="create" width="200" src="/assets/images/screenshots/create.PNG" />
|
||||
<img alt="lobby" width="200" src="/assets/images/screenshots/lobby.PNG" />
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<img alt="game" width="200" src="/assets/images/screenshots/game.PNG" />
|
||||
<img alt="killed" width="200" src="/assets/images/screenshots/killed.PNG" />
|
||||
<img alt="hunter" width="200" src="/assets/images/screenshots/hunter.PNG" />
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
All pixel art is my own, for better or for worse.
|
||||
|
||||
# Run Locally
|
||||
## Contributing and Developers' Guide
|
||||
|
||||
Run `npm install` from the root directory.
|
||||
### Running Locally
|
||||
|
||||
Run `node server.js` from the root directory, navigate to **localhost:5000**
|
||||
If you haven't already, install <a href="https://nodejs.org/en/">Node.js.</a> This should include the node package
|
||||
manager, <a href="https://www.npmjs.com/">npm</a>.
|
||||
|
||||
# Testing/Debugging
|
||||
Run `npm install` from the root directory to install the necessary dependencies.
|
||||
|
||||
Use `npm run test` to run unit tests using <a href='https://jasmine.github.io/'>Jasmine</a> (test coverage is barebones and is currently being expanded)
|
||||
<br><br>
|
||||
To turn on logging at the debug level, add the `debug` argument like so:
|
||||
These instructions assume you are somewhat familiar with Node.js and npm. At this point, we will use some of the run
|
||||
commands defined in `package.json`.
|
||||
|
||||
`node server.js -- debug`
|
||||
First, start a terminal in the root directory. Execute `npm run build:dev`. This uses <a href="https://webpack.js.org/">
|
||||
Webpack</a> to bundle javascript from the `client/src` directory and place it in the `client/dist` directory, which is ignored by Git.
|
||||
If you look at this command as defined in `package.json`, it uses the `--watch` flag, which means the process will continue
|
||||
to run in this terminal, watching for changes to JavaScript within the `client/src` directory and re-bundling automatically. You
|
||||
definitely want this if making frequent JavaScript changes to client-side source code. Any other changes, such as to HTML or CSS
|
||||
files, are not bundled, and thus your changes will be picked up simply by refreshing the browser.
|
||||
|
||||
# Contributing
|
||||
Next, in a separate terminal, we will start the application:
|
||||
|
||||
Contributions of any kind are welcome. Simply open an issue or pull request and I can respond accordingly.
|
||||
`npm run start:dev` (if developing on a linux machine)<br>
|
||||
`npm run start:dev:windows` (if developing on a windows machine)
|
||||
|
||||
This will start the application and serve it on the default port of **5000**. This command uses <a href="https://www.npmjs.com/package/nodemon">nodemon</a>
|
||||
to listen for changes to **server-side code** (Node.js modules) and automatically restart the server. If you do not want
|
||||
this, run instead `npm run start:dev:no-hot-reload` or `npm run start:dev:windows:no-hot-reload`.
|
||||
|
||||
And there we go! You should be able to navigate to and use the application on localhost. There are additional CLI arguments
|
||||
you can provide to the run commands that specify things such as port, HTTP vs HTTPS, or the log level. I **highly recommend**
|
||||
consulting these below.
|
||||
|
||||
### CLI Options
|
||||
|
||||
These options will be at the end of your run command following two dashes e.g. `npm run start:dev -- [options]`.
|
||||
Options are whitespace-delimited key-value pairs with the syntax `[key]=[value]` e.g. `port=4242`. Options include:
|
||||
|
||||
- `port`. Specify an integer port for the application.
|
||||
- `loglevel` the log level for the application. Can be `info`, `error`, `warn`, `debug`, or `trace`.
|
||||
- `protocol` either HTTP or HTTPS. If you specify HTTPS, the server will look in `client/certs` for localhost certificates
|
||||
before serving the application over HTTPS - otherwise it will revert to HTTP. Using HTTPS is particularly useful if you
|
||||
want to make the application public on your home network, which would allow you to test it on your mobile device. **Careful -
|
||||
I had to disable my computer's firewall for this to work, which would of course make browsing the internet much riskier.**
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests are written using <a href="https://jasmine.github.io/">Jasmine</a>. Execute them by running `npm run test`.
|
||||
They reside in the `spec/unit` directory, which maps 1:1 to the application directory structure - i.e. unit tests for
|
||||
`server/modules/GameManager` are found in `spec/unit/server/modules/GameManager_Spec.js`
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
theme: jekyll-theme-minimal
|
||||
logo: /assets/images/roles-small/wolf_logo.png
|
||||
19
app.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
runtime: nodejs
|
||||
env: flex
|
||||
network:
|
||||
session_affinity: true
|
||||
liveness_check:
|
||||
path: "/liveness_check"
|
||||
check_interval_sec: 60
|
||||
timeout_sec: 4
|
||||
failure_threshold: 2
|
||||
success_threshold: 2
|
||||
readiness_check:
|
||||
path: "/readiness_check"
|
||||
check_interval_sec: 60
|
||||
timeout_sec: 4
|
||||
failure_threshold: 2
|
||||
success_threshold: 2
|
||||
app_start_timeout_sec: 600
|
||||
manual_scaling:
|
||||
instances: 1
|
||||
@@ -1,90 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="123.22025mm"
|
||||
height="123.59821mm"
|
||||
viewBox="0 0 123.22025 123.59821"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
sodipodi:docname="gallery.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-228.27896"
|
||||
inkscape:cy="141.42857"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-43.389879,-62.654773)">
|
||||
<g
|
||||
id="g848"
|
||||
transform="translate(8.2381001,13.607143)"
|
||||
style="stroke:none">
|
||||
<rect
|
||||
y="49.04763"
|
||||
x="35.151779"
|
||||
height="53.672619"
|
||||
width="53.672619"
|
||||
id="rect815"
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
<rect
|
||||
y="49.04763"
|
||||
x="104.6994"
|
||||
height="53.672619"
|
||||
width="53.672619"
|
||||
id="rect815-7"
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
<rect
|
||||
y="118.97321"
|
||||
x="35.151779"
|
||||
height="53.672619"
|
||||
width="53.672619"
|
||||
id="rect815-1"
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
<rect
|
||||
y="118.97321"
|
||||
x="104.6994"
|
||||
height="53.672619"
|
||||
width="53.672619"
|
||||
id="rect815-5"
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>
|
||||
|
Before Width: | Height: | Size: 344 B |
@@ -1,14 +0,0 @@
|
||||
<svg width="223" height="223" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="225" width="225" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<path id="svg_1" d="m111.499982,3.500023c-59.646658,0 -107.999967,48.353386 -107.999967,107.999967c0,59.647903 48.353308,107.999993 107.999967,107.999993c59.647946,0 107.999993,-48.352082 107.999993,-107.999993c0,-59.646581 -48.352047,-107.999967 -107.999993,-107.999967zm-9.236728,48.553238l17.983007,0l0,19.128161l-17.983007,0l0,-19.128161zm29.760957,116.085726l-19.780144,0c-7.684691,0 -10.961591,-3.269729 -10.961591,-11.117474l0,-51.012233c0,-2.452006 -1.306774,-3.597168 -3.595768,-3.597168l-6.539459,0l0,-17.662679l19.781535,0c7.690393,0 10.953072,3.432714 10.953072,11.116049l0,51.176601c0,2.296157 1.306782,3.597237 3.595768,3.597237l6.539537,0l0,17.499659l0.00705,0l0,0.000009z" stroke-width="1.5" stroke="#000" fill="#bbb8b8"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,109 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="123.22025mm"
|
||||
height="123.59821mm"
|
||||
viewBox="0 0 123.22025 123.59821"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
sodipodi:docname="list.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-228.27896"
|
||||
inkscape:cy="141.42857"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-43.389879,-62.654773)">
|
||||
<circle
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path875"
|
||||
cx="58.697918"
|
||||
cy="82.385117"
|
||||
r="5.2916665" />
|
||||
<circle
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path875-4"
|
||||
cx="58.697918"
|
||||
cy="111.11131"
|
||||
r="5.2916665" />
|
||||
<circle
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path875-8"
|
||||
cx="58.697918"
|
||||
cy="139.23279"
|
||||
r="5.2916665" />
|
||||
<circle
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path875-6"
|
||||
cx="58.697918"
|
||||
cy="166.52264"
|
||||
r="5.2916665" />
|
||||
<rect
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect904"
|
||||
width="85.422623"
|
||||
height="10.583333"
|
||||
x="71.171135"
|
||||
y="77.093452" />
|
||||
<rect
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect904-4"
|
||||
width="85.422623"
|
||||
height="10.583333"
|
||||
x="71.171135"
|
||||
y="161.23097" />
|
||||
<rect
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect904-43"
|
||||
width="85.422623"
|
||||
height="10.583333"
|
||||
x="71.171135"
|
||||
y="133.94112" />
|
||||
<rect
|
||||
style="opacity:0.95999995;fill:whitesmoke;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect904-3"
|
||||
width="85.422623"
|
||||
height="10.583333"
|
||||
x="71.171135"
|
||||
y="105.81964" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="263" height="271" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<ellipse stroke="gray" ry="131" rx="126" id="svg_5" fill="none" cy="135.237498" cx="131.999999" stroke-opacity="null" stroke-width="8"/>
|
||||
<rect stroke="#7d0b0b" id="svg_1" height="123.000006" width="41" y="73.737494" x="77.5" stroke-width="0" fill="gray"/>
|
||||
<rect stroke="#7d0b0b" id="svg_3" height="123.000006" width="41" y="73.737494" x="144.5" stroke-width="0" fill="gray"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 491 B |
@@ -1,14 +0,0 @@
|
||||
<svg width="172" height="172" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="174" width="174" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<path id="svg_1" d="m139.382578,14.249727l-18.982996,-11.931355c-4.78947,-3.007365 -11.100429,-1.559921 -14.118291,3.230549l-7.482272,11.909092l36.315275,22.808817l7.488173,-11.903564c3.006409,-4.796172 1.57156,-11.112875 -3.219889,-14.113539l0,0zm-105.206717,106.097424l36.317254,22.808582l59.191104,-94.22748l-36.334543,-22.814538l-59.173821,94.233429l0.000006,0.000006zm-5.547453,28.979153l-0.802118,21.42371l18.953876,-10.02532l17.613535,-9.299898l-35.036418,-22.018086l-0.728868,19.9196l0,0l-0.000006,-0.000006z" stroke-width="1.5" stroke="none" fill="green"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg width="232" height="232" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="234" width="234" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<path id="svg_2" d="m116.249977,0.749996c-63.791862,0 -115.499977,51.711165 -115.499977,115.500022c0,63.792136 51.708115,115.499968 115.499977,115.499968c63.788876,0 115.500013,-51.707832 115.500013,-115.499968c0,-63.788858 -51.711128,-115.500022 -115.500013,-115.500022zm10.552801,182.023345l-21.511705,0l0,-20.629285l21.511705,0l0,20.629285zm0,-42.656193l0,6.817814l-21.511705,0l0,-8.400447c0,-25.349282 28.847653,-29.371968 28.847653,-47.388589c0,-8.215218 -7.345234,-14.508297 -16.96127,-14.508297c-9.964527,0 -18.704243,7.34212 -18.704243,7.34212l-12.247236,-15.218234c0,0 12.071466,-12.589655 32.876448,-12.589655c19.762633,0 38.112195,12.238096 38.112195,32.870404c0.009222,28.862919 -30.411843,32.191492 -30.411843,51.074874l0,0.000009z" stroke-width="3" stroke="whitesmoke" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 753 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 871 B |
|
Before Width: | Height: | Size: 824 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1012 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 24 KiB |
BIN
client/favicon_package/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
client/favicon_package/android-chrome-256x256.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
client/favicon_package/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
9
client/favicon_package/browserconfig.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
client/favicon_package/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
client/favicon_package/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
client/favicon_package/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
client/favicon_package/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
54
client/favicon_package/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="380.000000pt" height="380.000000pt" viewBox="0 0 380.000000 380.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,380.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1790 3160 c0 -18 -7 -20 -60 -20 -53 0 -60 -2 -60 -20 0 -18 -7 -20
|
||||
-80 -20 -73 0 -80 -2 -80 -20 0 -18 -7 -20 -60 -20 -53 0 -60 -2 -60 -20 0
|
||||
-17 -7 -20 -40 -20 -33 0 -40 -3 -40 -20 0 -17 -7 -20 -40 -20 l-40 0 0 -40 0
|
||||
-40 -40 0 c-33 0 -40 3 -40 20 0 18 -7 20 -60 20 -53 0 -60 2 -60 20 0 18 -7
|
||||
20 -60 20 -53 0 -60 2 -60 20 0 18 -7 20 -60 20 -53 0 -60 2 -60 20 0 13 -7
|
||||
20 -20 20 -13 0 -20 7 -20 20 0 18 -7 20 -60 20 -53 0 -60 -2 -60 -20 0 -17
|
||||
-7 -20 -40 -20 l-40 0 0 -160 c0 -153 1 -160 20 -160 13 0 20 -7 20 -20 0 -13
|
||||
7 -20 20 -20 18 0 20 -7 20 -60 0 -53 2 -60 20 -60 17 0 20 -7 20 -40 0 -33 3
|
||||
-40 20 -40 17 0 20 -7 20 -40 0 -33 3 -40 20 -40 17 0 20 -7 20 -40 0 -33 -3
|
||||
-40 -20 -40 -13 0 -20 -7 -20 -20 0 -13 -7 -20 -20 -20 -18 0 -20 -7 -20 -80
|
||||
0 -73 -2 -80 -20 -80 -19 0 -20 -7 -20 -160 0 -153 1 -160 20 -160 19 0 20 -7
|
||||
20 -100 0 -93 1 -100 20 -100 19 0 20 -7 20 -140 0 -133 1 -140 20 -140 13 0
|
||||
20 -7 20 -20 0 -13 7 -20 20 -20 17 0 20 -7 20 -40 l0 -40 40 0 c33 0 40 -3
|
||||
40 -20 0 -13 7 -20 20 -20 13 0 20 -7 20 -20 0 -13 7 -20 20 -20 13 0 20 -7
|
||||
20 -20 0 -17 7 -20 40 -20 33 0 40 -3 40 -20 0 -18 7 -20 80 -20 l80 0 0 -40
|
||||
c0 -33 3 -40 20 -40 17 0 20 -7 20 -40 l0 -40 80 0 80 0 0 -40 0 -40 40 0 40
|
||||
0 0 -40 c0 -33 3 -40 20 -40 13 0 20 -7 20 -20 0 -13 7 -20 20 -20 13 0 20 -7
|
||||
20 -20 0 -18 7 -20 60 -20 53 0 60 -2 60 -20 0 -13 7 -20 20 -20 17 0 20 -7
|
||||
20 -40 l0 -40 40 0 c33 0 40 -3 40 -20 0 -13 7 -20 20 -20 17 0 20 -7 20 -40
|
||||
0 -33 3 -40 20 -40 13 0 20 -7 20 -20 0 -17 7 -20 40 -20 l40 0 0 40 0 40 40
|
||||
0 40 0 0 40 0 40 40 0 c33 0 40 3 40 20 0 17 7 20 40 20 l40 0 0 40 0 40 40 0
|
||||
c33 0 40 3 40 20 0 13 7 20 20 20 13 0 20 7 20 20 0 18 7 20 60 20 53 0 60 2
|
||||
60 20 0 13 7 20 20 20 17 0 20 7 20 40 l0 40 40 0 c33 0 40 3 40 20 0 13 7 20
|
||||
20 20 13 0 20 7 20 20 0 18 7 20 60 20 53 0 60 2 60 20 0 13 7 20 20 20 13 0
|
||||
20 7 20 20 0 13 7 20 20 20 13 0 20 7 20 20 0 17 7 20 40 20 33 0 40 3 40 20
|
||||
0 13 7 20 20 20 18 0 20 7 20 60 0 53 2 60 20 60 17 0 20 7 20 40 0 33 3 40
|
||||
20 40 13 0 20 7 20 20 0 13 7 20 20 20 17 0 20 7 20 40 0 33 3 40 20 40 18 0
|
||||
20 7 20 80 0 73 2 80 20 80 19 0 20 7 20 100 0 93 1 100 20 100 13 0 20 7 20
|
||||
20 0 13 7 20 20 20 19 0 20 7 20 120 0 113 -1 120 -20 120 -19 0 -20 7 -20
|
||||
100 0 93 -1 100 -20 100 -17 0 -20 7 -20 40 l0 40 -40 0 c-33 0 -40 3 -40 20
|
||||
0 13 7 20 20 20 13 0 20 7 20 20 0 13 7 20 20 20 17 0 20 7 20 40 0 33 3 40
|
||||
20 40 13 0 20 7 20 20 0 13 7 20 20 20 17 0 20 7 20 40 0 33 3 40 20 40 17 0
|
||||
20 7 20 40 0 33 3 40 20 40 19 0 20 7 20 120 0 113 -1 120 -20 120 -18 0 -20
|
||||
7 -20 60 0 53 -2 60 -20 60 -13 0 -20 7 -20 20 0 18 -7 20 -80 20 -73 0 -80
|
||||
-2 -80 -20 0 -17 -7 -20 -40 -20 -33 0 -40 -3 -40 -20 0 -17 -7 -20 -40 -20
|
||||
-33 0 -40 -3 -40 -20 0 -17 -7 -20 -40 -20 -33 0 -40 -3 -40 -20 0 -18 -7 -20
|
||||
-60 -20 -53 0 -60 -2 -60 -20 0 -18 -7 -20 -80 -20 l-80 0 0 40 0 40 -40 0
|
||||
c-33 0 -40 3 -40 20 0 17 -7 20 -40 20 -33 0 -40 3 -40 20 0 13 -7 20 -20 20
|
||||
-13 0 -20 7 -20 20 0 13 -7 20 -20 20 -13 0 -20 7 -20 20 0 18 -7 20 -80 20
|
||||
-73 0 -80 2 -80 20 0 18 -7 20 -60 20 -53 0 -60 -2 -60 -20 0 -17 -7 -20 -40
|
||||
-20 -33 0 -40 3 -40 20 0 18 -7 20 -60 20 -53 0 -60 -2 -60 -20z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
19
client/favicon_package/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
2
client/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /api/
|
||||
47
client/src/config/defaultCards.js
Normal file
@@ -0,0 +1,47 @@
|
||||
export const defaultCards = [
|
||||
{
|
||||
role: 'Villager',
|
||||
team: 'good',
|
||||
description: 'During the day, find the wolves and kill them.'
|
||||
},
|
||||
{
|
||||
role: 'Werewolf',
|
||||
team: 'evil',
|
||||
description: "During the night, choose a villager to kill. Don't get killed."
|
||||
},
|
||||
{
|
||||
role: 'Dream Wolf',
|
||||
team: 'evil',
|
||||
description: "You are a Werewolf, but you don't wake up with the other Werewolves until one of them dies."
|
||||
},
|
||||
{
|
||||
role: 'Sorceress',
|
||||
team: 'evil',
|
||||
description: 'Each night, learn if a chosen person is the Seer.'
|
||||
},
|
||||
{
|
||||
role: 'Minion',
|
||||
team: 'evil',
|
||||
description: 'You are an evil Villager, and you know who the Werewolves are.'
|
||||
},
|
||||
{
|
||||
role: 'Blind Minion',
|
||||
team: 'evil',
|
||||
description: "You are an evil villager, but you don't know who the Werewolves are."
|
||||
},
|
||||
{
|
||||
role: 'Seer',
|
||||
team: 'good',
|
||||
description: 'Each night, learn if a chosen person is a Werewolf.'
|
||||
},
|
||||
{
|
||||
role: 'Parity Hunter',
|
||||
team: 'good',
|
||||
description: 'You beat a werewolf in a 1v1 situation, winning the game for the village.'
|
||||
},
|
||||
{
|
||||
role: 'Hunter',
|
||||
team: 'good',
|
||||
description: 'When you are eliminated, choose another player to go with you.'
|
||||
}
|
||||
];
|
||||
64
client/src/config/globals.js
Normal file
@@ -0,0 +1,64 @@
|
||||
export const globals = {
|
||||
USER_SIGNATURE_LENGTH: 25,
|
||||
CLOCK_TICK_INTERVAL_MILLIS: 10,
|
||||
MAX_CUSTOM_ROLE_NAME_LENGTH: 30,
|
||||
MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 500,
|
||||
TOAST_DURATION_DEFAULT: 6,
|
||||
ACCESS_CODE_LENGTH: 6,
|
||||
PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id',
|
||||
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
|
||||
COMMANDS: {
|
||||
FETCH_GAME_STATE: 'fetchGameState',
|
||||
GET_ENVIRONMENT: 'getEnvironment',
|
||||
START_GAME: 'startGame',
|
||||
PAUSE_TIMER: 'pauseTimer',
|
||||
RESUME_TIMER: 'resumeTimer',
|
||||
GET_TIME_REMAINING: 'getTimeRemaining',
|
||||
KILL_PLAYER: 'killPlayer',
|
||||
REVEAL_PLAYER: 'revealPlayer',
|
||||
TRANSFER_MODERATOR: 'transferModerator',
|
||||
CHANGE_NAME: 'changeName',
|
||||
END_GAME: 'endGame',
|
||||
FETCH_IN_PROGRESS_STATE: 'fetchInitialInProgressState'
|
||||
},
|
||||
STATUS: {
|
||||
LOBBY: 'lobby',
|
||||
IN_PROGRESS: 'in progress',
|
||||
ENDED: 'ended'
|
||||
},
|
||||
ALIGNMENT: {
|
||||
GOOD: 'good',
|
||||
EVIL: 'evil'
|
||||
},
|
||||
MESSAGES: {
|
||||
ENTER_NAME: 'Client must enter name.'
|
||||
},
|
||||
EVENTS: {
|
||||
PLAYER_JOINED: 'playerJoined',
|
||||
SYNC_GAME_STATE: 'syncGameState',
|
||||
START_TIMER: 'startTimer',
|
||||
KILL_PLAYER: 'killPlayer',
|
||||
REVEAL_PLAYER: 'revealPlayer',
|
||||
CHANGE_NAME: 'changeName',
|
||||
START_GAME: 'startGame',
|
||||
PLAYER_LEFT: 'playerLeft'
|
||||
},
|
||||
USER_TYPES: {
|
||||
MODERATOR: 'moderator',
|
||||
PLAYER: 'player',
|
||||
TEMPORARY_MODERATOR: 'player / temp mod',
|
||||
KILLED_PLAYER: 'killed',
|
||||
SPECTATOR: 'spectator'
|
||||
},
|
||||
ENVIRONMENT: {
|
||||
LOCAL: 'local',
|
||||
PRODUCTION: 'production'
|
||||
},
|
||||
USER_TYPE_ICONS: {
|
||||
player: ' \uD83C\uDFAE',
|
||||
moderator: ' \uD83D\uDC51',
|
||||
'player / temp mod': ' \uD83C\uDFAE\uD83D\uDC51',
|
||||
spectator: ' \uD83D\uDC7B',
|
||||
killed: '\uD83D\uDC80'
|
||||
}
|
||||
};
|
||||
BIN
client/src/images/GitHub-Mark-32px.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
client/src/images/GitHub-Mark-Light-120px-plus.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
client/src/images/Werewolf_Small.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
1
client/src/images/add.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="#00a718" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="384px" height="384px"><path d="M 12 2 C 6.4889971 2 2 6.4889971 2 12 C 2 17.511003 6.4889971 22 12 22 C 17.511003 22 22 17.511003 22 12 C 22 6.4889971 17.511003 2 12 2 z M 12 4 C 16.430123 4 20 7.5698774 20 12 C 20 16.430123 16.430123 20 12 20 C 7.5698774 20 4 16.430123 4 12 C 4 7.5698774 7.5698774 4 12 4 z M 11 7 L 11 11 L 7 11 L 7 13 L 11 13 L 11 17 L 13 17 L 13 13 L 17 13 L 17 11 L 13 11 L 13 7 L 11 7 z"/></svg>
|
||||
|
After Width: | Height: | Size: 502 B |
14
client/src/images/clock.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="158" height="155" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="157" width="160" y="-1" x="-1"/>
|
||||
<g display="none" id="canvasGrid">
|
||||
<rect fill="none" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<path stroke="none" id="svg_1" d="m78.750021,0.749998c-43.078721,0 -78.000016,34.473574 -78.000016,76.999988c0,42.526414 34.921295,76.999988 78.000016,76.999988c43.078721,0 78.000016,-34.473574 78.000016,-76.999988c0,-42.526414 -34.921295,-76.999988 -78.000016,-76.999988zm0,138.100913c-34.137753,0 -61.899834,-27.411445 -61.899834,-61.100926c0,-33.694779 27.762081,-61.106224 61.899834,-61.106224c34.137753,0 61.894467,27.406147 61.894467,61.106224c0,33.700077 -27.767448,61.100926 -61.894467,61.100926zm-0.005367,-118.71582c-2.9678,0 -5.361361,2.373469 -5.361361,5.297921l0,48.852132l-32.447234,14.007704c-2.715564,1.176139 -3.949911,4.301912 -2.758498,6.98266c0.880143,1.98672 2.844366,3.173455 4.910556,3.173455c0.719141,0 1.454383,-0.143044 2.152058,-0.450323l35.559936,-15.353376c0.026834,-0.010596 0.048301,-0.021192 0.069767,-0.031788l0.021467,-0.010596c0.080501,-0.031788 0.123435,-0.105958 0.198569,-0.132448c0.55814,-0.275492 1.078712,-0.598665 1.497317,-1.033095c0.182469,-0.180129 0.284437,-0.413238 0.423971,-0.619857c0.257603,-0.339067 0.542039,-0.672836 0.697675,-1.080776c0.128801,-0.317875 0.139535,-0.66224 0.203936,-1.001307c0.069767,-0.344365 0.203936,-0.646346 0.203936,-0.990711l0,-52.316972c0,-2.919155 -2.404294,-5.292623 -5.372094,-5.292623z" stroke-width="1.5" fill="#d7d7d7"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
210
client/src/images/copy.svg
Normal file
@@ -0,0 +1,210 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="121.00714mm"
|
||||
height="144.81964mm"
|
||||
viewBox="0 0 121.00714 144.81964"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)"
|
||||
sodipodi:docname="copy.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
effect="powerclip"
|
||||
id="path-effect951"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
inverse="true"
|
||||
flatten="false"
|
||||
hide_clip="false"
|
||||
message="Use fill-rule evenodd on <b>fill and stroke</b> dialog if no flatten result after convert clip to paths." />
|
||||
<inkscape:path-effect
|
||||
effect="powerclip"
|
||||
id="path-effect937"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
inverse="true"
|
||||
flatten="false"
|
||||
hide_clip="false"
|
||||
message="Use fill-rule evenodd on <b>fill and stroke</b> dialog if no flatten result after convert clip to paths." />
|
||||
<inkscape:path-effect
|
||||
effect="powerclip"
|
||||
id="path-effect923"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
inverse="true"
|
||||
flatten="false"
|
||||
hide_clip="false"
|
||||
message="Use fill-rule evenodd on <b>fill and stroke</b> dialog if no flatten result after convert clip to paths." />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath913">
|
||||
<g
|
||||
id="g921"
|
||||
style="display:none">
|
||||
<path
|
||||
id="path915"
|
||||
style="opacity:0.996;fill:#f9f9f9;fill-opacity:1;stroke:whitesmoke;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 109.61309,67.279762 h 52.91667 c 2.51278,0 4.53571,2.022929 4.53571,4.535714 0,2.512786 -2.02293,4.535715 -4.53571,4.535715 h -52.91667 c -2.51278,0 -4.53571,-2.022929 -4.53571,-4.535715 0,-2.512785 2.02293,-4.535714 4.53571,-4.535714 z" />
|
||||
<path
|
||||
id="path917"
|
||||
style="opacity:0.996;fill:#f9f9f9;fill-opacity:1;stroke:whitesmoke;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 109.12738,83.424988 h 52.91667 c 2.51278,0 4.53571,2.022928 4.53571,4.535714 0,2.512786 -2.02293,4.535714 -4.53571,4.535714 h -52.91667 c -2.51278,0 -4.53571,-2.022928 -4.53571,-4.535714 0,-2.512786 2.02293,-4.535714 4.53571,-4.535714 z" />
|
||||
<path
|
||||
id="path919"
|
||||
style="opacity:0.996;fill:#f9f9f9;fill-opacity:1;stroke:whitesmoke;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 109.12738,99.375595 h 52.91667 c 2.51278,0 4.53571,2.022925 4.53571,4.535715 0,2.51278 -2.02293,4.53571 -4.53571,4.53571 h -52.91667 c -2.51278,0 -4.53571,-2.02293 -4.53571,-4.53571 0,-2.51279 2.02293,-4.535715 4.53571,-4.535715 z" />
|
||||
</g>
|
||||
<path
|
||||
id="lpe_path-effect923"
|
||||
class="powerclip"
|
||||
d="M 55.205951,21.944047 H 156.46071 V 151.16905 H 55.205951 Z m 54.407139,45.335715 c -2.51278,0 -4.53571,2.022929 -4.53571,4.535714 0,2.512786 2.02293,4.535715 4.53571,4.535715 h 52.91667 c 2.51278,0 4.53571,-2.022929 4.53571,-4.535715 0,-2.512785 -2.02293,-4.535714 -4.53571,-4.535714 z m -0.48571,16.145226 c -2.51278,0 -4.53571,2.022928 -4.53571,4.535714 0,2.512786 2.02293,4.535714 4.53571,4.535714 h 52.91667 c 2.51278,0 4.53571,-2.022928 4.53571,-4.535714 0,-2.512786 -2.02293,-4.535714 -4.53571,-4.535714 z m 0,15.950607 c -2.51278,0 -4.53571,2.022925 -4.53571,4.535715 0,2.51278 2.02293,4.53571 4.53571,4.53571 h 52.91667 c 2.51278,0 4.53571,-2.02293 4.53571,-4.53571 0,-2.51279 -2.02293,-4.535715 -4.53571,-4.535715 z" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath927">
|
||||
<g
|
||||
id="g935"
|
||||
transform="translate(0.24285867,0.02228802)"
|
||||
style="display:none">
|
||||
<path
|
||||
id="path929"
|
||||
style="opacity:0.996;fill:#f9f9f9;fill-opacity:1;stroke:whitesmoke;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 109.61309,67.279762 h 52.91667 c 2.51278,0 4.53571,2.022929 4.53571,4.535714 0,2.512786 -2.02293,4.535715 -4.53571,4.535715 h -52.91667 c -2.51278,0 -4.53571,-2.022929 -4.53571,-4.535715 0,-2.512785 2.02293,-4.535714 4.53571,-4.535714 z" />
|
||||
<path
|
||||
id="path931"
|
||||
style="opacity:0.996;fill:#f9f9f9;fill-opacity:1;stroke:whitesmoke;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 109.12738,83.424988 h 52.91667 c 2.51278,0 4.53571,2.022928 4.53571,4.535714 0,2.512786 -2.02293,4.535714 -4.53571,4.535714 h -52.91667 c -2.51278,0 -4.53571,-2.022928 -4.53571,-4.535714 0,-2.512786 2.02293,-4.535714 4.53571,-4.535714 z" />
|
||||
<path
|
||||
id="path933"
|
||||
style="opacity:0.996;fill:#f9f9f9;fill-opacity:1;stroke:whitesmoke;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 109.12738,99.375595 h 52.91667 c 2.51278,0 4.53571,2.022925 4.53571,4.535715 0,2.51278 -2.02293,4.53571 -4.53571,4.53571 h -52.91667 c -2.51278,0 -4.53571,-2.02293 -4.53571,-4.53571 0,-2.51279 2.02293,-4.535715 4.53571,-4.535715 z" />
|
||||
</g>
|
||||
<path
|
||||
id="lpe_path-effect937"
|
||||
class="powerclip"
|
||||
d="M 84.958331,47.538688 H 186.21309 V 176.76369 H 84.958331 Z m 24.654759,19.741074 c -2.51278,0 -4.53571,2.022929 -4.53571,4.535714 0,2.512786 2.02293,4.535715 4.53571,4.535715 h 52.91667 c 2.51278,0 4.53571,-2.022929 4.53571,-4.535715 0,-2.512785 -2.02293,-4.535714 -4.53571,-4.535714 z m -0.48571,16.145226 c -2.51278,0 -4.53571,2.022928 -4.53571,4.535714 0,2.512786 2.02293,4.535714 4.53571,4.535714 h 52.91667 c 2.51278,0 4.53571,-2.022928 4.53571,-4.535714 0,-2.512786 -2.02293,-4.535714 -4.53571,-4.535714 z m 0,15.950607 c -2.51278,0 -4.53571,2.022925 -4.53571,4.535715 0,2.51278 2.02293,4.53571 4.53571,4.53571 h 52.91667 c 2.51278,0 4.53571,-2.02293 4.53571,-4.53571 0,-2.51279 -2.02293,-4.535715 -4.53571,-4.535715 z" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath941">
|
||||
<g
|
||||
id="g949"
|
||||
style="display:none">
|
||||
<rect
|
||||
style="opacity:0.996;fill:whitesmoke;stroke:#a5ccd1;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect943"
|
||||
width="39.498516"
|
||||
height="11.483695"
|
||||
x="116.32217"
|
||||
y="66.357109"
|
||||
d="m 116.32217,66.357109 h 39.49851 v 11.483695 h -39.49851 z" />
|
||||
<rect
|
||||
style="opacity:0.996;fill:whitesmoke;stroke:#a5ccd1;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect945"
|
||||
width="39.498516"
|
||||
height="11.483695"
|
||||
x="115.93681"
|
||||
y="82.326607"
|
||||
d="m 115.93681,82.326607 h 39.49852 v 11.483695 h -39.49852 z" />
|
||||
<rect
|
||||
style="opacity:0.996;fill:whitesmoke;stroke:#a5ccd1;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect947"
|
||||
width="39.498516"
|
||||
height="11.483695"
|
||||
x="115.91958"
|
||||
y="98.25666"
|
||||
d="m 115.91958,98.25666 h 39.49851 v 11.4837 h -39.49851 z" />
|
||||
</g>
|
||||
<path
|
||||
id="lpe_path-effect951"
|
||||
class="powerclip"
|
||||
d="M 55.205951,21.944047 H 156.46071 V 151.16905 H 55.205951 Z m 61.116219,44.413062 v 11.483695 h 39.49851 V 66.357109 Z m -0.38536,15.969498 v 11.483695 h 39.49852 V 82.326607 Z m -0.0172,15.930053 v 11.4837 h 39.49851 v -11.4837 z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="229.70018"
|
||||
inkscape:cy="291.6465"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="g862"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:snap-bbox-midpoints="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-60.205951,-26.944047)">
|
||||
<g
|
||||
id="g862">
|
||||
<g
|
||||
id="g870">
|
||||
<path
|
||||
id="rect833"
|
||||
style="opacity:0.996;fill:whitesmoke;fill-opacity:0;stroke:whitesmoke;stroke-width:8.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 77.863093,30.994047 h 55.940477 c 7.53836,0 13.60714,6.068786 13.60714,13.607143 v 83.91071 c 0,7.53836 -6.06878,13.60715 -13.60714,13.60715 H 77.863093 c -7.538357,0 -13.607142,-6.06879 -13.607142,-13.60715 V 44.60119 c 0,-7.538357 6.068785,-13.607143 13.607142,-13.607143 z"
|
||||
clip-path="url(#clipPath941)"
|
||||
inkscape:original-d="m 77.863093,30.994047 h 55.940477 c 7.53836,0 13.60714,6.068786 13.60714,13.607143 v 83.91071 c 0,7.53836 -6.06878,13.60715 -13.60714,13.60715 H 77.863093 c -7.538357,0 -13.607142,-6.06879 -13.607142,-13.60715 V 44.60119 c 0,-7.538357 6.068785,-13.607143 13.607142,-13.607143 z"
|
||||
inkscape:path-effect="#path-effect923;#path-effect951" />
|
||||
<path
|
||||
id="rect833-1"
|
||||
style="opacity:0.996;fill:whitesmoke;fill-opacity:1;stroke:whitesmoke;stroke-width:8.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 107.61547,56.588688 h 55.94048 c 7.53836,0 13.60714,6.068785 13.60714,13.607142 v 83.91072 c 0,7.53835 -6.06878,13.60714 -13.60714,13.60714 h -55.94048 c -7.53835,0 -13.607139,-6.06879 -13.607139,-13.60714 V 70.19583 c 0,-7.538357 6.068789,-13.607142 13.607139,-13.607142 z"
|
||||
clip-path="url(#clipPath927)"
|
||||
inkscape:original-d="m 107.61547,56.588688 h 55.94048 c 7.53836,0 13.60714,6.068785 13.60714,13.607142 v 83.91072 c 0,7.53835 -6.06878,13.60714 -13.60714,13.60714 h -55.94048 c -7.53835,0 -13.607139,-6.06879 -13.607139,-13.60714 V 70.19583 c 0,-7.538357 6.068789,-13.607142 13.607139,-13.607142 z"
|
||||
inkscape:path-effect="#path-effect937" />
|
||||
<path
|
||||
style="opacity:0.996;fill:#cfced2;fill-opacity:0;stroke:whitesmoke;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 181.02138,183.03773 c -5.53501,-2.80025 -7.14286,-5.82668 -7.14286,-13.44493 0,-16.70234 2.35705,-17.05861 112.85714,-17.05861 110.5001,0 112.85715,0.35627 112.85715,17.05861 0,16.70235 -2.35705,17.05862 -112.85715,17.05862 -73.8022,0 -100.3663,-0.90805 -105.71428,-3.61369 z"
|
||||
id="path953"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,60.205951,26.944047)" />
|
||||
<path
|
||||
style="opacity:0.996;fill:#cfced2;fill-opacity:0;stroke:whitesmoke;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 174.32134,241.51069 c -5.9592,-6.58484 -6.06152,-15.89102 -0.23874,-21.7138 3.92103,-3.92103 17.98077,-4.4898 110.98575,-4.4898 99.543,0 106.80467,0.34113 111.22449,5.22497 6.28388,6.94361 5.96667,13.53689 -0.98575,20.48931 -5.5075,5.5075 -9.52381,5.71429 -110.98575,5.71429 -98.35673,0 -105.58205,-0.3432 -110,-5.22497 z"
|
||||
id="path955"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,60.205951,26.944047)" />
|
||||
<path
|
||||
style="opacity:0.996;fill:#cfced2;fill-opacity:0;stroke:whitesmoke;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 174.32134,301.51069 c -6.21551,-6.86807 -5.99825,-15.59745 0.53536,-21.51029 4.74851,-4.29734 15.71793,-4.69038 112.03415,-4.01425 l 106.77025,0.74951 4.22446,7.52791 c 3.7896,6.753 3.72048,8.29708 -0.67144,15 l -4.8959,7.47209 H 285.68405 c -99.6769,0 -106.94268,-0.3409 -111.36271,-5.22497 z"
|
||||
id="path957"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,60.205951,26.944047)" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -9,6 +9,6 @@
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<path id="svg_1" d="m0.749996,50.93695l50.18695,-50.18695l53.312819,53.312382l53.312819,-53.312382l50.187422,50.18695l-53.312833,53.312819l53.312833,53.312819l-50.187422,50.187422l-53.312819,-53.312833l-53.312819,53.312833l-50.18695,-50.187422l53.312382,-53.312819l-53.312382,-53.312819z" stroke-width="1.5" stroke="none" fill="red"/>
|
||||
<path id="svg_1" d="m0.749996,50.93695l50.18695,-50.18695l53.312819,53.312382l53.312819,-53.312382l50.187422,50.18695l-53.312833,53.312819l53.312833,53.312819l-50.187422,50.187422l-53.312819,-53.312833l-53.312819,53.312833l-50.18695,-50.187422l53.312382,-53.312819l-53.312382,-53.312819z" stroke-width="0" stroke="none" fill="#e73333"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 840 B After Width: | Height: | Size: 842 B |
1
client/src/images/email.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" width="24" height="24" viewBox="0 0 24 24"><path d="M12 .02c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm6.99 6.98l-6.99 5.666-6.991-5.666h13.981zm.01 10h-14v-8.505l7 5.673 7-5.672v8.504z"/></svg>
|
||||
|
After Width: | Height: | Size: 274 B |
3
client/src/images/eye.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="#d7d7d7" d="M15 12c0 1.654-1.346 3-3 3s-3-1.346-3-3 1.346-3 3-3 3 1.346 3 3zm9-.449s-4.252 8.449-11.985 8.449c-7.18 0-12.015-8.449-12.015-8.449s4.446-7.551 12.015-7.551c7.694 0 11.985 7.551 11.985 7.551zm-7 .449c0-2.757-2.243-5-5-5s-5 2.243-5 5 2.243 5 5 5 5-2.243 5-5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 380 B |
1
client/src/images/info.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0"?><svg fill="#d7d7d7" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="384px" height="384px"> <path d="M 12 2 C 6.4889971 2 2 6.4889971 2 12 C 2 17.511003 6.4889971 22 12 22 C 17.511003 22 22 17.511003 22 12 C 22 6.4889971 17.511003 2 12 2 z M 12 4 C 16.430123 4 20 7.5698774 20 12 C 20 16.430123 16.430123 20 12 20 C 7.5698774 20 4 16.430123 4 12 C 4 7.5698774 7.5698774 4 12 4 z M 11 7 L 11 9 L 13 9 L 13 7 L 11 7 z M 11 11 L 11 17 L 13 17 L 13 11 L 11 11 z"/></svg>
|
||||
|
After Width: | Height: | Size: 505 B |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB |
BIN
client/src/images/logo_cropped.gif
Normal file
|
After Width: | Height: | Size: 162 KiB |
8
client/src/images/pause-button.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="263" height="271" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<ellipse stroke="#d7d7d7" ry="131" rx="126" id="svg_5" fill="none" cy="135.237498" cx="131.999999" stroke-opacity="null" stroke-width="8"/>
|
||||
<rect stroke="#d7d7d7" id="svg_1" height="123.000006" width="41" y="73.737494" x="77.5" stroke-width="0" fill="#d7d7d7"/>
|
||||
<rect stroke="#d7d7d7" id="svg_3" height="123.000006" width="41" y="73.737494" x="144.5" stroke-width="0" fill="#d7d7d7"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 500 B |
@@ -9,6 +9,6 @@
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<path id="svg_2" d="m124.961908,17.099698l-20.881294,-13.124489c-5.268417,-3.308101 -12.210471,-1.715912 -15.530119,3.553603l-8.230499,13.1l39.946799,25.089697l8.23699,-13.093919c3.307049,-5.275788 1.728716,-12.224161 -3.541878,-15.524891l0,0zm-115.727379,116.707156l39.948976,25.089439l65.110209,-103.65022l-39.967994,-25.095989l-65.091198,103.656764l0.000007,0.000007zm-6.102198,31.877066l-0.88233,23.566079l20.849261,-11.027851l19.374887,-10.229887l-38.540057,-24.219893l-0.801755,21.911559l0,0l-0.000007,-0.000007z" stroke-width="4.5" stroke="none" fill="black"/>
|
||||
<path id="svg_2" d="m124.961908,17.099698l-20.881294,-13.124489c-5.268417,-3.308101 -12.210471,-1.715912 -15.530119,3.553603l-8.230499,13.1l39.946799,25.089697l8.23699,-13.093919c3.307049,-5.275788 1.728716,-12.224161 -3.541878,-15.524891l0,0zm-115.727379,116.707156l39.948976,25.089439l65.110209,-103.65022l-39.967994,-25.095989l-65.091198,103.656764l0.000007,0.000007zm-6.102198,31.877066l-0.88233,23.566079l20.849261,-11.027851l19.374887,-10.229887l-38.540057,-24.219893l-0.801755,21.911559l0,0l-0.000007,-0.000007z" stroke-width="4.5" stroke="none" fill="#d7d7d7"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -79,7 +79,7 @@
|
||||
id="layer1"
|
||||
transform="translate(-44.488903,-69.97024)">
|
||||
<circle
|
||||
style="opacity:0.95999995;fill:none;fill-opacity:1;stroke:whitesmoke;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
style="opacity:0.95999995;fill:none;fill-opacity:1;stroke:#d7d7d7;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path4518"
|
||||
cx="104.32143"
|
||||
cy="101.96429"
|
||||
@@ -93,14 +93,14 @@
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4524"
|
||||
d="m 44.60119,189.65476 c 6.047799,-10.58365 12.095417,-21.16698 21.920308,-26.58444 9.824892,-5.41745 23.437104,-5.66953 37.043972,-5.92151"
|
||||
style="fill:none;stroke:whitesmoke;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
style="fill:none;stroke:#d7d7d7;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:original-d="m 44.60119,189.65476 c 6.047883,-10.5836 12.095501,-21.16693 18.142858,-31.75 13.607002,-2.6e-4 27.214549,-0.50423 40.821422,-0.75595"
|
||||
inkscape:path-effect="#path-effect4526-4"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4524-1"
|
||||
d="m 44.60119,189.65476 c 6.047799,-10.58365 12.095417,-21.16698 21.920308,-26.58444 9.824892,-5.41745 23.437104,-5.66953 37.043972,-5.92151"
|
||||
style="fill:none;stroke:whitesmoke;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
style="fill:none;stroke:#d7d7d7;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="matrix(-1,0,0,1,207.13094,0)" />
|
||||
</g>
|
||||
</g>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -1,7 +1,7 @@
|
||||
<svg width="263" height="271" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<ellipse stroke="gray" ry="131" rx="126" id="svg_5" cy="135.237498" cx="129.999999" fill-opacity="null" stroke-opacity="null" stroke-width="8" fill="none"/>
|
||||
<path transform="rotate(90.18392181396484 140.08586120605474,135.38354492187497) " stroke="#7d0b0b" id="svg_7" d="m86.585877,180.883554l53.499995,-91.000007l53.499995,91.000007l-106.99999,0z" fill-opacity="null" stroke-opacity="null" stroke-width="0" fill="gray"/>
|
||||
<ellipse stroke="#d7d7d7" ry="131" rx="126" id="svg_5" cy="135.237498" cx="129.999999" fill-opacity="null" stroke-opacity="null" stroke-width="8" fill="none"/>
|
||||
<path transform="rotate(90.18392181396484 140.08586120605474,135.38354492187497) " stroke="#7d0b0b" id="svg_7" d="m86.585877,180.883554l53.499995,-91.000007l53.499995,91.000007l-106.99999,0z" fill-opacity="null" stroke-opacity="null" stroke-width="0" fill="#d7d7d7"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 535 B After Width: | Height: | Size: 541 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
client/src/images/roles/Minion.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
client/src/images/roles/ParityHunter.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
client/src/images/roles/Villager2.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
client/src/images/roles/Werewolf.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
107
client/src/images/roles/custom-role.svg
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="119.66505mm"
|
||||
height="109.59733mm"
|
||||
viewBox="-17 0 150.66505 120.59733"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
sodipodi:docname="host.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4526"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4526-4"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-232.43275"
|
||||
inkscape:cy="116.16088"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-44.488903,-69.97024)">
|
||||
<circle
|
||||
style="opacity:0.95999995;fill:none;fill-opacity:1;stroke:#bfbfbf;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path4518"
|
||||
cx="104.32143"
|
||||
cy="101.96429"
|
||||
r="30.994047" />
|
||||
<g
|
||||
id="g4548"
|
||||
transform="translate(0.75595639,-10.583334)">
|
||||
<path
|
||||
inkscape:original-d="m 44.60119,189.65476 c 6.047883,-10.5836 12.095501,-21.16693 18.142858,-31.75 13.607002,-2.6e-4 27.214549,-0.50423 40.821422,-0.75595"
|
||||
inkscape:path-effect="#path-effect4526"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4524"
|
||||
d="m 44.60119,189.65476 c 6.047799,-10.58365 12.095417,-21.16698 21.920308,-26.58444 9.824892,-5.41745 23.437104,-5.66953 37.043972,-5.92151"
|
||||
style="fill:none;stroke:#bfbfbf;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:original-d="m 44.60119,189.65476 c 6.047883,-10.5836 12.095501,-21.16693 18.142858,-31.75 13.607002,-2.6e-4 27.214549,-0.50423 40.821422,-0.75595"
|
||||
inkscape:path-effect="#path-effect4526-4"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4524-1"
|
||||
d="m 44.60119,189.65476 c 6.047799,-10.58365 12.095417,-21.16698 21.920308,-26.58444 9.824892,-5.41745 23.437104,-5.66953 37.043972,-5.92151"
|
||||
style="fill:none;stroke:#bfbfbf;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="matrix(-1,0,0,1,207.13094,0)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
BIN
client/src/images/screenshots/deckbuilder.PNG
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
client/src/images/screenshots/moderator.PNG
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
client/src/images/screenshots/player.PNG
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
client/src/images/tombstone.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
client/src/images/tutorial/custom-roles.PNG
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
client/src/images/tutorial/default-roles.PNG
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
client/src/images/tutorial/moderation-option.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 218 B |
14
client/src/images/x.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="209" height="209" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="172" width="172" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<path id="svg_1" d="m0.749996,50.93695l50.18695,-50.18695l53.312819,53.312382l53.312819,-53.312382l50.187422,50.18695l-53.312833,53.312819l53.312833,53.312819l-50.187422,50.187422l-53.312819,-53.312833l-53.312819,53.312833l-50.18695,-50.187422l53.312382,-53.312819l-53.312382,-53.312819z" stroke-width="1.5" stroke="none" fill="#d7d7d7"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 844 B |
9
client/src/model/Game.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export class Game {
|
||||
constructor (deck, hasTimer, hasDedicatedModerator, timerParams = null) {
|
||||
this.deck = deck;
|
||||
this.hasTimer = hasTimer;
|
||||
this.timerParams = timerParams;
|
||||
this.hasDedicatedModerator = hasDedicatedModerator;
|
||||
this.accessCode = null;
|
||||
}
|
||||
}
|
||||
157
client/src/modules/DeckStateManager.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import { globals } from '../config/globals.js';
|
||||
import { toast } from './Toast.js';
|
||||
import { ModalManager } from './ModalManager';
|
||||
|
||||
export class DeckStateManager {
|
||||
constructor () {
|
||||
this.deck = null;
|
||||
this.customRoleOptions = [];
|
||||
this.createMode = false;
|
||||
this.currentlyEditingRoleName = null;
|
||||
}
|
||||
|
||||
addToDeck (role) {
|
||||
const option = this.customRoleOptions.find((option) => option.role === role);
|
||||
if (option) {
|
||||
option.quantity = 0;
|
||||
this.deck.push(option);
|
||||
this.customRoleOptions.splice(this.customRoleOptions.indexOf(option), 1);
|
||||
}
|
||||
}
|
||||
|
||||
addToCustomRoleOptions (role) {
|
||||
this.customRoleOptions.push(role);
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoleOptions.concat(this.deck.filter(card => card.custom === true))));
|
||||
}
|
||||
|
||||
updateCustomRoleOption (option, name, description, team) {
|
||||
option.role = name;
|
||||
option.description = description;
|
||||
option.team = team;
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoleOptions.concat(this.deck.filter(card => card.custom === true))));
|
||||
}
|
||||
|
||||
removeFromCustomRoleOptions (name) {
|
||||
const option = this.customRoleOptions.find((option) => option.role === name);
|
||||
if (option) {
|
||||
this.customRoleOptions.splice(this.customRoleOptions.indexOf(option), 1);
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoleOptions.concat(this.deck.filter(card => card.custom === true))));
|
||||
toast('"' + name + '" deleted.', 'error', true, true, 3);
|
||||
}
|
||||
}
|
||||
|
||||
addCopyOfCard (role) {
|
||||
const existingCard = this.deck.find((card) => card.role === role);
|
||||
if (existingCard) {
|
||||
existingCard.quantity += 1;
|
||||
}
|
||||
}
|
||||
|
||||
removeCopyOfCard (role) {
|
||||
const existingCard = this.deck.find((card) => card.role === role);
|
||||
if (existingCard && existingCard.quantity > 0) {
|
||||
existingCard.quantity -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentDeck () { return this.deck; }
|
||||
|
||||
getCard (role) {
|
||||
return this.deck.find(
|
||||
(card) => card.role.toLowerCase().trim() === role.toLowerCase().trim()
|
||||
);
|
||||
}
|
||||
|
||||
getCurrentCustomRoleOptions () { return this.customRoleOptions; }
|
||||
|
||||
getCustomRoleOption (role) {
|
||||
return this.customRoleOptions.find(
|
||||
(option) => option.role.toLowerCase().trim() === role.toLowerCase().trim()
|
||||
);
|
||||
};
|
||||
|
||||
getDeckSize () {
|
||||
let total = 0;
|
||||
for (const role of this.deck) {
|
||||
total += role.quantity;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
loadCustomRolesFromCookies () {
|
||||
const customRoles = localStorage.getItem('play-werewolf-custom-roles');
|
||||
if (customRoles !== null && validateCustomRoleCookie(customRoles)) {
|
||||
this.customRoleOptions = JSON.parse(customRoles); // we know it is valid JSON from the validate function
|
||||
}
|
||||
}
|
||||
|
||||
loadCustomRolesFromFile (file, updateRoleListFunction, loadDefaultCardsFn, showIncludedCardsFn) {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = (e) => {
|
||||
toast(reader.error.message, 'error', true, true, 5);
|
||||
};
|
||||
reader.onload = (e) => {
|
||||
let string;
|
||||
if (typeof e.target.result !== 'string') {
|
||||
string = new TextDecoder('utf-8').decode(e.target.result);
|
||||
} else {
|
||||
string = e.target.result;
|
||||
}
|
||||
if (validateCustomRoleCookie(string)) {
|
||||
this.customRoleOptions = JSON.parse(string); // we know it is valid JSON from the validate function
|
||||
ModalManager.dispelModal('upload-custom-roles-modal', 'modal-background');
|
||||
toast('Roles imported successfully', 'success', true, true, 3);
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoleOptions));
|
||||
updateRoleListFunction(this, document.getElementById('deck-select'));
|
||||
// loadDefaultCardsFn(this);
|
||||
// showIncludedCardsFn(this);
|
||||
} else {
|
||||
toast('Invalid formatting. Make sure you import the file as downloaded from this page.', 'error', true, true, 5);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// via https://stackoverflow.com/a/18197341
|
||||
downloadCustomRoles (filename, text) {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
// this is user-supplied, so we should validate it fully
|
||||
function validateCustomRoleCookie (cookie) {
|
||||
const valid = false;
|
||||
if (typeof cookie === 'string' && new Blob([cookie]).size <= 1000000) {
|
||||
try {
|
||||
const cookieJSON = JSON.parse(cookie);
|
||||
if (Array.isArray(cookieJSON)) {
|
||||
for (const entry of cookieJSON) {
|
||||
if (typeof entry === 'object') {
|
||||
if (typeof entry.role !== 'string' || entry.role.length > globals.MAX_CUSTOM_ROLE_NAME_LENGTH
|
||||
|| typeof entry.team !== 'string' || (entry.team !== globals.ALIGNMENT.GOOD && entry.team !== globals.ALIGNMENT.EVIL)
|
||||
|| typeof entry.description !== 'string' || entry.description.length > globals.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
646
client/src/modules/GameCreationStepManager.js
Normal file
@@ -0,0 +1,646 @@
|
||||
import { Game } from '../model/Game.js';
|
||||
import { cancelCurrentToast, toast } from './Toast.js';
|
||||
import { ModalManager } from './ModalManager.js';
|
||||
import { XHRUtility } from './XHRUtility.js';
|
||||
import { globals } from '../config/globals.js';
|
||||
import { templates } from './Templates.js';
|
||||
import { defaultCards } from '../config/defaultCards';
|
||||
|
||||
export class GameCreationStepManager {
|
||||
constructor (deckManager) {
|
||||
loadDefaultCards(deckManager);
|
||||
deckManager.loadCustomRolesFromCookies();
|
||||
this.step = 1;
|
||||
this.currentGame = new Game(null, null, null, null);
|
||||
this.deckManager = deckManager;
|
||||
this.defaultBackHandler = () => {
|
||||
cancelCurrentToast();
|
||||
this.removeStepElementsFromDOM(this.step);
|
||||
this.decrementStep();
|
||||
this.renderStep('creation-step-container', this.step);
|
||||
};
|
||||
this.steps = {
|
||||
1: {
|
||||
title: 'Select your method of moderation:',
|
||||
forwardHandler: () => {
|
||||
if (this.currentGame.hasDedicatedModerator !== null) {
|
||||
cancelCurrentToast();
|
||||
this.removeStepElementsFromDOM(this.step);
|
||||
this.incrementStep();
|
||||
this.renderStep('creation-step-container', this.step);
|
||||
} else {
|
||||
toast('You must select a moderation option.', 'error', true);
|
||||
}
|
||||
}
|
||||
},
|
||||
2: {
|
||||
title: 'Create your deck of cards:',
|
||||
forwardHandler: () => {
|
||||
if (this.deckManager.getDeckSize() >= 5 && this.deckManager.getDeckSize() <= 50) {
|
||||
this.currentGame.deck = deckManager.getCurrentDeck().filter((card) => card.quantity > 0);
|
||||
cancelCurrentToast();
|
||||
this.removeStepElementsFromDOM(this.step);
|
||||
this.incrementStep();
|
||||
this.renderStep('creation-step-container', this.step);
|
||||
} else {
|
||||
toast('You must have a deck for between 5 and 50 players', 'error', true);
|
||||
}
|
||||
},
|
||||
backHandler: this.defaultBackHandler
|
||||
},
|
||||
3: {
|
||||
title: 'Set an optional timer:',
|
||||
forwardHandler: () => {
|
||||
const hours = parseInt(document.getElementById('game-hours').value);
|
||||
const minutes = parseInt(document.getElementById('game-minutes').value);
|
||||
if ((isNaN(hours) && isNaN(minutes))
|
||||
|| (isNaN(hours) && minutes > 0 && minutes < 60)
|
||||
|| (isNaN(minutes) && hours > 0 && hours < 6)
|
||||
|| (hours === 0 && minutes > 0 && minutes < 60)
|
||||
|| (minutes === 0 && hours > 0 && hours < 6)
|
||||
|| (hours > 0 && hours < 6 && minutes >= 0 && minutes < 60)
|
||||
) {
|
||||
if (hasTimer(hours, minutes)) {
|
||||
this.currentGame.hasTimer = true;
|
||||
this.currentGame.timerParams = {
|
||||
hours: hours,
|
||||
minutes: minutes
|
||||
};
|
||||
} else {
|
||||
this.currentGame.hasTimer = false;
|
||||
this.currentGame.timerParams = null;
|
||||
}
|
||||
cancelCurrentToast();
|
||||
this.removeStepElementsFromDOM(this.step);
|
||||
this.incrementStep();
|
||||
this.renderStep('creation-step-container', this.step);
|
||||
} else {
|
||||
if (hours === 0 && minutes === 0) {
|
||||
toast('You must enter a non-zero amount of time.', 'error', true);
|
||||
} else {
|
||||
toast('Invalid timer options. Hours can be a max of 5, Minutes a max of 59.', 'error', true);
|
||||
}
|
||||
}
|
||||
},
|
||||
backHandler: this.defaultBackHandler
|
||||
},
|
||||
4: {
|
||||
title: 'Review and submit:',
|
||||
backHandler: this.defaultBackHandler,
|
||||
forwardHandler: (deck, hasTimer, hasDedicatedModerator, timerParams) => {
|
||||
XHRUtility.xhr(
|
||||
'/api/games/create',
|
||||
'POST',
|
||||
null,
|
||||
JSON.stringify(
|
||||
new Game(deck, hasTimer, hasDedicatedModerator, timerParams)
|
||||
)
|
||||
)
|
||||
.then((res) => {
|
||||
if (res
|
||||
&& typeof res === 'object'
|
||||
&& Object.prototype.hasOwnProperty.call(res, 'content')
|
||||
&& typeof res.content === 'string'
|
||||
) {
|
||||
window.location = ('/game/' + res.content);
|
||||
}
|
||||
}).catch((e) => {
|
||||
if (e.status === 429) {
|
||||
toast('You\'ve sent this request too many times.', 'error', true, true, 6);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
incrementStep () {
|
||||
if (this.step < Object.keys(this.steps).length) {
|
||||
this.step += 1;
|
||||
}
|
||||
}
|
||||
|
||||
decrementStep () {
|
||||
if (this.step > 1) {
|
||||
this.step -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
renderStep (containerId, step) {
|
||||
document.querySelectorAll('.animated-placeholder').forEach((el) => el.remove());
|
||||
document.querySelectorAll('.placeholder-row').forEach((el) => el.remove());
|
||||
document.getElementById('step-title').innerText = this.steps[step].title;
|
||||
switch (step) {
|
||||
case 1:
|
||||
renderModerationTypeStep(this.currentGame, containerId, step);
|
||||
showButtons(false, true, this.steps[step].forwardHandler, null);
|
||||
break;
|
||||
case 2:
|
||||
renderRoleSelectionStep(this.currentGame, containerId, step, this.deckManager);
|
||||
showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler);
|
||||
break;
|
||||
case 3:
|
||||
renderTimerStep(containerId, step, this.currentGame);
|
||||
showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler);
|
||||
break;
|
||||
case 4:
|
||||
renderReviewAndCreateStep(containerId, step, this.currentGame);
|
||||
showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler, this.currentGame);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
updateTracker(step);
|
||||
}
|
||||
|
||||
removeStepElementsFromDOM (stepNumber) {
|
||||
document.getElementById('step-' + stepNumber)?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function renderModerationTypeStep (game, containerId, stepNumber) {
|
||||
const stepContainer = document.createElement('div');
|
||||
setAttributes(stepContainer, { id: 'step-' + stepNumber, class: 'flex-row-container step' });
|
||||
|
||||
stepContainer.innerHTML =
|
||||
"<div id='moderation-dedicated'>I will be the <strong>dedicated mod.</strong> Don't deal me a card.</div>" +
|
||||
"<div id='moderation-self'>The <strong>first person out</strong> will mod. Deal me into the game <span>(mod will be assigned automatically).</span></div>";
|
||||
|
||||
const dedicatedOption = stepContainer.querySelector('#moderation-dedicated');
|
||||
if (game.hasDedicatedModerator) {
|
||||
dedicatedOption.classList.add('option-selected');
|
||||
}
|
||||
const selfOption = stepContainer.querySelector('#moderation-self');
|
||||
if (game.hasDedicatedModerator === false) {
|
||||
selfOption.classList.add('option-selected');
|
||||
}
|
||||
|
||||
dedicatedOption.addEventListener('click', () => {
|
||||
dedicatedOption.classList.add('option-selected');
|
||||
selfOption.classList.remove('option-selected');
|
||||
game.hasDedicatedModerator = true;
|
||||
});
|
||||
|
||||
selfOption.addEventListener('click', () => {
|
||||
selfOption.classList.add('option-selected');
|
||||
dedicatedOption.classList.remove('option-selected');
|
||||
game.hasDedicatedModerator = false;
|
||||
});
|
||||
|
||||
document.getElementById(containerId).appendChild(stepContainer);
|
||||
}
|
||||
|
||||
function renderRoleSelectionStep (game, containerId, step, deckManager) {
|
||||
const stepContainer = document.createElement('div');
|
||||
setAttributes(stepContainer, { id: 'step-' + step, class: 'flex-row-container-left-align step' });
|
||||
|
||||
stepContainer.innerHTML = templates.CREATE_GAME_CUSTOM_ROLES;
|
||||
stepContainer.innerHTML += templates.CREATE_GAME_DECK_STATUS;
|
||||
stepContainer.innerHTML += templates.CREATE_GAME_DECK;
|
||||
|
||||
document.getElementById(containerId).appendChild(stepContainer);
|
||||
document.querySelector('#custom-roles-export').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
deckManager.downloadCustomRoles('play-werewolf-custom-roles', JSON.stringify(deckManager.getCurrentCustomRoleOptions()));
|
||||
});
|
||||
|
||||
document.querySelector('#custom-roles-import').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
ModalManager.displayModal('upload-custom-roles-modal', 'modal-background', 'close-upload-custom-roles-modal-button');
|
||||
});
|
||||
|
||||
document.getElementById('upload-custom-roles-form').onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const fileList = document.getElementById('upload-custom-roles').files;
|
||||
if (fileList.length > 0) {
|
||||
const file = fileList[0];
|
||||
if (file.size > 1000000) {
|
||||
toast('Your file is too large (max 1MB)', 'error', true, true, 5);
|
||||
return;
|
||||
}
|
||||
if (file.type !== 'text/plain') {
|
||||
toast('Your file must be a text file', 'error', true, true, 5);
|
||||
return;
|
||||
}
|
||||
|
||||
deckManager.loadCustomRolesFromFile(file, updateCustomRoleOptionsList, loadDefaultCards, showIncludedCards);
|
||||
} else {
|
||||
toast('You must upload a text file', 'error', true, true, 5);
|
||||
}
|
||||
};
|
||||
|
||||
const clickHandler = () => {
|
||||
console.log('fired');
|
||||
const actions = document.getElementById('custom-role-actions');
|
||||
if (actions.style.display !== 'none') {
|
||||
actions.style.display = 'none';
|
||||
} else {
|
||||
actions.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('custom-role-hamburger').addEventListener('click', clickHandler);
|
||||
|
||||
showIncludedCards(deckManager);
|
||||
|
||||
loadCustomRoles(deckManager);
|
||||
|
||||
updateDeckStatus(deckManager);
|
||||
|
||||
initializeRemainingEventListeners(deckManager);
|
||||
}
|
||||
|
||||
function renderTimerStep (containerId, stepNumber, game) {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('id', 'step-' + stepNumber);
|
||||
div.classList.add('step');
|
||||
|
||||
const timeContainer = document.createElement('div');
|
||||
timeContainer.setAttribute('id', 'game-time');
|
||||
|
||||
const hoursDiv = document.createElement('div');
|
||||
const hoursLabel = document.createElement('label');
|
||||
hoursLabel.setAttribute('for', 'game-hours');
|
||||
hoursLabel.innerText = 'Hours (max 5)';
|
||||
const hours = document.createElement('input');
|
||||
setAttributes(hours, { type: 'number', id: 'game-hours', name: 'game-hours', min: '0', max: '5', value: game.timerParams?.hours });
|
||||
|
||||
const minutesDiv = document.createElement('div');
|
||||
const minsLabel = document.createElement('label');
|
||||
minsLabel.setAttribute('for', 'game-minutes');
|
||||
minsLabel.innerText = 'Minutes';
|
||||
const minutes = document.createElement('input');
|
||||
setAttributes(minutes, { type: 'number', id: 'game-minutes', name: 'game-minutes', min: '1', max: '60', value: game.timerParams?.minutes });
|
||||
|
||||
hoursDiv.appendChild(hoursLabel);
|
||||
hoursDiv.appendChild(hours);
|
||||
minutesDiv.appendChild(minsLabel);
|
||||
minutesDiv.appendChild(minutes);
|
||||
timeContainer.appendChild(hoursDiv);
|
||||
timeContainer.appendChild(minutesDiv);
|
||||
div.appendChild(timeContainer);
|
||||
|
||||
document.getElementById(containerId).appendChild(div);
|
||||
}
|
||||
|
||||
function renderReviewAndCreateStep (containerId, stepNumber, game) {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('id', 'step-' + stepNumber);
|
||||
div.classList.add('step');
|
||||
|
||||
div.innerHTML =
|
||||
'<div>' +
|
||||
"<label for='mod-option'>Moderation</label>" +
|
||||
"<div id='mod-option' class='review-option'></div>" +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<label for='timer-option'>Timer</label>" +
|
||||
"<div id='timer-option' class='review-option'></div>" +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<label for='roles-option'>Game Deck</label>" +
|
||||
"<div id='roles-option' class='review-option'></div>" +
|
||||
'</div>';
|
||||
|
||||
div.querySelector('#mod-option').innerText = game.hasDedicatedModerator
|
||||
? "I will be the dedicated mod. Don't deal me a card."
|
||||
: 'The first person out will mod. Deal me into the game.';
|
||||
|
||||
if (game.hasTimer) {
|
||||
const formattedHours = !isNaN(game.timerParams.hours)
|
||||
? game.timerParams.hours + ' Hours'
|
||||
: '0 Hours';
|
||||
|
||||
const formattedMinutes = !isNaN(game.timerParams.minutes)
|
||||
? game.timerParams.minutes + ' Minutes'
|
||||
: '0 Minutes';
|
||||
|
||||
div.querySelector('#timer-option').innerText = formattedHours + ' ' + formattedMinutes;
|
||||
} else {
|
||||
div.querySelector('#timer-option').innerText = 'untimed';
|
||||
}
|
||||
|
||||
for (const card of game.deck) {
|
||||
const roleEl = document.createElement('div');
|
||||
roleEl.innerText = card.quantity + 'x ' + card.role;
|
||||
div.querySelector('#roles-option').appendChild(roleEl);
|
||||
}
|
||||
|
||||
document.getElementById(containerId).appendChild(div);
|
||||
}
|
||||
|
||||
function setAttributes (element, attributeObject) {
|
||||
for (const key of Object.keys(attributeObject)) {
|
||||
element.setAttribute(key, attributeObject[key]);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTracker (step) {
|
||||
document.querySelectorAll('.creation-step').forEach((element, i) => {
|
||||
if ((i + 1) <= step) {
|
||||
element.classList.add('creation-step-filled');
|
||||
} else {
|
||||
element.classList.remove('creation-step-filled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showButtons (back, forward, forwardHandler, backHandler, builtGame = null) {
|
||||
document.querySelector('#step-back-button')?.remove();
|
||||
document.querySelector('#step-forward-button')?.remove();
|
||||
document.querySelector('#create-game')?.remove();
|
||||
if (back) {
|
||||
const backButton = document.createElement('button');
|
||||
backButton.innerText = '\u25C0';
|
||||
backButton.addEventListener('click', backHandler);
|
||||
backButton.setAttribute('id', 'step-back-button');
|
||||
backButton.classList.add('cancel');
|
||||
backButton.classList.add('app-button');
|
||||
document.getElementById('tracker-container').prepend(backButton);
|
||||
}
|
||||
|
||||
if (forward && builtGame === null) {
|
||||
const fwdButton = document.createElement('button');
|
||||
fwdButton.innerHTML = '\u25b6';
|
||||
fwdButton.addEventListener('click', forwardHandler);
|
||||
fwdButton.setAttribute('id', 'step-forward-button');
|
||||
fwdButton.classList.add('app-button');
|
||||
document.getElementById('tracker-container').appendChild(fwdButton);
|
||||
} else if (forward && builtGame !== null) {
|
||||
const createButton = document.createElement('button');
|
||||
createButton.innerText = 'Create';
|
||||
createButton.setAttribute('id', 'create-game');
|
||||
createButton.classList.add('app-button');
|
||||
createButton.addEventListener('click', () => {
|
||||
forwardHandler(
|
||||
builtGame.deck.filter((card) => card.quantity > 0),
|
||||
builtGame.hasTimer,
|
||||
builtGame.hasDedicatedModerator,
|
||||
builtGame.timerParams
|
||||
);
|
||||
});
|
||||
document.getElementById('tracker-container').appendChild(createButton);
|
||||
}
|
||||
}
|
||||
|
||||
// Display a widget for each default card that allows copies of it to be added/removed. Set initial deck state.
|
||||
function showIncludedCards (deckManager) {
|
||||
document.querySelectorAll('.compact-card').forEach((el) => { el.remove(); });
|
||||
for (let i = 0; i < deckManager.getCurrentDeck().length; i++) {
|
||||
const card = deckManager.getCurrentDeck()[i];
|
||||
const cardEl = constructCompactDeckBuilderElement(card, deckManager);
|
||||
if (card.team === globals.ALIGNMENT.GOOD) {
|
||||
document.getElementById('deck-good').appendChild(cardEl);
|
||||
} else {
|
||||
document.getElementById('deck-evil').appendChild(cardEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Display a dropdown containing all the custom roles. Adding one will add it to the game deck and
|
||||
create a widget for it */
|
||||
function loadCustomRoles (deckManager) {
|
||||
addOptionsToList(deckManager, document.getElementById('deck-select'));
|
||||
}
|
||||
|
||||
function loadDefaultCards (deckManager) {
|
||||
defaultCards.sort((a, b) => {
|
||||
if (a.team !== b.team) {
|
||||
return a.team === globals.ALIGNMENT.GOOD ? 1 : -1;
|
||||
}
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
const deck = [];
|
||||
for (let i = 0; i < defaultCards.length; i++) {
|
||||
const card = defaultCards[i];
|
||||
card.quantity = 0;
|
||||
deck.push(card);
|
||||
}
|
||||
deckManager.deck = deck;
|
||||
}
|
||||
|
||||
function constructCompactDeckBuilderElement (card, deckManager) {
|
||||
const cardContainer = document.createElement('div');
|
||||
const alignmentClass = card.team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL;
|
||||
|
||||
cardContainer.setAttribute('class', 'compact-card ' + alignmentClass);
|
||||
|
||||
cardContainer.setAttribute('id', 'card-' + card.role.replaceAll(' ', '-'));
|
||||
|
||||
cardContainer.innerHTML =
|
||||
"<div class='compact-card-left'>" +
|
||||
'<p>-</p>' +
|
||||
'</div>' +
|
||||
"<div class='compact-card-header'>" +
|
||||
"<p class='card-role'></p>" +
|
||||
"<div class='card-quantity'></div>" +
|
||||
'</div>' +
|
||||
"<div class='compact-card-right'>" +
|
||||
'<p>+</p>' +
|
||||
'</div>';
|
||||
|
||||
cardContainer.querySelector('.card-role').innerText = card.role;
|
||||
cardContainer.title = card.role;
|
||||
cardContainer.querySelector('.card-quantity').innerText = card.quantity;
|
||||
|
||||
if (card.quantity > 0) {
|
||||
cardContainer.classList.add('selected-card');
|
||||
}
|
||||
|
||||
cardContainer.querySelector('.compact-card-right').addEventListener('click', () => {
|
||||
deckManager.addCopyOfCard(card.role);
|
||||
updateDeckStatus(deckManager);
|
||||
cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity;
|
||||
if (deckManager.getCard(card.role).quantity > 0) {
|
||||
document.getElementById('card-' + card.role.replaceAll(' ', '-')).classList.add('selected-card');
|
||||
}
|
||||
});
|
||||
cardContainer.querySelector('.compact-card-left').addEventListener('click', () => {
|
||||
deckManager.removeCopyOfCard(card.role);
|
||||
updateDeckStatus(deckManager);
|
||||
cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity;
|
||||
if (deckManager.getCard(card.role).quantity === 0) {
|
||||
document.getElementById('card-' + card.role.replaceAll(' ', '-')).classList.remove('selected-card');
|
||||
}
|
||||
});
|
||||
return cardContainer;
|
||||
}
|
||||
|
||||
function initializeRemainingEventListeners (deckManager) {
|
||||
document.getElementById('role-form').onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('role-name').value.trim();
|
||||
const description = document.getElementById('role-description').value.trim();
|
||||
const team = document.getElementById('role-alignment').value.toLowerCase().trim();
|
||||
if (deckManager.createMode) {
|
||||
if (!deckManager.getCustomRoleOption(name) && !deckManager.getCard(name)) { // confirm there is no existing custom role with the same name
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, false);
|
||||
} else {
|
||||
toast('There is already a role with this name', 'error', true, true, 3);
|
||||
}
|
||||
} else {
|
||||
const option = deckManager.getCustomRoleOption(deckManager.currentlyEditingRoleName);
|
||||
if (name === option.role) { // did they edit the name?
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, true, option);
|
||||
} else {
|
||||
if (!deckManager.getCustomRoleOption(name) && !deckManager.getCard(name)) {
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, true, option);
|
||||
} else {
|
||||
toast('There is already a role with this name', 'error', true, true, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
document.getElementById('custom-role-btn').addEventListener(
|
||||
'click', () => {
|
||||
const createBtn = document.getElementById('create-role-button');
|
||||
createBtn.setAttribute('value', 'Create');
|
||||
deckManager.createMode = true;
|
||||
deckManager.currentlyEditingRoleName = null;
|
||||
document.getElementById('role-name').value = '';
|
||||
document.getElementById('role-alignment').value = globals.ALIGNMENT.GOOD;
|
||||
document.getElementById('role-description').value = '';
|
||||
ModalManager.displayModal(
|
||||
'role-modal',
|
||||
'modal-background',
|
||||
'close-modal-button'
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function processNewCustomRoleSubmission (name, description, team, deckManager, isUpdate, option = null) {
|
||||
if (name.length > 40) {
|
||||
toast('Your name is too long (max 40 characters).', 'error', true);
|
||||
return;
|
||||
}
|
||||
if (description.length > 500) {
|
||||
toast('Your description is too long (max 500 characters).', 'error', true);
|
||||
return;
|
||||
}
|
||||
if (isUpdate) {
|
||||
deckManager.updateCustomRoleOption(option, name, description, team);
|
||||
ModalManager.dispelModal('role-modal', 'modal-background');
|
||||
toast('Role Updated', 'success', true);
|
||||
} else {
|
||||
deckManager.addToCustomRoleOptions({ role: name, description: description, team: team, custom: true });
|
||||
ModalManager.dispelModal('role-modal', 'modal-background');
|
||||
toast('Role Created', 'success', true);
|
||||
}
|
||||
|
||||
updateCustomRoleOptionsList(deckManager, document.getElementById('deck-select'));
|
||||
}
|
||||
|
||||
function updateCustomRoleOptionsList (deckManager, selectEl) {
|
||||
document.querySelectorAll('#deck-select .deck-select-role').forEach(e => e.remove());
|
||||
addOptionsToList(deckManager, selectEl);
|
||||
}
|
||||
|
||||
function addOptionsToList (deckManager, selectEl) {
|
||||
const options = deckManager.getCurrentCustomRoleOptions();
|
||||
options.sort((a, b) => {
|
||||
if (a.team !== b.team) {
|
||||
return a.team === globals.ALIGNMENT.GOOD ? 1 : -1;
|
||||
}
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const optionEl = document.createElement('div');
|
||||
optionEl.innerHTML = templates.DECK_SELECT_ROLE;
|
||||
optionEl.classList.add('deck-select-role');
|
||||
const alignmentClass = options[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL;
|
||||
optionEl.classList.add(alignmentClass);
|
||||
optionEl.querySelector('.deck-select-role-name').innerText = options[i].role;
|
||||
selectEl.appendChild(optionEl);
|
||||
}
|
||||
|
||||
addCustomRoleEventListeners(deckManager, selectEl);
|
||||
}
|
||||
|
||||
function addCustomRoleEventListeners (deckManager, select) {
|
||||
document.querySelectorAll('.deck-select-role').forEach((role) => {
|
||||
const name = role.querySelector('.deck-select-role-name').innerText;
|
||||
role.querySelector('.deck-select-include').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!deckManager.getCard(name)) {
|
||||
deckManager.addToDeck(name);
|
||||
const cardEl = constructCompactDeckBuilderElement(deckManager.getCard(name), deckManager);
|
||||
toast('"' + name + '" made available below.', 'success', true, true, 4);
|
||||
if (deckManager.getCard(name).team === globals.ALIGNMENT.GOOD) {
|
||||
document.getElementById('deck-good').appendChild(cardEl);
|
||||
} else {
|
||||
document.getElementById('deck-evil').appendChild(cardEl);
|
||||
}
|
||||
updateCustomRoleOptionsList(deckManager, select);
|
||||
} else {
|
||||
toast('"' + select.value + '" already included.', 'error', true, true, 3);
|
||||
}
|
||||
});
|
||||
|
||||
role.querySelector('.deck-select-remove').addEventListener('click', (e) => {
|
||||
if (confirm("Delete the role '" + name + "'?")) {
|
||||
e.preventDefault();
|
||||
deckManager.removeFromCustomRoleOptions(name);
|
||||
updateCustomRoleOptionsList(deckManager, select);
|
||||
}
|
||||
});
|
||||
|
||||
role.querySelector('.deck-select-info').addEventListener('click', (e) => {
|
||||
const alignmentEl = document.getElementById('custom-role-info-modal-alignment');
|
||||
alignmentEl.classList.remove(globals.ALIGNMENT.GOOD);
|
||||
alignmentEl.classList.remove(globals.ALIGNMENT.EVIL);
|
||||
e.preventDefault();
|
||||
const option = deckManager.getCustomRoleOption(name);
|
||||
document.getElementById('custom-role-info-modal-name').innerText = name;
|
||||
alignmentEl.classList.add(option.team);
|
||||
document.getElementById('custom-role-info-modal-description').innerText = option.description;
|
||||
alignmentEl.innerText = option.team;
|
||||
ModalManager.displayModal('custom-role-info-modal', 'modal-background', 'close-custom-role-info-modal-button');
|
||||
});
|
||||
|
||||
role.querySelector('.deck-select-edit').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const option = deckManager.getCustomRoleOption(name);
|
||||
document.getElementById('role-name').value = option.role;
|
||||
document.getElementById('role-alignment').value = option.team;
|
||||
document.getElementById('role-description').value = option.description;
|
||||
deckManager.createMode = false;
|
||||
deckManager.currentlyEditingRoleName = option.role;
|
||||
const createBtn = document.getElementById('create-role-button');
|
||||
createBtn.setAttribute('value', 'Update');
|
||||
ModalManager.displayModal('role-modal', 'modal-background', 'close-modal-button');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateDeckStatus (deckManager) {
|
||||
document.querySelectorAll('.deck-role').forEach((el) => el.remove());
|
||||
document.getElementById('deck-count').innerText = deckManager.getDeckSize() + ' Players';
|
||||
if (deckManager.getDeckSize() === 0) {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.setAttribute('id', 'deck-list-placeholder');
|
||||
placeholder.innerText = 'Add a card from the available roles below.';
|
||||
document.getElementById('deck-list').appendChild(placeholder);
|
||||
} else {
|
||||
if (document.getElementById('deck-list-placeholder')) {
|
||||
document.getElementById('deck-list-placeholder').remove();
|
||||
}
|
||||
for (const card of deckManager.getCurrentDeck()) {
|
||||
if (card.quantity > 0) {
|
||||
const roleEl = document.createElement('div');
|
||||
roleEl.classList.add('deck-role');
|
||||
if (card.team === globals.ALIGNMENT.GOOD) {
|
||||
roleEl.classList.add(globals.ALIGNMENT.GOOD);
|
||||
} else {
|
||||
roleEl.classList.add(globals.ALIGNMENT.EVIL);
|
||||
}
|
||||
roleEl.innerText = card.quantity + 'x ' + card.role;
|
||||
document.getElementById('deck-list').appendChild(roleEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasTimer (hours, minutes) {
|
||||
return (!isNaN(hours) || !isNaN(minutes));
|
||||
}
|
||||
486
client/src/modules/GameStateRenderer.js
Normal file
@@ -0,0 +1,486 @@
|
||||
import { globals } from '../config/globals.js';
|
||||
import { toast } from './Toast.js';
|
||||
import { templates } from './Templates.js';
|
||||
import { ModalManager } from './ModalManager.js';
|
||||
|
||||
export class GameStateRenderer {
|
||||
constructor (stateBucket, socket) {
|
||||
this.stateBucket = stateBucket;
|
||||
this.socket = socket;
|
||||
this.killPlayerHandlers = {};
|
||||
this.revealRoleHandlers = {};
|
||||
this.transferModHandlers = {};
|
||||
this.startGameHandler = (e) => {
|
||||
e.preventDefault();
|
||||
if (confirm('Start the game and deal roles?')) {
|
||||
socket.emit(globals.COMMANDS.START_GAME, this.stateBucket.currentGameState.accessCode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
renderLobbyPlayers () {
|
||||
document.querySelectorAll('.lobby-player').forEach((el) => el.remove());
|
||||
const lobbyPlayersContainer = document.getElementById('lobby-players');
|
||||
if (this.stateBucket.currentGameState.moderator.userType === globals.USER_TYPES.MODERATOR) {
|
||||
lobbyPlayersContainer.appendChild(
|
||||
renderLobbyPerson(
|
||||
this.stateBucket.currentGameState.moderator.name,
|
||||
this.stateBucket.currentGameState.moderator.userType
|
||||
)
|
||||
);
|
||||
}
|
||||
for (const person of this.stateBucket.currentGameState.people) {
|
||||
lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name, person.userType));
|
||||
}
|
||||
const playerCount = this.stateBucket.currentGameState.people.length;
|
||||
document.querySelector("label[for='lobby-players']").innerText =
|
||||
'Participants (' + playerCount + '/' + getGameSize(this.stateBucket.currentGameState.deck) + ' Players)';
|
||||
}
|
||||
|
||||
renderLobbyHeader () {
|
||||
removeExistingTitle();
|
||||
const title = document.createElement('h1');
|
||||
title.innerText = 'Lobby';
|
||||
document.getElementById('game-title').appendChild(title);
|
||||
const gameLinkContainer = document.getElementById('game-link');
|
||||
const linkDiv = document.createElement('div');
|
||||
linkDiv.innerText = window.location;
|
||||
gameLinkContainer.prepend(linkDiv);
|
||||
gameLinkContainer.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(gameLinkContainer.innerText).then(() => {
|
||||
toast('Link copied!', 'success', true);
|
||||
});
|
||||
});
|
||||
const copyImg = document.createElement('img');
|
||||
copyImg.setAttribute('src', '../images/copy.svg');
|
||||
gameLinkContainer.appendChild(copyImg);
|
||||
|
||||
const time = document.getElementById('game-time');
|
||||
const playerCount = document.getElementById('game-player-count');
|
||||
playerCount.innerText = getGameSize(this.stateBucket.currentGameState.deck) + ' Players';
|
||||
|
||||
if (this.stateBucket.currentGameState.timerParams) {
|
||||
let timeString = '';
|
||||
const hours = this.stateBucket.currentGameState.timerParams.hours;
|
||||
const minutes = this.stateBucket.currentGameState.timerParams.minutes;
|
||||
if (hours) {
|
||||
timeString += hours > 1
|
||||
? hours + ' hours '
|
||||
: hours + ' hour ';
|
||||
}
|
||||
if (minutes) {
|
||||
timeString += minutes > 1
|
||||
? minutes + ' minutes '
|
||||
: minutes + ' minute ';
|
||||
}
|
||||
time.innerText = timeString;
|
||||
} else {
|
||||
time.innerText = 'untimed';
|
||||
}
|
||||
}
|
||||
|
||||
renderLobbyFooter () {
|
||||
for (const card of this.stateBucket.currentGameState.deck) {
|
||||
const cardEl = document.createElement('div');
|
||||
cardEl.innerText = card.quantity + 'x ' + card.role;
|
||||
cardEl.classList.add('lobby-card');
|
||||
}
|
||||
}
|
||||
|
||||
renderGameHeader () {
|
||||
removeExistingTitle();
|
||||
// let title = document.createElement("h1");
|
||||
// title.innerText = "Game";
|
||||
// document.getElementById("game-title").appendChild(title);
|
||||
}
|
||||
|
||||
renderModeratorView () {
|
||||
createEndGamePromptComponent(this.socket, this.stateBucket);
|
||||
|
||||
const modTransferButton = document.getElementById('mod-transfer-button');
|
||||
modTransferButton.addEventListener(
|
||||
'click', () => {
|
||||
this.displayAvailableModerators();
|
||||
ModalManager.displayModal(
|
||||
'transfer-mod-modal',
|
||||
'transfer-mod-modal-background',
|
||||
'close-mod-transfer-modal-button'
|
||||
);
|
||||
}
|
||||
);
|
||||
this.renderPlayersWithRoleAndAlignmentInfo();
|
||||
}
|
||||
|
||||
renderTempModView () {
|
||||
createEndGamePromptComponent(this.socket, this.stateBucket);
|
||||
|
||||
renderPlayerRole(this.stateBucket.currentGameState);
|
||||
this.renderPlayersWithNoRoleInformationUnlessRevealed(true);
|
||||
}
|
||||
|
||||
renderPlayerView (isKilled = false) {
|
||||
if (isKilled) {
|
||||
const clientUserType = document.getElementById('client-user-type');
|
||||
if (clientUserType) {
|
||||
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
|
||||
}
|
||||
}
|
||||
renderPlayerRole(this.stateBucket.currentGameState);
|
||||
this.renderPlayersWithNoRoleInformationUnlessRevealed(false);
|
||||
}
|
||||
|
||||
renderSpectatorView () {
|
||||
this.renderPlayersWithNoRoleInformationUnlessRevealed();
|
||||
}
|
||||
|
||||
refreshPlayerList (isModerator) {
|
||||
if (isModerator) {
|
||||
this.renderPlayersWithRoleAndAlignmentInfo();
|
||||
} else {
|
||||
this.renderPlayersWithNoRoleInformationUnlessRevealed();
|
||||
}
|
||||
}
|
||||
|
||||
renderPlayersWithRoleAndAlignmentInfo () {
|
||||
removeExistingPlayerElements(this.killPlayerHandlers, this.revealRoleHandlers);
|
||||
this.stateBucket.currentGameState.people.sort((a, b) => {
|
||||
return a.name >= b.name ? 1 : -1;
|
||||
});
|
||||
const teamGood = this.stateBucket.currentGameState.people.filter((person) => person.alignment === globals.ALIGNMENT.GOOD);
|
||||
const teamEvil = this.stateBucket.currentGameState.people.filter((person) => person.alignment === globals.ALIGNMENT.EVIL);
|
||||
renderGroupOfPlayers(
|
||||
teamEvil,
|
||||
this.killPlayerHandlers,
|
||||
this.revealRoleHandlers,
|
||||
this.stateBucket.currentGameState.accessCode,
|
||||
globals.ALIGNMENT.EVIL,
|
||||
this.stateBucket.currentGameState.moderator.userType,
|
||||
this.socket
|
||||
);
|
||||
renderGroupOfPlayers(
|
||||
teamGood,
|
||||
this.killPlayerHandlers,
|
||||
this.revealRoleHandlers,
|
||||
this.stateBucket.currentGameState.accessCode,
|
||||
globals.ALIGNMENT.GOOD,
|
||||
this.stateBucket.currentGameState.moderator.userType,
|
||||
this.socket
|
||||
);
|
||||
document.getElementById('players-alive-label').innerText =
|
||||
'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' +
|
||||
this.stateBucket.currentGameState.people.length + ' Alive';
|
||||
}
|
||||
|
||||
renderPlayersWithNoRoleInformationUnlessRevealed (tempMod = false) {
|
||||
if (tempMod) {
|
||||
document.querySelectorAll('.game-player').forEach((el) => {
|
||||
const pointer = el.dataset.pointer;
|
||||
if (pointer && this.killPlayerHandlers[pointer]) {
|
||||
el.removeEventListener('click', this.killPlayerHandlers[pointer]);
|
||||
delete this.killPlayerHandlers[pointer];
|
||||
}
|
||||
if (pointer && this.revealRoleHandlers[pointer]) {
|
||||
el.removeEventListener('click', this.revealRoleHandlers[pointer]);
|
||||
delete this.revealRoleHandlers[pointer];
|
||||
}
|
||||
el.remove();
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('.game-player').forEach((el) => el.remove());
|
||||
sortPeopleByStatus(this.stateBucket.currentGameState.people);
|
||||
const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null;
|
||||
renderGroupOfPlayers(
|
||||
this.stateBucket.currentGameState.people,
|
||||
this.killPlayerHandlers,
|
||||
this.revealRoleHandlers,
|
||||
this.stateBucket.currentGameState.accessCode,
|
||||
null,
|
||||
modType,
|
||||
this.socket
|
||||
);
|
||||
document.getElementById('players-alive-label').innerText =
|
||||
'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' +
|
||||
this.stateBucket.currentGameState.people.length + ' Alive';
|
||||
}
|
||||
|
||||
updatePlayerCardToKilledState () {
|
||||
document.querySelector('#role-image').classList.add('killed-card');
|
||||
document.getElementById('role-image').setAttribute(
|
||||
'src',
|
||||
'../images/tombstone.png'
|
||||
);
|
||||
}
|
||||
|
||||
displayAvailableModerators () {
|
||||
document.getElementById('transfer-mod-modal-content').innerText = '';
|
||||
document.querySelectorAll('.potential-moderator').forEach((el) => {
|
||||
const pointer = el.dataset.pointer;
|
||||
if (pointer && this.transferModHandlers[pointer]) {
|
||||
el.removeEventListener('click', this.transferModHandlers[pointer]);
|
||||
delete this.transferModHandlers[pointer];
|
||||
}
|
||||
el.remove();
|
||||
});
|
||||
renderPotentialMods(
|
||||
this.stateBucket.currentGameState,
|
||||
this.stateBucket.currentGameState.people,
|
||||
this.transferModHandlers,
|
||||
this.socket
|
||||
);
|
||||
renderPotentialMods( // spectators can also be made mods.
|
||||
this.stateBucket.currentGameState,
|
||||
this.stateBucket.currentGameState.spectators,
|
||||
this.transferModHandlers,
|
||||
this.socket
|
||||
);
|
||||
|
||||
if (document.querySelectorAll('.potential-moderator').length === 0) {
|
||||
document.getElementById('transfer-mod-modal-content').innerText = 'There is nobody available to transfer to.';
|
||||
}
|
||||
}
|
||||
|
||||
renderEndOfGame () {
|
||||
this.renderPlayersWithNoRoleInformationUnlessRevealed();
|
||||
}
|
||||
}
|
||||
|
||||
function renderPotentialMods (gameState, group, transferModHandlers, socket) {
|
||||
const modalContent = document.getElementById('transfer-mod-modal-content');
|
||||
for (const member of group) {
|
||||
if ((member.out || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) {
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('potential-moderator');
|
||||
container.dataset.pointer = member.id;
|
||||
container.innerText = member.name;
|
||||
transferModHandlers[member.id] = () => {
|
||||
if (confirm('Transfer moderator powers to ' + member.name + '?')) {
|
||||
socket.emit(globals.COMMANDS.TRANSFER_MODERATOR, gameState.accessCode, member.id);
|
||||
}
|
||||
};
|
||||
|
||||
container.addEventListener('click', transferModHandlers[member.id]);
|
||||
modalContent.appendChild(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderLobbyPerson (name, userType) {
|
||||
const el = document.createElement('div');
|
||||
const personNameEl = document.createElement('div');
|
||||
const personTypeEl = document.createElement('div');
|
||||
personNameEl.innerText = name;
|
||||
personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType];
|
||||
el.classList.add('lobby-player');
|
||||
|
||||
el.appendChild(personNameEl);
|
||||
el.appendChild(personTypeEl);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function sortPeopleByStatus (people) {
|
||||
people.sort((a, b) => {
|
||||
if (a.out !== b.out) {
|
||||
return a.out ? 1 : -1;
|
||||
} else {
|
||||
if (a.revealed !== b.revealed) {
|
||||
return a.revealed ? -1 : 1;
|
||||
}
|
||||
return a.name >= b.name ? 1 : -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getGameSize (cards) {
|
||||
let quantity = 0;
|
||||
for (const card of cards) {
|
||||
quantity += card.quantity;
|
||||
}
|
||||
|
||||
return quantity;
|
||||
}
|
||||
|
||||
function removeExistingTitle () {
|
||||
const existingTitle = document.querySelector('#game-title h1');
|
||||
if (existingTitle) {
|
||||
existingTitle.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor to reduce the cyclomatic complexity of this function
|
||||
function renderGroupOfPlayers (
|
||||
people,
|
||||
killPlayerHandlers,
|
||||
revealRoleHandlers,
|
||||
accessCode = null,
|
||||
alignment = null,
|
||||
moderatorType,
|
||||
socket = null
|
||||
) {
|
||||
for (const player of people) {
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('game-player');
|
||||
if (moderatorType) {
|
||||
container.dataset.pointer = player.id;
|
||||
container.innerHTML = templates.MODERATOR_PLAYER;
|
||||
} else {
|
||||
container.innerHTML = templates.GAME_PLAYER;
|
||||
}
|
||||
container.querySelector('.game-player-name').innerText = player.name;
|
||||
const roleElement = container.querySelector('.game-player-role');
|
||||
|
||||
if (moderatorType) {
|
||||
roleElement.classList.add(alignment);
|
||||
if (moderatorType === globals.USER_TYPES.MODERATOR) {
|
||||
roleElement.innerText = player.gameRole;
|
||||
document.getElementById('player-list-moderator-team-' + alignment).appendChild(container);
|
||||
} else {
|
||||
if (player.revealed) {
|
||||
roleElement.innerText = player.gameRole;
|
||||
roleElement.classList.add(player.alignment);
|
||||
} else {
|
||||
roleElement.innerText = 'Unknown';
|
||||
}
|
||||
document.getElementById('game-player-list').appendChild(container);
|
||||
}
|
||||
} else if (player.revealed) {
|
||||
roleElement.classList.add(player.alignment);
|
||||
roleElement.innerText = player.gameRole;
|
||||
document.getElementById('game-player-list').appendChild(container);
|
||||
} else {
|
||||
roleElement.innerText = 'Unknown';
|
||||
document.getElementById('game-player-list').appendChild(container);
|
||||
}
|
||||
|
||||
if (player.out) {
|
||||
container.classList.add('killed');
|
||||
if (moderatorType) {
|
||||
container.querySelector('.kill-player-button')?.remove();
|
||||
insertPlaceholderButton(container, false, 'killed');
|
||||
}
|
||||
} else {
|
||||
if (moderatorType) {
|
||||
killPlayerHandlers[player.id] = () => {
|
||||
if (confirm('KILL ' + player.name + '?')) {
|
||||
socket.emit(globals.COMMANDS.KILL_PLAYER, accessCode, player.id);
|
||||
}
|
||||
};
|
||||
container.querySelector('.kill-player-button').addEventListener('click', killPlayerHandlers[player.id]);
|
||||
}
|
||||
}
|
||||
|
||||
if (player.revealed) {
|
||||
if (moderatorType) {
|
||||
container.querySelector('.reveal-role-button')?.remove();
|
||||
insertPlaceholderButton(container, true, 'revealed');
|
||||
}
|
||||
} else {
|
||||
if (moderatorType) {
|
||||
revealRoleHandlers[player.id] = () => {
|
||||
if (confirm('REVEAL ' + player.name + '?')) {
|
||||
socket.emit(globals.COMMANDS.REVEAL_PLAYER, accessCode, player.id);
|
||||
}
|
||||
};
|
||||
container.querySelector('.reveal-role-button').addEventListener('click', revealRoleHandlers[player.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlayerRole (gameState) {
|
||||
const name = document.querySelector('#role-name');
|
||||
name.innerText = gameState.client.gameRole;
|
||||
if (gameState.client.alignment === globals.ALIGNMENT.GOOD) {
|
||||
document.getElementById('game-role').classList.add('game-role-good');
|
||||
name.classList.add('good');
|
||||
} else {
|
||||
document.getElementById('game-role').classList.add('game-role-evil');
|
||||
name.classList.add('evil');
|
||||
}
|
||||
name.setAttribute('title', gameState.client.gameRole);
|
||||
if (gameState.client.out) {
|
||||
document.querySelector('#role-image').classList.add('killed-card');
|
||||
document.getElementById('role-image').setAttribute(
|
||||
'src',
|
||||
'../images/tombstone.png'
|
||||
);
|
||||
} else {
|
||||
if (gameState.client.gameRole.toLowerCase() === 'villager') {
|
||||
document.getElementById('role-image').setAttribute(
|
||||
'src',
|
||||
'../images/roles/Villager' + Math.ceil(Math.random() * 2) + '.png'
|
||||
);
|
||||
} else {
|
||||
if (gameState.client.customRole) {
|
||||
document.getElementById('role-image').setAttribute(
|
||||
'src',
|
||||
'../images/roles/custom-role.svg'
|
||||
);
|
||||
} else {
|
||||
document.getElementById('role-image').setAttribute(
|
||||
'src',
|
||||
'../images/roles/' + gameState.client.gameRole.replaceAll(' ', '') + '.png'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector('#role-description').innerText = gameState.client.gameRoleDescription;
|
||||
|
||||
document.getElementById('game-role-back').addEventListener('click', () => {
|
||||
document.getElementById('game-role').style.display = 'flex';
|
||||
document.getElementById('game-role-back').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('game-role').addEventListener('click', () => {
|
||||
document.getElementById('game-role-back').style.display = 'flex';
|
||||
document.getElementById('game-role').style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function insertPlaceholderButton (container, append, type) {
|
||||
const button = document.createElement('div');
|
||||
button.classList.add('placeholder-button');
|
||||
if (type === 'killed') {
|
||||
button.innerText = 'Killed';
|
||||
} else {
|
||||
button.innerText = 'Revealed';
|
||||
}
|
||||
if (append) {
|
||||
container.querySelector('.player-action-buttons').appendChild(button);
|
||||
} else {
|
||||
container.querySelector('.player-action-buttons').prepend(button);
|
||||
}
|
||||
}
|
||||
|
||||
function removeExistingPlayerElements (killPlayerHandlers, revealRoleHandlers) {
|
||||
document.querySelectorAll('.game-player').forEach((el) => {
|
||||
const pointer = el.dataset.pointer;
|
||||
if (pointer && killPlayerHandlers[pointer]) {
|
||||
el.removeEventListener('click', killPlayerHandlers[pointer]);
|
||||
delete killPlayerHandlers[pointer];
|
||||
}
|
||||
if (pointer && revealRoleHandlers[pointer]) {
|
||||
el.removeEventListener('click', revealRoleHandlers[pointer]);
|
||||
delete revealRoleHandlers[pointer];
|
||||
}
|
||||
el.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function createEndGamePromptComponent (socket, stateBucket) {
|
||||
if (document.querySelector('#end-game-prompt') === null) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = templates.END_GAME_PROMPT;
|
||||
div.querySelector('#end-game-button').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (confirm('End the game?')) {
|
||||
socket.emit(
|
||||
globals.COMMANDS.END_GAME,
|
||||
stateBucket.currentGameState.accessCode
|
||||
);
|
||||
}
|
||||
});
|
||||
document.getElementById('game-content').appendChild(div);
|
||||
}
|
||||
}
|
||||
178
client/src/modules/GameTimerManager.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import { globals } from '../config/globals.js';
|
||||
|
||||
export class GameTimerManager {
|
||||
constructor (stateBucket, socket) {
|
||||
this.stateBucket = stateBucket;
|
||||
this.playListener = () => {
|
||||
socket.emit(globals.COMMANDS.RESUME_TIMER, this.stateBucket.currentGameState.accessCode);
|
||||
};
|
||||
this.pauseListener = () => {
|
||||
socket.emit(globals.COMMANDS.PAUSE_TIMER, this.stateBucket.currentGameState.accessCode);
|
||||
};
|
||||
}
|
||||
|
||||
resumeGameTimer (totalTime, tickRate, soundManager, timerWorker) {
|
||||
if (window.Worker) {
|
||||
if (
|
||||
this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|
||||
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|
||||
) {
|
||||
this.swapToPauseButton();
|
||||
}
|
||||
const instance = this;
|
||||
const timer = document.getElementById('game-timer');
|
||||
timer.classList.remove('paused');
|
||||
timer.classList.remove('paused-low');
|
||||
timer.classList.remove('low');
|
||||
if (totalTime < 60000) {
|
||||
timer.classList.add('low');
|
||||
}
|
||||
timer.innerText = totalTime < 60000
|
||||
? returnHumanReadableTime(totalTime, true)
|
||||
: returnHumanReadableTime(totalTime);
|
||||
timerWorker.onmessage = function (e) {
|
||||
if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds >= 0) {
|
||||
if (e.data.timeRemainingInMilliseconds === 0) {
|
||||
instance.displayExpiredTime();
|
||||
} else if (e.data.timeRemainingInMilliseconds < 60000) {
|
||||
timer.classList.add('low');
|
||||
timer.innerText = e.data.displayTime;
|
||||
} else {
|
||||
timer.innerText = e.data.displayTime;
|
||||
}
|
||||
}
|
||||
};
|
||||
timerWorker.postMessage({ totalTime: totalTime, tickInterval: tickRate });
|
||||
}
|
||||
}
|
||||
|
||||
pauseGameTimer (timerWorker, timeRemaining) {
|
||||
if (window.Worker) {
|
||||
if (
|
||||
this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|
||||
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|
||||
) {
|
||||
this.swapToPlayButton();
|
||||
}
|
||||
|
||||
timerWorker.postMessage('stop');
|
||||
const timer = document.getElementById('game-timer');
|
||||
if (timeRemaining < 60000) {
|
||||
timer.innerText = returnHumanReadableTime(timeRemaining, true);
|
||||
timer.classList.add('paused-low');
|
||||
timer.classList.add('low');
|
||||
} else {
|
||||
timer.innerText = returnHumanReadableTime(timeRemaining);
|
||||
timer.classList.add('paused');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
displayPausedTime (time) {
|
||||
if (
|
||||
this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|
||||
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|
||||
) {
|
||||
this.swapToPlayButton();
|
||||
}
|
||||
|
||||
const timer = document.getElementById('game-timer');
|
||||
if (time < 60000) {
|
||||
timer.innerText = returnHumanReadableTime(time, true);
|
||||
timer.classList.add('paused-low');
|
||||
timer.classList.add('low');
|
||||
} else {
|
||||
timer.innerText = returnHumanReadableTime(time);
|
||||
timer.classList.add('paused');
|
||||
}
|
||||
}
|
||||
|
||||
displayExpiredTime () {
|
||||
const currentBtn = document.querySelector('#play-pause img');
|
||||
if (currentBtn) {
|
||||
currentBtn.removeEventListener('click', this.pauseListener);
|
||||
currentBtn.removeEventListener('click', this.playListener);
|
||||
currentBtn.remove();
|
||||
}
|
||||
|
||||
const timer = document.getElementById('game-timer');
|
||||
timer.innerText = returnHumanReadableTime(0, true);
|
||||
}
|
||||
|
||||
attachTimerSocketListeners (socket, timerWorker, gameStateRenderer) {
|
||||
if (!socket.hasListeners(globals.COMMANDS.PAUSE_TIMER)) {
|
||||
socket.on(globals.COMMANDS.PAUSE_TIMER, (timeRemaining) => {
|
||||
this.pauseGameTimer(timerWorker, timeRemaining);
|
||||
});
|
||||
}
|
||||
|
||||
if (!socket.hasListeners(globals.COMMANDS.RESUME_TIMER)) {
|
||||
socket.on(globals.COMMANDS.RESUME_TIMER, (timeRemaining) => {
|
||||
this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
|
||||
});
|
||||
}
|
||||
|
||||
if (!socket.hasListeners(globals.COMMANDS.GET_TIME_REMAINING)) {
|
||||
socket.on(globals.COMMANDS.GET_TIME_REMAINING, (timeRemaining, paused) => {
|
||||
if (paused) {
|
||||
this.displayPausedTime(timeRemaining);
|
||||
} else if (timeRemaining === 0) {
|
||||
this.displayExpiredTime();
|
||||
} else {
|
||||
this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
swapToPlayButton () {
|
||||
const currentBtn = document.querySelector('#play-pause img');
|
||||
if (currentBtn) {
|
||||
currentBtn.removeEventListener('click', this.pauseListener);
|
||||
currentBtn.remove();
|
||||
}
|
||||
|
||||
const playBtn = document.createElement('img');
|
||||
playBtn.setAttribute('src', '../images/play-button.svg');
|
||||
playBtn.addEventListener('click', this.playListener);
|
||||
document.getElementById('play-pause').appendChild(playBtn);
|
||||
}
|
||||
|
||||
swapToPauseButton () {
|
||||
const currentBtn = document.querySelector('#play-pause img');
|
||||
if (currentBtn) {
|
||||
currentBtn.removeEventListener('click', this.playListener);
|
||||
currentBtn.remove();
|
||||
}
|
||||
|
||||
const pauseBtn = document.createElement('img');
|
||||
pauseBtn.setAttribute('src', '../images/pause-button.svg');
|
||||
pauseBtn.addEventListener('click', this.pauseListener);
|
||||
document.getElementById('play-pause').appendChild(pauseBtn);
|
||||
}
|
||||
|
||||
processTimeRemaining (timeRemaining, paused, timerWorker) {
|
||||
if (paused) {
|
||||
this.displayPausedTime(timeRemaining);
|
||||
} else if (timeRemaining === 0) {
|
||||
this.displayExpiredTime();
|
||||
} else {
|
||||
this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function returnHumanReadableTime (milliseconds, tenthsOfSeconds = false) {
|
||||
const tenths = Math.floor((milliseconds / 100) % 10);
|
||||
let seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
let minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
|
||||
let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
|
||||
|
||||
hours = hours < 10 ? '0' + hours : hours;
|
||||
minutes = minutes < 10 ? '0' + minutes : minutes;
|
||||
seconds = seconds < 10 ? '0' + seconds : seconds;
|
||||
|
||||
return tenthsOfSeconds
|
||||
? hours + ':' + minutes + ':' + seconds + '.' + tenths
|
||||
: hours + ':' + minutes + ':' + seconds;
|
||||
}
|
||||
33
client/src/modules/ModalManager.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export const ModalManager = {
|
||||
displayModal: displayModal,
|
||||
dispelModal: dispelModal
|
||||
};
|
||||
|
||||
function displayModal (modalId, backgroundId, closeButtonId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
const modalOverlay = document.getElementById(backgroundId);
|
||||
const closeBtn = document.getElementById(closeButtonId);
|
||||
let closeModalHandler;
|
||||
if (modal && modalOverlay && closeBtn) {
|
||||
modal.style.display = 'flex';
|
||||
modalOverlay.style.display = 'flex';
|
||||
modalOverlay.removeEventListener('click', closeModalHandler);
|
||||
modalOverlay.addEventListener('click', closeModalHandler = function (e) {
|
||||
e.preventDefault();
|
||||
dispelModal(modalId, backgroundId);
|
||||
});
|
||||
closeBtn.removeEventListener('click', closeModalHandler);
|
||||
closeBtn.addEventListener('click', closeModalHandler);
|
||||
} else {
|
||||
throw new Error('One or more of the ids supplied to ModalManager.displayModal is invalid.');
|
||||
}
|
||||
}
|
||||
|
||||
function dispelModal (modalId, backgroundId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
const modalOverlay = document.getElementById(backgroundId);
|
||||
if (modal && modalOverlay) {
|
||||
modal.style.display = 'none';
|
||||
modalOverlay.style.display = 'none';
|
||||
}
|
||||
}
|
||||
54
client/src/modules/Navbar.js
Normal file
@@ -0,0 +1,54 @@
|
||||
export const injectNavbar = (page = null) => {
|
||||
if (document.getElementById('navbar') !== null) {
|
||||
document.getElementById('navbar').innerHTML =
|
||||
"<button name='Mobile Navbar' aria-label='Mobile Navbar' id=\"navbar-hamburger\" class=\"hamburger hamburger--collapse\" type=\"button\">" +
|
||||
'<span class="hamburger-box">' +
|
||||
'<span class="hamburger-inner"></span>' +
|
||||
'</span>' +
|
||||
'</button>' +
|
||||
'<div id="mobile-menu" class="hidden">' +
|
||||
'<div id="mobile-links">' +
|
||||
getNavbarLinks(page, 'mobile') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</a>' +
|
||||
'<div id="desktop-menu">' +
|
||||
'<div id="desktop-links">' +
|
||||
getNavbarLinks(page, 'desktop') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="navbar-profile"></div>';
|
||||
}
|
||||
attachHamburgerListener();
|
||||
};
|
||||
|
||||
function flipHamburger () {
|
||||
const hamburger = document.getElementById('navbar-hamburger');
|
||||
if (hamburger.classList.contains('is-active')) {
|
||||
hamburger.classList.remove('is-active');
|
||||
document.getElementById('mobile-menu').classList.add('hidden');
|
||||
document.getElementById('mobile-menu-background-overlay').classList.remove('overlay');
|
||||
} else {
|
||||
hamburger.classList.add('is-active');
|
||||
document.getElementById('mobile-menu-background-overlay').classList.add('overlay');
|
||||
document.getElementById('mobile-menu').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function getNavbarLinks (page = null, device) {
|
||||
const linkClass = device === 'mobile' ? 'mobile-link' : 'desktop-link';
|
||||
return '<a href="/" class="logo ' + linkClass + '">' +
|
||||
'<img alt="logo" src="../images/Werewolf_Small.png"/>' +
|
||||
'</a>' +
|
||||
'<a class="' + linkClass + '" href="/">Home</a>' +
|
||||
'<a class="' + linkClass + '" href="/create">Create</a>' +
|
||||
'<a class="' + linkClass + '" href="/how-to-use">How to Use</a>' +
|
||||
'<a class="' + linkClass + ' "href="mailto:play.werewolf.contact@gmail.com?Subject=Werewolf App" target="_top">Contact</a>' +
|
||||
'<a class="' + linkClass + '" href="https://www.buymeacoffee.com/alecm33">Support the App</a>';
|
||||
}
|
||||
|
||||
function attachHamburgerListener () {
|
||||
if (document.getElementById('navbar') !== null && document.getElementById('navbar').style.display !== 'none') {
|
||||
document.getElementById('navbar-hamburger').addEventListener('click', flipHamburger);
|
||||
}
|
||||
}
|
||||
8
client/src/modules/StateBucket.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/* It started getting confusing where I am reading/writing to the game state, and thus the state started to get inconsistent.
|
||||
Creating a bucket to hold it so I can overwrite the gameState object whilst still preserving a reference to the containing bucket.
|
||||
Now several components can read a shared game state.
|
||||
*/
|
||||
export const stateBucket = {
|
||||
currentGameState: null,
|
||||
timerWorker: null
|
||||
};
|
||||
266
client/src/modules/Templates.js
Normal file
@@ -0,0 +1,266 @@
|
||||
export const templates = {
|
||||
LOBBY:
|
||||
"<div id='lobby-header'>" +
|
||||
'<div>' +
|
||||
"<label for='game-link'>Share Link</label>" +
|
||||
"<div id='game-link'></div>" +
|
||||
'</div>' +
|
||||
"<div id='game-parameters'>" +
|
||||
'<div>' +
|
||||
"<img alt='clock' src='/images/clock.svg'/>" +
|
||||
"<div id='game-time'></div>" +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<img alt='person' src='/images/person.svg'/>" +
|
||||
"<div id='game-player-count'></div>" +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<button id='role-info-button' class='app-button'>View Role Info <img src='/images/info.svg'</button>" +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<div id='lobby-people-container'>" +
|
||||
"<label for='lobby-players'>Other People</label>" +
|
||||
"<div id='lobby-players'></div>" +
|
||||
'</div>' +
|
||||
"<div id='lobby-footer'>" +
|
||||
"<div id='game-deck'></div>" +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
START_GAME_PROMPT:
|
||||
"<div id='start-game-prompt'>" +
|
||||
"<button id='start-game-button'>Start Game</button>" +
|
||||
'</div>',
|
||||
END_GAME_PROMPT:
|
||||
"<div id='end-game-prompt'>" +
|
||||
"<button id='end-game-button'>End Game</button>" +
|
||||
'</div>',
|
||||
PLAYER_GAME_VIEW:
|
||||
"<div id='game-header'>" +
|
||||
'<div>' +
|
||||
"<label for='game-timer'>Time Remaining</label>" +
|
||||
"<div id='game-timer'></div>" +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<button id='role-info-button' class='app-button'>View Role Info <img src='/images/info.svg'</button>" +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
"<div id='game-role' style='display:none'>" +
|
||||
"<h4 id='role-name'></h4>" +
|
||||
"<img alt='role' id='role-image'/>" +
|
||||
"<p id='role-description'></p>" +
|
||||
'</div>' +
|
||||
"<div id='game-role-back'>" +
|
||||
'<h4>Click to show your role</h4>' +
|
||||
'<p>(click again to hide)</p>' +
|
||||
'</div>' +
|
||||
"<div id='game-people-container'>" +
|
||||
"<label id='players-alive-label'></label>" +
|
||||
"<div id='game-player-list'></div>" +
|
||||
'</div>',
|
||||
SPECTATOR_GAME_VIEW:
|
||||
"<div id='game-header'>" +
|
||||
'<div>' +
|
||||
"<label for='game-timer'>Time Remaining</label>" +
|
||||
"<div id='game-timer'></div>" +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<button id='role-info-button' class='app-button'>View Role Info <img src='/images/info.svg'</button>" +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
"<div id='game-people-container'>" +
|
||||
"<label id='players-alive-label'></label>" +
|
||||
"<div id='game-player-list'></div>" +
|
||||
'</div>',
|
||||
MODERATOR_GAME_VIEW:
|
||||
"<div id='transfer-mod-modal-background' class='modal-background' style='display: none'></div>" +
|
||||
"<div id='transfer-mod-modal' class='modal' style='display: none'>" +
|
||||
'<h3>Transfer Mod Powers 👑</h3>' +
|
||||
"<div id='transfer-mod-modal-content'></div>" +
|
||||
"<div class='modal-button-container'>" +
|
||||
"<button id='close-mod-transfer-modal-button' class='app-button cancel'>Cancel</button>" +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
"<div id='game-header'>" +
|
||||
"<div class='timer-container-moderator'>" +
|
||||
'<div>' +
|
||||
"<label for='game-timer'>Time Remaining</label>" +
|
||||
"<div id='game-timer'></div>" +
|
||||
'</div>' +
|
||||
"<div id='play-pause'>" + '</div>' +
|
||||
'</div>' +
|
||||
"<button id='mod-transfer-button' class='moderator-player-button make-mod-button'>Transfer Mod Powers \uD83D\uDD00</button>" +
|
||||
'<div>' +
|
||||
"<button id='role-info-button' class='app-button'>View Role Info <img src='/images/info.svg'</button>" +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<label id='players-alive-label'></label>" +
|
||||
"<div id='game-player-list'>" +
|
||||
"<div class='evil-players'>" +
|
||||
"<label class='evil'>Team Evil</label>" +
|
||||
"<div id='player-list-moderator-team-evil'></div>" +
|
||||
'</div>' +
|
||||
"<div class='good-players'>" +
|
||||
"<label class='good'>Team Good</label>" +
|
||||
"<div id='player-list-moderator-team-good'></div>" +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
TEMP_MOD_GAME_VIEW:
|
||||
"<div id='transfer-mod-modal-background' class='modal-background' style='display: none'></div>" +
|
||||
"<div id='transfer-mod-modal' class='modal' style='display: none'>" +
|
||||
"<form id='transfer-mod-form'>" +
|
||||
"<div id='transfer-mod-form-content'>" +
|
||||
'<h3>Transfer Mod Powers 👑</h3>' +
|
||||
'</div>' +
|
||||
"<div class='modal-button-container'>" +
|
||||
"<button id='close-modal-button' class='cancel app-button'>Cancel</button>" +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'</div>' +
|
||||
"<div id='game-header'>" +
|
||||
"<div class='timer-container-moderator'>" +
|
||||
'<div>' +
|
||||
"<label for='game-timer'>Time Remaining</label>" +
|
||||
"<div id='game-timer'></div>" +
|
||||
'</div>' +
|
||||
"<div id='play-pause'>" + '</div>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<button id='role-info-button' class='app-button'>View Role Info <img src='/images/info.svg'</button>" +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
"<div id='game-role' style='display:none'>" +
|
||||
"<h4 id='role-name'></h4>" +
|
||||
"<img alt='role' id='role-image'/>" +
|
||||
"<p id='role-description'></p>" +
|
||||
'</div>' +
|
||||
"<div id='game-role-back'>" +
|
||||
'<h4>Click to show your role</h4>' +
|
||||
'<p>(click again to hide)</p>' +
|
||||
'</div>' +
|
||||
"<div id='game-people-container'>" +
|
||||
"<label id='players-alive-label'></label>" +
|
||||
"<div id='game-player-list'></div>" +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
MODERATOR_PLAYER:
|
||||
'<div>' +
|
||||
"<div class='game-player-name'></div>" +
|
||||
"<div class='game-player-role'></div>" +
|
||||
'</div>' +
|
||||
"<div class='player-action-buttons'>" +
|
||||
"<button class='moderator-player-button kill-player-button'>Kill \uD83D\uDC80</button>" +
|
||||
"<button class='moderator-player-button reveal-role-button'>Reveal <img src='../images/eye.svg'/></button>" +
|
||||
'</div>',
|
||||
GAME_PLAYER:
|
||||
'<div>' +
|
||||
"<div class='game-player-name'></div>" +
|
||||
"<div class='game-player-role'></div>" +
|
||||
'</div>',
|
||||
INITIAL_GAME_DOM:
|
||||
"<div id='game-title'></div>" +
|
||||
"<div id='client-container'>" +
|
||||
"<label for='client'>You</label>" +
|
||||
"<div id='client'>" +
|
||||
"<div id='client-name'></div>" +
|
||||
"<div id='client-user-type'></div>" +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
"<div id='game-state-container'></div>",
|
||||
// via https://loading.io/css/
|
||||
SPINNER:
|
||||
'<div class="lds-spinner">' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'<div></div>' +
|
||||
'</div>',
|
||||
NAME_CHANGE_MODAL:
|
||||
"<div id='change-name-modal-background' class='modal-background'></div>" +
|
||||
"<div id='change-name-modal' class='modal'>" +
|
||||
"<form id='change-name-form'>" +
|
||||
"<div id='transfer-mod-form-content'>" +
|
||||
"<label for='player-new-name'>Your name:</label>" +
|
||||
"<input id='player-new-name' type='text'/>" +
|
||||
'</div>' +
|
||||
"<div class='modal-button-container'>" +
|
||||
"<input type='submit' id='submit-new-name' value='Set Name'/>" +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'</div>',
|
||||
ROLE_INFO_MODAL:
|
||||
"<div id='role-info-modal-background' class='modal-background'></div>" +
|
||||
"<div id='role-info-modal' class='modal'>" +
|
||||
'<h2>Roles in this game:</h2>' +
|
||||
"<div id='game-role-info-container'></div>" +
|
||||
"<div class='modal-button-container'>" +
|
||||
"<button id='close-role-info-modal-button' class='app-button'>Close</button>" +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
END_OF_GAME_VIEW:
|
||||
'<h2>The moderator has ended the game. Roles are revealed.</h2>' +
|
||||
"<div id='end-of-game-header'>" +
|
||||
'<div>' +
|
||||
"<button id='role-info-button' class='app-button'>View Role Info <img src='/images/info.svg'</button>" +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<a href='/'>" +
|
||||
"<button class='app-button'>Go Home \uD83C\uDFE0</button>" +
|
||||
'</a>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
"<div id='game-people-container'>" +
|
||||
"<label id='players-alive-label'></label>" +
|
||||
"<div id='game-player-list'></div>" +
|
||||
'</div>',
|
||||
CREATE_GAME_DECK:
|
||||
"<div id='deck-container'>" +
|
||||
'<div>' +
|
||||
"<label for='deck-good'>Available Good Roles</label>" +
|
||||
"<div id='deck-good'></div>" +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<label for='deck-evil'>Available Evil Roles</label>" +
|
||||
"<div id='deck-evil'></div>" +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
CREATE_GAME_CUSTOM_ROLES:
|
||||
'<div id="custom-roles-container">' +
|
||||
'<button id="custom-role-hamburger" class="hamburger hamburger--collapse" type="button">' +
|
||||
'<span class="hamburger-box">' +
|
||||
'<span class="hamburger-inner"></span>' +
|
||||
'</span>' +
|
||||
'</button>' +
|
||||
'<div id="custom-role-actions" style="display:none">' +
|
||||
'<div class="custom-role-action" id="custom-roles-export">Export</div>' +
|
||||
'<div class="custom-role-action" id="custom-roles-import">Import</div>' +
|
||||
'</div>' +
|
||||
'<label for="add-card-to-deck-form">Custom Role Box</label>' +
|
||||
'<div id="deck-select"></div>' +
|
||||
'<button id="custom-role-btn" class="app-button">+ Create Custom Role</button>' +
|
||||
'</div>',
|
||||
CREATE_GAME_DECK_STATUS:
|
||||
'<div id="deck-status-container">' +
|
||||
'<div id="deck-count">0 Players</div>' +
|
||||
'<div id="deck-list"></div>' +
|
||||
'</div>',
|
||||
DECK_SELECT_ROLE:
|
||||
'<div class="deck-select-role-name"></div>' +
|
||||
'<div class="deck-select-role-options">' +
|
||||
'<img class="deck-select-include" src="images/add.svg" title="make available" alt="include"/>' +
|
||||
'<img class="deck-select-info" src="images/info.svg" title="info" alt="info"/>' +
|
||||
'<img class="deck-select-edit" src="images/pencil.svg" title="edit" alt="edit"/>' +
|
||||
'<img class="deck-select-remove" src="images/delete.svg" title="remove" alt="remove"/>' +
|
||||
'</div>'
|
||||
};
|
||||
121
client/src/modules/Timer.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
A timer using setTimeout that compensates for drift. Drift can happen for several reasons:
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#reasons_for_delays
|
||||
|
||||
This means the timer may very well be late in executing the next call (but never early).
|
||||
This timer is accurate to within a few ms for any amount of time provided. It's meant to be utilized as a Web Worker.
|
||||
See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
|
||||
*/
|
||||
|
||||
const messageParameters = {
|
||||
STOP: 'stop',
|
||||
TICK_INTERVAL: 'tickInterval',
|
||||
TOTAL_TIME: 'totalTime'
|
||||
};
|
||||
|
||||
let timer;
|
||||
|
||||
onmessage = function (e) {
|
||||
if (typeof e.data === 'object'
|
||||
&& e.data.hasOwnProperty(messageParameters.TOTAL_TIME)
|
||||
&& e.data.hasOwnProperty(messageParameters.TICK_INTERVAL)
|
||||
) {
|
||||
timer = new Singleton(e.data.totalTime, e.data.tickInterval);
|
||||
timer.startTimer();
|
||||
} else if (e.data === 'stop') {
|
||||
timer.stopTimer();
|
||||
}
|
||||
};
|
||||
|
||||
function stepFn (expected, interval, start, totalTime) {
|
||||
const now = Date.now();
|
||||
if (now - start >= totalTime) {
|
||||
postMessage({
|
||||
timeRemainingInMilliseconds: 0,
|
||||
displayTime: returnHumanReadableTime(0, true)
|
||||
});
|
||||
return;
|
||||
}
|
||||
const delta = now - expected;
|
||||
expected += interval;
|
||||
const displayTime = (totalTime - (now - start)) < 60000
|
||||
? returnHumanReadableTime(totalTime - (now - start), true)
|
||||
: returnHumanReadableTime(totalTime - (now - start));
|
||||
postMessage({
|
||||
timeRemainingInMilliseconds: totalTime - (now - start),
|
||||
displayTime: displayTime
|
||||
});
|
||||
Singleton.setNewTimeoutReference(setTimeout(() => {
|
||||
stepFn(expected, interval, start, totalTime);
|
||||
}, Math.max(0, interval - delta)
|
||||
)); // take into account drift - also retain a reference to this clock tick so it can be cleared later
|
||||
}
|
||||
|
||||
class Timer {
|
||||
constructor (totalTime, tickInterval) {
|
||||
this.timeoutId = undefined;
|
||||
this.totalTime = totalTime;
|
||||
this.tickInterval = tickInterval;
|
||||
}
|
||||
|
||||
startTimer () {
|
||||
if (!isNaN(this.tickInterval)) {
|
||||
const interval = this.tickInterval;
|
||||
const start = Date.now();
|
||||
const expected = Date.now() + this.tickInterval;
|
||||
const totalTime = this.totalTime;
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
this.timeoutId = setTimeout(() => {
|
||||
stepFn(expected, interval, start, totalTime);
|
||||
}, this.tickInterval);
|
||||
}
|
||||
}
|
||||
|
||||
stopTimer () {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Singleton {
|
||||
constructor (totalTime, tickInterval) {
|
||||
if (!Singleton.instance) {
|
||||
Singleton.instance = new Timer(totalTime, tickInterval);
|
||||
} else {
|
||||
// This allows the same timer to be configured to run for different intervals / at a different granularity.
|
||||
Singleton.setNewTimerParameters(totalTime, tickInterval);
|
||||
}
|
||||
return Singleton.instance;
|
||||
}
|
||||
|
||||
static setNewTimerParameters (totalTime, tickInterval) {
|
||||
if (Singleton.instance) {
|
||||
Singleton.instance.totalTime = totalTime;
|
||||
Singleton.instance.tickInterval = tickInterval;
|
||||
}
|
||||
}
|
||||
|
||||
static setNewTimeoutReference (timeoutId) {
|
||||
if (Singleton.instance) {
|
||||
Singleton.instance.timeoutId = timeoutId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function returnHumanReadableTime (milliseconds, tenthsOfSeconds = false) {
|
||||
const tenths = Math.floor((milliseconds / 100) % 10);
|
||||
let seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
let minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
|
||||
let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
|
||||
|
||||
hours = hours < 10 ? '0' + hours : hours;
|
||||
minutes = minutes < 10 ? '0' + minutes : minutes;
|
||||
seconds = seconds < 10 ? '0' + seconds : seconds;
|
||||
|
||||
return tenthsOfSeconds
|
||||
? hours + ':' + minutes + ':' + seconds + '.' + tenths
|
||||
: hours + ':' + minutes + ':' + seconds;
|
||||
}
|
||||
48
client/src/modules/Toast.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { globals } from '../config/globals.js';
|
||||
|
||||
export const toast = (message, type, positionAtTop = true, dispelAutomatically = true, duration = null) => {
|
||||
if (message && type) {
|
||||
buildAndInsertMessageElement(message, type, positionAtTop, dispelAutomatically, duration);
|
||||
}
|
||||
};
|
||||
|
||||
function buildAndInsertMessageElement (message, type, positionAtTop, dispelAutomatically, duration) {
|
||||
cancelCurrentToast();
|
||||
let backgroundColor, border;
|
||||
const position = positionAtTop ? 'top:2rem;' : 'bottom: 90px;';
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
backgroundColor = '#fff5b1';
|
||||
border = '3px solid #c7c28a';
|
||||
break;
|
||||
case 'error':
|
||||
backgroundColor = '#fdaeb7';
|
||||
border = '3px solid #c78a8a';
|
||||
break;
|
||||
case 'success':
|
||||
backgroundColor = '#bef5cb';
|
||||
border = '3px solid #8ac78a;';
|
||||
break;
|
||||
}
|
||||
|
||||
const durationInSeconds = duration ? duration + 's' : globals.TOAST_DURATION_DEFAULT + 's';
|
||||
let animation = '';
|
||||
if (dispelAutomatically) {
|
||||
animation = 'animation:fade-in-slide-down-then-exit ' + durationInSeconds + ' ease normal forwards';
|
||||
} else {
|
||||
animation = 'animation:fade-in-slide-down ' + durationInSeconds + ' ease normal forwards';
|
||||
}
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.setAttribute('id', 'current-info-message');
|
||||
messageEl.setAttribute('style', 'background-color:' + backgroundColor + ';' + 'border:' + border + ';' + position + animation);
|
||||
messageEl.setAttribute('class', 'info-message');
|
||||
messageEl.innerText = message;
|
||||
document.body.prepend(messageEl);
|
||||
}
|
||||
|
||||
export function cancelCurrentToast () {
|
||||
const currentMessage = document.getElementById('current-info-message');
|
||||
if (currentMessage !== null) {
|
||||
currentMessage.remove();
|
||||
}
|
||||
}
|
||||