From 226b0c25e20d84b6e55b308048ae9c220a8fb9b4 Mon Sep 17 00:00:00 2001 From: Alec Date: Mon, 8 Nov 2021 23:38:41 -0500 Subject: [PATCH] new create page logic --- README.md | 2 +- client/config/customCards.js | 12 ++ client/config/{cards.js => defaultCards.js} | 9 +- client/modules/DeckStateManager.js | 48 ++++++++ client/modules/ModalManager.js | 30 +++++ client/modules/Toast.js | 35 ++++++ client/scripts/create.js | 119 +++++++++++++++++- client/styles/GLOBAL.css | 63 +++++++++- client/styles/create.css | 128 ++++++++++++++++++++ client/styles/modal.css | 42 +++++++ client/views/create.html | 41 ++++++- client/views/home.html | 8 +- client/webfonts/Diavlo_LIGHT_II_37.woff2 | Bin 0 -> 17216 bytes client/webfonts/OFL.txt | 94 ++++++++++++++ client/webfonts/SignikaNegative-Light.woff2 | Bin 0 -> 14112 bytes server/routes/static-router.js | 37 ++++++ 16 files changed, 656 insertions(+), 12 deletions(-) create mode 100644 client/config/customCards.js rename client/config/{cards.js => defaultCards.js} (80%) create mode 100644 client/modules/DeckStateManager.js create mode 100644 client/modules/ModalManager.js create mode 100644 client/modules/Toast.js create mode 100644 client/styles/modal.css create mode 100644 client/webfonts/Diavlo_LIGHT_II_37.woff2 create mode 100644 client/webfonts/OFL.txt create mode 100644 client/webfonts/SignikaNegative-Light.woff2 diff --git a/README.md b/README.md index 8e86ca0..953a56f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This is a Javascript application running on a node express main. I am using the All pixel art is my own (for better or for worse). -This is meant to facilitate the game in a face-to-face social setting and provide utility/convenience - not control all aspects of the game flow. The app allows players to create or join a game lobby where state is synchronized. The creator of the game can build a deck from either the standard set of provided cards, or from any number of custom cards the user creates. Once the game begins, this deck will be randomly dealt to all participants. +This is meant to facilitate the game in a face-to-face social setting and provide utility/convenience - not control all aspects of the game flow. The app allows players to create or join a game lobby where state is synchronized. The creator of the game can build a deck from either the standard set of provided defaultCards, or from any number of custom defaultCards the user creates. Once the game begins, this deck will be randomly dealt to all participants. Players will see their card (which can be flipped up and down), an optional timer, and a button to say that they have been killed off. If a player presses the button, they will be removed from the game, and their role revealed to other players. The game will continue until the end of the game is detected, or the timer expires. diff --git a/client/config/customCards.js b/client/config/customCards.js new file mode 100644 index 0000000..e6b46cb --- /dev/null +++ b/client/config/customCards.js @@ -0,0 +1,12 @@ +export const customCards = [ + { + role: "Santa", + team: "evil", + description: "hohoho", + }, + { + role: "Mason", + team: "good", + description: "you are a mason", + }, +]; diff --git a/client/config/cards.js b/client/config/defaultCards.js similarity index 80% rename from client/config/cards.js rename to client/config/defaultCards.js index 9229e93..b24fd81 100644 --- a/client/config/cards.js +++ b/client/config/defaultCards.js @@ -1,4 +1,4 @@ -export const cards = [ +export const defaultCards = [ { role: "Villager", team: "good", @@ -15,10 +15,15 @@ export const cards = [ description: "If a Werewolf dies, you become a Werewolf. You do not wake up with the Werewolves until this happens. You count for parity only after converting to a wolf.", }, { - role: "Minion", + role: "Knowing Minion", team: "evil", description: "You are an evil villager - you know who the wolves are, and you want them to win.", }, + { + role: "Double-Blind Minion", + team: "evil", + description: "You are an evil villager. You don't know who the wolves are, but you want them to win.", + }, { role: "Seer", team: "good", diff --git a/client/modules/DeckStateManager.js b/client/modules/DeckStateManager.js new file mode 100644 index 0000000..398c72a --- /dev/null +++ b/client/modules/DeckStateManager.js @@ -0,0 +1,48 @@ +export class DeckStateManager { + constructor() { + this.deck = null; + this.customRoleOptions = null; + } + + addToDeck(role) { + let option = this.customRoleOptions.find((option) => option.role === role) + let existingCard = this.deck.find((card) => card.role === role) + if (option && !existingCard) { + option.quantity = 0; + this.deck.push(option); + this.customRoleOptions.splice(this.customRoleOptions.indexOf(option), 1); + } + } + + addToCustomRoleOptions(role) { + this.customRoleOptions.push(role); + } + + addCopyOfCard(role) { + let existingCard = this.deck.find((card) => card.role === role) + if (existingCard) { + existingCard.quantity += 1; + } + } + + removeCopyOfCard(role) { + let existingCard = this.deck.find((card) => card.role === role) + if (existingCard && existingCard.quantity > 0) { + existingCard.quantity -= 1; + } + } + + getCurrentDeck() { return this.deck } + + getCard(role) { return this.deck.find((card) => card.role === role) } + + getCurrentCustomRoleOptions() { return this.customRoleOptions } + + getDeckSize() { + let total = 0; + for (let role of this.deck) { + total += role.quantity; + } + return total; + } +} diff --git a/client/modules/ModalManager.js b/client/modules/ModalManager.js new file mode 100644 index 0000000..142245d --- /dev/null +++ b/client/modules/ModalManager.js @@ -0,0 +1,30 @@ +export const ModalManager = { + displayModal: displayModal +} + +function displayModal(modalId, backgroundId, closeButtonId) { + const modal = document.getElementById(modalId); + const modalOverlay = document.getElementById(backgroundId); + const closeBtn = document.getElementById(closeButtonId); + let closeModalHandler; + if (modal && modalOverlay && closeBtn) { + modal.style.display = 'flex'; + modalOverlay.style.display = 'flex'; + modalOverlay.removeEventListener("click", closeModalHandler); + modalOverlay.addEventListener("click", closeModalHandler = function(e) { + e.preventDefault(); + dispelModal(modalId, backgroundId); + }); + closeBtn.removeEventListener("click", closeModalHandler); + closeBtn.addEventListener("click", closeModalHandler); + } +} + +function dispelModal(modalId, backgroundId) { + const modal = document.getElementById(modalId); + const modalOverlay = document.getElementById(backgroundId); + if (modal && modalOverlay) { + modal.style.display = 'none'; + modalOverlay.style.display = 'none'; + } +} diff --git a/client/modules/Toast.js b/client/modules/Toast.js new file mode 100644 index 0000000..c5b5691 --- /dev/null +++ b/client/modules/Toast.js @@ -0,0 +1,35 @@ +export const toast = (message, type, positionAtTop = true) => { + if (message && type) { + buildAndInsertMessageElement(message, type, positionAtTop); + } +}; + +function buildAndInsertMessageElement (message, type, positionAtTop) { + cancelCurrentMessage(); + let backgroundColor; + const position = positionAtTop ? 'top:4rem;' : 'bottom: 15px;'; + switch (type) { + case 'warning': + backgroundColor = '#fff5b1'; + break; + case 'error': + backgroundColor = '#fdaeb7'; + break; + case 'success': + backgroundColor = '#bef5cb'; + break; + } + const messageEl = document.createElement("div"); + messageEl.setAttribute("id", "current-info-message"); + messageEl.setAttribute("style", 'background-color:' + backgroundColor + ';' + position) + messageEl.setAttribute("class", 'info-message'); + messageEl.innerText = message; + document.body.prepend(messageEl); +} + +function cancelCurrentMessage () { + const currentMessage = document.getElementById('current-info-message'); + if (currentMessage !== null) { + currentMessage.remove(); + } +} diff --git a/client/scripts/create.js b/client/scripts/create.js index d00368b..c17f190 100644 --- a/client/scripts/create.js +++ b/client/scripts/create.js @@ -1,3 +1,120 @@ -export const create = () => { +import { toast } from "../modules/Toast.js"; +import { ModalManager } from "../modules/ModalManager.js"; +import { defaultCards } from "../config/defaultCards.js"; +import { customCards } from "../config/customCards.js"; +import { DeckStateManager } from "../modules/DeckStateManager.js"; +export const create = () => { + let deckManager = new DeckStateManager(); + loadDefaultCards(deckManager); + loadCustomCards(deckManager); + document.getElementById("game-form").onsubmit = (e) => { + e.preventDefault(); + } + document.getElementById("custom-role-btn").addEventListener( + "click", () => { + ModalManager.displayModal( + "add-role-modal", + "add-role-modal-background", + "close-modal-button" + ) + } + ) +} + +// Display a widget for each default card that allows copies of it to be added/removed. Set initial deck state. +function loadDefaultCards(deckManager) { + defaultCards.sort((a, b) => { + return a.role.localeCompare(b.role); + }); + let deck = []; + for (let i = 0; i < defaultCards.length; i ++) { // each dropdown should include every + let card = defaultCards[i]; + card.quantity = 0; + let cardEl = constructCompactDeckBuilderElement(defaultCards[i], deckManager); + document.getElementById("deck").appendChild(cardEl); + deck.push(card); + } + deckManager.deck = deck; +} + +/* Display a dropdown containing all the custom roles. Adding one will add it to the game deck and +create a widget for it */ +function loadCustomCards(deckManager) { + let form = document.getElementById("add-card-to-deck-form"); + customCards.sort((a, b) => { + return a.role.localeCompare(b.role); + }); + let selectEl = document.createElement("select"); + selectEl.setAttribute("id", "deck-select"); + addOptionsToList(customCards, selectEl); + form.appendChild(selectEl); + let submitBtn = document.createElement("input"); + submitBtn.setAttribute("type", "submit"); + submitBtn.setAttribute("value", "Add Role to Deck"); + submitBtn.addEventListener('click', (e) => { + e.preventDefault(); + if (selectEl.selectedIndex > 0) { + deckManager.addToDeck(selectEl.value); + let cardEl = constructCompactDeckBuilderElement(deckManager.getCard(selectEl.value), deckManager); + updateCustomRoleOptionsList(deckManager, selectEl); + document.getElementById("deck").appendChild(cardEl); + } + }) + form.appendChild(submitBtn); + + + deckManager.customRoleOptions = customCards; +} + +function updateCustomRoleOptionsList(deckManager, selectEl) { + document.querySelectorAll('#deck-select option').forEach(e => e.remove()); + addOptionsToList(deckManager.customRoleOptions, selectEl); +} + +function addOptionsToList(options, selectEl) { + let noneSelected = document.createElement("option"); + noneSelected.innerText = "None selected" + noneSelected.disabled = true; + noneSelected.selected = true; + selectEl.appendChild(noneSelected); + for (let i = 0; i < options.length; i ++) { // each dropdown should include every + let optionEl = document.createElement("option"); + optionEl.setAttribute("value", customCards[i].role); + optionEl.innerText = customCards[i].role; + selectEl.appendChild(optionEl); + } +} + +function constructCompactDeckBuilderElement(card, deckManager) { + const cardContainer = document.createElement("div"); + + cardContainer.setAttribute("class", "compact-card"); + + cardContainer.setAttribute("id", "card-" + card.role); + cardContainer.setAttribute("id", "card-" + card.role); + + cardContainer.innerHTML = + "
" + + "

-

" + + "
" + + "
" + + "

" + card.role + "

" + + "
0
" + + "
" + + "
" + + "

+

" + + "
"; + + cardContainer.querySelector('.compact-card-right').addEventListener('click', () => { + deckManager.addCopyOfCard(card.role); + cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity; + document.querySelector('label[for="deck"]').innerText = 'Game Deck: ' + deckManager.getDeckSize() + ' Players'; + }); + cardContainer.querySelector('.compact-card-left').addEventListener('click', () => { + deckManager.removeCopyOfCard(card.role); + cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity; + document.querySelector('label[for="deck"]').innerText = 'Game Deck: ' + deckManager.getDeckSize() + ' Players'; + }); + return cardContainer; } diff --git a/client/styles/GLOBAL.css b/client/styles/GLOBAL.css index c1ffd0f..af97402 100644 --- a/client/styles/GLOBAL.css +++ b/client/styles/GLOBAL.css @@ -11,7 +11,68 @@ th, thead, tr, tt, u, ul, var { border: 0; background: transparent; } +@font-face { + font-family: 'diavlo'; + src: url("../webfonts/Diavlo_LIGHT_II_37.woff2") format("woff2"); +} + +@font-face { + font-family: 'signika-negative'; + src: url("../webfonts/SignikaNegative-Light.woff2") format("woff2"); +} html { - font-family: sans-serif; + font-family: 'signika-negative', sans-serif !important; + background-color: #23282b !important; +} + +body { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 95%; + margin: 0 auto; + max-width: 75em; +} + +h1 { + font-family: 'diavlo', sans-serif; + color: #ab2626; + filter: drop-shadow(2px 2px 4px black); + font-size: 40px; + margin: 0.5em 0; +} + +h3 { + color: #d7d7d7; + font-family: 'signika-negative', sans-serif; + font-weight: normal; + font-size: 18px; + margin: 0.5em 0; +} + +label { + color: #d7d7d7; + font-family: 'signika-negative', sans-serif; + font-size: 18px; + font-weight: normal; +} + +button, input[type="submit"] { + font-family: 'signika-negative', sans-serif !important; + padding: 10px; + background-color: #1f1f1f; + border: none; + border-radius: 3px; + color: white; + font-size: 18px; + cursor: pointer; +} + +button:hover, input[type="submit"]:hover { + background-color: #4f4f4f; +} + +input { + padding: 10px; } diff --git a/client/styles/create.css b/client/styles/create.css index e69de29..ce3e0ee 100644 --- a/client/styles/create.css +++ b/client/styles/create.css @@ -0,0 +1,128 @@ +.compact-card { + text-align: center; + cursor: pointer; + position: relative; + margin: 0.3em; + background-color: #393a40; + color: gray; + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.4); + border-radius: 3px; + user-select: none; + max-width: 15em; + min-width: 12em; + display: flex; + height: max-content; +} + +.compact-card h1 { + display: flex; + align-items: center; + font-size: 14px; + margin: 0 10px 0 10px; +} + +.compact-card .card-role { + color: #bfb8b8; + margin: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 8em; +} + +.compact-card-right p { + font-size: 40px; + margin: 0 10px 0 0; + display: flex; + justify-content: flex-end; +} + +.compact-card-left p { + font-size: 40px; + margin: 0 0 0 10px; + display: flex; + justify-content: flex-start; +} + +.compact-card-left, .compact-card-right { + width: 50%; +} + +.compact-card .card-quantity { + text-align: center; + margin: 0; +} + +.compact-card-header { + position: absolute; + left: 0; + right: 0; + margin: auto; + flex-direction: column; + align-items: center; + justify-content: center; + display: flex; + top: 0; + pointer-events: none; + text-align: center; +} + +#deck-container, #deck, #custom-roles-container { + width: fit-content; +} + +#deck-container, #custom-roles-container { + margin: 0.5em 0; +} + +#deck { + display: flex; + flex-wrap: wrap; +} + +form { + width: 100%; + margin: 1em 0; +} + +select { + padding: 10px; + font-size: 18px; + font-family: 'signika-negative', sans-serif; +} + +#deck-container, #custom-roles-container { + border: 1px solid #3d4448; + padding: 10px; + border-radius: 3px; +} + +#game-form > div { + display: flex; + flex-direction: column; + border: 1px solid #3d4448; + padding: 10px; + border-radius: 3px; + width: fit-content; +} + +#game-form > div > label { + display: flex; +} + +#game-time label, #game-time input { + margin-right: 10px; +} + +label[for="game-time"], label[for="add-card-to-deck-form"], label[for="deck"] { + color: #0075F2; + font-size: 30px; + border-radius: 3px; + margin-bottom: 10px; +} + +#create-game{ + color: #45a445; + font-size: 30px; + margin-top: 2em; +} diff --git a/client/styles/modal.css b/client/styles/modal.css new file mode 100644 index 0000000..50ddde9 --- /dev/null +++ b/client/styles/modal.css @@ -0,0 +1,42 @@ +.modal { + border-radius: 2px; + text-align: center; + position: fixed; + width: 100%; + height: 100%; + z-index: 100; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #f7f7f7; + align-items: center; + justify-content: center; + max-width: 17em; + max-height: 20em; + font-family: sans-serif; + font-size: 22px; + flex-direction: column; + padding: 1em; +} + +.modal-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: calc(100% + 100px); + background-color: rgba(0, 0, 0, 0.55); + z-index: 50; + cursor: pointer; +} + +.modal > form > div { + display: flex; + flex-direction: column; + margin-bottom: 1em; +} + +.modal > form > div > label { + display: flex; + font-size: 16px; +} diff --git a/client/views/create.html b/client/views/create.html index 5198cf1..b0bc825 100644 --- a/client/views/create.html +++ b/client/views/create.html @@ -15,16 +15,49 @@ + + +

Create A Game

- Creating a game gives you the moderator role. You will not be dealt a card. You will know everyone's role and can - remove any player from the game. You can also play/pause the optional timer and, if desired, delegate your moderator - role to any other player. + Creating a game gives you the moderator role with certain special permissions. You will not be dealt a card.

+
+ +
+ +
+
+ +
+
- +
+ +
+ + + + +
+
+