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 + "
" + + "+
" + + "