webpack
12
client/src/config/customCards.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export const customCards = [
|
||||
{
|
||||
role: "Santa",
|
||||
team: "evil",
|
||||
description: "hohoho",
|
||||
},
|
||||
{
|
||||
role: "The Thing",
|
||||
team: "good",
|
||||
description: "you go bump in the night",
|
||||
},
|
||||
];
|
||||
42
client/src/config/defaultCards.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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: "If a Werewolf dies, you become a Werewolf. You do not wake up with the Werewolves until this happens. You count for parity only after converting to a wolf.",
|
||||
},
|
||||
{
|
||||
role: "Knowing Minion",
|
||||
team: "evil",
|
||||
description: "You are an evil villager - you know who the wolves are, and you want them to win.",
|
||||
},
|
||||
{
|
||||
role: "Double-Blind Minion",
|
||||
team: "evil",
|
||||
description: "You are an evil villager. You don't know who the wolves are, but you want them to win.",
|
||||
},
|
||||
{
|
||||
role: "Seer",
|
||||
team: "good",
|
||||
description: "During each night, choose one person. The moderator will tell you whether that player is a wolf.",
|
||||
},
|
||||
{
|
||||
role: "Hunter",
|
||||
team: "good",
|
||||
description: "If you are alive with a wolf at the end of the game, you best the wolf, and the village wins.",
|
||||
},
|
||||
{
|
||||
role: "Mason",
|
||||
team: "good",
|
||||
description: "Masons know who the other masons are.",
|
||||
}
|
||||
];
|
||||
59
client/src/config/globals.js
Normal file
@@ -0,0 +1,59 @@
|
||||
export const globals = {
|
||||
USER_SIGNATURE_LENGTH: 25,
|
||||
CLOCK_TICK_INTERVAL_MILLIS: 10,
|
||||
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'
|
||||
},
|
||||
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'
|
||||
},
|
||||
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-32px.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
client/src/images/Werewolf.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
client/src/images/Werewolf_Small.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
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 |
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 |
14
client/src/images/info.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<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="none" fill="#d7d7d7"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
client/src/images/logo.gif
Normal file
|
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 |
107
client/src/images/person.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="0 0 119.66505 109.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:#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"
|
||||
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:#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:#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>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
7
client/src/images/play-button.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<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" 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>
|
||||
|
After Width: | Height: | Size: 541 B |
BIN
client/src/images/roles/Double-BlindMinion.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
client/src/images/roles/DreamWolf.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
client/src/images/roles/Hunter.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
client/src/images/roles/KnowingMinion.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
client/src/images/roles/Mason.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
client/src/images/roles/Seer.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
client/src/images/roles/Shadow.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
client/src/images/roles/Sorcerer.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
client/src/images/roles/Villager-2.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
client/src/images/roles/Villager.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
client/src/images/roles/Werewolf.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
client/src/images/tombstone.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
client/src/images/vanilla_js.png
Normal file
|
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;
|
||||
}
|
||||
}
|
||||
58
client/src/modules/DeckStateManager.js
Normal file
@@ -0,0 +1,58 @@
|
||||
export class DeckStateManager {
|
||||
constructor() {
|
||||
this.deck = null;
|
||||
this.customRoleOptions = null;
|
||||
}
|
||||
|
||||
addToDeck(role) {
|
||||
let option = this.customRoleOptions.find((option) => option.role === role);
|
||||
let existingCard = this.deck.find((card) => card.role === role);
|
||||
if (option && !existingCard) {
|
||||
option.quantity = 0;
|
||||
this.deck.push(option);
|
||||
this.customRoleOptions.splice(this.customRoleOptions.indexOf(option), 1);
|
||||
}
|
||||
}
|
||||
|
||||
addToCustomRoleOptions(role) {
|
||||
this.customRoleOptions.push(role);
|
||||
}
|
||||
|
||||
addCopyOfCard(role) {
|
||||
let existingCard = this.deck.find((card) => card.role === role)
|
||||
if (existingCard) {
|
||||
existingCard.quantity += 1;
|
||||
}
|
||||
}
|
||||
|
||||
removeCopyOfCard(role) {
|
||||
let 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 (let role of this.deck) {
|
||||
total += role.quantity;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
494
client/src/modules/GameCreationStepManager.js
Normal file
@@ -0,0 +1,494 @@
|
||||
import { Game } from "../model/Game.js";
|
||||
import { cancelCurrentToast, toast } from "./Toast.js";
|
||||
import { customCards } from "../config/customCards.js";
|
||||
import { ModalManager } from "./ModalManager.js";
|
||||
import {XHRUtility} from "./XHRUtility.js";
|
||||
import {globals} from "../config/globals.js";
|
||||
|
||||
export class GameCreationStepManager {
|
||||
constructor(deckManager) {
|
||||
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.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 include enough cards for 5 players.", "error", true);
|
||||
}
|
||||
},
|
||||
backHandler: this.defaultBackHandler
|
||||
},
|
||||
3: {
|
||||
title: "Set an optional timer:",
|
||||
forwardHandler: () => {
|
||||
let hours = parseInt(document.getElementById("game-hours").value);
|
||||
let 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;
|
||||
}
|
||||
console.log(this.currentGame);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>";
|
||||
|
||||
let dedicatedOption = stepContainer.querySelector('#moderation-dedicated');
|
||||
if (game.hasDedicatedModerator) {
|
||||
dedicatedOption.classList.add('option-selected');
|
||||
}
|
||||
let 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'});
|
||||
|
||||
let div = document.createElement("div");
|
||||
div.setAttribute("id", "deck-container");
|
||||
let deckContainer = document.createElement("div");
|
||||
deckContainer.setAttribute("id", "deck");
|
||||
|
||||
deckContainer = loadIncludedCards(deckManager, deckContainer);
|
||||
|
||||
let deckLabel = document.createElement("label");
|
||||
deckLabel.setAttribute("for", "deck");
|
||||
deckLabel.innerText = 'Game Deck: ' + deckManager.getDeckSize() + ' Players';
|
||||
div.prepend(deckLabel);
|
||||
div.appendChild(deckContainer);
|
||||
stepContainer.appendChild(div);
|
||||
|
||||
let customForm = loadCustomRoles(deckManager);
|
||||
|
||||
stepContainer.prepend(customForm);
|
||||
|
||||
document.getElementById(containerId).appendChild(stepContainer);
|
||||
|
||||
initializeRemainingEventListeners(deckManager);
|
||||
}
|
||||
|
||||
function renderTimerStep(containerId, stepNumber, game) {
|
||||
let div = document.createElement("div");
|
||||
div.setAttribute("id", "step-" + stepNumber);
|
||||
div.classList.add('step');
|
||||
|
||||
let timeContainer = document.createElement("div");
|
||||
timeContainer.setAttribute("id", "game-time");
|
||||
|
||||
let hoursDiv = document.createElement("div");
|
||||
let hoursLabel = document.createElement("label");
|
||||
hoursLabel.setAttribute("for", "game-hours");
|
||||
hoursLabel.innerText = "Hours (max 5)"
|
||||
let hours = document.createElement("input");
|
||||
setAttributes(hours, {type: "number", id: "game-hours", name: "game-hours", min: "0", max: "5", value: game.timerParams?.hours})
|
||||
|
||||
let minutesDiv = document.createElement("div");
|
||||
let minsLabel = document.createElement("label");
|
||||
minsLabel.setAttribute("for", "game-minutes");
|
||||
minsLabel.innerText = "Minutes"
|
||||
let 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) {
|
||||
let 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) {
|
||||
let formattedHours = !isNaN(game.timerParams.hours)
|
||||
? game.timerParams.hours + ' Hours'
|
||||
: '0 Hours'
|
||||
|
||||
let 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 (let card of game.deck) {
|
||||
let 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 (let 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) {
|
||||
let backButton = document.createElement("button");
|
||||
backButton.innerText = "\u2bc7 Back";
|
||||
backButton.addEventListener('click', backHandler);
|
||||
backButton.setAttribute("id", "step-back-button");
|
||||
document.getElementById("tracker-container").prepend(backButton);
|
||||
}
|
||||
|
||||
if (forward && builtGame === null) {
|
||||
let fwdButton = document.createElement("button");
|
||||
fwdButton.innerHTML = "Next \u25b6";
|
||||
fwdButton.addEventListener('click', forwardHandler);
|
||||
fwdButton.setAttribute("id", "step-forward-button");
|
||||
document.getElementById("tracker-container").appendChild(fwdButton);
|
||||
} else if (forward && builtGame !== null) {
|
||||
let createButton = document.createElement("button");
|
||||
createButton.innerText = "Create";
|
||||
createButton.setAttribute("id", "create-game");
|
||||
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 loadIncludedCards(deckManager, deckContainer) {
|
||||
|
||||
for (let i = 0; i < deckManager.getCurrentDeck().length; i ++) { // each dropdown should include every
|
||||
let card = deckManager.getCurrentDeck()[i];
|
||||
let cardEl = constructCompactDeckBuilderElement(card, deckManager);
|
||||
deckContainer.appendChild(cardEl);
|
||||
}
|
||||
|
||||
return deckContainer;
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
let customContainer = document.createElement("div");
|
||||
customContainer.setAttribute("id", "custom-roles-container");
|
||||
|
||||
let formLabel = document.createElement("label");
|
||||
formLabel.innerText = 'Custom Roles';
|
||||
formLabel.setAttribute("for", "add-card-to-deck-form");
|
||||
|
||||
let createRoleButton = document.createElement("button");
|
||||
createRoleButton.setAttribute("id", "custom-role-btn");
|
||||
createRoleButton.innerText = '+ Create Custom Role';
|
||||
|
||||
let form = document.createElement("form");
|
||||
form.setAttribute("id", "add-card-to-deck-form");
|
||||
|
||||
let selectEl = document.createElement("select");
|
||||
selectEl.setAttribute("id", "deck-select");
|
||||
addOptionsToList(deckManager.getCurrentCustomRoleOptions(), selectEl);
|
||||
|
||||
form.appendChild(selectEl);
|
||||
|
||||
let submitBtn = document.createElement("input");
|
||||
submitBtn.setAttribute("type", "submit");
|
||||
submitBtn.setAttribute("value", "Include Role");
|
||||
submitBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (selectEl.value && selectEl.value.length > 0) {
|
||||
deckManager.addToDeck(selectEl.value);
|
||||
let cardEl = constructCompactDeckBuilderElement(deckManager.getCard(selectEl.value), deckManager);
|
||||
toast('"' + selectEl.value + '" included.', 'success', true, true, 3);
|
||||
updateCustomRoleOptionsList(deckManager, selectEl);
|
||||
document.getElementById("deck").appendChild(cardEl);
|
||||
}
|
||||
})
|
||||
form.appendChild(submitBtn);
|
||||
|
||||
customContainer.appendChild(formLabel);
|
||||
customContainer.appendChild(form);
|
||||
customContainer.appendChild(createRoleButton);
|
||||
|
||||
return customContainer;
|
||||
}
|
||||
|
||||
function constructCompactDeckBuilderElement(card, deckManager) {
|
||||
const cardContainer = document.createElement("div");
|
||||
let 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);
|
||||
cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity;
|
||||
document.querySelector('label[for="deck"]').innerText = 'Game Deck: ' + deckManager.getDeckSize() + ' Players';
|
||||
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);
|
||||
cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity;
|
||||
document.querySelector('label[for="deck"]').innerText = 'Game Deck: ' + deckManager.getDeckSize() + ' Players';
|
||||
if (deckManager.getCard(card.role).quantity === 0) {
|
||||
document.getElementById('card-' + card.role.replaceAll(' ', '-')).classList.remove('selected-card')
|
||||
}
|
||||
});
|
||||
return cardContainer;
|
||||
}
|
||||
|
||||
function initializeRemainingEventListeners(deckManager) {
|
||||
document.getElementById("add-role-form").onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let name = document.getElementById("role-name").value.trim();
|
||||
let description = document.getElementById("role-description").value.trim();
|
||||
if (!deckManager.getCustomRoleOption(name)) { // confirm there is no existing custom role with the same name
|
||||
deckManager.addToCustomRoleOptions({role: name, description: description});
|
||||
updateCustomRoleOptionsList(deckManager, document.getElementById("deck-select"))
|
||||
ModalManager.dispelModal("add-role-modal", "add-role-modal-background");
|
||||
toast("Role Added", "success", true);
|
||||
} else {
|
||||
toast("There is already a custom role with this name.", "error", true);
|
||||
}
|
||||
}
|
||||
document.getElementById("custom-role-btn").addEventListener(
|
||||
"click", () => {
|
||||
ModalManager.displayModal(
|
||||
"add-role-modal",
|
||||
"add-role-modal-background",
|
||||
"close-modal-button"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function updateCustomRoleOptionsList(deckManager, selectEl) {
|
||||
document.querySelectorAll('#deck-select option').forEach(e => e.remove());
|
||||
addOptionsToList(deckManager.customRoleOptions, selectEl);
|
||||
}
|
||||
|
||||
function addOptionsToList(options, selectEl) {
|
||||
options.sort((a, b) => {
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
for (let i = 0; i < options.length; i ++) {
|
||||
let optionEl = document.createElement("option");
|
||||
let alignmentClass = customCards[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL
|
||||
optionEl.classList.add(alignmentClass);
|
||||
optionEl.setAttribute("value", customCards[i].role);
|
||||
optionEl.innerText = customCards[i].role;
|
||||
selectEl.appendChild(optionEl);
|
||||
}
|
||||
}
|
||||
|
||||
function hasTimer(hours, minutes) {
|
||||
return (!isNaN(hours) || !isNaN(minutes));
|
||||
}
|
||||
459
client/src/modules/GameStateRenderer.js
Normal file
@@ -0,0 +1,459 @@
|
||||
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 = {};
|
||||
}
|
||||
|
||||
renderLobbyPlayers() {
|
||||
document.querySelectorAll('.lobby-player').forEach((el) => el.remove())
|
||||
let lobbyPlayersContainer = document.getElementById("lobby-players");
|
||||
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.PLAYER
|
||||
&& this.stateBucket.currentGameState.moderator.userType === globals.USER_TYPES.MODERATOR
|
||||
) {
|
||||
lobbyPlayersContainer.appendChild(
|
||||
renderLobbyPerson(
|
||||
this.stateBucket.currentGameState.moderator.name,
|
||||
this.stateBucket.currentGameState.moderator.userType
|
||||
)
|
||||
)
|
||||
}
|
||||
for (let person of this.stateBucket.currentGameState.people) {
|
||||
lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name,person.userType))
|
||||
}
|
||||
let playerCount = this.stateBucket.currentGameState.people.length;
|
||||
document.querySelector("label[for='lobby-players']").innerText =
|
||||
"People (" + playerCount + "/" + getGameSize(this.stateBucket.currentGameState.deck) + " Players)";
|
||||
}
|
||||
|
||||
renderLobbyHeader() {
|
||||
removeExistingTitle();
|
||||
let title = document.createElement("h1");
|
||||
title.innerText = "Lobby";
|
||||
document.getElementById("game-title").appendChild(title);
|
||||
let gameLinkContainer = document.getElementById("game-link");
|
||||
gameLinkContainer.innerText = window.location;
|
||||
gameLinkContainer.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(gameLinkContainer.innerText).then(() => {
|
||||
toast('Link copied!', 'success', true);
|
||||
});
|
||||
});
|
||||
let copyImg = document.createElement("img");
|
||||
copyImg.setAttribute("src", "../images/copy.svg");
|
||||
gameLinkContainer.appendChild(copyImg);
|
||||
|
||||
let time = document.getElementById("game-time");
|
||||
let playerCount = document.getElementById("game-player-count");
|
||||
playerCount.innerText = getGameSize(this.stateBucket.currentGameState.deck) + ' Players'
|
||||
|
||||
if (this.stateBucket.currentGameState.timerParams) {
|
||||
let timeString = "";
|
||||
let hours = this.stateBucket.currentGameState.timerParams.hours;
|
||||
let 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() {
|
||||
let gameDeckContainer = document.getElementById("game-deck");
|
||||
for (let card of this.stateBucket.currentGameState.deck) {
|
||||
let 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() {
|
||||
let div = document.createElement("div");
|
||||
div.innerHTML = templates.END_GAME_PROMPT;
|
||||
document.getElementById("game-content").appendChild(div);
|
||||
document.getElementById("end-game-button").addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (confirm("End the game?")) {
|
||||
this.socket.emit(
|
||||
globals.COMMANDS.END_GAME,
|
||||
this.stateBucket.currentGameState.accessCode
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let 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() {
|
||||
let div = document.createElement("div");
|
||||
div.innerHTML = templates.END_GAME_PROMPT;
|
||||
document.body.appendChild(div);
|
||||
|
||||
renderPlayerRole(this.stateBucket.currentGameState);
|
||||
this.renderPlayersWithNoRoleInformationUnlessRevealed(true);
|
||||
}
|
||||
|
||||
renderPlayerView(isKilled=false) {
|
||||
if (isKilled) {
|
||||
let 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;
|
||||
});
|
||||
let teamGood = this.stateBucket.currentGameState.people.filter((person) => person.alignment === globals.ALIGNMENT.GOOD);
|
||||
let 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) => {
|
||||
let 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);
|
||||
let 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.querySelectorAll('.potential-moderator').forEach((el) => {
|
||||
let 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
|
||||
);
|
||||
}
|
||||
|
||||
renderEndOfGame() {
|
||||
this.renderPlayersWithNoRoleInformationUnlessRevealed();
|
||||
}
|
||||
}
|
||||
|
||||
function renderPotentialMods(gameState, group, transferModHandlers, socket) {
|
||||
let modalContent = document.getElementById("transfer-mod-modal-content");
|
||||
for (let member of group) {
|
||||
if ((member.out || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) {
|
||||
let 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);
|
||||
console.log('test');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderLobbyPerson(name, userType) {
|
||||
let el = document.createElement("div");
|
||||
let personNameEl = document.createElement("div");
|
||||
let 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 (let card of cards) {
|
||||
quantity += card.quantity;
|
||||
}
|
||||
|
||||
return quantity;
|
||||
}
|
||||
|
||||
function removeExistingTitle() {
|
||||
let 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 (let player of people) {
|
||||
let 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;
|
||||
let 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) {
|
||||
let name = document.querySelector('#role-name');
|
||||
name.innerText = gameState.client.gameRole;
|
||||
if (gameState.client.alignment === globals.ALIGNMENT.GOOD) {
|
||||
name.classList.add('good');
|
||||
} else {
|
||||
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 {
|
||||
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) {
|
||||
let 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) => {
|
||||
let 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();
|
||||
});
|
||||
}
|
||||
170
client/src/modules/GameTimerManager.js
Normal file
@@ -0,0 +1,170 @@
|
||||
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();
|
||||
}
|
||||
let instance = this;
|
||||
let 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');
|
||||
let 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();
|
||||
}
|
||||
|
||||
let 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() {
|
||||
let currentBtn = document.querySelector('#play-pause img');
|
||||
if (currentBtn) {
|
||||
currentBtn.removeEventListener('click', this.pauseListener);
|
||||
currentBtn.removeEventListener('click', this.playListener);
|
||||
currentBtn.remove();
|
||||
}
|
||||
|
||||
let 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) => {
|
||||
console.log('received time remaining from server');
|
||||
if (paused) {
|
||||
this.displayPausedTime(timeRemaining);
|
||||
} else if (timeRemaining === 0) {
|
||||
this.displayExpiredTime();
|
||||
} else {
|
||||
this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
swapToPlayButton() {
|
||||
let currentBtn = document.querySelector('#play-pause img');
|
||||
if (currentBtn) {
|
||||
currentBtn.removeEventListener('click', this.pauseListener);
|
||||
currentBtn.remove();
|
||||
}
|
||||
|
||||
let playBtn = document.createElement('img');
|
||||
playBtn.setAttribute('src', '../images/play-button.svg');
|
||||
playBtn.addEventListener('click', this.playListener);
|
||||
document.getElementById('play-pause').appendChild(playBtn);
|
||||
}
|
||||
|
||||
swapToPauseButton() {
|
||||
let currentBtn = document.querySelector('#play-pause img');
|
||||
if (currentBtn) {
|
||||
currentBtn.removeEventListener('click', this.playListener);
|
||||
currentBtn.remove();
|
||||
}
|
||||
|
||||
let pauseBtn = document.createElement('img');
|
||||
pauseBtn.setAttribute('src', '../images/pause-button.svg');
|
||||
pauseBtn.addEventListener('click', this.pauseListener);
|
||||
document.getElementById('play-pause').appendChild(pauseBtn);
|
||||
}
|
||||
}
|
||||
|
||||
function returnHumanReadableTime(milliseconds, tenthsOfSeconds=false) {
|
||||
|
||||
let 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';
|
||||
}
|
||||
}
|
||||
7
client/src/modules/StateBucket.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/* 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.
|
||||
*/
|
||||
export const stateBucket = {
|
||||
currentGameState: null,
|
||||
timerWorker: null
|
||||
}
|
||||
225
client/src/modules/Templates.js
Normal file
@@ -0,0 +1,225 @@
|
||||
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'>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'>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 reveal 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'>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 id='modal-button-container'>" +
|
||||
"<button id='close-mod-transfer-modal-button'>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'>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 id='modal-button-container'>" +
|
||||
"<button id='close-modal-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>" +
|
||||
"<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 reveal 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 Role <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 id='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 id='modal-button-container'>" +
|
||||
"<button id='close-role-info-modal-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'>View Role Info <img src='/images/info.svg'</button>" +
|
||||
"</div>" +
|
||||
"<div>" +
|
||||
"<a href='/'>" +
|
||||
"<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>"
|
||||
|
||||
}
|
||||
122
client/src/modules/Timer.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
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;
|
||||
let 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) {
|
||||
|
||||
let 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;
|
||||
}
|
||||
|
||||
let 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();
|
||||
}
|
||||
}
|
||||
63
client/src/modules/UserUtility.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { globals } from '../config/globals.js';
|
||||
|
||||
/*
|
||||
we will use sessionStorage during local development to aid in testing, vs. localStorage for production.
|
||||
sessionStorage does not persist across tabs, allowing developers to join a game as different players from different windows.
|
||||
*/
|
||||
export const UserUtility = {
|
||||
|
||||
createNewAnonymousUserId (force = true, environment) {
|
||||
let newId, currentId;
|
||||
|
||||
if (environment === globals.ENVIRONMENT.LOCAL) {
|
||||
currentId = sessionStorage.getItem(globals.PLAYER_ID_COOKIE_KEY);
|
||||
} else {
|
||||
currentId = localStorage.getItem(globals.PLAYER_ID_COOKIE_KEY);
|
||||
}
|
||||
if (currentId !== null && !force) {
|
||||
newId = currentId;
|
||||
} else {
|
||||
newId = createRandomUserId();
|
||||
if (environment === globals.ENVIRONMENT.LOCAL) {
|
||||
sessionStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, newId);
|
||||
} else {
|
||||
localStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, newId);
|
||||
}
|
||||
}
|
||||
return newId;
|
||||
},
|
||||
|
||||
setAnonymousUserId (id, environment) {
|
||||
if (environment === globals.ENVIRONMENT.LOCAL) {
|
||||
sessionStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, id);
|
||||
} else {
|
||||
localStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, id);
|
||||
}
|
||||
},
|
||||
|
||||
validateAnonUserSignature (environment) {
|
||||
let userSig;
|
||||
if (environment === globals.ENVIRONMENT.LOCAL) {
|
||||
userSig = sessionStorage.getItem(globals.PLAYER_ID_COOKIE_KEY);
|
||||
} else {
|
||||
userSig = localStorage.getItem(globals.PLAYER_ID_COOKIE_KEY);
|
||||
}
|
||||
return (
|
||||
userSig
|
||||
&& typeof userSig === 'string'
|
||||
&& /^[a-zA-Z0-9]+$/.test(userSig)
|
||||
&& userSig.length === globals.USER_SIGNATURE_LENGTH
|
||||
)
|
||||
? userSig
|
||||
: false;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function createRandomUserId () {
|
||||
let id = '';
|
||||
for (let i = 0; i < globals.USER_SIGNATURE_LENGTH; i++) {
|
||||
id += globals.ACCESS_CODE_CHAR_POOL[Math.floor(Math.random() * globals.ACCESS_CODE_CHAR_POOL.length)];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
40
client/src/modules/XHRUtility.js
Normal file
@@ -0,0 +1,40 @@
|
||||
export const XHRUtility =
|
||||
{
|
||||
standardHeaders: [['Content-Type', 'application/json'], ['Accept', 'application/json'], ['X-Requested-With', 'XMLHttpRequest']],
|
||||
|
||||
// Easily make XHR calls with a promise wrapper. Defaults to GET and MIME type application/JSON
|
||||
xhr (url, method = 'GET', headers, body = null) {
|
||||
if (headers === undefined || headers === null) {
|
||||
headers = this.standardHeaders;
|
||||
}
|
||||
if (typeof url !== 'string' || url.trim().length < 1) {
|
||||
return Promise.reject('Cannot request with empty URL: ' + url);
|
||||
}
|
||||
|
||||
const req = new XMLHttpRequest();
|
||||
req.open(method, url.trim());
|
||||
|
||||
for (const hdr of headers) {
|
||||
if (hdr.length !== 2) continue;
|
||||
req.setRequestHeader(hdr[0], hdr[1]);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onload = function () {
|
||||
const response = {
|
||||
status: this.status,
|
||||
statusText: this.statusText,
|
||||
content: this.responseText
|
||||
};
|
||||
if (this.status >= 200 && this.status < 400) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(response);
|
||||
}
|
||||
};
|
||||
body ? req.send(body) : req.send();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
38
client/src/scripts/create.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defaultCards } from "../config/defaultCards.js";
|
||||
import { customCards } from "../config/customCards.js";
|
||||
import { DeckStateManager } from "../modules/DeckStateManager.js";
|
||||
import { GameCreationStepManager } from "../modules/GameCreationStepManager.js";
|
||||
|
||||
const create = () => {
|
||||
let deckManager = new DeckStateManager();
|
||||
let gameCreationStepManager = new GameCreationStepManager(deckManager);
|
||||
loadDefaultCards(deckManager);
|
||||
loadCustomRoles(deckManager);
|
||||
gameCreationStepManager.renderStep("creation-step-container", 1);
|
||||
}
|
||||
|
||||
function loadDefaultCards(deckManager) {
|
||||
defaultCards.sort((a, b) => {
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
let deck = [];
|
||||
for (let i = 0; i < defaultCards.length; i ++) {
|
||||
let card = defaultCards[i];
|
||||
card.quantity = 0;
|
||||
deck.push(card);
|
||||
}
|
||||
deckManager.deck = deck;
|
||||
}
|
||||
|
||||
function loadCustomRoles(deckManager) {
|
||||
customCards.sort((a, b) => {
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
deckManager.customRoleOptions = customCards;
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = create;
|
||||
} else {
|
||||
create();
|
||||
}
|
||||
375
client/src/scripts/game.js
Normal file
@@ -0,0 +1,375 @@
|
||||
import { UserUtility } from "../modules/UserUtility.js";
|
||||
import { globals } from "../config/globals.js";
|
||||
import {templates} from "../modules/Templates.js";
|
||||
import {GameStateRenderer} from "../modules/GameStateRenderer.js";
|
||||
import {cancelCurrentToast, toast} from "../modules/Toast.js";
|
||||
import {GameTimerManager} from "../modules/GameTimerManager.js";
|
||||
import {ModalManager} from "../modules/ModalManager.js";
|
||||
import {stateBucket} from "../modules/StateBucket.js";
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
const game = () => {
|
||||
let timerWorker;
|
||||
const socket = io('/in-game');
|
||||
socket.on('disconnect', () => {
|
||||
if (timerWorker) {
|
||||
timerWorker.terminate();
|
||||
}
|
||||
toast('Disconnected. Attempting reconnect...', 'error', true, false);
|
||||
});
|
||||
socket.on('connect', () => {
|
||||
socket.emit(globals.COMMANDS.GET_ENVIRONMENT, function(returnedEnvironment) {
|
||||
timerWorker = new Worker(new URL('../modules/Timer.js', import.meta.url));
|
||||
prepareGamePage(returnedEnvironment, socket, timerWorker);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
function prepareGamePage(environment, socket, timerWorker) {
|
||||
let userId = UserUtility.validateAnonUserSignature(environment);
|
||||
const splitUrl = window.location.href.split('/game/');
|
||||
const accessCode = splitUrl[1];
|
||||
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
|
||||
socket.emit(globals.COMMANDS.FETCH_GAME_STATE, accessCode, userId, function (gameState) {
|
||||
stateBucket.currentGameState = gameState;
|
||||
document.querySelector('.spinner-container')?.remove();
|
||||
document.querySelector('.spinner-background')?.remove();
|
||||
|
||||
if (gameState === null) {
|
||||
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
|
||||
}
|
||||
|
||||
document.getElementById("game-content").innerHTML = templates.INITIAL_GAME_DOM;
|
||||
toast('You are connected.', 'success', true, true, 2);
|
||||
console.log(gameState);
|
||||
userId = gameState.client.cookie;
|
||||
UserUtility.setAnonymousUserId(userId, environment);
|
||||
let gameStateRenderer = new GameStateRenderer(stateBucket, socket);
|
||||
let gameTimerManager;
|
||||
if (stateBucket.currentGameState.timerParams) {
|
||||
gameTimerManager = new GameTimerManager(stateBucket, socket);
|
||||
}
|
||||
initializeGame(stateBucket, socket, timerWorker, userId, gameStateRenderer, gameTimerManager);
|
||||
|
||||
if (!gameState.client.hasEnteredName) {
|
||||
document.getElementById("prompt").innerHTML = templates.NAME_CHANGE_MODAL;
|
||||
document.getElementById("change-name-form").onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let name = document.getElementById("player-new-name").value;
|
||||
if (validateName(name)) {
|
||||
socket.emit(globals.COMMANDS.CHANGE_NAME, gameState.accessCode, { name: name, personId: gameState.client.id }, (result) => {
|
||||
switch (result) {
|
||||
case "taken":
|
||||
toast('This name is already taken.', 'error', true, true, 8);
|
||||
break;
|
||||
case "changed":
|
||||
ModalManager.dispelModal("change-name-modal", "change-name-modal-background")
|
||||
toast('Name set.', 'success', true, true, 5);
|
||||
propagateNameChange(stateBucket.currentGameState, name, stateBucket.currentGameState.client.id);
|
||||
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast("Name must be between 1 and 30 characters.", 'error', true, true, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeGame(stateBucket, socket, timerWorker, userId, gameStateRenderer, gameTimerManager) {
|
||||
setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager);
|
||||
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer);
|
||||
}
|
||||
|
||||
function processGameState (currentGameState, userId, socket, gameStateRenderer) {
|
||||
displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
|
||||
switch (currentGameState.status) {
|
||||
case globals.STATUS.LOBBY:
|
||||
document.getElementById("game-state-container").innerHTML = templates.LOBBY;
|
||||
gameStateRenderer.renderLobbyHeader();
|
||||
gameStateRenderer.renderLobbyPlayers();
|
||||
if (
|
||||
currentGameState.isFull
|
||||
&& (
|
||||
currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|
||||
|| currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|
||||
)
|
||||
) {
|
||||
displayStartGamePromptForModerators(currentGameState, socket);
|
||||
}
|
||||
break;
|
||||
case globals.STATUS.IN_PROGRESS:
|
||||
gameStateRenderer.renderGameHeader();
|
||||
switch (currentGameState.client.userType) {
|
||||
case globals.USER_TYPES.PLAYER:
|
||||
document.getElementById("game-state-container").innerHTML = templates.PLAYER_GAME_VIEW;
|
||||
gameStateRenderer.renderPlayerView();
|
||||
break;
|
||||
case globals.USER_TYPES.KILLED_PLAYER:
|
||||
document.querySelector("#end-game-prompt")?.remove();
|
||||
document.getElementById("game-state-container").innerHTML = templates.PLAYER_GAME_VIEW;
|
||||
gameStateRenderer.renderPlayerView(true);
|
||||
break;
|
||||
case globals.USER_TYPES.MODERATOR:
|
||||
document.querySelector("#start-game-prompt")?.remove();
|
||||
document.getElementById("game-state-container").innerHTML = templates.MODERATOR_GAME_VIEW;
|
||||
gameStateRenderer.renderModeratorView();
|
||||
break;
|
||||
case globals.USER_TYPES.TEMPORARY_MODERATOR:
|
||||
document.querySelector("#start-game-prompt")?.remove();
|
||||
document.getElementById("game-state-container").innerHTML = templates.TEMP_MOD_GAME_VIEW;
|
||||
gameStateRenderer.renderTempModView();
|
||||
break;
|
||||
case globals.USER_TYPES.SPECTATOR:
|
||||
document.querySelector("#end-game-prompt")?.remove();
|
||||
document.getElementById("game-state-container").innerHTML = templates.SPECTATOR_GAME_VIEW;
|
||||
gameStateRenderer.renderSpectatorView();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (currentGameState.timerParams) {
|
||||
socket.emit(globals.COMMANDS.GET_TIME_REMAINING, currentGameState.accessCode);
|
||||
} else {
|
||||
document.querySelector('#game-timer')?.remove();
|
||||
document.querySelector('label[for="game-timer"]')?.remove();
|
||||
}
|
||||
break;
|
||||
case globals.STATUS.ENDED:
|
||||
let container = document.getElementById("game-state-container")
|
||||
container.innerHTML = templates.END_OF_GAME_VIEW;
|
||||
container.classList.add('vertical-flex');
|
||||
gameStateRenderer.renderEndOfGame();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
activateRoleInfoButton(stateBucket.currentGameState.deck);
|
||||
}
|
||||
|
||||
function displayClientInfo(name, userType) {
|
||||
document.getElementById("client-name").innerText = name;
|
||||
document.getElementById("client-user-type").innerText = userType;
|
||||
document.getElementById("client-user-type").innerText += globals.USER_TYPE_ICONS[userType];
|
||||
}
|
||||
|
||||
function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager) {
|
||||
if (!socket.hasListeners(globals.EVENTS.PLAYER_JOINED)) {
|
||||
socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => {
|
||||
toast(player.name + " joined!", "success", false, true, 3);
|
||||
stateBucket.currentGameState.people.push(player);
|
||||
gameStateRenderer.renderLobbyPlayers();
|
||||
if (
|
||||
gameIsFull
|
||||
&& (
|
||||
stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|
||||
|| stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|
||||
)
|
||||
) {
|
||||
displayStartGamePromptForModerators(stateBucket.currentGameState, socket);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!socket.hasListeners(globals.EVENTS.SYNC_GAME_STATE)) {
|
||||
socket.on(globals.EVENTS.SYNC_GAME_STATE, () => {
|
||||
socket.emit(
|
||||
globals.COMMANDS.FETCH_GAME_STATE,
|
||||
stateBucket.currentGameState.accessCode,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
function (gameState) {
|
||||
stateBucket.currentGameState = gameState;
|
||||
processGameState(stateBucket.currentGameState, gameState.client.cookie, socket, gameStateRenderer);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (timerWorker && gameTimerManager) {
|
||||
gameTimerManager.attachTimerSocketListeners(socket, timerWorker, gameStateRenderer);
|
||||
}
|
||||
|
||||
if (!socket.hasListeners(globals.EVENTS.KILL_PLAYER)) {
|
||||
socket.on(globals.EVENTS.KILL_PLAYER, (id) => {
|
||||
let killedPerson = stateBucket.currentGameState.people.find((person) => person.id === id);
|
||||
if (killedPerson) {
|
||||
killedPerson.out = true;
|
||||
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
|
||||
toast(killedPerson.name + ' killed.', 'success', true, true, 6);
|
||||
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(stateBucket.currentGameState.status === globals.STATUS.ENDED)
|
||||
} else {
|
||||
if (killedPerson.id === stateBucket.currentGameState.client.id) {
|
||||
let clientUserType = document.getElementById("client-user-type");
|
||||
if (clientUserType) {
|
||||
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80'
|
||||
}
|
||||
gameStateRenderer.updatePlayerCardToKilledState();
|
||||
toast('You have been killed!', 'warning', true, true, 6);
|
||||
} else {
|
||||
toast(killedPerson.name + ' was killed!', 'warning', true, true, 6);
|
||||
}
|
||||
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
|
||||
} else {
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!socket.hasListeners(globals.EVENTS.REVEAL_PLAYER)) {
|
||||
socket.on(globals.EVENTS.REVEAL_PLAYER, (revealData) => {
|
||||
let revealedPerson = stateBucket.currentGameState.people.find((person) => person.id === revealData.id);
|
||||
if (revealedPerson) {
|
||||
revealedPerson.revealed = true;
|
||||
revealedPerson.gameRole = revealData.gameRole;
|
||||
revealedPerson.alignment = revealData.alignment;
|
||||
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
|
||||
toast(revealedPerson.name + ' revealed.', 'success', true, true, 6);
|
||||
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(stateBucket.currentGameState.status === globals.STATUS.ENDED)
|
||||
} else {
|
||||
if (revealedPerson.id === stateBucket.currentGameState.client.id) {
|
||||
toast('Your role has been revealed!', 'warning', true, true, 6);
|
||||
} else {
|
||||
toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', true, true, 6);
|
||||
}
|
||||
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
|
||||
} else {
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!socket.hasListeners(globals.EVENTS.CHANGE_NAME)) {
|
||||
socket.on(globals.EVENTS.CHANGE_NAME, (personId, name) => {
|
||||
propagateNameChange(stateBucket.currentGameState, name, personId);
|
||||
updateDOMWithNameChange(stateBucket.currentGameState, gameStateRenderer);
|
||||
processGameState(stateBucket.currentGameState, stateBucket.currentGameState.client.cookie, socket, gameStateRenderer);
|
||||
});
|
||||
}
|
||||
|
||||
if (!socket.hasListeners(globals.COMMANDS.END_GAME)) {
|
||||
socket.on(globals.COMMANDS.END_GAME, (people) => {
|
||||
stateBucket.currentGameState.people = people;
|
||||
stateBucket.currentGameState.status = globals.STATUS.ENDED;
|
||||
processGameState(stateBucket.currentGameState, stateBucket.currentGameState.client.cookie, socket, gameStateRenderer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function displayStartGamePromptForModerators(gameState, socket) {
|
||||
let div = document.createElement("div");
|
||||
div.innerHTML = templates.START_GAME_PROMPT;
|
||||
div.querySelector('#start-game-button').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (confirm("Start the game and deal roles?")) {
|
||||
socket.emit(globals.COMMANDS.START_GAME, gameState.accessCode);
|
||||
}
|
||||
|
||||
});
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
function runGameTimer (hours, minutes, tickRate, soundManager, timerWorker) {
|
||||
if (window.Worker) {
|
||||
timerWorker.onmessage = function (e) {
|
||||
if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds > 0) {
|
||||
document.getElementById('game-timer').innerText = e.data.displayTime;
|
||||
}
|
||||
};
|
||||
timerWorker.postMessage({ hours: hours, minutes: minutes, tickInterval: tickRate });
|
||||
}
|
||||
}
|
||||
|
||||
function validateName(name) {
|
||||
return typeof name === 'string' && name.length > 0 && name.length <= 30;
|
||||
}
|
||||
|
||||
function propagateNameChange(gameState, name, personId) {
|
||||
gameState.client.name = name;
|
||||
let matchingPerson = gameState.people.find((person) => person.id === personId);
|
||||
if (matchingPerson) {
|
||||
matchingPerson.name = name;
|
||||
}
|
||||
|
||||
if (gameState.moderator.id === personId) {
|
||||
gameState.moderator.name = name;
|
||||
}
|
||||
|
||||
let matchingSpectator = gameState.spectators?.find((spectator) => spectator.id === personId);
|
||||
if (matchingSpectator) {
|
||||
matchingSpectator.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDOMWithNameChange(gameState, gameStateRenderer) {
|
||||
switch (gameState.client.userType) {
|
||||
case globals.USER_TYPES.PLAYER:
|
||||
case globals.USER_TYPES.KILLED_PLAYER:
|
||||
case globals.USER_TYPES.SPECTATOR:
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
|
||||
break;
|
||||
case globals.USER_TYPES.MODERATOR:
|
||||
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(gameState.status === globals.STATUS.ENDED);
|
||||
break;
|
||||
case globals.USER_TYPES.TEMPORARY_MODERATOR:
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function activateRoleInfoButton(deck) {
|
||||
deck.sort((a, b) => {
|
||||
return a.team === globals.ALIGNMENT.GOOD ? 1 : -1;
|
||||
})
|
||||
document.getElementById("role-info-button").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById("prompt").innerHTML = templates.ROLE_INFO_MODAL;
|
||||
let modalContent = document.getElementById('game-role-info-container');
|
||||
for (let card of deck) {
|
||||
let roleDiv = document.createElement("div");
|
||||
let roleNameDiv = document.createElement("div");
|
||||
|
||||
roleNameDiv.classList.add('role-info-name');
|
||||
|
||||
let roleName = document.createElement("h5");
|
||||
let roleQuantity = document.createElement("h5");
|
||||
let roleDescription = document.createElement("p");
|
||||
|
||||
roleDescription.innerText = card.description;
|
||||
roleName.innerText = card.role;
|
||||
roleQuantity.innerText = card.quantity + 'x';
|
||||
|
||||
if (card.team === globals.ALIGNMENT.GOOD) {
|
||||
roleName.classList.add(globals.ALIGNMENT.GOOD);
|
||||
} else {
|
||||
roleName.classList.add(globals.ALIGNMENT.EVIL);
|
||||
}
|
||||
|
||||
roleNameDiv .appendChild(roleQuantity);
|
||||
roleNameDiv .appendChild(roleName);
|
||||
|
||||
roleDiv.appendChild(roleNameDiv);
|
||||
roleDiv.appendChild(roleDescription);
|
||||
|
||||
modalContent.appendChild(roleDiv);
|
||||
}
|
||||
ModalManager.displayModal('role-info-modal', 'role-info-modal-background', 'close-role-info-modal-button');
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = game;
|
||||
} else {
|
||||
game();
|
||||
}
|
||||
47
client/src/scripts/home.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { XHRUtility } from "../modules/XHRUtility.js";
|
||||
import { toast } from "../modules/Toast.js";
|
||||
|
||||
const home = () => {
|
||||
document.getElementById("join-form").onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let userCode = document.getElementById("room-code").value;
|
||||
if (roomCodeIsValid(userCode)) {
|
||||
attemptToJoinGame(userCode);
|
||||
} else {
|
||||
toast('Invalid code. Codes are 6 numbers or letters.', 'error', true, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function roomCodeIsValid(code) {
|
||||
return typeof code === "string" && /^[a-z0-9]{6}$/.test(code.toLowerCase());
|
||||
}
|
||||
|
||||
function attemptToJoinGame(code) {
|
||||
XHRUtility.xhr(
|
||||
'/api/games/availability/' + code.toLowerCase().trim(),
|
||||
'GET',
|
||||
null,
|
||||
null
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
window.location = '/game/' + res.content;
|
||||
}
|
||||
}).catch((res) => {
|
||||
if (res.status === 404) {
|
||||
toast("Game not found", "error", true);
|
||||
} else if (res.status === 400) {
|
||||
toast(res.content, "error", true);
|
||||
} else {
|
||||
toast("An unknown error occurred. Please try again later.", "error", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = home;
|
||||
} else {
|
||||
home();
|
||||
}
|
||||
|
||||
508
client/src/styles/GLOBAL.css
Normal file
@@ -0,0 +1,508 @@
|
||||
canvas, caption, center, cite, code,
|
||||
dd, del, dfn, div, dl, dt, em, embed,
|
||||
fieldset, font, form, h1, h2, h3, h4,
|
||||
h5, h6, hr, i, iframe, img, ins, kbd,
|
||||
label, legend, li, menu, object, ol, p,
|
||||
pre, q, s, samp, small, span, strike,
|
||||
strong, sub, sup, table, tbody, td, tfoot,
|
||||
th, thead, tr, tt, u, ul, var {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'diavlo';
|
||||
src: url("../webfonts/Diavlo_LIGHT_II_37.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'signika-negative';
|
||||
src: url("../webfonts/SignikaNegative-Light.woff2") format("woff2");
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: 'signika-negative', sans-serif !important;
|
||||
background-color: #121314 !important;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'diavlo', sans-serif;
|
||||
color: #ab2626;
|
||||
filter: drop-shadow(2px 2px 4px black);
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: whitesmoke;
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #d7d7d7;
|
||||
font-family: 'signika-negative', sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
#footer {
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
color: #d7d7d7;
|
||||
font-size: 14px;
|
||||
margin-top: 3em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
#footer a img {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
#footer a {
|
||||
color: #f7f7f7;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-family: 'diavlo', sans-serif;
|
||||
}
|
||||
|
||||
#footer a:hover {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
#footer div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#footer > div, #footer > a {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
#footer div:nth-child(2) > a, #footer div:nth-child(2) > p {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #d7d7d7;
|
||||
font-family: 'signika-negative', sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: transparent;
|
||||
border: 1px solid white;
|
||||
border-radius: 3px;
|
||||
color: #f7f7f7;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
textarea, input {
|
||||
font-family: 'signika-negative', sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button, input[type="submit"] {
|
||||
font-family: 'signika-negative', sans-serif !important;
|
||||
padding: 10px;
|
||||
background-color: #13762b;
|
||||
border-radius: 3px;
|
||||
color: whitesmoke;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: background-color 0.3s ease-out;
|
||||
text-shadow: 0 3px 4px rgb(0 0 0 / 55%);
|
||||
}
|
||||
|
||||
button:active, input[type=submit]:active {
|
||||
border: 2px solid #21ba45;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 95%;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button:hover, input[type="submit"]:hover, #game-link:hover {
|
||||
background-color: #326243;
|
||||
border: 2px solid #1c8a36;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
font-family: 'signika-negative', sans-serif;
|
||||
font-weight: 100;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,0.11),
|
||||
0 2px 2px rgba(0,0,0,0.11),
|
||||
0 4px 4px rgba(0,0,0,0.11),
|
||||
0 8px 8px rgba(0,0,0,0.11),
|
||||
0 16px 16px rgba(0,0,0,0.11),
|
||||
0 32px 32px rgba(0,0,0,0.11);
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: fit-content;
|
||||
max-width: 80%;
|
||||
min-width: 15em;
|
||||
font-size: 20px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
width: 100%;
|
||||
background-color: #333243;
|
||||
}
|
||||
|
||||
#navbar img {
|
||||
margin: 0 1em;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
#navbar a {
|
||||
color: #f7f7f7;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-size: 25px;
|
||||
font-family: 'diavlo', sans-serif;
|
||||
}
|
||||
|
||||
#navbar a:hover {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.flex-row-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-row-container-left-align {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.animated-placeholder {
|
||||
animation-fill-mode: forwards;
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: pulse-background;
|
||||
animation-timing-function: ease-in;
|
||||
animation-direction: alternate;
|
||||
background: rgb(238,238,238);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
opacity: 0.15;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.animated-placeholder-short {
|
||||
width: 100%;
|
||||
max-width: 15em;
|
||||
height: 2em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.animated-placeholder-long {
|
||||
width: 100%;
|
||||
max-width: 30em;
|
||||
height: 8em;
|
||||
margin: 0 auto 1em auto;
|
||||
}
|
||||
|
||||
.animated-placeholder-invisible {
|
||||
background-color: transparent;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.placeholder-row {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.placeholder-row .animated-placeholder-short {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.good, .compact-card.good .card-role {
|
||||
color: #4b6bfa;
|
||||
}
|
||||
|
||||
.evil, .compact-card.evil .card-role {
|
||||
color: #e73333;
|
||||
}
|
||||
|
||||
@keyframes placeholder {
|
||||
0%{
|
||||
background-position: 50% 0
|
||||
}
|
||||
100%{
|
||||
background-position: -50% 0
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-background {
|
||||
from {
|
||||
background-color: rgb(120 120 120);
|
||||
} to {
|
||||
background-color: rgb(255 255 255);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes fade-in-slide-down-then-exit {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
5% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
95% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-slide-down {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
5% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
95% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 550px) {
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
#step-1 div {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media(min-width: 551px) {
|
||||
h1 {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
#step-1 div {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% + 100px);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
|
||||
/* via https://loading.io/css/ */
|
||||
|
||||
.spinner-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spinner-container p {
|
||||
margin: auto;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: -8em;
|
||||
font-size: 20px;
|
||||
z-index: 51;
|
||||
text-align: center;
|
||||
right: 0;
|
||||
color: #d7d7d7;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.lds-spinner {
|
||||
margin: auto;
|
||||
position: fixed;
|
||||
top: -80px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 51;
|
||||
right: 0;
|
||||
height: fit-content;
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
}
|
||||
.lds-spinner div {
|
||||
transform-origin: 40px 40px;
|
||||
animation: lds-spinner 1.2s linear infinite;
|
||||
}
|
||||
.lds-spinner div:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 37px;
|
||||
width: 6px;
|
||||
height: 18px;
|
||||
border-radius: 20%;
|
||||
background: #d7d7d7;
|
||||
}
|
||||
.lds-spinner div:nth-child(1) {
|
||||
transform: rotate(0deg);
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
.lds-spinner div:nth-child(2) {
|
||||
transform: rotate(30deg);
|
||||
animation-delay: -1s;
|
||||
}
|
||||
.lds-spinner div:nth-child(3) {
|
||||
transform: rotate(60deg);
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
.lds-spinner div:nth-child(4) {
|
||||
transform: rotate(90deg);
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
.lds-spinner div:nth-child(5) {
|
||||
transform: rotate(120deg);
|
||||
animation-delay: -0.7s;
|
||||
}
|
||||
.lds-spinner div:nth-child(6) {
|
||||
transform: rotate(150deg);
|
||||
animation-delay: -0.6s;
|
||||
}
|
||||
.lds-spinner div:nth-child(7) {
|
||||
transform: rotate(180deg);
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.lds-spinner div:nth-child(8) {
|
||||
transform: rotate(210deg);
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
.lds-spinner div:nth-child(9) {
|
||||
transform: rotate(240deg);
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-spinner div:nth-child(10) {
|
||||
transform: rotate(270deg);
|
||||
animation-delay: -0.2s;
|
||||
}
|
||||
.lds-spinner div:nth-child(11) {
|
||||
transform: rotate(300deg);
|
||||
animation-delay: -0.1s;
|
||||
}
|
||||
.lds-spinner div:nth-child(12) {
|
||||
transform: rotate(330deg);
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.bmc-button span:nth-child(1) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.bmc-button {
|
||||
line-height: 35px !important;
|
||||
height:40px !important;
|
||||
text-decoration: none !important;
|
||||
display:inline-flex !important;
|
||||
align-items: center !important;
|
||||
color:#ffffff !important;
|
||||
background-color:#333243 !important;
|
||||
border-radius: 5px !important;
|
||||
border: 1px solid transparent !important;
|
||||
padding: 7px 15px 7px 10px !important;
|
||||
font-size: 15px !important;
|
||||
box-shadow: 0px 1px 1px rgba(190, 190, 190, 0.5) !important;
|
||||
-webkit-box-shadow: 0px 1px 2px 1px rgba(190, 190, 190, 0.5) !important;
|
||||
font-family: sitewide-sans-serif, sans-serif !important;
|
||||
-webkit-box-sizing: border-box !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.bmc-button:hover, .bmc-button:active, .bmc-button:focus {
|
||||
-webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
|
||||
text-decoration: none !important;box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
|
||||
opacity: 0.85 !important;color:#ffffff !important;
|
||||
}
|
||||
|
||||
|
||||
@keyframes lds-spinner {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
363
client/src/styles/create.css
Normal file
@@ -0,0 +1,363 @@
|
||||
.compact-card {
|
||||
border: 2px solid transparent;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
margin: 0.3em;
|
||||
background-color: #393a40;
|
||||
color: gray;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.4);
|
||||
border-radius: 3px;
|
||||
user-select: none;
|
||||
max-width: 15em;
|
||||
min-width: 9em;
|
||||
display: flex;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
.compact-card h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
margin: 0 10px 0 10px;
|
||||
}
|
||||
|
||||
.compact-card .card-role {
|
||||
color: #bfb8b8;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 8em;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.selected-card {
|
||||
border: 2px solid #c5c5c5;
|
||||
}
|
||||
|
||||
.card-role {
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.compact-card-right p {
|
||||
font-size: 40px;
|
||||
margin: 0 10px 0 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.compact-card-left p {
|
||||
font-size: 40px;
|
||||
margin: 0 0 0 10px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.compact-card-left, .compact-card-right {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.compact-card .card-quantity {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.compact-card-header {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#deck-container, #deck, #custom-roles-container {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
#deck-container, #custom-roles-container {
|
||||
margin: 1em 0;
|
||||
background-color: #1f1f1f;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#step-3 {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
border-radius: 3px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #1f1f1f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#step-4 > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
width: 25em;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
#step-4 > div label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#deck {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow: auto;
|
||||
max-height: 20em;
|
||||
}
|
||||
|
||||
#moderation-self span {
|
||||
color: gray;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 10px;
|
||||
font-size: 18px;
|
||||
font-family: 'signika-negative', sans-serif;
|
||||
background-color: transparent;
|
||||
color: #d7d7d7;
|
||||
border-radius: 3px;
|
||||
min-width: 10em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#game-form > div {
|
||||
background-color: #1f1f1f;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
width: fit-content;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#game-form > div > label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#game-time {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: #1f1f1f;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.step {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
#game-time label, #game-time input {
|
||||
margin-right: 10px;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
#game-time div {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
label[for="game-time"], label[for="add-card-to-deck-form"], label[for="deck"] {
|
||||
color: whitesmoke;
|
||||
font-size: 20px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
min-width: 3em;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
#add-card-to-deck-form {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#create-game {
|
||||
background-color: #1c8a36;
|
||||
color: whitesmoke;
|
||||
font-size: 30px;
|
||||
padding: 10px 50px;
|
||||
}
|
||||
|
||||
#create-game:hover {
|
||||
background-color: #326243;
|
||||
border: 2px solid #1c8a36;
|
||||
}
|
||||
|
||||
#deck-select {
|
||||
margin: 0.5em 1em 1.5em 0;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.creation-step {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: transparent;
|
||||
border-radius: 50%;
|
||||
border: 2px solid whitesmoke;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.creation-step-filled {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
#creation-step-container {
|
||||
margin-top: 2em;
|
||||
width: 100%;
|
||||
min-height: 16em;
|
||||
}
|
||||
|
||||
#creation-step-container > div:nth-child(2) {
|
||||
animation: fade-in 0.5s ease-out;
|
||||
}
|
||||
|
||||
#step-title {
|
||||
margin: 0 auto 1em auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#creation-step-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 2em;
|
||||
position: relative;
|
||||
margin-bottom: 8em;
|
||||
}
|
||||
|
||||
#game-creation-container {
|
||||
width: 95%;
|
||||
max-width: 60em;
|
||||
}
|
||||
|
||||
#tracker-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 2em;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#creation-step-tracker {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
#step-forward-button, #step-back-button, #create-game {
|
||||
font-family: sans-serif;
|
||||
font-size: 20px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
#step-forward-button, #create-game {
|
||||
right: 15%;
|
||||
}
|
||||
|
||||
#step-back-button {
|
||||
left: 15%;
|
||||
background-color: #762323;
|
||||
}
|
||||
|
||||
#step-1 div {
|
||||
background-color: black;
|
||||
color: whitesmoke;
|
||||
padding: 1em;
|
||||
max-width: 20em;
|
||||
margin: 0.5em;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 3px;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
#step-1 div.option-selected {
|
||||
border: 2px solid whitesmoke;
|
||||
background-color: #3a3c46;
|
||||
}
|
||||
|
||||
#step-1 div > strong {
|
||||
color: #00a718;
|
||||
}
|
||||
|
||||
#step-1 div:hover {
|
||||
border: 2px solid whitesmoke;
|
||||
}
|
||||
|
||||
.review-option {
|
||||
background-color: #1f1f1f;
|
||||
color: whitesmoke;
|
||||
padding: 10px;
|
||||
font-size: 18px;
|
||||
width: fit-content;
|
||||
border-radius: 3px;
|
||||
margin: 0.5em 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
} to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 550px) {
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
}
|
||||
|
||||
#step-1 div {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.creation-step {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
#step-forward-button, #step-back-button, #create-game {
|
||||
padding: 10px 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media(min-width: 551px) {
|
||||
h1 {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
#step-1 div {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
762
client/src/styles/game.css
Normal file
@@ -0,0 +1,762 @@
|
||||
.lobby-player, #moderator {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: black;
|
||||
color: whitesmoke;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 17px;
|
||||
width: fit-content;
|
||||
min-width: 15em;
|
||||
border: 2px solid transparent;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
#lobby-players {
|
||||
overflow-y: auto;
|
||||
max-height: 30em;
|
||||
overflow-x: hidden;
|
||||
padding: 0 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#lobby-people-container label {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.lobby-player-client {
|
||||
border: 2px solid #21ba45;
|
||||
}
|
||||
|
||||
.lobby-player div:nth-child(2) {
|
||||
color: #21ba45;
|
||||
}
|
||||
|
||||
#moderator.moderator-client {
|
||||
border: 2px solid lightgray;
|
||||
}
|
||||
|
||||
#game-state-container {
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
width: 95%;
|
||||
margin: 1em auto 0 auto;
|
||||
}
|
||||
|
||||
#game-state-container h2 {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
#lobby-header {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 0.5em auto;
|
||||
}
|
||||
|
||||
#game-state-container > div:not(#transfer-mod-modal-background):not(#transfer-mod-modal){
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
#game-content .placeholder-row:nth-child(1) {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
#footer.game-page-footer {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#footer.game-page-footer a {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
#end-of-game-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#end-of-game-header button {
|
||||
margin: 0.5em;
|
||||
}
|
||||
.potential-moderator {
|
||||
display: flex;
|
||||
color: #d7d7d7;
|
||||
background-color: black;
|
||||
border: 2px solid transparent;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
justify-content: space-between;
|
||||
margin: 0.5em 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.potential-moderator:hover {
|
||||
border: 2px solid #d7d7d7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.potential-moderator:active {
|
||||
border: 2px solid #21ba45;
|
||||
transition: border 0.2s ease-out;
|
||||
}
|
||||
|
||||
#game-link {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
padding: 7px;
|
||||
border-radius: 3px;
|
||||
background-color: #121314;
|
||||
border: 2px solid #333243;
|
||||
color: whitesmoke;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.role-info-name {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.role-info-name h5:nth-child(1) {
|
||||
margin-right: 10px;
|
||||
color: #21ba45;
|
||||
}
|
||||
|
||||
#role-info-button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#role-info-button img {
|
||||
height: 25px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#game-role-info-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin: 1em 0;
|
||||
overflow-y: auto;
|
||||
max-height: 35em;
|
||||
}
|
||||
|
||||
#game-role-info-container > div {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
#transfer-mod-modal-content {
|
||||
overflow-y: auto;
|
||||
max-height: 35em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.potential-moderator {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#role-info-modal #modal-button-container {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#game-role-info-container .role-info-name {
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 20px;
|
||||
font-family: signika-negative, sans-serif;
|
||||
margin: 0.5em 0;
|
||||
background-color: #15191c;
|
||||
}
|
||||
|
||||
#role-info-modal h2 {
|
||||
color: #d7d7d7;
|
||||
font-family: diavlo, sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#game-role-info-container p, #game-role-info-container h5 {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#game-role-info-container p {
|
||||
color: #d7d7d7;
|
||||
font-size: 14px;
|
||||
font-family: signika-negative, sans-serif;
|
||||
}
|
||||
|
||||
#game-role {
|
||||
position: relative;
|
||||
border: 5px solid transparent;
|
||||
background-color: #e7e7e7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
max-width: 17em;
|
||||
border-radius: 3px;
|
||||
height: 23em;
|
||||
margin: 0 auto 2em auto;
|
||||
width: 100%;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,0.11),
|
||||
0 2px 2px rgba(0,0,0,0.11),
|
||||
0 4px 4px rgba(0,0,0,0.11),
|
||||
0 8px 8px rgba(0,0,0,0.11),
|
||||
0 16px 16px rgba(0,0,0,0.11),
|
||||
0 32px 32px rgba(0,0,0,0.11);
|
||||
/*perspective: 1000px;*/
|
||||
/*transform-style: preserve-3d;*/
|
||||
}
|
||||
|
||||
#game-role-back {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #171522;
|
||||
border: 5px solid #61606a;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
max-width: 17em;
|
||||
border-radius: 3px;
|
||||
height: 23em;
|
||||
margin: 0 auto 2em auto;
|
||||
width: 100%;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,0.11),
|
||||
0 2px 2px rgba(0,0,0,0.11),
|
||||
0 4px 4px rgba(0,0,0,0.11),
|
||||
0 8px 8px rgba(0,0,0,0.11),
|
||||
0 16px 16px rgba(0,0,0,0.11),
|
||||
0 32px 32px rgba(0,0,0,0.11);
|
||||
/*perspective: 1000px;*/
|
||||
/*transform-style: preserve-3d;*/
|
||||
}
|
||||
|
||||
#game-role-back h4 {
|
||||
font-size: 24px;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
color: #e7e7e7;
|
||||
}
|
||||
|
||||
#game-role-back p {
|
||||
color: #c3c3c3;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#game-timer {
|
||||
padding: 10px;
|
||||
margin-top: 5px;
|
||||
background-color: #262626;
|
||||
color: whitesmoke;
|
||||
border-radius: 3px;
|
||||
font-size: 35px;
|
||||
text-shadow: 0 3px 4px rgb(0 0 0 / 85%);
|
||||
border: 1px solid #747474;
|
||||
}
|
||||
|
||||
#game-timer.low {
|
||||
color: #e71c0d;
|
||||
border: 1px solid #ca1b17;
|
||||
background-color: #361a1a;
|
||||
}
|
||||
|
||||
#role-name {
|
||||
position: absolute;
|
||||
top: 6%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 20px;
|
||||
font-family: 'diavlo', sans-serif;
|
||||
width: 95%;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#role-image {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
position: absolute;
|
||||
top: 37%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 78%;
|
||||
}
|
||||
|
||||
#role-description {
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
bottom: 8%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
font-size: 15px;
|
||||
width: 78%;
|
||||
max-height: 6em;
|
||||
}
|
||||
|
||||
#game-link img {
|
||||
width: 20px;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
#game-title {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#client-container {
|
||||
max-width: 35em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#client {
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #333243;
|
||||
border-radius: 3px;
|
||||
min-width: 15em;
|
||||
}
|
||||
|
||||
#client-name {
|
||||
color: whitesmoke;
|
||||
font-family: 'diavlo', sans-serif;
|
||||
font-size: 30px;
|
||||
margin: 0.25em 2em 0.25em 0;
|
||||
}
|
||||
|
||||
#client-user-type {
|
||||
color: #21ba45;
|
||||
font-family: 'signika-negative', sans-serif;
|
||||
font-size: 25px;
|
||||
background-color: black;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
padding: 0 5px;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
label[for='moderator'] {
|
||||
font-family: 'diavlo', sans-serif;
|
||||
color: lightgray;
|
||||
filter: drop-shadow(2px 2px 4px black);
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
#start-game-prompt, #end-game-prompt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
border-radius: 3px;
|
||||
font-family: 'signika-negative', sans-serif;
|
||||
font-weight: 100;
|
||||
box-shadow: 0 -2px 6px 0 rgb(0 0 0 / 45%);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* width: fit-content; */
|
||||
font-size: 20px;
|
||||
height: 85px;
|
||||
margin: 0 auto;
|
||||
animation: fade-in-slide-up 10s ease;
|
||||
animation-fill-mode: forwards;
|
||||
animation-direction: normal;
|
||||
width: 100%;
|
||||
background-color: #333243;
|
||||
}
|
||||
|
||||
#end-game-prompt {
|
||||
box-shadow: 0 -6px 40px black;
|
||||
}
|
||||
|
||||
#start-game-button, #end-game-button {
|
||||
font-family: 'signika-negative', sans-serif !important;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
color: whitesmoke;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: background-color, border 0.3s ease-out;
|
||||
text-shadow: 0 3px 4px rgb(0 0 0 / 85%);
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
#start-game-button {
|
||||
background-color: #1c8a36;
|
||||
}
|
||||
|
||||
#end-game-button {
|
||||
background-color: #8a1c1c;
|
||||
}
|
||||
|
||||
#start-game-button:hover {
|
||||
background-color: #326243;
|
||||
border: 2px solid #1c8a36;
|
||||
}
|
||||
|
||||
#end-game-button:hover {
|
||||
background-color: #623232;
|
||||
border: 2px solid #8a1c1c;
|
||||
}
|
||||
|
||||
#play-pause {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
#play-pause img {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
#play-pause img:hover {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
|
||||
#play-pause img:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.paused {
|
||||
animation: pulse 0.75s linear infinite alternate;
|
||||
}
|
||||
|
||||
.paused-low {
|
||||
animation: pulse-low 0.75s linear infinite alternate;
|
||||
}
|
||||
|
||||
#game-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.timer-container-moderator {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.game-player {
|
||||
border-left: 3px solid #21ba45;
|
||||
display: flex;
|
||||
color: #d7d7d7;
|
||||
background-color: black;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
justify-content: space-between;
|
||||
margin: 0.5em 0;
|
||||
position: relative;
|
||||
box-shadow: 2px 3px 6px rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
.game-player-name {
|
||||
position: relative;
|
||||
width: 10em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.kill-player-button, .reveal-role-button {
|
||||
font-family: 'signika-negative', sans-serif !important;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
color: whitesmoke;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: background-color, border 0.3s ease-out;
|
||||
text-shadow: 0 3px 4px rgb(0 0 0 / 55%);
|
||||
margin: 5px 0 5px 25px;
|
||||
width: 117px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.placeholder-button {
|
||||
font-family: 'signika-negative', sans-serif !important;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
color: #767676;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border: 2px solid transparent;
|
||||
text-shadow: 0 3px 4px rgb(0 0 0 / 55%);
|
||||
margin: 5px 0 5px 25px;
|
||||
width: 103px;
|
||||
}
|
||||
|
||||
#game-link:hover {
|
||||
background-color: #26282a;
|
||||
border: 2px solid #d7d7d7;
|
||||
}
|
||||
|
||||
.reveal-role-button {
|
||||
background-color: #3f5256;
|
||||
}
|
||||
|
||||
.reveal-role-button img {
|
||||
width: 18px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#game-player-list > .game-player.killed::after {
|
||||
content: '\01F480';
|
||||
font-size: 24px;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.killed, .killed .game-player-role {
|
||||
/*color: gray !important;*/
|
||||
}
|
||||
|
||||
.game-player.killed {
|
||||
border-left: 3px solid #444444;
|
||||
}
|
||||
|
||||
.reveal-role-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.make-mod-button {
|
||||
background-color: #3f5256;
|
||||
font-size: 18px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.good-players {
|
||||
background-color: #1c1a36;
|
||||
}
|
||||
|
||||
.evil-players {
|
||||
background-color: #361a1a;
|
||||
}
|
||||
|
||||
.kill-player-button {
|
||||
background-color: #9f4747;
|
||||
}
|
||||
|
||||
.killed-card {
|
||||
width: 55% !important;
|
||||
}
|
||||
|
||||
.game-player > div:nth-child(2) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#game-player-list {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
max-height: 37em;
|
||||
}
|
||||
|
||||
#game-player-list > div {
|
||||
padding: 2px 10px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#game-parameters {
|
||||
color: #d7d7d7;
|
||||
font-size: 25px;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
#game-parameters > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#game-parameters img {
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#players-alive-label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
#transfer-mod-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#transfer-mod-form #modal-button-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#transfer-mod-modal #modal-button-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#change-name-modal-background {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#lobby-people-container , #game-people-container {
|
||||
background-color: #333243;
|
||||
padding: 10px 10px 0 10px;
|
||||
border-radius: 3px;
|
||||
min-height: 25em;
|
||||
min-width: 15em;
|
||||
max-width: 30em;
|
||||
}
|
||||
|
||||
#transfer-mod-modal-content {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
#game-state-container.vertical-flex {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media(max-width: 500px) {
|
||||
#client-name {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
#client-user-type, #game-parameters {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#game-state-container {
|
||||
margin: 0 auto 0 auto;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#play-pause img {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
.make-mod-button {
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.game-player-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#game-timer {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
#players-alive-label {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#start-game-prompt, #end-game-prompt {
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
#start-game-button, #end-game-button {
|
||||
font-size: 20px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#game-role, #game-role-back {
|
||||
height: 20em;
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
#client-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#game-role-back p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#game-role-back h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
color: rgba(255, 255, 255, 0.1);
|
||||
} to {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-low {
|
||||
from {
|
||||
color: rgba(231, 28, 13 , 0.1);
|
||||
} to {
|
||||
color: rgba(231, 28, 13, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-slide-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
5% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
91
client/src/styles/home.css
Normal file
@@ -0,0 +1,91 @@
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 20px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 1em 0;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
background-color: #1f1f1f;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a button {
|
||||
text-shadow: 0 3px 4px rgb(0 0 0 / 85%);
|
||||
}
|
||||
|
||||
#join-button {
|
||||
min-width: 6em;
|
||||
max-height: 3em;
|
||||
background-color: #1c8a36;
|
||||
color: whitesmoke;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#join-button:hover {
|
||||
background-color: #326243;
|
||||
border: 2px solid #1c8a36;
|
||||
}
|
||||
|
||||
#join-form div:nth-child(1) {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 1em;
|
||||
padding: 1em;
|
||||
max-width: 23em;
|
||||
font-family: 'diavlo', sans-serif;
|
||||
}
|
||||
|
||||
img[src='../images/logo_cropped.gif'] {
|
||||
max-width: 400px;
|
||||
width: 63vw;
|
||||
min-width: 250px;
|
||||
margin: 3em 0 1em 0;
|
||||
}
|
||||
|
||||
form > div {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#join-container {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
#join-container > label {
|
||||
font-size: 35px;
|
||||
font-family: 'diavlo', sans-serif;
|
||||
color: #ab2626;
|
||||
filter: drop-shadow(2px 2px 4px black);
|
||||
}
|
||||
|
||||
label[for="room-code"], label[for="player-name"] {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
button {
|
||||
font-size: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 701px) {
|
||||
button {
|
||||
font-size: 5vw;
|
||||
}
|
||||
}
|
||||
51
client/src/styles/modal.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.modal {
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
width: 85%;
|
||||
z-index: 100;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #23282b;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 25em;
|
||||
height: fit-content;
|
||||
font-family: sans-serif;
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.modal-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% + 100px);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
z-index: 50;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal > form > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.modal > form > div > label {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#close-modal-button {
|
||||
background-color: #762323;
|
||||
}
|
||||
|
||||
#modal-button-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
}
|
||||
72
client/src/views/404.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Create A Game</title>
|
||||
<meta name="description" content="Create a game of Werewolf using your custom set of roles.">
|
||||
<meta property="og:title" content="Not Found">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://play-werewolf.herokuapp.com/not-found">
|
||||
<meta property="og:description" content="The page you are looking for could not be found.">
|
||||
<meta property="og:image" content="image.png">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/styles/GLOBAL.css">
|
||||
<link rel="stylesheet" href="/styles/create.css">
|
||||
<link rel="stylesheet" href="/styles/modal.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar">
|
||||
<img src="/images/Werewolf_Small.png"/>
|
||||
<a href="/">Home</a>
|
||||
</div>
|
||||
<span>
|
||||
<h1>404</h1>
|
||||
<h3>The game or other resource that you are looking for could not be found, or you don't have permission to access it.
|
||||
If this error is unexpected, the application may have restarted.</h3>
|
||||
</span>
|
||||
<style>
|
||||
|
||||
#navbar {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
max-width: 40em;
|
||||
font-size: 20px;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media(min-width: 700px) {
|
||||
h1 {
|
||||
font-size: 200px;
|
||||
}
|
||||
}
|
||||
@media(max-width: 700px) {
|
||||
h1 {
|
||||
font-size: 35vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
97
client/src/views/create.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Create A Game</title>
|
||||
<meta name="description" content="Create a game of Werewolf using your custom set of roles.">
|
||||
<meta property="og:title" content="Werewolf Utility - Create A Game">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://play-werewolf.herokuapp.com/create">
|
||||
<meta property="og:description" content="Create a game of Werewolf using your custom set of roles.">
|
||||
<meta property="og:image" content="image.png">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="./styles/GLOBAL.css">
|
||||
<link rel="stylesheet" href="./styles/create.css">
|
||||
<link rel="stylesheet" href="./styles/modal.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar">
|
||||
<img src="../images/Werewolf_Small.png"/>
|
||||
<a href="/">Home</a>
|
||||
</div>
|
||||
<div id="game-creation-container" class="container">
|
||||
<div id="add-role-modal-background" class="modal-background" style="display: none"></div>
|
||||
<div id="add-role-modal" class="modal" style="display: none">
|
||||
<form id="add-role-form">
|
||||
<div>
|
||||
<label for="role-name">Role Name</label>
|
||||
<input id="role-name" type="text" placeholder="Name your role..." required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="role-alignment">Role Alignment</label>
|
||||
<select id="role-alignment" required>
|
||||
<option value="good">Good</option>
|
||||
<option value="evil">Evil</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="role-description">Description</label>
|
||||
<textarea style="resize:none" id="role-description" rows="10" cols="30" placeholder="Describe your role..." required></textarea>
|
||||
</div>
|
||||
<div id="modal-button-container">
|
||||
<button id="close-modal-button">Close</button>
|
||||
<input type="submit" id="create-role-button" value="Create Role"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<h1>Create A Game</h1>
|
||||
<div id="tracker-container">
|
||||
<div id="creation-step-tracker">
|
||||
<div id="tracker-step-1" class="creation-step creation-step-filled"></div>
|
||||
<div id="tracker-step-2" class="creation-step"></div>
|
||||
<div id="tracker-step-3" class="creation-step"></div>
|
||||
<div id="tracker-step-4" class="creation-step"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="creation-step-container">
|
||||
<h2 id="step-title">Select your method of moderation:</h2>
|
||||
<div class="placeholder-row">
|
||||
<div class="animated-placeholder animated-placeholder-short"></div>
|
||||
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
|
||||
</div>
|
||||
<div class="animated-placeholder animated-placeholder-long"></div>
|
||||
<div class="animated-placeholder animated-placeholder-long"></div>
|
||||
</div>
|
||||
<!-- <div id="deck-container">-->
|
||||
<!-- <label for="deck">Game Deck: 0 Players</label>-->
|
||||
<!-- <div id="deck"></div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <form id="game-form">-->
|
||||
<!-- <div>-->
|
||||
<!-- <label for="game-time">Timer (Optional)</label>-->
|
||||
<!-- <div id="game-time">-->
|
||||
<!-- <label for="game-hours">Hours (max 5)</label>-->
|
||||
<!-- <input type="number" id="game-hours" name="game-hours"-->
|
||||
<!-- min="0" max="5" />-->
|
||||
<!-- <label for="game-hours">Minutes</label>-->
|
||||
<!-- <input type="number" id="game-minutes" name="game-minutes"-->
|
||||
<!-- min="1" max="60" />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div>-->
|
||||
<!-- <label for="mod-name">Your Name</label>-->
|
||||
<!-- <input id="mod-name" type="text" maxlength="30" required/>-->
|
||||
<!-- </div>-->
|
||||
<!-- <input id="create-game" type="submit" value="Create"/>-->
|
||||
<!-- </form>-->
|
||||
</div>
|
||||
<script src="/dist/create-bundle.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const socket = io();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
75
client/src/views/game.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Active Game</title>
|
||||
<meta name="description" content="Join or spectate this game of werewolf.">
|
||||
<meta property="og:title" content="Werewolf Utility - Active Game">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://play-werewolf.herokuapp.com/create">
|
||||
<meta property="og:description" content="Join or spectate this game of werewolf.">
|
||||
<meta property="og:image" content="image.png">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/styles/GLOBAL.css">
|
||||
<link rel="stylesheet" href="/styles/game.css">
|
||||
<link rel="stylesheet" href="/styles/modal.css">
|
||||
<link rel="preload" as="font" href="/webfonts/SignikaNegative-Light.woff2" crossorigin/>
|
||||
<link rel="preload" as="font" href="/webfonts/Diavlo_LIGHT_II_37.woff2" crossorigin/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="prompt"></div>
|
||||
<div class="spinner-background"></div>
|
||||
<div class="spinner-container">
|
||||
<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>
|
||||
<p>Connecting to game...</p>
|
||||
</div>
|
||||
<div id="navbar">
|
||||
<img src="/images/Werewolf_Small.png"/>
|
||||
<a href="/">Home</a>
|
||||
</div>
|
||||
<div id="game-content" class="container">
|
||||
<div class="placeholder-row">
|
||||
<div class="animated-placeholder animated-placeholder-short"></div>
|
||||
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
|
||||
</div>
|
||||
<div class="placeholder-row">
|
||||
<div class="animated-placeholder animated-placeholder-short"></div>
|
||||
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
|
||||
</div>
|
||||
<div class="animated-placeholder animated-placeholder-long"></div>
|
||||
<div class="animated-placeholder animated-placeholder-long"></div>
|
||||
<div class="placeholder-row">
|
||||
<div class="animated-placeholder animated-placeholder-short"></div>
|
||||
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
|
||||
</div>
|
||||
<div class="animated-placeholder animated-placeholder-long"></div>
|
||||
</div>
|
||||
<footer id="footer" class="game-page-footer">
|
||||
<div>
|
||||
<a href="https://github.com/AlecM33/Werewolf"><img src='/images/GitHub-Mark-Light-32px.png'/></a>
|
||||
<a href="mailto:leohfx@gmail.com?Subject=Werewolf App Question" target="_top"><img src='/images/email.svg'/></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>Werewolf created by Andrew Plotkin</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="/dist/game-bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
57
client/src/views/home.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Werewolf Utility</title>
|
||||
<meta name="description" content="A utility to deal Werewolf defaultCards and run games in any setting, on any device.">
|
||||
<meta property="og:title" content="Werewolf Utility">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://play-werewolf.herokuapp.com/">
|
||||
<meta property="og:description" content="A utility to deal Werewolf defaultCards and run games in any setting, on any device.">
|
||||
<meta property="og:image" content="image.png">
|
||||
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
<link rel="stylesheet" href="./styles/GLOBAL.css">
|
||||
<link rel="stylesheet" href="./styles/home.css">
|
||||
</head>
|
||||
<body>
|
||||
<img src="../images/logo_cropped.gif"/>
|
||||
<h3>A tool to run werewolf when not in-person, or in any setting without a deck of cards.</h3>
|
||||
<a href="/create">
|
||||
<button>Create A Game</button>
|
||||
</a>
|
||||
<div id="join-container">
|
||||
<label for="join-form">Join A Game</label>
|
||||
<form id="join-form">
|
||||
<div>
|
||||
<label for="room-code">Room Code</label>
|
||||
<input id="room-code" type="text" placeholder="six-character code..." required/>
|
||||
</div>
|
||||
<input id="join-button" type="submit" value="Join"/>
|
||||
</form>
|
||||
</div>
|
||||
<footer id="footer">
|
||||
<a class="bmc-button" target="_blank" href="https://www.buymeacoffee.com/alecm33">
|
||||
<span alt="Buy me a beer">🍺</span>
|
||||
<span style="margin-left:5px;">Buy me a beer</span>
|
||||
</a>
|
||||
<div>
|
||||
<a href="https://github.com/AlecM33/Werewolf"><img src='/images/GitHub-Mark-Light-32px.png'/></a>
|
||||
<a href="mailto:leohfx@gmail.com?Subject=Werewolf App Question" target="_top"><img src='/images/email.svg'/></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>Werewolf created by Andrew Plotkin</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="/dist/home-bundle.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const socket = io();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
client/src/webfonts/Diavlo_LIGHT_II_37.woff2
Normal file
94
client/src/webfonts/OFL.txt
Normal file
@@ -0,0 +1,94 @@
|
||||
Copyright (c) 2011 by Anna Giedryś (http://ancymonic.com),
|
||||
with Reserved Font Names "Signika".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||