This commit is contained in:
Alec
2021-12-23 02:42:13 -05:00
parent 00bdf000e9
commit e3117879f4
67 changed files with 129 additions and 67 deletions

View 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",
},
];

View 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.",
}
];

View 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'
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View 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
View 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 &lt;b&gt;fill and stroke&lt;/b&gt; 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 &lt;b&gt;fill and stroke&lt;/b&gt; 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 &lt;b&gt;fill and stroke&lt;/b&gt; 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

14
client/src/images/x.svg Normal file
View 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
View 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;
}
}

View 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;
}
}

View 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));
}

View 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();
});
}

View 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;
}

View 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';
}
}

View 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
}

View 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 &#128081;</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 &#128081;</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
View 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;
}

View 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();
}
}

View 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;
}

View 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();
});
},
};

View 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
View 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();
}

View 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();
}

View 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;
}
}

View 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
View 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);
}
}

View 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;
}
}

View 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
View 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>

View 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>

View 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>

View 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>

Binary file not shown.

View 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.

Binary file not shown.