commit
20ab8d61a5
80 changed files with 13661 additions and 0 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#=============================================================================#
|
||||||
|
# The files added manually by Oliver
|
||||||
|
node_modules
|
||||||
|
.vscode
|
||||||
|
server/server.toml
|
||||||
|
server/built/*
|
||||||
|
server/dist/*
|
||||||
|
|
||||||
|
#=============================================================================#
|
||||||
|
# The files that were auto-generated into a .gitignore by Vue-cli
|
||||||
|
/web/.DS_Store
|
||||||
|
/web/node_modules
|
||||||
|
/web/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
/web/.env.local
|
||||||
|
/web/.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
/web/npm-debug.log*
|
||||||
|
/web/yarn-debug.log*
|
||||||
|
/web/yarn-error.log*
|
||||||
|
/web/pnpm-debug.log*
|
||||||
|
#=============================================================================#
|
||||||
15
server/docs/events/CreateGame.md
Normal file
15
server/docs/events/CreateGame.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# `CreateGame`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
Triggered when a user is creating a game.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| name | String | The name of the person starting the game
|
||||||
|
|
||||||
|
## Response Payload: (`GameCreated`)
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| id | String | The user's game ID, this should be stored to permit users to re-join the game if they get disconnected.
|
||||||
|
| game_code | String | The game's code that other players can use to connect to the game.
|
||||||
6
server/docs/events/General_Response.md
Normal file
6
server/docs/events/General_Response.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Properties Of All Response Payloads
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| status | Integer | The response code of the server. This follows HTTP standards as described by [Mozilla's Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
|
||||||
|
| message? | String | More information relating to the response. This is set iff `success` is a non 2XX value.
|
||||||
|
| source? | String | The event name that caused this response to be sent. This is set iff is a non 2XX value.
|
||||||
14
server/docs/events/GetPastQuestions.md
Normal file
14
server/docs/events/GetPastQuestions.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# `GetPastQuestions`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
This event is sent from the client to the server when it has the PastQuestions component visible.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| team | Integer | The team we are requesting the questions for. This can be one of `1`, or `2`, any other value will cause an error to be returned.
|
||||||
|
|
||||||
|
## Response Payload: (`PastQuestions`)
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| questions | String[] | All the previously chosen questions for the team.
|
||||||
16
server/docs/events/JoinGame.md
Normal file
16
server/docs/events/JoinGame.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# `JoinGame`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
Triggered by the client when it attempts to connect to a game.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| name | String | The user's name
|
||||||
|
| game_code | String | The game code for the game the user is trying to join.
|
||||||
|
| id? | String | The user's ID that was stored if they have already joined a game and not finished it.
|
||||||
|
|
||||||
|
## Response Payload: (`GameJoined`)
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| id | String | The user's game ID, if `id` is set in the payload, this is the same, otherwise it is a brand new ID.
|
||||||
12
server/docs/events/NewHand.md
Normal file
12
server/docs/events/NewHand.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# `NewHand`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
This is the event client sends it requests a new 7 cards for the hand.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| team | Integer | The team that is requesting a new hand.
|
||||||
|
|
||||||
|
## Response Payload:
|
||||||
|
This event's response comes in the form of the `UpdateHand` event with the mode set to `"replace"`
|
||||||
13
server/docs/events/ObjectList.md
Normal file
13
server/docs/events/ObjectList.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# `ObjectList`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
This event is sent by the client when they are wanting to retrieve the list of
|
||||||
|
objects that the spirits can pick from.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
No payload is read from for this event.
|
||||||
|
|
||||||
|
## Response Payload: (`ObjectList`)
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| objects | String[] | The objects that are on the chosen card.
|
||||||
14
server/docs/events/SelectObject.md
Normal file
14
server/docs/events/SelectObject.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# `SelectObject`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
The event sent by the clients when they are selecting an object from the card.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| object | String | The name of the object that the spirits are selecting.
|
||||||
|
|
||||||
|
## Response Payload: (`ChosenObject`)
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| object | String | The object that has been selected.
|
||||||
15
server/docs/events/SendCard.md
Normal file
15
server/docs/events/SendCard.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# `SendCard`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
Sends a card to the server, the server either then either forwards the card to the spirit or the past questions pile. The action that is taken is dependant on what the `from` property is set to.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| game_code | String | The game code for the player.
|
||||||
|
| text | String | The text of the card that is being sent.
|
||||||
|
| from | String | The source of where the card is being sent from. This can be one of `"writer"`, `"guesser"`, any other value will return an error.
|
||||||
|
| team | Integer | The team that is sending the card. This can be either `1` or `2`, any other value will return an error.
|
||||||
|
|
||||||
|
## Response Payload: (`NewCards`)
|
||||||
|
The response to this event comes in the form of an `UpdateHand` event with the `mode` set to `"append"`.
|
||||||
18
server/docs/events/UpdateAnswer.md
Normal file
18
server/docs/events/UpdateAnswer.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# `UpdateAnswer`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
This event is sent to and from the server when a spirit is typing into one of the text boxes.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| team | Integer | The team indicator number. This can be either `1`, or `2`, any other value will cause this event to return an error.
|
||||||
|
| answer | Integer | The answer that is being updated. This can be any number between `1` and `8` (inclusive), any other value will cause this event return an error.
|
||||||
|
| value | String | The new text for the input box.
|
||||||
|
|
||||||
|
## Response Payload: (`UpdateAnswer`)
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| team | Integer | The team indicator number. This can be either `1`, or `2`.
|
||||||
|
| answer | Integer | The answer that is being updated. This can be any number between `1` and `8` (inclusive).
|
||||||
|
| value | String | The new text for the input box.
|
||||||
13
server/docs/events/UpdateHand.md
Normal file
13
server/docs/events/UpdateHand.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# `UpdateHand`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
Tells the client to update their hand, using one of the provided modes.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
This event is never sent to the server
|
||||||
|
|
||||||
|
## Response Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| questions | String[] | The cards that the operation for the hand will use.
|
||||||
|
| mode | String | This is one of `"append"` or `"replace"`
|
||||||
28
server/docs/events/UpdatePlayer.md
Normal file
28
server/docs/events/UpdatePlayer.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# `UpdatePlayer`:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
This event is sent as a result of a player joining/changing team, this event is also fired when a new player joins the game. The client sends the event to update what team/role they are on.
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| action | String | The action to take on the player. This can be one of `"modify"`, or `"remove"`, any other value will throw an error.
|
||||||
|
| name | String | The player's name
|
||||||
|
| to | PlayerData | The player's new data.
|
||||||
|
| from | PlayerData | The player's old data.
|
||||||
|
|
||||||
|
|
||||||
|
### `PlayerData`:
|
||||||
|
The below table describes the properties for the player data object.
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| team | Integer | The team to join. Accepted values are: `1`, and `2`, any other value will throw an error.
|
||||||
|
| role | String | The role the player is assuming. This can be `"writer"` or `"guesser"`, any other value will throw an error.
|
||||||
|
|
||||||
|
## Response Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| action | String | The action that we are responding, this can be one of `"new"`, `"modify"`, or `"remove"`.
|
||||||
|
| name | String | The name of the player that is being updated.
|
||||||
|
| role | String | The role the user is becoming. This can be `"writer"`, or `"guesser"`, and will only be set if the `action` is set to `modify`.
|
||||||
|
| team | String | The team that the user is joining. This can be `1`, or `2`, and will only be set if the `action` is set to `modify`.
|
||||||
13
server/docs/events/_template.md
Normal file
13
server/docs/events/_template.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# ``:
|
||||||
|
|
||||||
|
## Description:
|
||||||
|
|
||||||
|
|
||||||
|
## Request Payload:
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
|
||||||
|
|
||||||
|
## Response Payload: (``)
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
8
server/docs/types/Deck.md
Normal file
8
server/docs/types/Deck.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
## Deck<T>
|
||||||
|
This represents a stack of cards that can be drawn from and discarded to.
|
||||||
|
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| _discard | T[] | The used cards from the deck.
|
||||||
|
| _unknown | T[] | The cards that are neither in the deck or the discard.
|
||||||
|
| (get) size | Integer | The number of the cards that are left in the deck.
|
||||||
16
server/docs/types/Game.md
Normal file
16
server/docs/types/Game.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
## Game:
|
||||||
|
This is a representation of the game and it's corresponding data.
|
||||||
|
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| id | String | The game's ID to join with.
|
||||||
|
| host | Player | The player who created the game.
|
||||||
|
| ingame | Boolean | Whether or not this game is being played or is in the lobby.
|
||||||
|
| teams | Team[] | The teams that are a part of this game.
|
||||||
|
| players | Player[] | All of the players that are in this game.
|
||||||
|
| _questons | Deck | The deck and discard of the question cards.
|
||||||
|
| _objects | Deck | The deck and discard of the object cards.
|
||||||
|
| _objectCard | String[] | The card that was drawn from the deck for the spirits to choose an object from.
|
||||||
|
| object | String | The chosen object that the spirits decided on.
|
||||||
|
| (get) questions | Deck | Returns the deck from the private attribute.
|
||||||
|
| (get) objects | String[] | Returns the objects from the card that was drawn for the game.
|
||||||
9
server/docs/types/Player.md
Normal file
9
server/docs/types/Player.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
## Player:
|
||||||
|
A player's information for the game.
|
||||||
|
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| name | String | The player's name.
|
||||||
|
| socket | Socket | The socket object used to send events directly to the player.
|
||||||
|
| isHost | Boolean | Whether or not the player is the host of the game.
|
||||||
|
| connected | Boolean | Whether or not the socket is connected.
|
||||||
13
server/docs/types/Team.md
Normal file
13
server/docs/types/Team.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
## Team
|
||||||
|
A representation of a team, this consists of which player is the spirit and a complete list of all the players on the team,
|
||||||
|
|
||||||
|
| Property | Type | Description
|
||||||
|
| -------- | ---- | -----------
|
||||||
|
| guessers | Player[] | All the Players that are on this team.
|
||||||
|
| writer | Player | The player that is acting as the team's spirit.
|
||||||
|
| _hand | String[] | The cards that are in the medium's hand.
|
||||||
|
| _questions | String[] | The questions that the mediums have asked the spirit. (This is not equivalent to the Spirit's hand)
|
||||||
|
| _answers | String[] | The answers that the spirit has given.
|
||||||
|
| (get) hand | String[] | Returns the medium's hand.
|
||||||
|
| (get) answers | String[] | Returns the team's answers.
|
||||||
|
| (get) questions | String[] | Returns the team's questions.
|
||||||
24
server/package.json
Normal file
24
server/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"directories": {
|
||||||
|
"doc": "docs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "Oliver Akins",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/engine.io": "^3.1.4",
|
||||||
|
"@types/node": "^14.14.14",
|
||||||
|
"@types/socket.io": "^2.1.12",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"neat-csv": "^6.0.0",
|
||||||
|
"socket.io": "^3.0.4",
|
||||||
|
"toml": "^3.0.0",
|
||||||
|
"tslog": "^3.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
273
server/pnpm-lock.yaml
generated
Normal file
273
server/pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
dependencies:
|
||||||
|
'@types/engine.io': 3.1.4
|
||||||
|
'@types/node': 14.14.14
|
||||||
|
'@types/socket.io': 2.1.12
|
||||||
|
fs: 0.0.1-security
|
||||||
|
neat-csv: 6.0.0
|
||||||
|
socket.io: 3.0.4
|
||||||
|
toml: 3.0.0
|
||||||
|
tslog: 3.0.2
|
||||||
|
lockfileVersion: 5.2
|
||||||
|
packages:
|
||||||
|
/@types/component-emitter/1.2.10:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==
|
||||||
|
/@types/cookie/0.4.0:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==
|
||||||
|
/@types/cors/2.8.9:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg==
|
||||||
|
/@types/engine.io/3.1.4:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 14.14.14
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-98rXVukLD6/ozrQ2O80NAlWDGA4INg+tqsEReWJldqyi2fulC9V7Use/n28SWgROXKm6003ycWV4gZHoF8GA6w==
|
||||||
|
/@types/node/14.14.14:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==
|
||||||
|
/@types/socket.io-parser/2.2.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 14.14.14
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-+JNb+7N7tSINyXPxAJb62+NcpC1x/fPn7z818W4xeNCdPTp6VsO/X8fCsg6+ug4a56m1v9sEiTIIUKVupcHOFQ==
|
||||||
|
/@types/socket.io/2.1.12:
|
||||||
|
dependencies:
|
||||||
|
'@types/engine.io': 3.1.4
|
||||||
|
'@types/node': 14.14.14
|
||||||
|
'@types/socket.io-parser': 2.2.1
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-oStc5VFkpb0AsjOxQUj9ztX5Iziatyla/rjZTYbFGoVrrKwd+JU2mtxk7iSl5RGYx9WunLo6UXW1fBzQok/ZyA==
|
||||||
|
/accepts/1.3.7:
|
||||||
|
dependencies:
|
||||||
|
mime-types: 2.1.28
|
||||||
|
negotiator: 0.6.2
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>= 0.6'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
|
||||||
|
/base64-arraybuffer/0.1.4:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>= 0.6.0'
|
||||||
|
resolution:
|
||||||
|
integrity: sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
|
||||||
|
/base64id/2.0.0:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: ^4.5.0 || >= 5.9
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
|
||||||
|
/buffer-from/1.1.1:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||||
|
/component-emitter/1.3.0:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
|
||||||
|
/cookie/0.4.1:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>= 0.6'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
|
||||||
|
/cors/2.8.5:
|
||||||
|
dependencies:
|
||||||
|
object-assign: 4.1.1
|
||||||
|
vary: 1.1.2
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>= 0.10'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
|
||||||
|
/csv-parser/3.0.0:
|
||||||
|
dependencies:
|
||||||
|
minimist: 1.2.5
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>= 10'
|
||||||
|
hasBin: true
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==
|
||||||
|
/debug/4.1.1:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
deprecated: 'Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)'
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||||
|
/engine.io-parser/4.0.2:
|
||||||
|
dependencies:
|
||||||
|
base64-arraybuffer: 0.1.4
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=8.0.0'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
|
||||||
|
/engine.io/4.0.5:
|
||||||
|
dependencies:
|
||||||
|
accepts: 1.3.7
|
||||||
|
base64id: 2.0.0
|
||||||
|
cookie: 0.4.1
|
||||||
|
cors: 2.8.5
|
||||||
|
debug: 4.1.1
|
||||||
|
engine.io-parser: 4.0.2
|
||||||
|
ws: 7.4.1
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=10.0.0'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-Ri+whTNr2PKklxQkfbGjwEo+kCBUM4Qxk4wtLqLrhH+b1up2NFL9g9pjYWiCV/oazwB0rArnvF/ZmZN2ab5Hpg==
|
||||||
|
/fs/0.0.1-security:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha1-invTcYa23d84E/I4WLV+yq9eQdQ=
|
||||||
|
/get-stream/6.0.0:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=10'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==
|
||||||
|
/mime-db/1.45.0:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>= 0.6'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
|
||||||
|
/mime-types/2.1.28:
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.45.0
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>= 0.6'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
|
||||||
|
/minimist/1.2.5:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||||
|
/ms/2.1.3:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
/neat-csv/6.0.0:
|
||||||
|
dependencies:
|
||||||
|
csv-parser: 3.0.0
|
||||||
|
get-stream: 6.0.0
|
||||||
|
to-readable-stream: 2.1.0
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=10'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-Nouw+x6hJzKAOvAevtYQ9QsG0EYHoKBz1H2rKxBj+vxiR9Ya27MXjjHKJ4txXF1T0x3CXuH5A9/5VDnd8MdJmg==
|
||||||
|
/negotiator/0.6.2:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>= 0.6'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||||
|
/object-assign/4.1.1:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=0.10.0'
|
||||||
|
resolution:
|
||||||
|
integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||||
|
/socket.io-adapter/2.0.3:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ==
|
||||||
|
/socket.io-parser/4.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/component-emitter': 1.2.10
|
||||||
|
component-emitter: 1.3.0
|
||||||
|
debug: 4.1.1
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=10.0.0'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-Bs3IYHDivwf+bAAuW/8xwJgIiBNtlvnjYRc4PbXgniLmcP1BrakBoq/QhO24rgtgW7VZ7uAaswRGxutUnlAK7g==
|
||||||
|
/socket.io/3.0.4:
|
||||||
|
dependencies:
|
||||||
|
'@types/cookie': 0.4.0
|
||||||
|
'@types/cors': 2.8.9
|
||||||
|
'@types/node': 14.14.14
|
||||||
|
accepts: 1.3.7
|
||||||
|
base64id: 2.0.0
|
||||||
|
debug: 4.1.1
|
||||||
|
engine.io: 4.0.5
|
||||||
|
socket.io-adapter: 2.0.3
|
||||||
|
socket.io-parser: 4.0.2
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=10.0.0'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-Vj1jUoO75WGc9txWd311ZJJqS9Dr8QtNJJ7gk2r7dcM/yGe9sit7qOijQl3GAwhpBOz/W8CwkD7R6yob07nLbA==
|
||||||
|
/source-map-support/0.5.19:
|
||||||
|
dependencies:
|
||||||
|
buffer-from: 1.1.1
|
||||||
|
source-map: 0.6.1
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
|
||||||
|
/source-map/0.6.1:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=0.10.0'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||||
|
/to-readable-stream/2.1.0:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=8'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w==
|
||||||
|
/toml/3.0.0:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==
|
||||||
|
/tslog/3.0.2:
|
||||||
|
dependencies:
|
||||||
|
source-map-support: 0.5.19
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=10'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-cXGmeiVkqI/uUK+4C6ZZAUTfXzKWDXRmrOqFjzwpiO/VnPUNUvOAbmebCc6AqJIlSfKPG139ayMEV/nOuCwCHw==
|
||||||
|
/vary/1.1.2:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>= 0.8'
|
||||||
|
resolution:
|
||||||
|
integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
||||||
|
/ws/7.4.1:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=8.3.0'
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: ^5.0.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==
|
||||||
|
specifiers:
|
||||||
|
'@types/engine.io': ^3.1.4
|
||||||
|
'@types/node': ^14.14.14
|
||||||
|
'@types/socket.io': ^2.1.12
|
||||||
|
fs: ^0.0.1-security
|
||||||
|
neat-csv: ^6.0.0
|
||||||
|
socket.io: ^3.0.4
|
||||||
|
toml: ^3.0.0
|
||||||
|
tslog: ^3.0.2
|
||||||
21
server/resources/GW_objects.csv
Normal file
21
server/resources/GW_objects.csv
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
Answer 1,Answer 2,Answer 3,Answer 4,Answer 5
|
||||||
|
microphone,lamp,chair,crown,brain
|
||||||
|
screwdriver,chainsaw,sunglasses,dinosaur,tree
|
||||||
|
flag,ship,pie,pineapple,helmet
|
||||||
|
ruler,pen,guitar,skateboard,tomato
|
||||||
|
bathtub,computer,whale,telescope,submarine
|
||||||
|
flower,blanket,pacifier,harp,acorn
|
||||||
|
window,doll,stove,skeleton,vacuum
|
||||||
|
printer,shoe,bed,fork,shield
|
||||||
|
catapult,apple,envelope,broom,windmill
|
||||||
|
flashlight,pyramid,brick,bicycle,truck
|
||||||
|
train,horse,knife,sponge,shirt
|
||||||
|
newspaper,clock,basketball,mirror,backpack
|
||||||
|
phone,car,dragon,platypus,scissors
|
||||||
|
broccoli,shovel,scorpion,sandwich,piano
|
||||||
|
pillow,pencil,ladder,laptop,headphones
|
||||||
|
tent,toothbrush,hammer,coin,peanut
|
||||||
|
bottle,key,pants,fish,pizza
|
||||||
|
bowl,bread,wheelchair,rope,giraffe
|
||||||
|
toilet,house,cloak,soap,banana
|
||||||
|
painting,tractor,sword,book,umbrella
|
||||||
|
82
server/resources/GW_questions.csv
Normal file
82
server/resources/GW_questions.csv
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
Question Text
|
||||||
|
Where would I be most likely to find it?
|
||||||
|
What continent/region would I find the most of these?
|
||||||
|
What country am I most likely to find it in?
|
||||||
|
What city am I most likely to find it in?
|
||||||
|
Where in my house am I most likely to find it?
|
||||||
|
What kind of store am I most likely to find it in?
|
||||||
|
What era did it first appear in?
|
||||||
|
What's something that's about as dangerous as it?
|
||||||
|
What's a variety it comes in?
|
||||||
|
What subcategory does it fall under?
|
||||||
|
What's something that's like it?
|
||||||
|
What color is it most commonly?
|
||||||
|
What material is it made of?
|
||||||
|
What does it feel like if I touch it?
|
||||||
|
How is it made?
|
||||||
|
What is it used for?
|
||||||
|
What's a common brand of it?
|
||||||
|
What's it about the same size as?
|
||||||
|
What's it about the same weight as?
|
||||||
|
What does it cost about the same as?
|
||||||
|
What would happen if I ate it?
|
||||||
|
What's your favorite version/variety of it?
|
||||||
|
Who's a fictional character that has/uses it?
|
||||||
|
Who's a celebrity that has/uses it?
|
||||||
|
What kind of people commonly have or use it?
|
||||||
|
What abstract concept is associated with it?
|
||||||
|
What does it smell like?
|
||||||
|
What does it taste like?
|
||||||
|
What noise does it make when dropped?
|
||||||
|
Who at this table likes it the most?
|
||||||
|
"If it was an animal, what animal would it be?"
|
||||||
|
What's a book or movie/TV that it appears prominently in?
|
||||||
|
What age group likes it the most?
|
||||||
|
How do you feel about it?
|
||||||
|
Who dislikes it?
|
||||||
|
What time of day is it used?
|
||||||
|
What do I wear when I use it?
|
||||||
|
Why do people use it?
|
||||||
|
With what frequency do most people use it?
|
||||||
|
What part of the body do I use it with?
|
||||||
|
What do I clean it with?
|
||||||
|
How is it transported?
|
||||||
|
Where is it stored?
|
||||||
|
Where do you use it?
|
||||||
|
What holiday is it most associated with?
|
||||||
|
What would I use to write on it?
|
||||||
|
What's a game that it appears in?
|
||||||
|
What climate is it most associated with?
|
||||||
|
What website can I find it on?
|
||||||
|
How long will it last if left alone?
|
||||||
|
How does it affect the things around it?
|
||||||
|
Where would I get one?
|
||||||
|
How would it react to being pushed down a hill?
|
||||||
|
What happens if I light it on fire?
|
||||||
|
What happens if I put it under water?
|
||||||
|
What superpower would this thing want?
|
||||||
|
How would I use it as a weapon?
|
||||||
|
How do I feel after using it?
|
||||||
|
What would happen if I bent it?
|
||||||
|
What would I use to destroy it?
|
||||||
|
What's inside it?
|
||||||
|
What's its opposite?
|
||||||
|
What profession works with it?
|
||||||
|
"If it were a musical instrument, what instrument would it be?"
|
||||||
|
"If it was a social media website, what site would it be?"
|
||||||
|
What genre of movie/book is most likely to feature it?
|
||||||
|
"If it had a favorite food, what would it be?"
|
||||||
|
What's something you can make using it?
|
||||||
|
What powers it?
|
||||||
|
What container would I keep it in?
|
||||||
|
What's the secondary material it's made of? (not its main material)
|
||||||
|
Who or what can lift it?
|
||||||
|
What ancient Greek/Roman god is it most associated with?
|
||||||
|
What habitat would I be most like to find it in?
|
||||||
|
What is a group of them called?
|
||||||
|
Who or what makes it?
|
||||||
|
What field of science studies it?
|
||||||
|
"Where does it go when it dies, breaks, or is no longer useful?"
|
||||||
|
What superhero is it most related to?
|
||||||
|
What month would I find it or use it in?
|
||||||
|
"If it was an alcoholic drink, what would it be?"
|
||||||
|
34
server/src/events/CreateGame.ts
Normal file
34
server/src/events/CreateGame.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Game } from '../objects/Game';
|
||||||
|
import { Player } from '../objects/Player';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
import { conf, games, log } from '../main';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: CreateGame) => {
|
||||||
|
try {
|
||||||
|
let host = new Player(data.name, socket, true);
|
||||||
|
|
||||||
|
// Create the game object to save
|
||||||
|
let game = new Game(conf, host);
|
||||||
|
games[game.id] = game;
|
||||||
|
game.players.push(host);
|
||||||
|
game.log = log.getChildLogger({
|
||||||
|
displayLoggerName: true,
|
||||||
|
name: game.id,
|
||||||
|
})
|
||||||
|
game.log.info(`New game created (host=${host.name})`);
|
||||||
|
|
||||||
|
socket.join(game.id);
|
||||||
|
socket.emit(`GameCreated`, {
|
||||||
|
status: 200,
|
||||||
|
game_code: game.id,
|
||||||
|
players: game.playerData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`GameCreated`, {
|
||||||
|
status: 500,
|
||||||
|
message: err.message,
|
||||||
|
source: `CreateGame`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
45
server/src/events/DeleteGame.ts
Normal file
45
server/src/events/DeleteGame.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { games, log } from '../main';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: DeleteGame) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Ensure game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't delete game that doesn't exist: ${data.game_code}`);
|
||||||
|
socket.emit(`GameDeleted`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code ${data.game_code} could not be found`,
|
||||||
|
source: `DeleteGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let game = games[data.game_code];
|
||||||
|
|
||||||
|
// Ensure user is the host
|
||||||
|
let player = game.players.find(x => x.isHost);
|
||||||
|
|
||||||
|
if (player != null && player.socket !== socket) {
|
||||||
|
game.log.warn(`${player.name} attempted to delete game.`);
|
||||||
|
socket.emit(`GameDeleted`, {
|
||||||
|
status: 403,
|
||||||
|
message: `Not allowed to delete a game that you are not the host of.`,
|
||||||
|
source: `DeleteGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete game
|
||||||
|
game.log.debug(`Game deleted.`)
|
||||||
|
delete games[data.game_code];
|
||||||
|
io.to(game.id).emit(`GameDeleted`, { status: 200 });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`GameDeleted`, {
|
||||||
|
status: 500,
|
||||||
|
message: err.message,
|
||||||
|
source: `DeleteGame`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
32
server/src/events/GetHand.ts
Normal file
32
server/src/events/GetHand.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
import { games, log } from '../main';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: GetHand) => {
|
||||||
|
try {
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't find game with code: ${data.game_code}`);
|
||||||
|
socket.emit(`UpdateHand`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Can't find game with code: ${data.game_code}`,
|
||||||
|
source: `GetHand`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
let hand = game.teams[data.team - 1].hand;
|
||||||
|
|
||||||
|
game.log.silly(`Client requested guesser hand`);
|
||||||
|
socket.emit(`UpdateHand`, {
|
||||||
|
status: 200,
|
||||||
|
mode: "replace",
|
||||||
|
questions: hand
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`QuestionList`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: `GetQuestions`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
33
server/src/events/GetPastQuestions.ts
Normal file
33
server/src/events/GetPastQuestions.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { games, log } from '../main';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: GetPastQuestions) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't find game with code: ${data.game_code}`);
|
||||||
|
socket.emit(`PastQuestions`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code ${data.game_code} could not be found`,
|
||||||
|
source: `GetPastQuestions`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
let team = game.teams[data.team - 1];
|
||||||
|
|
||||||
|
game.log.silly(`Past questions retrieved for team ${data.team}`);
|
||||||
|
socket.emit(`PastQuestions`, {
|
||||||
|
status: 200,
|
||||||
|
questions: team.questions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`PastQuestions`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: `GetPastQuestions`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
86
server/src/events/JoinGame.ts
Normal file
86
server/src/events/JoinGame.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { games, log } from '../main';
|
||||||
|
import { Player } from '../objects/Player';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: JoinGame) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't join game that doesn't exist: ${data.game_code}`);
|
||||||
|
socket.emit(`GameJoined`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code "${data.game_code}" could not be found`,
|
||||||
|
source: `JoinGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
|
||||||
|
|
||||||
|
// Ensure no one has the same name as the player that is joining
|
||||||
|
let sameName = game.players.find(x => x.name == data.name);
|
||||||
|
if (sameName != null) {
|
||||||
|
if (!game.ingame) {
|
||||||
|
game.log.info(`Client attempted to connect using name already in use.`);
|
||||||
|
socket.emit(`GameJoined`, {
|
||||||
|
status: 400,
|
||||||
|
message: `A player already has that name in the game.`,
|
||||||
|
source: `JoinGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Player has the same name but is allowed to rejoin if they
|
||||||
|
// disconnect in the middle of the game
|
||||||
|
if (!sameName.socket.connected) {
|
||||||
|
game.log.info(`Player Reconnected to the game (name=${data.name})`);
|
||||||
|
socket.emit(`GameRejoined`, { status: 200 });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
game.log.debug(`${socket.id} attempted to claim ${sameName.socket.id}'s game spot.`);
|
||||||
|
socket.emit(`GameJoined`, {
|
||||||
|
status: 403,
|
||||||
|
message: `Can't connect to an already connected client`,
|
||||||
|
source: `JoinGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Assert game is not in-progess
|
||||||
|
if (game.ingame) {
|
||||||
|
game.log.debug(`${data.name} tried to connect in the middle of a game.`);
|
||||||
|
socket.emit(`GameJoined`, {
|
||||||
|
status: 403,
|
||||||
|
message: `Cannot connect to a game that's in progress.`,
|
||||||
|
source: `JoinGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let player = new Player(data.name, socket);
|
||||||
|
game.players.push(player);
|
||||||
|
|
||||||
|
game.log.debug(`${data.name} joined the game`);
|
||||||
|
socket.join(game.id);
|
||||||
|
socket.emit(`GameJoined`, {
|
||||||
|
status: 200,
|
||||||
|
players: game.playerData,
|
||||||
|
});
|
||||||
|
socket.to(game.id).emit(`PlayerUpdate`, {
|
||||||
|
status: 200,
|
||||||
|
action: "new",
|
||||||
|
name: player.name,
|
||||||
|
players: game.playerData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`GameJoined`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: `JoinGame`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
73
server/src/events/LeaveGame.ts
Normal file
73
server/src/events/LeaveGame.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { games, log } from '../main';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: LeaveGame) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Could not find a game with ID ${data.game_code} to leave`);
|
||||||
|
socket.emit(`GameJoined`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code "${data.game_code}" could not be found`,
|
||||||
|
source: `JoinGame`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
|
||||||
|
// Ensure it's not the host trying to leave so that the game can
|
||||||
|
// actually start
|
||||||
|
if (game.host.socket == socket) {
|
||||||
|
game.log.debug(`Host attempted to leave game. (name=${game.host.name})`);
|
||||||
|
socket.emit(`GameLeft`, {
|
||||||
|
status: 303,
|
||||||
|
message: `Can't leave the game as the host, use "DeleteGame".`,
|
||||||
|
source: `LeaveGame`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let player = game.players.find(p => p.socket === socket);
|
||||||
|
|
||||||
|
// Ensure we found a player
|
||||||
|
if (player != null) {
|
||||||
|
let role = player.role;
|
||||||
|
let teamID = player.team;
|
||||||
|
|
||||||
|
// Ensure the team is defined
|
||||||
|
if (teamID) {
|
||||||
|
let team = game.teams[teamID - 1];
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case "guesser":
|
||||||
|
game.log.silly(`Removed ${player.name} from guesser role.`);
|
||||||
|
team.guessers = team.guessers.filter(p => p.socket !== socket);
|
||||||
|
break;
|
||||||
|
case "writer":
|
||||||
|
game.log.silly(`Removed ${player.name} from writer role.`);
|
||||||
|
team.writer = null;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
game.players = game.players.filter(p => p.socket != socket);
|
||||||
|
|
||||||
|
game.log.debug(`${player.name} left the game.`);
|
||||||
|
socket.to(game.id).emit(`PlayerUpdate`, {
|
||||||
|
status: 200,
|
||||||
|
action: `remove`,
|
||||||
|
players: game.playerData,
|
||||||
|
});
|
||||||
|
socket.emit(`GameLeft`, { status: 200 });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`GameLeft`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: ``,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
45
server/src/events/NewHand.ts
Normal file
45
server/src/events/NewHand.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { conf, games, log } from '../main';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: NewHand) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't find game with code: ${data.game_code}`);
|
||||||
|
socket.emit(`UpdateHand`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code ${data.game_code} could not be found`,
|
||||||
|
source: `NewHand`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
let team = game.teams[data.team - 1];
|
||||||
|
let deck = game.questions;
|
||||||
|
|
||||||
|
// Empty the medium's hand to the discard pile so we know where the
|
||||||
|
// cards are.
|
||||||
|
for (var card of team.hand) {
|
||||||
|
deck.discard(card);
|
||||||
|
team.removeCard(card);
|
||||||
|
game.log.silly(`Removing card: '${card}' from team ${data.team}'s hand.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the questions and then alert the game clients about the changes
|
||||||
|
team.addCardsToHand(deck.draw(conf.game.hand_size));
|
||||||
|
game.log.silly(`Drew a new hand of cards for team ${data.team}.`);
|
||||||
|
io.to(game.id).emit(`UpdateHand`, {
|
||||||
|
status: 200,
|
||||||
|
mode: `replace`,
|
||||||
|
questions: team.hand,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`UpdateHand`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: `NewHand`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
31
server/src/events/ObjectList.ts
Normal file
31
server/src/events/ObjectList.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { games, log } from '../main';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: ObjectList) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't get objects for game that doesn't exist: ${data.game_code}`);
|
||||||
|
socket.emit(`Error`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code ${data.game_code} could not be found`,
|
||||||
|
source: `ObjectList`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
|
||||||
|
game.log.silly(`Sent client object card`);
|
||||||
|
socket.emit(`ObjectList`, {
|
||||||
|
objects: game.objects
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`Error`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: `ObjectList`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
44
server/src/events/SelectObject.ts
Normal file
44
server/src/events/SelectObject.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { games, log } from '../main';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: SelectObject) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't choose an object for a game that doesn't exist: ${data.game_code}`);
|
||||||
|
socket.emit(`ChosenObject`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code ${data.game_code} could not be found`,
|
||||||
|
source: `SelectObject`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
|
||||||
|
// Assert that the object is actually a valid choice
|
||||||
|
if (!game.objects.includes(data.choice)) {
|
||||||
|
game.log.warn(`Someone tried selecting an object that doesn't exist: ${data.choice}`);
|
||||||
|
socket.emit(`ChosenObject`, {
|
||||||
|
status: 409,
|
||||||
|
message: `That object isn't on the card.`,
|
||||||
|
source: `SelectObject`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
game.log.debug(`Object has been chosen: ${data.choice}`);
|
||||||
|
game.object = data.choice;
|
||||||
|
io.to(`${game.id}:*:writer`).emit(`ChosenObject`, {
|
||||||
|
status: 200,
|
||||||
|
choice: data.choice,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`ChosenObject`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: `SelectObject`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
76
server/src/events/SendCard.ts
Normal file
76
server/src/events/SendCard.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
import { conf, games, log } from '../main';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: SendCard) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't send a card in a game that doesn't exist: ${data.game_code}`);
|
||||||
|
socket.emit(`UpdateHand`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code ${data.game_code} could not be found`,
|
||||||
|
source: `SendCard`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
let team = game.teams[data.team - 1];
|
||||||
|
let deck = game.questions;
|
||||||
|
|
||||||
|
// The writer is answering
|
||||||
|
if (data.from === "writer") {
|
||||||
|
game.log.debug(` Writer selected question to answer.`);
|
||||||
|
deck.discard(data.text);
|
||||||
|
team.selectQuestion(data.text);
|
||||||
|
|
||||||
|
socket.emit(`UpdateHand`, {
|
||||||
|
status: 200,
|
||||||
|
mode: "replace",
|
||||||
|
questions: []
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The writer is sending the card to the writer
|
||||||
|
else if (data.from === "guesser") {
|
||||||
|
game.log.debug(`Guesser is sending the card to the writer.`);
|
||||||
|
|
||||||
|
// Update the team's hand
|
||||||
|
team.removeCard(data.text);
|
||||||
|
team.addCardsToHand(game.questions.draw(conf.game.hand_size - team.hand.length));
|
||||||
|
|
||||||
|
// send the question text to the writer player
|
||||||
|
io.to(`${game.id}:${team.id}:writer`).emit(`UpdateHand`, {
|
||||||
|
status: 200,
|
||||||
|
mode: "append",
|
||||||
|
questions: [data.text]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alert all the guessers of the team
|
||||||
|
io.to(`${game.id}:${team.id}:guesser`).emit(`UpdateHand`, {
|
||||||
|
status: 200,
|
||||||
|
mode: "replace",
|
||||||
|
questions: team.hand
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
game.log.warn(`Unknown role in the "from" property: ${data.from}`);
|
||||||
|
socket.emit(`UpdateHand`, {
|
||||||
|
status: 400,
|
||||||
|
message: `Unknown role in the "from" property: ${data.from}`,
|
||||||
|
source: `SendCard`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`UpdateHand`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: `SendCard`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
74
server/src/events/StartGame.ts
Normal file
74
server/src/events/StartGame.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
import { conf, games, log } from '../main';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: StartGame) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Could not find a game with ID ${data.game_code} to start`);
|
||||||
|
socket.emit(`GameJoined`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code "${data.game_code}" could not be found`,
|
||||||
|
source: `StartGame`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
|
||||||
|
// Make sure we can't start a game that is already started
|
||||||
|
if (game.ingame) {
|
||||||
|
socket.emit(`GameStarted`, {
|
||||||
|
status: 405,
|
||||||
|
message: `Can't start a game that is already started`,
|
||||||
|
source: `StartGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure the questions deck got populated
|
||||||
|
if (game.questions.size <= 0) {
|
||||||
|
game.log.error(`Questions deck has no cards before the game started.`);
|
||||||
|
socket.emit(`GameStarted`, {
|
||||||
|
status: 507,
|
||||||
|
message: `Questions deck failed to parse, try again in a few seconds or start a new game.`,
|
||||||
|
source: `StartGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var team of game.teams) {
|
||||||
|
if (team.writer == null) {
|
||||||
|
game.log.info(`No writer on team ${team.id}, aborting start.`);
|
||||||
|
socket.emit(`GameStarted`, {
|
||||||
|
status: 418,
|
||||||
|
message: `A team doesn't have a ${conf.game.writer_name}.`,
|
||||||
|
source: `StartGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (team.guessers.length <= 0) {
|
||||||
|
game.log.info(`No guessers on team ${team.id}, aborting start.`);
|
||||||
|
socket.emit(`GameStarted`, {
|
||||||
|
status: 418,
|
||||||
|
message: `A team does not have any ${conf.game.guesser_name}s.`,
|
||||||
|
source: `StartGame`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the game
|
||||||
|
game.ingame = true;
|
||||||
|
game.teams[0].addCardsToHand(game.questions.draw(conf.game.hand_size));
|
||||||
|
game.teams[1].addCardsToHand(game.questions.draw(conf.game.hand_size));
|
||||||
|
|
||||||
|
io.to(game.id).emit(`GameStarted`, { status: 200 });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`GameStarted`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: ``,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
35
server/src/events/UpdateAnswer.ts
Normal file
35
server/src/events/UpdateAnswer.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { games, log } from '../main';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: UpdateAnswer) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't update answer in a game that doesn't exist: ${data.game_code}`);
|
||||||
|
socket.emit(`UpdateAnswer`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code ${data.game_code} could not be found`,
|
||||||
|
source: `UpdateAnswer`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let game = games[data.game_code];
|
||||||
|
let team = game.teams[data.team - 1];
|
||||||
|
|
||||||
|
// Update the answers for the other players to keep them in sync
|
||||||
|
team.modifyAnswer(data.answer, data.value);
|
||||||
|
socket.to(game.id).emit(`UpdateAnswer`, {
|
||||||
|
answer: data.answer,
|
||||||
|
value: data.value,
|
||||||
|
team: data.team
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`UpdateAnswer`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: `UpdateAnswer`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
302
server/src/events/UpdatePlayer.ts
Normal file
302
server/src/events/UpdatePlayer.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
import { conf, games, log } from '../main';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: UpdatePlayer) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Assert game exists
|
||||||
|
if (!games[data.game_code]) {
|
||||||
|
log.debug(`Can't modify player in a game that doesn't exist: ${data.game_code}`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Game with code ${data.game_code} could not be found`,
|
||||||
|
source: `UpdatePlayer`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the corresponding action code
|
||||||
|
switch (data.action) {
|
||||||
|
case "modify":
|
||||||
|
modifyPlayer(io, socket, data);
|
||||||
|
break;
|
||||||
|
case "remove":
|
||||||
|
removePlayer(io, socket, data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 400,
|
||||||
|
message: `Unknown player action: ${data.action}`,
|
||||||
|
source: `UpdatePlayer`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: `UpdatePlayer`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const modifyPlayer = (io: Server, socket: Socket, data: UpdatePlayer): void => {
|
||||||
|
let game = games[data.game_code];
|
||||||
|
let player = game.players.find(x => x.name === data.name);
|
||||||
|
|
||||||
|
// Ensure that the player was found correctly so it is not undefined
|
||||||
|
if (player == null) {
|
||||||
|
game.log.debug(`Can't modify a player that doesn't exist. (name=${data.name})`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Cannot find player with the name: ${data.name}`,
|
||||||
|
source: `UpdatePlayer.Modify`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert the player is modifying themselves
|
||||||
|
if (player.socket !== socket) {
|
||||||
|
game.log.debug(`${socket.id} is trying to modify a different player: ${data.name}`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 403,
|
||||||
|
message: `Cannot modify other players`,
|
||||||
|
source: `UpdatePlayer.Modify`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data.to) {
|
||||||
|
game.log.debug(`Client did not include a "to" object in request.`)
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 400,
|
||||||
|
message: `The "to" property must to be specified in the request.`,
|
||||||
|
source: `UpdatePlayer.Modify`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The player is joining a team for the first time
|
||||||
|
if (!data.from) {
|
||||||
|
game.log.silly(`${data.name} included a "to" but not a "from" property`);
|
||||||
|
let team = game.teams[data.to.team - 1];
|
||||||
|
|
||||||
|
switch (data.to.role) {
|
||||||
|
case "guesser":
|
||||||
|
if (team.guessers.length >= 7) {
|
||||||
|
game.log.debug(`Game cannot have more than 7 guessers`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 403,
|
||||||
|
message: `A team can't have 8 or more ${conf.game.guesser_name}`,
|
||||||
|
source: `UpdatePlayer.Modify`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
team.guessers.push(player);
|
||||||
|
game.log.silly(`${player.name} became a guesser`);
|
||||||
|
|
||||||
|
// Move the rooms the player is in
|
||||||
|
player.socket.join(`${game.id}:${data.to.team}:guesser`);
|
||||||
|
player.socket.join(`${game.id}:*:guesser`);
|
||||||
|
break;
|
||||||
|
case "writer":
|
||||||
|
if (team.writer) {
|
||||||
|
game.log.debug(`Game cannot have more than 1 writer`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 403,
|
||||||
|
message: `Someone on that team is already the ${conf.game.writer_name}`,
|
||||||
|
source: `UpdatePlayer.Modify`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Change team object
|
||||||
|
team.writer = player;
|
||||||
|
game.log.silly(`${player.name} became a writer`);
|
||||||
|
|
||||||
|
// Move the rooms the player is in
|
||||||
|
player.socket.join(`${game.id}:${data.to.team}:writer`);
|
||||||
|
player.socket.join(`${game.id}:*:writer`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the player is just swapping roles on the same team
|
||||||
|
else if (data.from.team === data.to.team) {
|
||||||
|
game.log.silly(`${data.name} provided "to" and "from" objects for the same team.`)
|
||||||
|
let team = game.teams[data.to.team - 1];
|
||||||
|
switch (data.to.role) {
|
||||||
|
case "guesser":
|
||||||
|
if (team.guessers.length >= 7) {
|
||||||
|
game.log.debug(`Game cannot have more than 7 guessers`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 403,
|
||||||
|
message: `A team can't have 8 or more ${conf.game.guesser_name}`,
|
||||||
|
source: `UpdatePlayer.Modify`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
team.guessers.push(player);
|
||||||
|
team.writer = null;
|
||||||
|
game.log.silly(`${data.name} became a guesser`);
|
||||||
|
|
||||||
|
// Move the rooms the player is in
|
||||||
|
player.socket.join(`${game.id}:${data.to.team}:guesser`);
|
||||||
|
player.socket.join(`${game.id}:*:guesser`);
|
||||||
|
player.socket.leave(`${game.id}:${data.from.team}:writer`);
|
||||||
|
player.socket.leave(`${game.id}:*:writer`);
|
||||||
|
break;
|
||||||
|
case "writer":
|
||||||
|
if (team.writer) {
|
||||||
|
game.log.debug(`Game cannot have more than 1 ${conf.game.writer_name}`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 403,
|
||||||
|
message: `Someone on that team is already the ${conf.game.writer_name}`,
|
||||||
|
source: `UpdatePlayer.Modify`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Change team object
|
||||||
|
team.writer = player;
|
||||||
|
team.guessers = team.guessers.filter(x => x.socket !== socket);
|
||||||
|
game.log.silly(`${data.name} became the writer`);
|
||||||
|
|
||||||
|
// Move the rooms the player is in
|
||||||
|
player.socket.join(`${game.id}:${data.to.team}:writer`);
|
||||||
|
player.socket.join(`${game.id}:*:writer`);
|
||||||
|
player.socket.leave(`${game.id}:${data.from.team}:guesser`);
|
||||||
|
player.socket.leave(`${game.id}:*:guesser`);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The player is swapping roles and teams
|
||||||
|
else {
|
||||||
|
game.log.silly(`${data.name} provided both "to" and "from" for different teams.`);
|
||||||
|
let oldTeam = game.teams[data.from.team - 1];
|
||||||
|
let newTeam = game.teams[data.to.team - 1];
|
||||||
|
|
||||||
|
// Add the player to the new team to make sure that it's a valid move
|
||||||
|
switch (data.to.role) {
|
||||||
|
case "guesser":
|
||||||
|
// Ensure we don't get 8 guessers
|
||||||
|
if (newTeam.guessers.length >= 7) {
|
||||||
|
game.log.debug(`Game cannot have 8 or more guessers on a team.`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 403,
|
||||||
|
message: `Cannot have 8 players as ${conf.game.guesser_name}s on a single team.`,
|
||||||
|
source: `UpdatePlayer.Modify`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
game.log.silly(`${data.name} became a guesser`);
|
||||||
|
newTeam.guessers.push(player);
|
||||||
|
player.socket.join(`${game.id}:${data.to.team}:guesser`)
|
||||||
|
player.socket.join(`${game.id}:*:guesser`)
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
case "writer":
|
||||||
|
// Ensure we don't already have a writer
|
||||||
|
if (newTeam.writer) {
|
||||||
|
game.log.debug(`Game cannot have more than 1 writer on a team.`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 403,
|
||||||
|
message: `Someone on that team is already the ${conf.game.writer_name}`,
|
||||||
|
source: `UpdatePlayer.Modify`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
newTeam.writer = player;
|
||||||
|
player.socket.join(`${game.id}:${data.to.team}:writer`);
|
||||||
|
player.socket.join(`${game.id}:*:writer`);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the player from their old team since we've added them to the
|
||||||
|
// new team
|
||||||
|
switch (data.from.role) {
|
||||||
|
case "guesser":
|
||||||
|
game.log.debug(`${player.name} left the guessers`);
|
||||||
|
oldTeam.guessers = oldTeam.guessers.filter(x => x.socket !== socket);
|
||||||
|
player.socket.leave(`${game.id}:${data.from.team}:guesser`);
|
||||||
|
|
||||||
|
// Ensure we don't remove the general role room if the player
|
||||||
|
// is taking the same role, but on the other team.
|
||||||
|
if (data.from.role !== data.to.role) {
|
||||||
|
player.socket.leave(`${game.id}:*:guesser`);
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "writer":
|
||||||
|
game.log.debug(`${player.name} stopped being a writer`);
|
||||||
|
oldTeam.writer = null;
|
||||||
|
player.socket.leave(`${game.id}:${data.from.team}:writer`);
|
||||||
|
|
||||||
|
// Ensure we don't remove the general role room when the player
|
||||||
|
// is taking the same role, but on the other team.
|
||||||
|
if (data.from.role !== data.to.role) {
|
||||||
|
player.socket.leave(`${game.id}:*:writer`);
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
player.role = data.to.role;
|
||||||
|
player.team = data.to.team;
|
||||||
|
|
||||||
|
io.to(game.id).emit(`PlayerUpdate`, {
|
||||||
|
status: 200,
|
||||||
|
action: `modify`,
|
||||||
|
name: data.name,
|
||||||
|
role: data.to.role,
|
||||||
|
team: data.to.team,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const removePlayer = (io: Server, socket: Socket, data: UpdatePlayer): void => {
|
||||||
|
let game = games[data.game_code];
|
||||||
|
let player = game.players.find(x => x.name === data.name);
|
||||||
|
let host = game.host;
|
||||||
|
|
||||||
|
// Ensure that the player was found correctly so it is not undefined
|
||||||
|
if (player == null) {
|
||||||
|
game.log.debug(`Can't delete a player that doesn't exist. (name=${data.name})`);
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 404,
|
||||||
|
message: `Cannot find player with the name: ${data.name}`,
|
||||||
|
source: `UpdatePlayer.Remove`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure that the player who is removing the player is the host
|
||||||
|
if (host.socket !== socket) {
|
||||||
|
socket.emit(`PlayerUpdate`, {
|
||||||
|
status: 403,
|
||||||
|
message: `Cannot kick a player when you are not the host`,
|
||||||
|
source: `UpdatePlayer.Remove`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the player from the team object so it doesn't interfere with
|
||||||
|
// other players changing their team data
|
||||||
|
for (var team of game.teams) {
|
||||||
|
if (team.writer == player) {
|
||||||
|
team.writer = null;
|
||||||
|
game.log.silly(`Removed ${player.name} from the writer role.`);
|
||||||
|
} else if (team.guessers.includes(player)) {
|
||||||
|
team.guessers = team.guessers.filter(x => x !== player);
|
||||||
|
game.log.silly(`Removed ${player.name} from the guesser role`);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
game.players = game.players.filter(x => x !== player);
|
||||||
|
|
||||||
|
game.log.info(`${host.name} kicked ${player.name} from game`);
|
||||||
|
io.to(game.id).emit(`PlayerUpdate`, {
|
||||||
|
action: "remove",
|
||||||
|
name: player.name,
|
||||||
|
});
|
||||||
|
};
|
||||||
17
server/src/events/_template.ts
Normal file
17
server/src/events/_template.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export default (io: Server, socket: Socket, data: any) => {
|
||||||
|
try {
|
||||||
|
socket.emit(`Error`, {
|
||||||
|
status: 501,
|
||||||
|
message: `: Not Implemented Yet`,
|
||||||
|
source: ``,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
socket.emit(`Error`, {
|
||||||
|
status: 500,
|
||||||
|
message: `${err.name}: ${err.message}`,
|
||||||
|
source: ``,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
23
server/src/main.ts
Normal file
23
server/src/main.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import * as toml from "toml";
|
||||||
|
import { Logger } from "tslog";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { Game } from "./objects/Game";
|
||||||
|
import startWebsocket from "./websocket";
|
||||||
|
import { Validate } from "./utils/validate";
|
||||||
|
|
||||||
|
export const conf: config = toml.parse(readFileSync(`server.toml`, `utf-8`));
|
||||||
|
|
||||||
|
export var games: {[index: string]: Game} = {};
|
||||||
|
|
||||||
|
export const log: Logger = new Logger({
|
||||||
|
displayFunctionName: false,
|
||||||
|
displayLoggerName: true,
|
||||||
|
displayFilePath: `hidden`,
|
||||||
|
displayLogLevel: true,
|
||||||
|
minLevel: conf.log.level,
|
||||||
|
name: `GLOBAL`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Validate.config(conf)) {
|
||||||
|
startWebsocket(conf);
|
||||||
|
}
|
||||||
59
server/src/objects/Deck.ts
Normal file
59
server/src/objects/Deck.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
export class Deck<T> {
|
||||||
|
private _discard: T[];
|
||||||
|
private _unknown: T[];
|
||||||
|
private _deck: T[];
|
||||||
|
|
||||||
|
|
||||||
|
constructor(cards: T[]) {
|
||||||
|
this._deck = cards;
|
||||||
|
this._discard = [];
|
||||||
|
this._unknown = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
get size(): number { return this._deck.length; }
|
||||||
|
|
||||||
|
|
||||||
|
public draw(quantity: number): T[] {
|
||||||
|
/**
|
||||||
|
* Draws X cards from the deck
|
||||||
|
*
|
||||||
|
* @param quantity -> The number of cards to draw
|
||||||
|
* @throws Error -> If quantity is <= 0
|
||||||
|
* @throws Error -> If quantity > size
|
||||||
|
*/
|
||||||
|
if (quantity <= 0) {
|
||||||
|
throw new Error(`Cannot get ${quantity} cards.`);
|
||||||
|
} else if (quantity > this.size) {
|
||||||
|
throw new Error(`Cannot draw more cards than there are in the deck.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
let cards: T[] = [];
|
||||||
|
|
||||||
|
// Draw the cards for the player and move them into the unknown group
|
||||||
|
for (var i = 0; i < quantity; i++) {
|
||||||
|
|
||||||
|
// Determine the card for the player(s)
|
||||||
|
let index = Math.floor(Math.random() * this.size);
|
||||||
|
let card = this._deck[index];
|
||||||
|
|
||||||
|
// Move it from the arrays
|
||||||
|
cards.push(card);
|
||||||
|
this._deck.splice(index, 1);
|
||||||
|
this._unknown.push(card);
|
||||||
|
};
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public discard(card: T) {
|
||||||
|
/**
|
||||||
|
* Adds the specific card to the discard pile
|
||||||
|
*
|
||||||
|
* @param card -> The card to add to the discard pile
|
||||||
|
*/
|
||||||
|
this._unknown = this._unknown.filter(x => x != card);
|
||||||
|
this._discard.push(card);
|
||||||
|
};
|
||||||
|
};
|
||||||
128
server/src/objects/Game.ts
Normal file
128
server/src/objects/Game.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { Team } from "./Team";
|
||||||
|
import { Deck } from "./Deck";
|
||||||
|
import neatCSV from "neat-csv";
|
||||||
|
import { Logger } from "tslog";
|
||||||
|
import { games } from "../main";
|
||||||
|
import { Player } from "./Player";
|
||||||
|
import { readFile } from "fs";
|
||||||
|
|
||||||
|
export class Game {
|
||||||
|
readonly id: string;
|
||||||
|
readonly host: Player;
|
||||||
|
public log: Logger;
|
||||||
|
public ingame: boolean;
|
||||||
|
public teams: [Team, Team];
|
||||||
|
public players: Player[];
|
||||||
|
private _questions: Deck<question_deck>;
|
||||||
|
private _objects: Deck<object_deck>;
|
||||||
|
private _objectCard: string[];
|
||||||
|
public object: string;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(conf: config, host: Player) {
|
||||||
|
this.id = Game.generateID(conf.game.code_length);
|
||||||
|
this.host = host;
|
||||||
|
this.ingame = false;
|
||||||
|
this.players = [];
|
||||||
|
|
||||||
|
// Get the decks based on what type of data they are.
|
||||||
|
switch (conf.game.cards.type) {
|
||||||
|
case "csv":
|
||||||
|
this.parseDeckCSV(conf);
|
||||||
|
break;
|
||||||
|
case "sheets":
|
||||||
|
this.parseDeckGoogleSheets(conf);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
// Instantiate everything for the teams
|
||||||
|
this.teams = [ new Team(1), new Team(2) ];
|
||||||
|
};
|
||||||
|
|
||||||
|
get questions() { return this._questions; };
|
||||||
|
|
||||||
|
get objects() {
|
||||||
|
/**
|
||||||
|
* Return the objects that the spirits can choose from for the game.
|
||||||
|
*/
|
||||||
|
if (!this._objectCard) {
|
||||||
|
this._objectCard = this._objects.draw(1)[0];
|
||||||
|
};
|
||||||
|
return this._objectCard;
|
||||||
|
};
|
||||||
|
|
||||||
|
get playerData() {
|
||||||
|
let players: player[] = [];
|
||||||
|
for (var player of this.players) {
|
||||||
|
players.push({
|
||||||
|
name: player.name,
|
||||||
|
role: player.role,
|
||||||
|
team: player.team,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return players
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private parseDeckCSV(conf: config): any {
|
||||||
|
/**
|
||||||
|
* Parses out the CSV files and creates the decks for the game to run on
|
||||||
|
*
|
||||||
|
* @param path -> The filepath of the CSV file
|
||||||
|
*/
|
||||||
|
|
||||||
|
// parse the questions from the CSV
|
||||||
|
readFile(conf.game.cards.questions.fingerprint, `utf-8`, (err, filebuffer) => {
|
||||||
|
if (err) throw err;
|
||||||
|
neatCSV(filebuffer)
|
||||||
|
.then((data) => {
|
||||||
|
let questions: question_deck[] = [];
|
||||||
|
for (var entry of data) {
|
||||||
|
questions.push(Object.values(entry)[conf.game.cards.questions.column]);
|
||||||
|
};
|
||||||
|
this._questions = new Deck(questions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse the object deck from CSV
|
||||||
|
readFile(conf.game.cards.objects.fingerprint, `utf-8`, (err, filebuffer) => {
|
||||||
|
if (err) throw err;
|
||||||
|
neatCSV(filebuffer)
|
||||||
|
.then((data) => {
|
||||||
|
let objects: object_deck[] = [];
|
||||||
|
for (var line of data) {
|
||||||
|
objects.push(Object.values(line));
|
||||||
|
};
|
||||||
|
this._objects = new Deck(objects);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private parseDeckGoogleSheets(conf: config): void {
|
||||||
|
/**
|
||||||
|
* Fetches and parses the CSV data from Google Sheets instead of local
|
||||||
|
* CSV files.
|
||||||
|
*
|
||||||
|
* @param conf -> The config object
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public static generateID(length: number): string {
|
||||||
|
/**
|
||||||
|
* Generates a game code with the given length
|
||||||
|
*
|
||||||
|
* @param length -> The length of the code we want to generate
|
||||||
|
*/
|
||||||
|
let code: string;
|
||||||
|
|
||||||
|
// Generate a code until we don't have a collision
|
||||||
|
do {
|
||||||
|
code = ``;
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
code += `${Math.floor(Math.random() * 9)}`;
|
||||||
|
};
|
||||||
|
} while (games[code]);
|
||||||
|
|
||||||
|
return code;
|
||||||
|
};
|
||||||
|
};
|
||||||
15
server/src/objects/Player.ts
Normal file
15
server/src/objects/Player.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Socket } from "socket.io";
|
||||||
|
|
||||||
|
export class Player {
|
||||||
|
readonly name: string;
|
||||||
|
public team: team|null = null;
|
||||||
|
public role: role|null = null;
|
||||||
|
public socket: Socket;
|
||||||
|
readonly isHost: boolean;
|
||||||
|
|
||||||
|
constructor(name: string, socket: Socket, isHost=false) {
|
||||||
|
this.name = name;
|
||||||
|
this.socket = socket;
|
||||||
|
this.isHost = isHost;
|
||||||
|
};
|
||||||
|
};
|
||||||
72
server/src/objects/Team.ts
Normal file
72
server/src/objects/Team.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { Player } from "./Player";
|
||||||
|
|
||||||
|
export class Team {
|
||||||
|
readonly id: team;
|
||||||
|
public guessers: Player[];
|
||||||
|
public writer: Player | null;
|
||||||
|
private _hand: string[];
|
||||||
|
private _questions: string[];
|
||||||
|
private _answers: string[];
|
||||||
|
|
||||||
|
constructor(id: team) {
|
||||||
|
this.id = id;
|
||||||
|
this._answers = new Array<string>(8).fill(``);
|
||||||
|
this._questions = [];
|
||||||
|
this._hand = [];
|
||||||
|
this.guessers = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The getters for the various class properties
|
||||||
|
*/
|
||||||
|
get hand(): string[] { return this._hand; };
|
||||||
|
get answers(): string[] { return this._answers; };
|
||||||
|
get questions(): string[] { return this._questions; };
|
||||||
|
|
||||||
|
|
||||||
|
public addCardsToHand(questions: string[]): void {
|
||||||
|
/**
|
||||||
|
* Adds the question(s) to the medium's hand
|
||||||
|
*
|
||||||
|
* @param questions -> The array of question text to add the medium's
|
||||||
|
* hand.
|
||||||
|
*/
|
||||||
|
this._hand.push(...questions);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public removeCard(question: string) {
|
||||||
|
/**
|
||||||
|
* Removes the given question from the medium's hand
|
||||||
|
*
|
||||||
|
* @param question -> The card text to remove from the hand.
|
||||||
|
*/
|
||||||
|
this._hand = this._hand.filter(x => x != question);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public selectQuestion(question: string) {
|
||||||
|
/**
|
||||||
|
* Adds the given question to the history of the questions.
|
||||||
|
*
|
||||||
|
* @param question -> The question the spirit is answering
|
||||||
|
*/
|
||||||
|
this._questions.push(question);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public modifyAnswer(answerIndex: answer, answer: string) {
|
||||||
|
/**
|
||||||
|
* Takes the value of an answer and modifies in the storage.
|
||||||
|
*
|
||||||
|
* @param answerIndex -> The value of the answer between 1 and 8 (inclusive)
|
||||||
|
* @param answer -> The new answer for that index
|
||||||
|
* @throws Error -> If the answerIndex is not in range
|
||||||
|
*/
|
||||||
|
if (answerIndex > this._answers.length || answerIndex <= 0) {
|
||||||
|
throw new Error(`Cannot set answer at index ${answerIndex}.`)
|
||||||
|
};
|
||||||
|
this._answers[answerIndex - 1] = answer;
|
||||||
|
};
|
||||||
|
};
|
||||||
30
server/src/types/config.d.ts
vendored
Normal file
30
server/src/types/config.d.ts
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
interface config {
|
||||||
|
log: {
|
||||||
|
level: `silly` | `debug` | `info` | `error` | `warn` | `fatal` | `trace`;
|
||||||
|
};
|
||||||
|
websocket: {
|
||||||
|
port: number;
|
||||||
|
permitted_hosts: string | string[];
|
||||||
|
};
|
||||||
|
webserver: {
|
||||||
|
enabled: boolean;
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
game: {
|
||||||
|
hand_size: number;
|
||||||
|
code_length: number;
|
||||||
|
writer_name: string;
|
||||||
|
guesser_name: string;
|
||||||
|
cards: {
|
||||||
|
type: `csv` | `sheets`;
|
||||||
|
key?: string;
|
||||||
|
questions: {
|
||||||
|
fingerprint: string;
|
||||||
|
column: number;
|
||||||
|
};
|
||||||
|
objects: {
|
||||||
|
fingerprint: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
136
server/src/types/data.d.ts
vendored
Normal file
136
server/src/types/data.d.ts
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
interface response {
|
||||||
|
status: number;
|
||||||
|
source?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type team = 1 | 2;
|
||||||
|
type role = "writer" | "guesser";
|
||||||
|
type answer = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||||
|
|
||||||
|
// Player specific data
|
||||||
|
interface player {
|
||||||
|
name: string;
|
||||||
|
role: role | null;
|
||||||
|
team: team | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface CreateGame {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
interface GameCreated extends response {
|
||||||
|
game_code?: string;
|
||||||
|
players?: player[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface DeleteGame {
|
||||||
|
game_code: string;
|
||||||
|
}
|
||||||
|
interface GameDeleted extends response {}
|
||||||
|
|
||||||
|
|
||||||
|
interface LeaveGame {
|
||||||
|
game_code: string;
|
||||||
|
}
|
||||||
|
interface GameLeft extends response {}
|
||||||
|
|
||||||
|
|
||||||
|
interface StartGame {
|
||||||
|
game_code: string;
|
||||||
|
}
|
||||||
|
interface GameStarted extends response {}
|
||||||
|
|
||||||
|
|
||||||
|
interface GetPastQuestions {
|
||||||
|
game_code: string;
|
||||||
|
team: team;
|
||||||
|
}
|
||||||
|
interface PastQuestions extends response {
|
||||||
|
questions?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface JoinGame {
|
||||||
|
game_code: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
interface GameJoined extends response {
|
||||||
|
players?: player[];
|
||||||
|
}
|
||||||
|
interface GameRejoined extends response {
|
||||||
|
players?: player[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface GetHand {
|
||||||
|
game_code: string;
|
||||||
|
team: team;
|
||||||
|
}
|
||||||
|
interface NewHand {
|
||||||
|
game_code: string;
|
||||||
|
team: team;
|
||||||
|
}
|
||||||
|
interface SendCard {
|
||||||
|
game_code: string;
|
||||||
|
text: string;
|
||||||
|
from: role;
|
||||||
|
team: team;
|
||||||
|
}
|
||||||
|
interface UpdateHand extends response{
|
||||||
|
mode?: "append" | "replace";
|
||||||
|
questions?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ObjectList {
|
||||||
|
game_code: string;
|
||||||
|
}
|
||||||
|
interface ObjectListResponse extends response {
|
||||||
|
objects?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface SelectObject {
|
||||||
|
game_code: string;
|
||||||
|
choice: string;
|
||||||
|
}
|
||||||
|
interface ChosenObject extends response {
|
||||||
|
choice?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface UpdateAnswer {
|
||||||
|
game_code: string;
|
||||||
|
answer: answer;
|
||||||
|
value: string;
|
||||||
|
team: team;
|
||||||
|
}
|
||||||
|
interface UpdateAnswerResponse extends response {
|
||||||
|
answer: answer;
|
||||||
|
value: string;
|
||||||
|
team: team;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface UpdatePlayer {
|
||||||
|
action: "modify" | "remove";
|
||||||
|
game_code: string;
|
||||||
|
name: string;
|
||||||
|
from?: {
|
||||||
|
team: team;
|
||||||
|
role: role;
|
||||||
|
};
|
||||||
|
to?: {
|
||||||
|
team: team;
|
||||||
|
role: role;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface PlayerUpdate extends response {
|
||||||
|
action?: "modify" | "new" | "remove";
|
||||||
|
name?: string; // action: all
|
||||||
|
team?: team; // action: "modify"
|
||||||
|
role?: role; // action: "modify"
|
||||||
|
}
|
||||||
2
server/src/types/deck_types.d.ts
vendored
Normal file
2
server/src/types/deck_types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
type question_deck = string;
|
||||||
|
type object_deck = string[];
|
||||||
45
server/src/utils/validate.ts
Normal file
45
server/src/utils/validate.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { log } from "../main";
|
||||||
|
|
||||||
|
export class Validate {
|
||||||
|
public static config(conf: config) {
|
||||||
|
|
||||||
|
let valid = true;
|
||||||
|
|
||||||
|
// Assert data in log object:
|
||||||
|
if (![`silly`,`debug`,`info`,`error`,`fatal`,`warn`,`trace`].includes(conf.log.level)) {
|
||||||
|
log.error(`Unknown log level: ${conf.log.level}`);
|
||||||
|
valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert data in the game object
|
||||||
|
if (![`csv`].includes(conf.game.cards.type)) {
|
||||||
|
log.error(`Unsupported cards type: ${conf.game.cards.type}`);
|
||||||
|
valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (conf.game.cards.type == `sheets` && !conf.game.cards.key) {
|
||||||
|
log.error(`Cannot have game.cards.type set to "sheets" and not have the game.cards.key set.`);
|
||||||
|
valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (conf.game.code_length <= 1) {
|
||||||
|
log.error(`Game codes can't have <= 1 characters: ${conf.game.code_length}`);
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert data in the web server object
|
||||||
|
if (conf.webserver.enabled) {
|
||||||
|
if (!conf.webserver.port) {
|
||||||
|
log.error(`Invalid webserver port value: ${conf.webserver.port}`);
|
||||||
|
valid = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (!conf.websocket.permitted_hosts) {
|
||||||
|
log.error(`Can't have a blank or null webserver.hostname`);
|
||||||
|
valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Config is valid
|
||||||
|
return valid;
|
||||||
|
};
|
||||||
|
};
|
||||||
56
server/src/websocket.ts
Normal file
56
server/src/websocket.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { log } from "./main";
|
||||||
|
import { Server, Socket } from "socket.io";
|
||||||
|
|
||||||
|
import GetHand from "./events/GetHand";
|
||||||
|
import NewHand from "./events/NewHand";
|
||||||
|
import JoinGame from "./events/JoinGame";
|
||||||
|
import SendCard from "./events/SendCard";
|
||||||
|
import LeaveGame from "./events/LeaveGame";
|
||||||
|
import StartGame from "./events/StartGame";
|
||||||
|
import CreateGame from "./events/CreateGame";
|
||||||
|
import DeleteGame from "./events/DeleteGame";
|
||||||
|
import ObjectList from "./events/ObjectList";
|
||||||
|
import UpdatePlayer from "./events/UpdatePlayer";
|
||||||
|
import SelectObject from "./events/SelectObject";
|
||||||
|
import UpdateAnswer from "./events/UpdateAnswer";
|
||||||
|
import GetPastQuestions from "./events/GetPastQuestions";
|
||||||
|
|
||||||
|
|
||||||
|
export default async (conf: config) => {
|
||||||
|
|
||||||
|
const io = new Server();
|
||||||
|
|
||||||
|
io.listen(conf.websocket.port, {
|
||||||
|
cors: {
|
||||||
|
origin: conf.websocket.permitted_hosts,
|
||||||
|
credentials: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
io.on(`connection`, (socket: Socket) => {
|
||||||
|
log.debug(`Client connected with ID: ${socket.id}`);
|
||||||
|
|
||||||
|
// Game Management
|
||||||
|
socket.on(`CreateGame`, (data: CreateGame) => CreateGame(io, socket, data));
|
||||||
|
socket.on(`StartGame`, (data: StartGame) => StartGame(io, socket, data));
|
||||||
|
socket.on(`DeleteGame`, (data: DeleteGame) => DeleteGame(io, socket, data));
|
||||||
|
|
||||||
|
|
||||||
|
// Player Management
|
||||||
|
socket.on(`JoinGame`, (data: JoinGame) => JoinGame(io, socket, data));
|
||||||
|
socket.on(`UpdatePlayer`, (data: UpdatePlayer) => UpdatePlayer(io, socket, data));
|
||||||
|
socket.on(`LeaveGame`, (data: LeaveGame) => LeaveGame(io, socket, data));
|
||||||
|
|
||||||
|
|
||||||
|
// Game Mechanisms
|
||||||
|
socket.on(`ObjectList`, (data: ObjectList) => ObjectList(io, socket, data));
|
||||||
|
socket.on(`SelectObject`, (data: SelectObject) => SelectObject(io, socket, data));
|
||||||
|
socket.on(`GetHand`, (data: GetHand) => GetHand(io, socket, data));
|
||||||
|
socket.on(`NewHand`, (data: NewHand) => NewHand(io, socket, data));
|
||||||
|
socket.on(`SendCard`, (data: SendCard) => SendCard(io, socket, data));
|
||||||
|
socket.on(`UpdateAnswer`, (data: UpdateAnswer) => UpdateAnswer(io, socket, data));
|
||||||
|
socket.on(`GetPastQuestions`, (data: GetPastQuestions) => GetPastQuestions(io, socket, data));
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(`Server started on port ${conf.websocket.port}`);
|
||||||
|
}
|
||||||
83
server/template.toml
Normal file
83
server/template.toml
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
[game]
|
||||||
|
|
||||||
|
# The hand size for the guessers
|
||||||
|
hand_size = 7
|
||||||
|
|
||||||
|
# The length of the game codes that are randomly generated to allow others
|
||||||
|
code_length = 6
|
||||||
|
|
||||||
|
# The names that the server uses for the writer/guessers, these are really only
|
||||||
|
# used in error responses.
|
||||||
|
writer_name = "Spirit"
|
||||||
|
guesser_name = "Medium"
|
||||||
|
|
||||||
|
|
||||||
|
[game.cards]
|
||||||
|
|
||||||
|
# The type of data we are fetching for using in the cards. Accepted values are:
|
||||||
|
# - "csv"
|
||||||
|
# - "sheets"
|
||||||
|
# Any other value will prevent the server from starting.
|
||||||
|
type = "csv"
|
||||||
|
|
||||||
|
|
||||||
|
# The Google Sheets document ID, this is used to specify the document as a whole,
|
||||||
|
# not the specific sheet inside, for that see the "fingerprint" property. This
|
||||||
|
# will be ignored if the type is set to "csv"
|
||||||
|
key = ''
|
||||||
|
|
||||||
|
|
||||||
|
[game.cards.questions]
|
||||||
|
# The identifier for the questions cards.
|
||||||
|
# - If "type" is "csv", then this is a filepath relative to the working
|
||||||
|
# directory of the CLI instantiating the server. (or an absolute path)
|
||||||
|
# - If "type" is "sheets", then this is a sheet identifier used by Google
|
||||||
|
# Sheets to indicate which sheet to use when downloading the content.
|
||||||
|
fingerprint = ''
|
||||||
|
|
||||||
|
# The zero-indexed column number to use when getting the question text.
|
||||||
|
column = 0
|
||||||
|
|
||||||
|
[game.cards.objects]
|
||||||
|
# The identifier for the object cards.
|
||||||
|
# - If "type" is "csv", then this is a filepath relative to the working
|
||||||
|
# directory of the CLI instantiating the server. (or an absolute path)
|
||||||
|
# - If "type" is "sheets", then this is a sheet identifier used by Google
|
||||||
|
# Sheets to indicate which sheet to use when downloading the content.
|
||||||
|
fingerprint = ''
|
||||||
|
|
||||||
|
|
||||||
|
[webserver]
|
||||||
|
# whether or not to enable the integrated webserver.
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
# The port the web server should run on
|
||||||
|
port = 8080
|
||||||
|
|
||||||
|
|
||||||
|
[websocket]
|
||||||
|
# The port the websocket server should run on. This must also be duplicated
|
||||||
|
# into the `main.js` file within the `web` folder so that the client attempts
|
||||||
|
# to connect to the correct port on the server.
|
||||||
|
port = 8081
|
||||||
|
|
||||||
|
# The hostnames (and protocols) that are allowed to connect to the server. For
|
||||||
|
# CORS protection, this should not be set to "*", an array of hostnames is
|
||||||
|
# allowed, an example can be seen commented out below:
|
||||||
|
# permitted_hosts = [
|
||||||
|
# "http://localhost:8080",
|
||||||
|
# "http://127.0.0.1:8080"
|
||||||
|
# ]
|
||||||
|
permitted_hosts = "http://localhost:8080"
|
||||||
|
|
||||||
|
|
||||||
|
[log]
|
||||||
|
# The log level to output to the CLI, this can be one of the following:
|
||||||
|
# - "silly"
|
||||||
|
# - "debug"
|
||||||
|
# - "info"
|
||||||
|
# - "warn"
|
||||||
|
# - "error"
|
||||||
|
# - "trace"
|
||||||
|
# any other value will prevent the server from starting at runtime.
|
||||||
|
level = "silly"
|
||||||
66
server/tsconfig.json
Normal file
66
server/tsconfig.json
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
"removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
// "strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
"strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
|
}
|
||||||
|
}
|
||||||
1
web/.npmrc
Normal file
1
web/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
shamefully-hoist=true
|
||||||
19
web/README.md
Normal file
19
web/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
pnpm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
pnpm run lint
|
||||||
|
```
|
||||||
5
web/Z-indexes.txt
Normal file
5
web/Z-indexes.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
0 -> Main components (default)
|
||||||
|
1 -> Eyes + the multiplier
|
||||||
|
3 -> Past Questions popup
|
||||||
|
10 -> Modal background
|
||||||
|
11 -> Modal foreground
|
||||||
5
web/babel.config.js
Normal file
5
web/babel.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
||||||
52
web/package.json
Normal file
52
web/package.json
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve --copy",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"socket.io-client": "^3.0.4",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-clipboard2": "^0.3.1",
|
||||||
|
"vue-socket.io-extended": "^4.0.5",
|
||||||
|
"vuex": "^3.4.0",
|
||||||
|
"vuex-persist": "^3.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"vue/no-unused-vars": "warn",
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"no-extra-semi": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
||||||
9164
web/pnpm-lock.yaml
generated
Normal file
9164
web/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
1
web/public/assets/eye.svg
Normal file
1
web/public/assets/eye.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49.63 37.19"><defs><style>.cls-1{fill: #000F3D}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_3" data-name="Layer 3"><g id="Layer_2-2" data-name="Layer 2-2"><path class="cls-1" d="M24.71,30.59C14,30.59,5,23.32,2.42,21.09a1.71,1.71,0,0,1-.16-2.41l.16-.16C5,16.29,14,9,24.71,9S44.46,16.3,47,18.52A1.74,1.74,0,0,1,47.14,21a1.21,1.21,0,0,1-.16.16C43.73,24,35,30.59,24.71,30.59Zm0-20.4c-10.29,0-19.08,7-21.52,9.2a.57.57,0,0,0,0,.83c2.46,2.16,11.26,9.21,21.53,9.21,9.94,0,18.35-6.41,21.51-9.17a.6.6,0,0,0,0-.87h0C43.77,17.23,35,10.19,24.71,10.19Z"/><path class="cls-1" d="M24.71,28a9.29,9.29,0,1,1,9.08-9.29A9.2,9.2,0,0,1,24.71,28Zm0-18.13a8.85,8.85,0,1,0,8.64,8.84A8.76,8.76,0,0,0,24.71,9.83Z"/><path class="cls-1" d="M24.73,29.65a11.41,11.41,0,0,1-1.16-.06,11,11,0,0,1-5.81-19.54L18,9.94A26.5,26.5,0,0,1,24.71,9a26.44,26.44,0,0,1,6.74.91l.21.11a11,11,0,0,1-6.93,19.6ZM18.4,11A9.87,9.87,0,0,0,17,24.84a9.91,9.91,0,0,0,6.65,3.6,9.88,9.88,0,0,0,10.89-9.82h0A9.77,9.77,0,0,0,31,11,24.12,24.12,0,0,0,18.4,11Z"/><rect class="cls-1" x="41.02" y="11.75" width="9.61" height="1.16" transform="translate(4.7 36.01) rotate(-45)"/><rect class="cls-1" x="32.19" y="6.72" width="9.61" height="1.16" transform="translate(17.62 39.65) rotate(-70.24)"/><rect class="cls-1" x="24.13" width="1.16" height="9.61"/><rect class="cls-1" x="3.23" y="7.52" width="1.16" height="9.61" transform="translate(-7.6 6.3) rotate(-45)"/><rect class="cls-1" x="12.05" y="2.49" width="1.16" height="9.61" transform="translate(-1.72 4.69) rotate(-19.73)"/><circle class="cls-1" cx="24.71" cy="18.67" r="2.33"/><path class="cls-1" d="M24.71,21.38a2.71,2.71,0,1,1,2.71-2.71A2.7,2.7,0,0,1,24.71,21.38Zm0-4.66a2,2,0,1,0,1.95,2A2,2,0,0,0,24.71,16.72Z"/><rect class="cls-1" x="44.64" y="22.58" width="1.16" height="6.99" transform="translate(-2.76 46.63) rotate(-53.15)"/><rect class="cls-1" x="36.42" y="27.37" width="1.16" height="7.52" transform="translate(-9.82 19.05) rotate(-25.61)"/><rect class="cls-1" x="24.13" y="29.99" width="1.16" height="7.2"/><rect class="cls-1" x="0.9" y="25.5" width="7" height="1.16" transform="matrix(0.8, -0.6, 0.6, 0.8, -14.74, 7.83)"/><rect class="cls-1" x="8.88" y="30.55" width="7.51" height="1.16" transform="translate(-20.9 29.09) rotate(-64.43)"/></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
1
web/public/assets/moon.svg
Normal file
1
web/public/assets/moon.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34.84 40.33"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#ede9c3;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_3" data-name="Layer 3"><path class="cls-1" d="M5.9,23.37c-.12.91-.16,1.12-.16,1.12s0-.18-.16-1.13a8.09,8.09,0,0,0-3-5l-.35-.22L2.57,18a7.12,7.12,0,0,0,3-4.48c.07-.35.15-.69.15-.69s.11.36.18.69a7.16,7.16,0,0,0,3,4.48l.37.19-.36.22A8.25,8.25,0,0,0,5.9,23.37ZM3.27,18.2a6.55,6.55,0,0,1,2.47,3.44A6.55,6.55,0,0,1,8.21,18.2a6.54,6.54,0,0,1-2.47-3.52A6.48,6.48,0,0,1,3.27,18.2Z"/><path class="cls-2" d="M5.78,26.13,5.4,24.57s-.05-.24-.17-1.17a7.77,7.77,0,0,0-2.88-4.73l-.85-.53.9-.48a6.69,6.69,0,0,0,2.84-4.25l.16-.69.31-1.26.37,1.24s.11.38.18.71a7,7,0,0,0,2.82,4.25l.91.48-.87.54a7.88,7.88,0,0,0-2.87,4.74h0c-.12.92-.16,1.13-.16,1.13ZM3.85,18.21a6.86,6.86,0,0,1,1.89,2.43,6.89,6.89,0,0,1,1.9-2.43,6.81,6.81,0,0,1-1.9-2.53A6.79,6.79,0,0,1,3.85,18.21Zm3.27,1.52a4.84,4.84,0,0,0-.33.46C6.9,20,7,19.87,7.12,19.73Z"/><path class="cls-2" d="M14.68,40.33A20.62,20.62,0,0,1,11,40a20.17,20.17,0,0,1-9.1-4.26L0,34.21l2.33.56.81.17a16.53,16.53,0,0,0,12.47-2.62,16.55,16.55,0,0,0,7-10.67A16.67,16.67,0,0,0,9.28,2.22l-.82-.14L6.09,1.76,8.36,1a20.18,20.18,0,0,1,10-.66h0a20.16,20.16,0,0,1-3.71,40ZM4.43,36.24A19.06,19.06,0,1,0,18.18,1.43h0a19.11,19.11,0,0,0-7.23,0A17.77,17.77,0,0,1,23.66,21.85a17.64,17.64,0,0,1-7.42,11.38A17.63,17.63,0,0,1,4.43,36.24Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
1
web/public/assets/sun.svg
Normal file
1
web/public/assets/sun.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45.87 46.6"><defs><style>.cls-1{fill:#ede9c3;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_3" data-name="Layer 3"><path class="cls-1" d="M23,38a14.81,14.81,0,1,1,14.8-14.81A14.82,14.82,0,0,1,23,38ZM23,9.46A13.77,13.77,0,1,0,36.72,23.23,13.78,13.78,0,0,0,23,9.46Z"/><path class="cls-1" d="M21.69,9.3c-.81-1.61-.15-2.33,1-3.33.95-.79.52-2.46.52-2.47l-1-3.5,1.91,3.08c.25.4,1.46,2.44,1,3.62a1.63,1.63,0,0,0,0,1.46c.06.22.14.48.21.8l-1,.25c-.07-.3-.14-.54-.2-.75a2.63,2.63,0,0,1,0-2.16,1.23,1.23,0,0,0,0-.66,2.72,2.72,0,0,1-.79,1.13c-1.19,1-1.21,1.21-.78,2.06Z"/><path class="cls-1" d="M15.66,11.31c-1.47-1-1.22-2-.63-3.43.47-1.15-.69-2.42-.7-2.43L11.84,2.81,15,4.64c.41.24,2.43,1.48,2.54,2.75a1.7,1.7,0,0,0,.69,1.29c.16.17.35.36.57.61L18,10c-.2-.23-.38-.42-.53-.57a2.65,2.65,0,0,1-1-1.92,1.29,1.29,0,0,0-.32-.59A2.55,2.55,0,0,1,16,8.27c-.59,1.45-.51,1.64.27,2.19Z"/><path class="cls-1" d="M11,15.78c-1.76-.39-1.9-1.36-1.92-2.91,0-1.25-1.59-2-1.6-2L4.23,9.46l3.52.45c.47.06,2.81.41,3.42,1.54.3.56.48.64,1.14.91l.75.34-.44.94c-.28-.14-.51-.23-.71-.32a2.61,2.61,0,0,1-1.66-1.38,1.14,1.14,0,0,0-.52-.41,2.56,2.56,0,0,1,.38,1.32c0,1.56.18,1.71,1.11,1.91Z"/><path class="cls-1" d="M8,21.83c-1.09,0-1.57-.74-2.16-1.86s-2.29-1-2.31-1L0,19.13l3.35-1.19c.44-.16,2.68-.91,3.74-.18a1.67,1.67,0,0,0,1.43.3l.82,0,0,1c-.31,0-.56,0-.78,0a2.68,2.68,0,0,1-2.09-.48,1.1,1.1,0,0,0-.66-.14,2.71,2.71,0,0,1,.94,1c.73,1.38.93,1.44,1.85,1.2l.27,1A3.46,3.46,0,0,1,8,21.83Z"/><path class="cls-1" d="M.37,29l2.53-2.5c.33-.33,2.06-2,3.31-1.74a1.71,1.71,0,0,0,1.43-.33c.2-.12.43-.25.72-.39l.47.93c-.28.13-.5.26-.69.36a2.62,2.62,0,0,1-2.1.45,1.19,1.19,0,0,0-.65.16,2.68,2.68,0,0,1,1.28.51c1.24.95,1.45.92,2.18.31l.66.8c-1.38,1.15-2.23.66-3.47-.28-1-.76-2.52,0-2.53.05Z"/><path class="cls-1" d="M5.24,38.3,6.47,35c.17-.44,1-2.65,2.27-3,.62-.17.73-.33,1.15-.91.14-.18.29-.4.5-.66l.81.65c-.19.24-.34.45-.47.62A2.65,2.65,0,0,1,9,33a1.1,1.1,0,0,0-.51.42,2.51,2.51,0,0,1,1.37-.08c1.53.34,1.7.22,2.11-.64l.94.44c-.77,1.63-1.75,1.55-3.27,1.22-1.23-.27-2.26,1.1-2.27,1.11Z"/><path class="cls-1" d="M13.55,44.54,13.27,41c0-.47-.18-2.84.79-3.66A1.68,1.68,0,0,0,14.72,36c0-.22.1-.49.17-.8l1,.24c-.07.3-.11.54-.16.76a2.64,2.64,0,0,1-1,1.9,1.24,1.24,0,0,0-.29.6,2.58,2.58,0,0,1,1.21-.65c1.53-.35,1.64-.52,1.64-1.48h1c0,1.8-.93,2.14-2.45,2.48-1.22.28-1.59,1.95-1.59,2Z"/><path class="cls-1" d="M23.81,46.6l-1.76-3.09c-.23-.41-1.36-2.49-.83-3.65a1.69,1.69,0,0,0,.05-1.46c-.06-.23-.12-.49-.19-.81l1-.21c.06.31.12.55.18.76a2.67,2.67,0,0,1-.11,2.15,1.21,1.21,0,0,0,0,.67,2.62,2.62,0,0,1,.83-1.1c1.23-1,1.26-1.17.86-2l.94-.44c.76,1.64.06,2.34-1.17,3.29-1,.76-.61,2.44-.61,2.46Z"/><path class="cls-1" d="M33.61,44.07l-3-2.1c-.38-.27-2.28-1.69-2.29-3a1.66,1.66,0,0,0-.57-1.35c-.15-.18-.32-.39-.51-.65l.83-.62c.18.25.34.45.48.62a2.65,2.65,0,0,1,.81,2,1.13,1.13,0,0,0,.27.61A2.69,2.69,0,0,1,30,38.26c.72-1.38.65-1.58-.07-2.2l.67-.79c1.37,1.16,1,2.09.32,3.47-.56,1.1.48,2.47.49,2.48Z"/><path class="cls-1" d="M41.33,37.55l-3.5-.64c-.46-.08-2.78-.56-3.33-1.71-.27-.59-.44-.67-1.08-1l-.74-.37.49-.92c.27.15.5.26.7.35a2.67,2.67,0,0,1,1.57,1.47,1.23,1.23,0,0,0,.5.44,2.63,2.63,0,0,1-.31-1.34c.06-1.56-.08-1.71-1-2l.28-1c1.73.48,1.82,1.46,1.76,3-.05,1.25,1.47,2,1.49,2Z"/><path class="cls-1" d="M40.18,29.5a2.68,2.68,0,0,1-1.66-.44,1.66,1.66,0,0,0-1.39-.43c-.23,0-.5,0-.83,0l.06-1c.31,0,.56,0,.78,0a2.68,2.68,0,0,1,2,.67,1.24,1.24,0,0,0,.64.19A2.68,2.68,0,0,1,39,27.36c-.6-1.44-.79-1.52-1.74-1.36l-.17-1c1.77-.3,2.26.55,2.87,2,.48,1.15,2.19,1.22,2.21,1.23l3.56.12-3.44.9A9.1,9.1,0,0,1,40.18,29.5Z"/><path class="cls-1" d="M37.47,23.71l-.38-1c.28-.11.52-.22.71-.31a2.67,2.67,0,0,1,2.14-.26,1.22,1.22,0,0,0,.66-.1,2.55,2.55,0,0,1-1.23-.63c-1.15-1-1.36-1-2.15-.49l-.59-.86c1.48-1,2.29-.46,3.44.59.92.84,2.51.18,2.53.17l3.27-1.39-2.74,2.27c-.36.3-2.21,1.77-3.45,1.44a1.7,1.7,0,0,0-1.45.21Z"/><path class="cls-1" d="M36.25,17.25l-.76-.71A7.67,7.67,0,0,0,36,16a2.64,2.64,0,0,1,1.83-1.14,1.23,1.23,0,0,0,.55-.37,2.57,2.57,0,0,1-1.37,0c-1.5-.46-1.68-.36-2.16.46l-.9-.52c.91-1.56,1.88-1.39,3.37-.93,1.19.37,2.34-.9,2.36-.91l2.38-2.65-1.53,3.22c-.2.42-1.26,2.54-2.52,2.77a1.68,1.68,0,0,0-1.22.8C36.65,16.81,36.47,17,36.25,17.25Z"/><path class="cls-1" d="M32.12,12l-1-.33c.09-.29.16-.53.22-.74a2.66,2.66,0,0,1,1.17-1.81,1.14,1.14,0,0,0,.35-.57,2.58,2.58,0,0,1-1.27.54c-1.54.21-1.67.38-1.76,1.33l-1-.1C29,8.5,29.93,8.24,31.47,8c1.23-.17,1.75-1.8,1.76-1.82l1-3.47,0,3.62c0,.47-.07,2.84-1.12,3.58-.52.37-.57.55-.77,1.24C32.3,11.4,32.23,11.66,32.12,12Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.5 KiB |
8
web/public/assets/trash.svg
Normal file
8
web/public/assets/trash.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg width="72" height="72" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" data-reactroot="">
|
||||||
|
<path stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1" stroke="#ede9c3" fill="none" d="M16.13 22H7.87C7.37 22 6.95 21.63 6.88 21.14L5 8H19L17.12 21.14C17.05 21.63 16.63 22 16.13 22Z"></path>
|
||||||
|
<path stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1" stroke="#ede9c3" d="M3.5 8H20.5"></path>
|
||||||
|
<path stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1" stroke="#ede9c3" d="M10 12V18"></path>
|
||||||
|
<path stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1" stroke="#ede9c3" d="M14 12V18"></path>
|
||||||
|
<path stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1" stroke="#ede9c3" fill="none" d="M16 5H8L9.7 2.45C9.89 2.17 10.2 2 10.54 2H13.47C13.8 2 14.12 2.17 14.3 2.45L16 5Z"></path>
|
||||||
|
<path stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1" stroke="#ede9c3" d="M3 5H21"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
17
web/public/index.html
Normal file
17
web/public/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title>Ghost Writer Online</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but Ghost Writer Online doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
154
web/src/App.vue
Normal file
154
web/src/App.vue
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
<template>
|
||||||
|
<div id="app" class="maximize">
|
||||||
|
<div v-if="!isMobile" class="maximize">
|
||||||
|
<transition name="top-slide">
|
||||||
|
<div
|
||||||
|
v-if="alert.message"
|
||||||
|
class="alert-bar"
|
||||||
|
:class="alert.type"
|
||||||
|
>
|
||||||
|
{{ alert.message }}
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<CreateJoin
|
||||||
|
v-if="gameState == `login`"
|
||||||
|
@error="handleError($event)"
|
||||||
|
/>
|
||||||
|
<GameLobby
|
||||||
|
v-else-if="gameState == `lobby`"
|
||||||
|
@error="handleError($event)"
|
||||||
|
/>
|
||||||
|
<InGame
|
||||||
|
v-else-if="gameState == `in-game`"
|
||||||
|
@error="handleError($event)"
|
||||||
|
/>
|
||||||
|
<Attributions />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="device-error"
|
||||||
|
>
|
||||||
|
<h1 class="centre">Ghost Writer Online</h1>
|
||||||
|
<p class="centre">
|
||||||
|
To use this site you must be using a laptop, desktop, or iPad.
|
||||||
|
If you are on one of those devices and you still see this message,
|
||||||
|
please contact "oliver {at} akins.me" with the following information:
|
||||||
|
<br><br>
|
||||||
|
{{ userAgent }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AttributionsBar from "./components/Attributions";
|
||||||
|
import CreateJoinGame from "./views/CreateJoin";
|
||||||
|
import GameLobby from "./views/Lobby";
|
||||||
|
import InGame from "./views/InGame";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: `App`,
|
||||||
|
components: {
|
||||||
|
"Attributions": AttributionsBar,
|
||||||
|
"CreateJoin": CreateJoinGame,
|
||||||
|
"GameLobby": GameLobby,
|
||||||
|
"InGame": InGame,
|
||||||
|
},
|
||||||
|
data() {return {
|
||||||
|
alert: {
|
||||||
|
message: null,
|
||||||
|
type: null,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
computed: {
|
||||||
|
gameState() {
|
||||||
|
return this.$store.state.view;
|
||||||
|
},
|
||||||
|
userAgent() {
|
||||||
|
if (navigator == null) {
|
||||||
|
return "Navigator Undefined";
|
||||||
|
};
|
||||||
|
return navigator.userAgent;
|
||||||
|
},
|
||||||
|
isMobile () {
|
||||||
|
return this.userAgent.match(/(Navigator Undefined)|iPhone|iPod|Android/) != null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleError(data) {
|
||||||
|
this.alert = {
|
||||||
|
message: `${data.status}${data.source ? ` @ ${data.source}` : ''} : ${data.message}`,
|
||||||
|
type: `error`,
|
||||||
|
};
|
||||||
|
console.error(`${this.alert.message}${data.extra ? ` ${data.extra}` : ''}`);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.alert = {
|
||||||
|
message: null,
|
||||||
|
type: null,
|
||||||
|
};
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sockets: {
|
||||||
|
GameDeleted(data) {
|
||||||
|
if (data.status < 200 || 300 <= data.status) {
|
||||||
|
this.handleError(data);
|
||||||
|
} else {
|
||||||
|
this.alert = {
|
||||||
|
message: `The game has been ended by the host.`,
|
||||||
|
type: `info`,
|
||||||
|
};
|
||||||
|
this.$store.commit(`resetState`);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.alert = {
|
||||||
|
message: null,
|
||||||
|
type: null,
|
||||||
|
};
|
||||||
|
}, 2000)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "css/theme.css";
|
||||||
|
@import "css/style.css";
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background-color: var(--background1);
|
||||||
|
color: var(--light-font-colour);
|
||||||
|
font-family: var(--fonts);
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-bar {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
height: 35px;
|
||||||
|
width: 100vw;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-bar.error {
|
||||||
|
background-color: #bb0000;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.alert-bar.info {
|
||||||
|
background-color: #7289da;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-error {
|
||||||
|
border: solid 2px red;
|
||||||
|
margin: 50% auto;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
84
web/src/components/Attributions.vue
Normal file
84
web/src/components/Attributions.vue
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div id="attributions-bar">
|
||||||
|
<div
|
||||||
|
class="bottom-bar clickable"
|
||||||
|
@click.stop="modal = true"
|
||||||
|
>
|
||||||
|
Made By: Oliver Akins (Alkali Metal)
|
||||||
|
</div>
|
||||||
|
<ModalAnimation
|
||||||
|
:show="modal"
|
||||||
|
@closed="modal = false"
|
||||||
|
>
|
||||||
|
<h2 class="centre">Attributions:</h2>
|
||||||
|
<p class="centre">
|
||||||
|
Made By: Oliver Akins
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<p>
|
||||||
|
Tooling:
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
v-for="(link, name) in tooling"
|
||||||
|
:key="name"
|
||||||
|
>
|
||||||
|
<a :href="link">{{ name }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
</ModalAnimation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Modal from "./Modal";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: `AttributionsBar`,
|
||||||
|
components: {
|
||||||
|
"ModalAnimation": Modal,
|
||||||
|
},
|
||||||
|
data() {return {
|
||||||
|
modal: false,
|
||||||
|
tooling: {
|
||||||
|
"Vue.JS (With VueX)": "https://vuejs.org",
|
||||||
|
"VueX-Persist": "https://www.npmjs.com/package/vuex-persist",
|
||||||
|
"Vue-Socket.io": "https://github.com/MetinSeylan/Vue-Socket.io",
|
||||||
|
"Vue-Clipboard2": "https://www.npmjs.com/package/vue-clipboard2",
|
||||||
|
"Toml": "https://www.npmjs.com/package/toml",
|
||||||
|
"tslog": "https://www.npmjs.com/package/tslog",
|
||||||
|
"Socket.io": "https://socket.io",
|
||||||
|
"neat-csv": "https://github.com/sindresorhus/neat-csv",
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#attributions-bar {
|
||||||
|
left: calc(50% - 20%);
|
||||||
|
position: absolute;
|
||||||
|
height: 35px;
|
||||||
|
width: 40%;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
background-color: var(--background2);
|
||||||
|
color: var(--background2-text);
|
||||||
|
border-radius: 15px 15px 0 0;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:visited {
|
||||||
|
color: var(--light-font-colour);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
128
web/src/components/ChooseObject.vue
Normal file
128
web/src/components/ChooseObject.vue
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<div id="ObjectBoard">
|
||||||
|
<div
|
||||||
|
class="object"
|
||||||
|
v-for="objectIndex in objects.length"
|
||||||
|
:key="`object-${objectIndex}`"
|
||||||
|
>
|
||||||
|
<span class="text">
|
||||||
|
{{ objectIndex }}. {{ objects[objectIndex - 1] }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="clickable"
|
||||||
|
@click.stop="selectObject(objectIndex)"
|
||||||
|
>
|
||||||
|
{{ buttonLabel }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: `ObjectSelector`,
|
||||||
|
components: {},
|
||||||
|
data() {return {
|
||||||
|
objects: [],
|
||||||
|
}},
|
||||||
|
computed: {
|
||||||
|
buttonLabel() {
|
||||||
|
return this.$store.state.writer_object_choose_button;
|
||||||
|
},
|
||||||
|
gameCode() {
|
||||||
|
return this.$store.state.game_code;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectObject(objectIndex) {
|
||||||
|
/**
|
||||||
|
* Sends the chosen object to the server so that the game can begin properly.
|
||||||
|
*/
|
||||||
|
let data = {
|
||||||
|
game_code: this.gameCode,
|
||||||
|
choice: this.objects[objectIndex - 1],
|
||||||
|
};
|
||||||
|
this.$socket.client.emit(`SelectObject`, data);
|
||||||
|
},
|
||||||
|
getObjects() {
|
||||||
|
/**
|
||||||
|
* Gets the objects on the card from the server. This method will
|
||||||
|
* return the same values for all spirit.
|
||||||
|
*/
|
||||||
|
this.$socket.client.emit(`ObjectList`, {
|
||||||
|
game_code: this.$store.state.game_code,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sockets: {
|
||||||
|
ObjectList(data) {
|
||||||
|
/**
|
||||||
|
* The response event from the server for the list of objects that
|
||||||
|
* are on the card which we have drawn for the round.
|
||||||
|
*/
|
||||||
|
this.objects = data.objects;
|
||||||
|
},
|
||||||
|
ChosenObject(data) {
|
||||||
|
/**
|
||||||
|
* Sent to all clients so that they can set their store data and in
|
||||||
|
* turn stay synchronized on what object they are trying to get
|
||||||
|
* their teammate to guess.
|
||||||
|
*/
|
||||||
|
if (data.status < 200 || 300 <= data.status) {
|
||||||
|
this.$emit(`error`, data);
|
||||||
|
};
|
||||||
|
this.$store.commit(`setObject`, data.choice);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getObjects();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#ObjectBoard {
|
||||||
|
background-color: var(--board-background);
|
||||||
|
color: var(--board-background-text);
|
||||||
|
justify-content: space-evenly;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
flex-direction: row;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 15px auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
display: flex;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object {
|
||||||
|
background-color: var(--board-background-alt);
|
||||||
|
color: var(--board-background-alt-text);
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: var(--card-button);
|
||||||
|
border-radius: 7px;
|
||||||
|
font-size: larger;
|
||||||
|
padding: 7px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
button:hover { background-color: var(--card-button-darken); }
|
||||||
|
button:focus { background-color: var(--board-background-alt-lighten); }
|
||||||
|
</style>
|
||||||
136
web/src/components/DiscardHandButton.vue
Normal file
136
web/src/components/DiscardHandButton.vue
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<template>
|
||||||
|
<div id="DiscardHandButton">
|
||||||
|
<div style="width: 100%; height: 100%;">
|
||||||
|
<button
|
||||||
|
class="discard-hand clickable"
|
||||||
|
@click.stop="confirmVisible = true"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`/assets/${buttonIcon}`"
|
||||||
|
alt="Discard entire hand"
|
||||||
|
class="icon"
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ModalAnimation
|
||||||
|
:show="confirmVisible"
|
||||||
|
:closable="false"
|
||||||
|
@closed="confirmVisible = false"
|
||||||
|
>
|
||||||
|
<h2 class="centre">Discard Hand?</h2>
|
||||||
|
<p class="centre">
|
||||||
|
Are you sure you want to discard your team's
|
||||||
|
<strong>entire</strong> hand?
|
||||||
|
</p>
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
class="confirm modal-button clickable"
|
||||||
|
@click.stop="discardHand()"
|
||||||
|
>
|
||||||
|
Discard Hand
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="cancel modal-button clickable"
|
||||||
|
@click.stop="confirmVisible = false"
|
||||||
|
>
|
||||||
|
Don't Discard Hand
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ModalAnimation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Modal from "./Modal";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: `DiscardHand`,
|
||||||
|
components: {
|
||||||
|
"ModalAnimation": Modal
|
||||||
|
},
|
||||||
|
data() {return {
|
||||||
|
confirmVisible: false,
|
||||||
|
}},
|
||||||
|
computed: {
|
||||||
|
buttonIcon() {
|
||||||
|
return this.$store.state.discard_hand_icon;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
discardHand() {
|
||||||
|
/**
|
||||||
|
* Tells the server to discard the hand of the mediums
|
||||||
|
*/
|
||||||
|
this.confirmVisible = false;
|
||||||
|
|
||||||
|
console.debug(`Telling server to discard team's hand.`);
|
||||||
|
this.$socket.client.emit(`NewHand`, {
|
||||||
|
game_code: this.$store.state.game_code,
|
||||||
|
team: this.$store.state.team
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#DiscardHandButton {
|
||||||
|
height: var(--size);
|
||||||
|
width: var(--size);
|
||||||
|
position: absolute;
|
||||||
|
--size: 120px;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-hand {
|
||||||
|
background-color: var(--background3);
|
||||||
|
border-radius: 100% 0 0 0;
|
||||||
|
position: relative;
|
||||||
|
border-style: none;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 15px;
|
||||||
|
right: 15px;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-button {
|
||||||
|
border-radius: 7px;
|
||||||
|
border-style: none;
|
||||||
|
font-size: larger;
|
||||||
|
padding: 7px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm {
|
||||||
|
background-color: var(--confirm-background);
|
||||||
|
color: var(--confirm-text);
|
||||||
|
}
|
||||||
|
.confirm:hover { background-color: var(--confirm-background-darken); }
|
||||||
|
.confirm:focus { background-color: var(--confirm-background-lighten); }
|
||||||
|
|
||||||
|
.cancel {
|
||||||
|
background-color: var(--cancel-background);
|
||||||
|
color: var(--cancel-text);
|
||||||
|
}
|
||||||
|
.cancel:hover { background-color: var(--cancel-background-darken); }
|
||||||
|
.cancel:focus { background-color: var(--cancel-background-lighten); }
|
||||||
|
</style>
|
||||||
253
web/src/components/GameBoard.vue
Normal file
253
web/src/components/GameBoard.vue
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
<template>
|
||||||
|
<div id="GameBoard">
|
||||||
|
<div id="other-team-answers" class="team-container">
|
||||||
|
<h2 class="centre">{{ $store.getters.otherTeamName }} Answers</h2>
|
||||||
|
<div class="answer-container">
|
||||||
|
<!--
|
||||||
|
Repeats to create the number of team answers that we need,
|
||||||
|
these text inputs are always disabled for the player as these
|
||||||
|
inputs are only ever for the opposing team. They still have
|
||||||
|
a model attribute to keep them synced correctly.
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="answer"
|
||||||
|
v-for="answerIndex in 8"
|
||||||
|
:key="`${otherTeamID}-answer-container-${answerIndex}`"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="other-team-answer"
|
||||||
|
:id="`${otherTeamID}-answer-${answerIndex}`"
|
||||||
|
v-model="answers[`team_${3 - $store.state.team}`][answerIndex-1]"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Display the number of eyes for the slot on the board -->
|
||||||
|
<span
|
||||||
|
class="other-team eye-container"
|
||||||
|
v-if="$store.state[`team_${3 - $store.state.team}`].eyes[answerIndex] > 0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="$store.state[`team_${3 - $store.state.team}`].eyes[answerIndex] > 1"
|
||||||
|
class="eye-multiplier"
|
||||||
|
>
|
||||||
|
{{ $store.state[`team_${3 - $store.state.team}`].eyes[answerIndex] }} x
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
class="eye"
|
||||||
|
:src="`/assets/${$store.state.eye_icon}`"
|
||||||
|
alt="reveal another letter"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="team-answers" class="team-container">
|
||||||
|
<h2 class="centre">{{ $store.getters.teamName }} Answers</h2>
|
||||||
|
<div class="answer-container">
|
||||||
|
<!--
|
||||||
|
This repeats to create the volume oftext inputs that we need,
|
||||||
|
only allowing the text inputs to be used by the spirit players
|
||||||
|
and having them be disabled for all other players
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="answer"
|
||||||
|
v-for="answerIndex in 8"
|
||||||
|
:key="`${teamID}-answer-container-${answerIndex}`"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="team-answer"
|
||||||
|
:id="`${teamID}-answer-${answerIndex}`"
|
||||||
|
@input.stop="answerInputHandler(answerIndex)"
|
||||||
|
v-model="answers[`team_${$store.state.team}`][answerIndex-1]"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Display the number of eyes for the slot on the board -->
|
||||||
|
<span
|
||||||
|
class="team eye-container"
|
||||||
|
v-if="$store.state[`team_${$store.state.team}`].eyes[answerIndex] > 0"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="eye"
|
||||||
|
:src="`/assets/${$store.state.eye_icon}`"
|
||||||
|
alt="reveal another letter"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="$store.state[`team_${$store.state.team}`].eyes[answerIndex] > 1"
|
||||||
|
class="eye-multiplier"
|
||||||
|
>
|
||||||
|
x {{ $store.state[`team_${$store.state.team}`].eyes[answerIndex] }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="past-questions-toggle clickable"
|
||||||
|
@click.self="visible = !visible"
|
||||||
|
>
|
||||||
|
{{ visible ? `Hide` : `Show` }} Past Questions
|
||||||
|
</button>
|
||||||
|
<transition name="expand-from-left">
|
||||||
|
<past-questions v-if="visible" @error="$emit(`error`, $event)" />
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import PastQuestions from './PastQuestions';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: `GameBoard`,
|
||||||
|
components: {
|
||||||
|
"past-questions": PastQuestions
|
||||||
|
},
|
||||||
|
data() {return {
|
||||||
|
visible: false,
|
||||||
|
answers: {
|
||||||
|
team_1: [ ``, ``, ``, ``, ``, ``, ``, `` ],
|
||||||
|
team_2: [ ``, ``, ``, ``, ``, ``, ``, `` ],
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
computed: {
|
||||||
|
teamID() {
|
||||||
|
return this.$store.getters.teamName.replace(/\s/g, `-`).toLowerCase();
|
||||||
|
},
|
||||||
|
otherTeamID() {
|
||||||
|
return this.$store.getters.otherTeamName.replace(/\s/g, `-`).toLowerCase();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
answerInputHandler(answerIndex) {
|
||||||
|
/**
|
||||||
|
* Sends input data updates to the server when they occur, indicating
|
||||||
|
* the data as necessary
|
||||||
|
*/
|
||||||
|
let team = this.$store.state.team;
|
||||||
|
let data = {
|
||||||
|
game_code: this.$store.state.game_code,
|
||||||
|
team: team,
|
||||||
|
answer: answerIndex,
|
||||||
|
value: this.answers[`team_${team}`][answerIndex - 1]
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$socket.client.emit(`UpdateAnswer`, data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sockets: {
|
||||||
|
UpdateAnswer(data) {
|
||||||
|
/**
|
||||||
|
* Receives the updates for the answer for both teams, updating the
|
||||||
|
* data for the text inputs to be displayed dynamically.
|
||||||
|
*
|
||||||
|
* data -> {
|
||||||
|
* team: 1 | 2,
|
||||||
|
* answer: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8,
|
||||||
|
* value: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
this.answers[`team_${data.team}`].splice(data.answer - 1, 1, data.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#GameBoard {
|
||||||
|
background-color: var(--board-background);
|
||||||
|
color: var(--board-background-text);
|
||||||
|
justify-content: space-evenly;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
flex-direction: row;
|
||||||
|
border-radius: 20px;
|
||||||
|
position: relative;
|
||||||
|
margin: 15px auto;
|
||||||
|
display: flex;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-container {
|
||||||
|
grid-template-rows: 50px 1fr;
|
||||||
|
display: grid;
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-container {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye-container {
|
||||||
|
position: absolute;
|
||||||
|
width: 70px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.team {
|
||||||
|
right: -40px;
|
||||||
|
top: 25%;
|
||||||
|
}
|
||||||
|
.other-team {
|
||||||
|
text-align: right;
|
||||||
|
left: -50px;
|
||||||
|
top: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye {
|
||||||
|
height: 25px;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye-multiplier {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
font-family: var(--fonts);
|
||||||
|
background-color: var(--board-background-alt);
|
||||||
|
color: var(--board-background-alt-text);
|
||||||
|
border-color: transparent;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 7px;
|
||||||
|
border-width: 2px;
|
||||||
|
font-size: larger;
|
||||||
|
outline: none;
|
||||||
|
margin: 7px 0;
|
||||||
|
padding: 7px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
input[type="text"]:focus {
|
||||||
|
border-color: var(--board-background-text);
|
||||||
|
}
|
||||||
|
input[type="text"].team-answer {
|
||||||
|
padding-right: 5%;
|
||||||
|
}
|
||||||
|
input[type="text"].other-team-answer {
|
||||||
|
padding-left: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--board-background-alt);
|
||||||
|
border-radius: 0 20px 0 7px;
|
||||||
|
position: absolute;
|
||||||
|
padding: 10px;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
button:hover { background-color: var(--board-background-alt-darken); }
|
||||||
|
button:focus { background-color: var(--board-background-alt-lighten); }
|
||||||
|
</style>
|
||||||
185
web/src/components/Hand.vue
Normal file
185
web/src/components/Hand.vue
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
<template>
|
||||||
|
<div id="PlayerHand">
|
||||||
|
<div class="recentQuestion" v-if="mostRecentQuestion">
|
||||||
|
{{ mostRecentQuestion }}
|
||||||
|
</div>
|
||||||
|
<div class="hand" v-else>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
v-for="cardIndex in questions.length"
|
||||||
|
:key="`card_${cardIndex}`"
|
||||||
|
@click.self="handleCardClick(cardIndex)"
|
||||||
|
>
|
||||||
|
<p class="card-text centre">
|
||||||
|
{{ questions[cardIndex - 1] }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="card-button clickable"
|
||||||
|
@click.stop="sendCard(cardIndex)"
|
||||||
|
>
|
||||||
|
{{ buttonLabel }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: `PlayerHand`,
|
||||||
|
components: {},
|
||||||
|
data() {return {
|
||||||
|
mostRecentQuestion: null,
|
||||||
|
}},
|
||||||
|
computed: {
|
||||||
|
userRole() {
|
||||||
|
return this.$store.state.role;
|
||||||
|
},
|
||||||
|
isGuesser() {
|
||||||
|
return this.userRole === `guesser`;
|
||||||
|
},
|
||||||
|
isWriter() {
|
||||||
|
return this.userRole === `writer`;
|
||||||
|
},
|
||||||
|
buttonLabel() {
|
||||||
|
if (this.isGuesser) {
|
||||||
|
return this.$store.state.guesser_card_button
|
||||||
|
} else if (this.isWriter) {
|
||||||
|
return this.$store.state.writer_card_button
|
||||||
|
} else {
|
||||||
|
return `Unknown Role`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
questions() {
|
||||||
|
return this.$store.state.questions;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sendCard(cardIndex) {
|
||||||
|
|
||||||
|
// Create the data object for the server to receive
|
||||||
|
let data = {
|
||||||
|
game_code: this.$store.state.game_code,
|
||||||
|
text: this.questions[cardIndex - 1],
|
||||||
|
from: this.userRole,
|
||||||
|
team: this.$store.state.team,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.questions.splice(cardIndex - 1, 1);
|
||||||
|
|
||||||
|
// Discard the rest of the writer's hand
|
||||||
|
if (this.isWriter) {
|
||||||
|
this.mostRecentQuestion = data.text;
|
||||||
|
this.$store.commit(`replaceHand`, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$socket.client.emit(`SendCard`, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.isGuesser) {
|
||||||
|
console.debug(`Getting hand from server`);
|
||||||
|
this.$socket.client.emit(`GetHand`, {
|
||||||
|
game_code: this.$store.state.game_code,
|
||||||
|
team: this.$store.state.team
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sockets: {
|
||||||
|
UpdateHand(data) {
|
||||||
|
/**
|
||||||
|
* Triggered when the client gets a new card for their hand, if the
|
||||||
|
* "from" property is set to either of the
|
||||||
|
*
|
||||||
|
* data = {
|
||||||
|
* questions: String[],
|
||||||
|
* mode: "append"|"replace",
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
console.debug(`Updating hand.`);
|
||||||
|
switch (data.mode) {
|
||||||
|
case `append`:
|
||||||
|
if (this.isWriter && this.mostRecentQuestion) {
|
||||||
|
this.mostRecentQuestion = null;
|
||||||
|
};
|
||||||
|
this.$store.commit(`appendToHand`, data.questions);
|
||||||
|
break;
|
||||||
|
case `replace`:
|
||||||
|
this.$store.commit(`replaceHand`, data.questions);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Server returned an unsupported mode: ${data.mode}`);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#PlayerHand {
|
||||||
|
background-color: var(--background2);
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0px;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentQuestion {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: var(--card-background);
|
||||||
|
color: var(--card-text);
|
||||||
|
flex-direction: column;
|
||||||
|
width: calc(100% / 9);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
height: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-quote {
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 7px;
|
||||||
|
font-size: larger;
|
||||||
|
padding: 7px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-button {
|
||||||
|
background: var(--card-button);
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
|
.card-button:hover { background-color: var(--card-button-darken); }
|
||||||
|
</style>
|
||||||
85
web/src/components/Modal.vue
Normal file
85
web/src/components/Modal.vue
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<transition name="fade" @after-enter="content = true">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="modal-container"
|
||||||
|
:class="closable ? 'clickable' : 'unclickable'"
|
||||||
|
@click.self.stop="handleBackgroundClick"
|
||||||
|
>
|
||||||
|
<transition name="burst" @after-leave="$emit('closed')">
|
||||||
|
<div v-if="content" class="modal unclickable">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: `ModalAnimation`,
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
closable: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {return {
|
||||||
|
content: false,
|
||||||
|
}},
|
||||||
|
methods: {
|
||||||
|
handleBackgroundClick() {
|
||||||
|
if (this.closable) {
|
||||||
|
this.content = false;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(newVal) {
|
||||||
|
/**
|
||||||
|
* This method is used to re-set the animation for the modal when
|
||||||
|
* the modal is closed through changing the `show` property rather
|
||||||
|
* than having a `closed` event emitted.
|
||||||
|
*/
|
||||||
|
if (!newVal) {
|
||||||
|
this.content = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
@import "../css/transitions.css";
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
background-color: var(--modal-background-blur);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 10;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background-color: var(--modal-content-background);
|
||||||
|
color: var(--modal-content-text);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 75%;
|
||||||
|
padding: 15px;
|
||||||
|
z-index: 11;
|
||||||
|
width: 40%
|
||||||
|
}
|
||||||
|
</style>
|
||||||
47
web/src/components/ObjectReminder.vue
Normal file
47
web/src/components/ObjectReminder.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<div id="ObjectReminder">
|
||||||
|
<span class="text">
|
||||||
|
Object:
|
||||||
|
<br>
|
||||||
|
{{ targetObject }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: `ObjectReminder`,
|
||||||
|
components: {},
|
||||||
|
computed: {
|
||||||
|
targetObject() {
|
||||||
|
return this.$store.state.chosen_object;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#ObjectReminder {
|
||||||
|
background-color: var(--background3);
|
||||||
|
color: var(--background3-text);
|
||||||
|
border-radius: 100% 0 0 0;
|
||||||
|
height: var(--size);
|
||||||
|
width: var(--size);
|
||||||
|
position: fixed;
|
||||||
|
--size: 120px;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
font-size: large;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
96
web/src/components/PastQuestions.vue
Normal file
96
web/src/components/PastQuestions.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<div id="PastQuestions">
|
||||||
|
<h2 class="centre">{{ $store.getters.teamName }} Questions</h2>
|
||||||
|
<div class="questions-container">
|
||||||
|
<div
|
||||||
|
class="question"
|
||||||
|
v-for="questionIndex in 8"
|
||||||
|
:key="`question_${questionIndex}`"
|
||||||
|
>
|
||||||
|
{{ questions[questionIndex - 1] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: `PastQuestions`,
|
||||||
|
components: {},
|
||||||
|
data() {return {
|
||||||
|
intervalID: null,
|
||||||
|
questions: [],
|
||||||
|
}},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
requestQuestions() {
|
||||||
|
console.debug(`Requesting questions for team ${this.$store.state.team}`);
|
||||||
|
this.$socket.client.emit(`GetPastQuestions`, {
|
||||||
|
game_code: this.$store.state.game_code,
|
||||||
|
team: this.$store.state.team
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sockets: {
|
||||||
|
PastQuestions(data) {
|
||||||
|
if (data.status < 200 || 300 <= data.status) {
|
||||||
|
this.$emit(`error`, data);
|
||||||
|
};
|
||||||
|
this.questions = data.questions;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.requestQuestions();
|
||||||
|
this.intervalID = setInterval(
|
||||||
|
this.requestQuestions,
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.intervalID != null) {
|
||||||
|
clearInterval(this.intervalID);
|
||||||
|
console.debug(`Cleared interval with ID: ${this.intervalID}`);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#PastQuestions {
|
||||||
|
background-color: var(--board-background-alt);
|
||||||
|
color: var(--board-background-alt-text);
|
||||||
|
border-radius: 20px 0 0 20px;
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
z-index: 3;
|
||||||
|
width: 50%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questions-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question {
|
||||||
|
background-color: var(--board-background);
|
||||||
|
color: var(--board-background-text);
|
||||||
|
padding: 10px 10px 0 10px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 7px;
|
||||||
|
display: flex;
|
||||||
|
margin: 5px;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
89
web/src/components/PlayerList.vue
Normal file
89
web/src/components/PlayerList.vue
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<div id="player_list" class="centre">
|
||||||
|
<h2>Players:</h2>
|
||||||
|
<div
|
||||||
|
v-for="player in $store.state.players"
|
||||||
|
:key="player.name"
|
||||||
|
>
|
||||||
|
{{ player.name }}
|
||||||
|
<span v-if="player.role" class="player-role">
|
||||||
|
( {{ teamName(player.team) }} {{ roleName(player.role) }} )
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: `PlayerList`,
|
||||||
|
components: {},
|
||||||
|
computed: {
|
||||||
|
isHost() {
|
||||||
|
return this.$store.state.is_host;
|
||||||
|
},
|
||||||
|
players() {
|
||||||
|
return this.$store.state.players;
|
||||||
|
},
|
||||||
|
gameCode() {
|
||||||
|
return this.$store.state.game_code;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
teamName(teamID) {
|
||||||
|
return this.$store.state[`team_${teamID}`].name;
|
||||||
|
},
|
||||||
|
roleName(role) {
|
||||||
|
return this.$store.state[`${role}_name`]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sockets: {
|
||||||
|
PlayerUpdate(data) {
|
||||||
|
/**
|
||||||
|
* data = {
|
||||||
|
* status: integer,
|
||||||
|
* mode: "modify" | "new" | "remove",
|
||||||
|
* name: string,
|
||||||
|
* team: integer,
|
||||||
|
* role: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
if (!(200 <= data.status && data.status < 300)) {
|
||||||
|
this.$emit(`error`, data);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (data.action) {
|
||||||
|
case "modify":
|
||||||
|
if (this.$store.state.name === data.name) {
|
||||||
|
this.$store.commit(`player`, {
|
||||||
|
role: data.role,
|
||||||
|
team: data.team,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.$store.commit(`updatePlayer`, data)
|
||||||
|
break;
|
||||||
|
case "new":
|
||||||
|
case "remove":
|
||||||
|
this.$store.commit(`playerList`, data.players);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown response type from "PlayerUpdate": ${data.mode}`);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#player_list {
|
||||||
|
background-color: var(--background2);
|
||||||
|
color: var(--background2-text);
|
||||||
|
padding: 0 15px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 5px 10px;
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
web/src/components/TeamReminder.vue
Normal file
58
web/src/components/TeamReminder.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<div id="TeamReminder">
|
||||||
|
<img
|
||||||
|
:class="`team_${teamNumber}`"
|
||||||
|
:src="`/assets/${teamIcon}`"
|
||||||
|
:alt="`${teamName} Team Icon`"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: `TeamReminder`,
|
||||||
|
components: {},
|
||||||
|
computed: {
|
||||||
|
teamNumber() {
|
||||||
|
return this.$store.state.team;
|
||||||
|
},
|
||||||
|
teamName() {
|
||||||
|
return this.$store.state[`team_${this.teamNumber}`].name;
|
||||||
|
},
|
||||||
|
teamIcon() {
|
||||||
|
return this.$store.state[`team_${this.teamNumber}`].icon;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#TeamReminder {
|
||||||
|
background-color: var(--background3);
|
||||||
|
border-radius: 0 100% 0 0;
|
||||||
|
height: var(--size);
|
||||||
|
width: var(--size);
|
||||||
|
position: fixed;
|
||||||
|
--size: 120px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.team_1 {
|
||||||
|
width: calc(var(--size) / 1.5);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 7px;
|
||||||
|
left: 7px;;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.team_2 {
|
||||||
|
width: calc(var(--size) / 2);
|
||||||
|
bottom: calc(var(--size) / 8);
|
||||||
|
left: calc(var(--size) / 8);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
web/src/components/TeamRoleSelect.vue
Normal file
95
web/src/components/TeamRoleSelect.vue
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div :id="`${teamName}-role-select`" class="team-select">
|
||||||
|
<h2 class="centre">{{ teamName }} Team</h2>
|
||||||
|
<button
|
||||||
|
class="clickable"
|
||||||
|
@click.stop="joinRole(`writer`)"
|
||||||
|
>
|
||||||
|
{{ $store.state.writer_name }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="clickable"
|
||||||
|
@click.stop="joinRole(`guesser`)"
|
||||||
|
>
|
||||||
|
{{ $store.state.guesser_name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: `TeamRoleSelection`,
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
teamID: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
teamName() {
|
||||||
|
return this.$store.state[`team_${this.teamID}`].name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
joinRole(role) {
|
||||||
|
|
||||||
|
if (this.teamID == this.$store.state.team && this.$store.state.role == role) {
|
||||||
|
this.$emit(`error`, {
|
||||||
|
status: 403,
|
||||||
|
message: `You are already that role on that team.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = {
|
||||||
|
action: `modify`,
|
||||||
|
game_code: this.$store.state.game_code,
|
||||||
|
name: this.$store.state.name,
|
||||||
|
to: {
|
||||||
|
team: this.teamID,
|
||||||
|
role: role,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.state.team != null) {
|
||||||
|
response.from = {
|
||||||
|
team: this.$store.state.team,
|
||||||
|
role: this.$store.state.role,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$socket.client.emit(`UpdatePlayer`, response);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
.team-select {
|
||||||
|
background-color: var(--background2);
|
||||||
|
color: var(--background2-text);
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 20px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0 10px 0;
|
||||||
|
display: flex;
|
||||||
|
margin: 5px;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--background3);
|
||||||
|
color: var(--background3-text);
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: larger;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px;
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
button:hover { background-color: var(--background3-darken); }
|
||||||
|
button:focus { background-color: var(--background3-lighten); }
|
||||||
|
</style>
|
||||||
17
web/src/css/style.css
Normal file
17
web/src/css/style.css
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
.maximize {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable { cursor: pointer; }
|
||||||
|
.unclickable { cursor: default; }
|
||||||
|
|
||||||
|
.centre {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-style: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
77
web/src/css/theme.css
Normal file
77
web/src/css/theme.css
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
:root {
|
||||||
|
|
||||||
|
/*
|
||||||
|
The fonts and font colours the site will use
|
||||||
|
*/
|
||||||
|
--fonts: "Roboto", "Open Sans", sans-serif;
|
||||||
|
--light-font-colour: #ECE3BB;
|
||||||
|
--dark-font-colour: #000F3D;
|
||||||
|
|
||||||
|
/*
|
||||||
|
The darkest colour in the trio for backgrounds, this is used for the
|
||||||
|
site-wide background and modal content.
|
||||||
|
*/
|
||||||
|
--background1: #001233;
|
||||||
|
--background1-darken: #000c24;
|
||||||
|
--background1-lighten: #001a49;
|
||||||
|
--background1-text: var(--light-font-colour);
|
||||||
|
|
||||||
|
/*
|
||||||
|
The middle colour in the trio for backgrounds.
|
||||||
|
Used for hand background, buttons, regions, etc.
|
||||||
|
*/
|
||||||
|
--background2: #24356E;
|
||||||
|
--background2-darken: #19295e;
|
||||||
|
--background2-lighten: #364a8d;
|
||||||
|
--background2-text: var(--light-font-colour);
|
||||||
|
|
||||||
|
/* The colours used for the lightest shades of the background regions. */
|
||||||
|
--background3: #4A5081;
|
||||||
|
--background3-darken: #454b7e;
|
||||||
|
--background3-lighten: #5a6192;
|
||||||
|
--background3-text: var(--light-font-colour);
|
||||||
|
|
||||||
|
/*
|
||||||
|
The colours for the cards in the hands of the players
|
||||||
|
*/
|
||||||
|
--card-background: #E1D098;
|
||||||
|
--card-text: var(--dark-font-colour);
|
||||||
|
--card-button: #ACA885;
|
||||||
|
--card-button-darken: #88845e;
|
||||||
|
--card-button-lighten: #d1ceaf;
|
||||||
|
|
||||||
|
/*
|
||||||
|
The colours used for the main game board. While playing, the alt colour
|
||||||
|
is used for the text input backgrounds, as well as the question toggle
|
||||||
|
button, then used as the main background of the past questions insert,
|
||||||
|
with the primary background being the text input background.
|
||||||
|
|
||||||
|
The text colours are also used for the SVG eyes, if you change the
|
||||||
|
"--board-background-text" variable, also change the fill value in the
|
||||||
|
/public/assets/eye.svg file to match it
|
||||||
|
*/
|
||||||
|
--board-background: #ACA885;
|
||||||
|
--board-background-alt: #E1D098;
|
||||||
|
--board-background-alt-darken: #cab981;
|
||||||
|
--board-background-alt-lighten: #f1e4b7;
|
||||||
|
--board-background-text: var(--dark-font-colour);
|
||||||
|
--board-background-alt-text: var(--dark-font-colour);
|
||||||
|
|
||||||
|
/* The fill colour of the eyes on the game board */
|
||||||
|
--eye-colour: #000000;
|
||||||
|
|
||||||
|
/* The colours for the modals */
|
||||||
|
--modal-background-blur: #000000a6;
|
||||||
|
--modal-content-background: var(--background1);
|
||||||
|
--modal-content-text: var(--background1-text);
|
||||||
|
|
||||||
|
/* The colours used for the buttons in the modal that are for confirming actions */
|
||||||
|
--confirm-background: #018501;
|
||||||
|
--confirm-background-darken: #006600;
|
||||||
|
--confirm-background-lighten: #00aa00;
|
||||||
|
--confirm-text: white;
|
||||||
|
--cancel-background: #aa0000;
|
||||||
|
--cancel-background-darken: #830101;
|
||||||
|
--cancel-background-lighten: #e71111;
|
||||||
|
--cancel-text: white;
|
||||||
|
}
|
||||||
115
web/src/css/transitions.css
Normal file
115
web/src/css/transitions.css
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
/* Transition for modal background appearing/disappearing */
|
||||||
|
.fade-enter-active, .fade-leave-active {
|
||||||
|
transition: opacity .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter, .fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Transition for modal card appearing disappearing */
|
||||||
|
.top-slide-enter-active {
|
||||||
|
animation: top-slide-in 1s;
|
||||||
|
}
|
||||||
|
.top-slide-leave-active { animation: top-slide-out 1s; }
|
||||||
|
|
||||||
|
@keyframes top-slide-in {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes top-slide-out {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Transition for past questions expanding in */
|
||||||
|
.expand-from-left-enter-active {
|
||||||
|
animation: expand-from-left 1s;
|
||||||
|
transform-origin: left;
|
||||||
|
}
|
||||||
|
.expand-from-left-leave-active {
|
||||||
|
animation: shrink-to-left 1s;
|
||||||
|
transform-origin: left;
|
||||||
|
}
|
||||||
|
@keyframes expand-from-left {
|
||||||
|
0% {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes shrink-to-left {
|
||||||
|
0% {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Transition for modal card appearing disappearing */
|
||||||
|
.burst-enter-active {
|
||||||
|
animation: burst-in .5s;
|
||||||
|
}
|
||||||
|
.burst-leave-active { animation: burst-out .5s; }
|
||||||
|
|
||||||
|
@keyframes burst-in {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes burst-out {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (min-width: 768px) {
|
||||||
|
@keyframes burst-in {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.25);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes burst-out {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.25);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
web/src/main.js
Normal file
16
web/src/main.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import store from './store';
|
||||||
|
import io from 'socket.io-client';
|
||||||
|
import clipboard from "vue-clipboard2";
|
||||||
|
import VueSocketIOExt from 'vue-socket.io-extended';
|
||||||
|
|
||||||
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
Vue.use(clipboard);
|
||||||
|
Vue.use(VueSocketIOExt, io(`http://${window.location.hostname}:8081`));
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
store,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app');
|
||||||
125
web/src/store/index.js
Normal file
125
web/src/store/index.js
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import VuexPersistence from 'vuex-persist';
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
state: {
|
||||||
|
team_1: {
|
||||||
|
name: `Sun`,
|
||||||
|
icon: `sun.svg`,
|
||||||
|
eyes: {
|
||||||
|
1: 0, 2: 0,
|
||||||
|
3: 0, 4: 0,
|
||||||
|
5: 0, 6: 0,
|
||||||
|
7: 0, 8: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
team_2: {
|
||||||
|
name: `Moon`,
|
||||||
|
icon: `moon.svg`,
|
||||||
|
eyes: {
|
||||||
|
1: 0, 2: 0,
|
||||||
|
3: 0, 4: 0,
|
||||||
|
5: 0, 6: 0,
|
||||||
|
7: 0, 8: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
writer_name: `Spirit`,
|
||||||
|
writer_card_button: `Answer Question`,
|
||||||
|
writer_object_choose_button: `Choose Object`,
|
||||||
|
|
||||||
|
guesser_name: `Medium`,
|
||||||
|
guesser_card_button: `Ask Spirit`,
|
||||||
|
eye_icon: `eye.svg`,
|
||||||
|
|
||||||
|
discard_hand_icon: `trash.svg`,
|
||||||
|
|
||||||
|
//===========================================================================//
|
||||||
|
// DO NOT EDIT ANYTHING BELOW THIS COMMENT
|
||||||
|
view: `login`,
|
||||||
|
role: null,
|
||||||
|
team: null,
|
||||||
|
name: ``,
|
||||||
|
is_host: false,
|
||||||
|
chosen_object: null,
|
||||||
|
questions: [],
|
||||||
|
game_code: null,
|
||||||
|
players: [],
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
teamName(state) {
|
||||||
|
if (state.team > 0) {
|
||||||
|
return state[`team_${state.team}`].name;
|
||||||
|
};
|
||||||
|
return ``;
|
||||||
|
},
|
||||||
|
otherTeamName(state) {
|
||||||
|
if (state.team > 0) {
|
||||||
|
return state[`team_${3 - state.team}`].name;
|
||||||
|
};
|
||||||
|
return ``;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
resetState(state) {
|
||||||
|
state.view = `login`;
|
||||||
|
state.role = null;
|
||||||
|
state.team = null;
|
||||||
|
state.name = ``;
|
||||||
|
state.is_host = false;
|
||||||
|
state.chosen_object = null;
|
||||||
|
state.questions = [];
|
||||||
|
state.game_code = null;
|
||||||
|
state.players = [];
|
||||||
|
},
|
||||||
|
player(state, data) {
|
||||||
|
if (data.name)
|
||||||
|
state.name = data.name
|
||||||
|
if (data.role)
|
||||||
|
state.role = data.role
|
||||||
|
if (data.team)
|
||||||
|
state.team = data.team
|
||||||
|
if (data.host)
|
||||||
|
state.is_host = data.host
|
||||||
|
},
|
||||||
|
game_code(state, game_code) {
|
||||||
|
state.game_code = game_code;
|
||||||
|
},
|
||||||
|
view(state, target) {
|
||||||
|
state.view = target;
|
||||||
|
},
|
||||||
|
playerList(state, players) {
|
||||||
|
state.players = players;
|
||||||
|
},
|
||||||
|
updatePlayer(state, data) {
|
||||||
|
for (var player of state.players) {
|
||||||
|
if (player.name == data.name) {
|
||||||
|
player.role = data.role;
|
||||||
|
player.team = data.team;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
newPlayer(state, player) {
|
||||||
|
state.players.push(player);
|
||||||
|
},
|
||||||
|
setObject(state, chosenObject) {
|
||||||
|
state.chosen_object = chosenObject;
|
||||||
|
},
|
||||||
|
replaceHand(state, questions) {
|
||||||
|
state.questions = questions;
|
||||||
|
},
|
||||||
|
appendToHand(state, questions) {
|
||||||
|
state.questions.push(...questions);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
},
|
||||||
|
modules: {
|
||||||
|
},
|
||||||
|
plugins:
|
||||||
|
process.env.NODE_ENV === `production`
|
||||||
|
? [new VuexPersistence({ key: `ghost-writer-save` }).plugin]
|
||||||
|
: []
|
||||||
|
});
|
||||||
132
web/src/views/CreateJoin.vue
Normal file
132
web/src/views/CreateJoin.vue
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
<template>
|
||||||
|
<div id="CreateJoinGame" class="maximize view">
|
||||||
|
<h1>Ghost Writer Online</h1>
|
||||||
|
<button
|
||||||
|
@click.stop="createGame()"
|
||||||
|
>Create Game</button>
|
||||||
|
<button
|
||||||
|
@click.stop="joinGame()"
|
||||||
|
>Join Game</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: `CreateJoinGame`,
|
||||||
|
components: {},
|
||||||
|
data() {return {
|
||||||
|
name: null,
|
||||||
|
game_code: null,
|
||||||
|
}},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
createGame() {
|
||||||
|
this.name = prompt(`What is your name?`);
|
||||||
|
|
||||||
|
// Assert that the user entered a name and didn't cancel
|
||||||
|
if (this.name) {
|
||||||
|
this.$socket.client.emit(`CreateGame`, {
|
||||||
|
name: this.name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
joinGame() {
|
||||||
|
// Get the user's name
|
||||||
|
this.name = prompt(`Enter a username:`);
|
||||||
|
|
||||||
|
if (!this.name) {
|
||||||
|
this.$emit(`error`, {
|
||||||
|
status: 406,
|
||||||
|
message: `Can't join a game without a name`,
|
||||||
|
extra: `(provided a false-y name = ${this.name})`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let qs = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
// Get the game code
|
||||||
|
if (qs.has(`game`)) {
|
||||||
|
this.game_code = qs.get(`game`);
|
||||||
|
} else {
|
||||||
|
this.game_code = prompt(`What is the game code?`);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$socket.client.emit(`JoinGame`, {
|
||||||
|
name: this.name,
|
||||||
|
game_code: this.game_code
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sockets: {
|
||||||
|
GameJoined(data) {
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
if (!(200 <= data.status && data.status < 300)) {
|
||||||
|
this.$emit(`error`, data);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
history.replaceState(null, ``, `?game=${this.game_code}`);
|
||||||
|
|
||||||
|
// Save the data in the store
|
||||||
|
this.$store.commit(`playerList`, data.players);
|
||||||
|
this.$store.commit(`game_code`, this.game_code);
|
||||||
|
this.$store.commit(`player`, {
|
||||||
|
name: this.name,
|
||||||
|
host: false,
|
||||||
|
});
|
||||||
|
this.$store.commit(`view`, `lobby`);
|
||||||
|
},
|
||||||
|
GameCreated(data) {
|
||||||
|
|
||||||
|
if (!(200 <= data.status && data.status < 300)) {
|
||||||
|
this.$emit(`error`, data);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
history.replaceState(null, ``, `?game=${data.game_code}`);
|
||||||
|
|
||||||
|
// Update storage
|
||||||
|
this.$store.commit(`playerList`, data.players);
|
||||||
|
this.$store.commit(`game_code`, data.game_code);
|
||||||
|
this.$store.commit(`player`, {
|
||||||
|
name: this.name,
|
||||||
|
host: true,
|
||||||
|
});
|
||||||
|
this.$store.commit(`view`, `lobby`);
|
||||||
|
},
|
||||||
|
GameRejoined(data) {
|
||||||
|
if (!(200 <= data.status && data.status < 300)) {
|
||||||
|
this.$emit(`error`, data);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// TODO -> Update all data that is received from the server
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#CreateJoinGame {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--background2);
|
||||||
|
color: var(--background2-text);
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: larger;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
button:hover { background-color: var(--background2-darken); }
|
||||||
|
button:focus { background-color: var(--background2-lighten); }
|
||||||
|
</style>
|
||||||
62
web/src/views/InGame.vue
Normal file
62
web/src/views/InGame.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div id="GameView" class="maximize">
|
||||||
|
<game-board
|
||||||
|
v-if="isGuesser || objectChosen"
|
||||||
|
@error="$emit(`error`, $event)"
|
||||||
|
/>
|
||||||
|
<object-selector
|
||||||
|
v-else
|
||||||
|
@error="$emit(`error`, $event)"
|
||||||
|
/>
|
||||||
|
<player-hand @error="$emit(`error`, $event)" />
|
||||||
|
<team-reminder @error="$emit(`error`, $event)" />
|
||||||
|
<discard-hand
|
||||||
|
v-if="isGuesser"
|
||||||
|
@error="$emit(`error`, $event)"
|
||||||
|
/>
|
||||||
|
<object-reminder v-else-if="isWriter && objectChosen" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ObjectReminder from "../components/ObjectReminder";
|
||||||
|
import DiscardHand from "../components/DiscardHandButton";
|
||||||
|
import ObjectSelector from "../components/ChooseObject";
|
||||||
|
import TeamReminder from "../components/TeamReminder";
|
||||||
|
import GameBoard from "../components/GameBoard";
|
||||||
|
import PlayerHand from "../components/Hand";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: `InGame`,
|
||||||
|
components: {
|
||||||
|
"object-selector": ObjectSelector,
|
||||||
|
"team-reminder": TeamReminder,
|
||||||
|
"discard-hand": DiscardHand,
|
||||||
|
"player-hand": PlayerHand,
|
||||||
|
"game-board": GameBoard,
|
||||||
|
"object-reminder": ObjectReminder
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isGuesser() {
|
||||||
|
return this.$store.state.role === `guesser`;
|
||||||
|
},
|
||||||
|
isWriter() {
|
||||||
|
return this.$store.state.role === `writer`;
|
||||||
|
},
|
||||||
|
objectChosen() {
|
||||||
|
return this.$store.state.chosen_object != null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#GameView {
|
||||||
|
grid-template-rows: 70% 1fr 50px;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
142
web/src/views/Lobby.vue
Normal file
142
web/src/views/Lobby.vue
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
<template>
|
||||||
|
<div id="GameLobby" class="maximize view">
|
||||||
|
<h1 class="centre">Ghost Writer Online</h1>
|
||||||
|
<div class="flex-row">
|
||||||
|
<button
|
||||||
|
class="clickable"
|
||||||
|
v-clipboard:copy="gameURL"
|
||||||
|
v-clipboard:success="copySuccess"
|
||||||
|
v-clipboard:error="copyError"
|
||||||
|
>
|
||||||
|
{{ copyURLButtonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
|
<role-select
|
||||||
|
:teamID="1"
|
||||||
|
@error="$emit(`error`, $event)"
|
||||||
|
/>
|
||||||
|
<player-list @error="$emit(`error`, $event)" />
|
||||||
|
<role-select
|
||||||
|
:teamID="2"
|
||||||
|
@error="$emit(`error`, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
|
<button
|
||||||
|
@click.stop="exitGame()"
|
||||||
|
>
|
||||||
|
{{ $store.state.is_host ? `Delete` : `Leave`}} Game
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="clickable"
|
||||||
|
@click.stop="startGame()"
|
||||||
|
>
|
||||||
|
Click to Start the Game
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TeamRoleSelect from "../components/TeamRoleSelect";
|
||||||
|
import PlayerList from "../components/PlayerList";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: `GameLobby`,
|
||||||
|
components: {
|
||||||
|
"RoleSelect": TeamRoleSelect,
|
||||||
|
"PlayerList": PlayerList,
|
||||||
|
},
|
||||||
|
data() {return {
|
||||||
|
copyURLButtonText: `Click to Copy Game Link`,
|
||||||
|
}},
|
||||||
|
computed: {
|
||||||
|
playerName() {
|
||||||
|
return this.$store.state.name;
|
||||||
|
},
|
||||||
|
gameURL() {
|
||||||
|
return `${window.location.protocol}//${window.location.host}/?game=${this.gameCode}`;
|
||||||
|
},
|
||||||
|
gameCode() {
|
||||||
|
return this.$store.state.game_code;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copySuccess() {
|
||||||
|
this.copyURLButtonText = `Game Link Copied!`;
|
||||||
|
setTimeout(() => { this.copyURLButtonText = `Click to Copy Game Link`; }, 1000)
|
||||||
|
},
|
||||||
|
copyError() {
|
||||||
|
this.$emit(`error`, {
|
||||||
|
status: 418,
|
||||||
|
message: `Failed to copy game URL`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
exitGame() {
|
||||||
|
// The user is the host, they can't leave the game, so kick
|
||||||
|
// everyone from the game.
|
||||||
|
if (this.$store.state.is_host) {
|
||||||
|
this.$socket.client.emit(`DeleteGame`, {
|
||||||
|
game_code: this.gameCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just a normal user, they can leave the game just fine
|
||||||
|
else {
|
||||||
|
this.$socket.client.emit(`LeaveGame`, {
|
||||||
|
game_code: this.gameCode
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
startGame() {
|
||||||
|
this.$socket.client.emit(`StartGame`, {
|
||||||
|
game_code: this.gameCode
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sockets: {
|
||||||
|
GameLeft(data) {
|
||||||
|
if (data.status < 200 || 300 <= data.status) {
|
||||||
|
return this.$emit(`error`, data);
|
||||||
|
};
|
||||||
|
this.$store.commit(`resetState`);
|
||||||
|
},
|
||||||
|
GameStarted(data) {
|
||||||
|
if (data.status < 200 || 300 <= data.status) {
|
||||||
|
return this.$emit(`error`, data);
|
||||||
|
};
|
||||||
|
this.$store.commit(`view`, `in-game`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import "../css/theme.css";
|
||||||
|
@import "../css/style.css";
|
||||||
|
|
||||||
|
#GameLobby {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-row {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--background2);
|
||||||
|
color: var(--background2-text);
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: larger;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
button:hover { background-color: var(--background2-darken); }
|
||||||
|
button:focus { background-color: var(--background2-lighten); }
|
||||||
|
</style>
|
||||||
1
web/vue.config.js
Normal file
1
web/vue.config.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = {}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue