rework of deck builder page
@@ -1,4 +1,4 @@
|
||||
export const defaultCards = [
|
||||
export const defaultRoles = [
|
||||
{
|
||||
role: 'Villager',
|
||||
team: 'good',
|
||||
@@ -20,7 +20,7 @@ export const defaultCards = [
|
||||
description: 'Each night, learn if a chosen person is the Seer.'
|
||||
},
|
||||
{
|
||||
role: 'Minion',
|
||||
role: 'Knowing Minion',
|
||||
team: 'evil',
|
||||
description: 'You are an evil Villager, and you know who the Werewolves are.'
|
||||
},
|
||||
@@ -1,4 +1,5 @@
|
||||
export const globals = {
|
||||
CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
|
||||
USER_SIGNATURE_LENGTH: 25,
|
||||
CLOCK_TICK_INTERVAL_MILLIS: 100,
|
||||
MAX_CUSTOM_ROLE_NAME_LENGTH: 30,
|
||||
|
||||
BIN
client/src/images/framed-phone-screenshot-2.png
Normal file
|
After Width: | Height: | Size: 723 KiB |
10
client/src/images/remove.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="-1 -1 18 18" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#a70000" d="M8,0C3.589,0,0,3.589,0,8s3.589,8,8,8s8-3.589,8-8S12.411,0,8,0z M8,14c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6
|
||||
S11.309,14,8,14z"/>
|
||||
<rect fill="#a70000" x="4" y="7" width="8" height="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
BIN
client/src/images/roles/BrutalHunter.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
BIN
client/src/images/tutorial/add-role-to-deck.gif
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
client/src/images/tutorial/create-custom-role.gif
Normal file
|
After Width: | Height: | Size: 508 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 882 KiB After Width: | Height: | Size: 775 KiB |
@@ -1,43 +1,14 @@
|
||||
import { globals } from '../config/globals.js';
|
||||
import { toast } from './Toast.js';
|
||||
import { ModalManager } from './ModalManager.js';
|
||||
import { HTMLFragments } from './HTMLFragments.js';
|
||||
|
||||
export class DeckStateManager {
|
||||
constructor () {
|
||||
this.deck = null;
|
||||
this.customRoleOptions = [];
|
||||
this.createMode = false;
|
||||
this.currentlyEditingRoleName = null;
|
||||
this.deck = [];
|
||||
}
|
||||
|
||||
addToDeck (role) {
|
||||
const option = this.customRoleOptions.find((option) => option.role === role);
|
||||
if (option) {
|
||||
option.quantity = 0;
|
||||
this.deck.push(option);
|
||||
this.customRoleOptions.splice(this.customRoleOptions.indexOf(option), 1);
|
||||
}
|
||||
}
|
||||
|
||||
addToCustomRoleOptions (role) {
|
||||
this.customRoleOptions.push(role);
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoleOptions.concat(this.deck.filter(card => card.custom === true))));
|
||||
}
|
||||
|
||||
updateCustomRoleOption (option, name, description, team) {
|
||||
option.role = name;
|
||||
option.description = description;
|
||||
option.team = team;
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoleOptions.concat(this.deck.filter(card => card.custom === true))));
|
||||
}
|
||||
|
||||
removeFromCustomRoleOptions (name) {
|
||||
const option = this.customRoleOptions.find((option) => option.role === name);
|
||||
if (option) {
|
||||
this.customRoleOptions.splice(this.customRoleOptions.indexOf(option), 1);
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoleOptions.concat(this.deck.filter(card => card.custom === true))));
|
||||
toast('"' + name + '" deleted.', 'error', true, true, 'short');
|
||||
}
|
||||
role.quantity = 1;
|
||||
this.deck.push(role);
|
||||
}
|
||||
|
||||
addCopyOfCard (role) {
|
||||
@@ -49,27 +20,25 @@ export class DeckStateManager {
|
||||
|
||||
removeCopyOfCard (role) {
|
||||
const existingCard = this.deck.find((card) => card.role === role);
|
||||
if (existingCard && existingCard.quantity > 0) {
|
||||
if (existingCard.quantity > 0) {
|
||||
existingCard.quantity -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentDeck () { return this.deck; }
|
||||
removeRoleEntirelyFromDeck (entry) {
|
||||
const existingCard = this.deck.find((card) => card.role === entry.role);
|
||||
if (existingCard) {
|
||||
existingCard.quantity = 0;
|
||||
this.updateDeckStatus();
|
||||
}
|
||||
}
|
||||
|
||||
getCard (role) {
|
||||
hasRole (role) {
|
||||
return this.deck.find(
|
||||
(card) => card.role.toLowerCase().trim() === role.toLowerCase().trim()
|
||||
);
|
||||
}
|
||||
|
||||
getCurrentCustomRoleOptions () { return this.customRoleOptions; }
|
||||
|
||||
getCustomRoleOption (role) {
|
||||
return this.customRoleOptions.find(
|
||||
(option) => option.role.toLowerCase().trim() === role.toLowerCase().trim()
|
||||
);
|
||||
};
|
||||
|
||||
getDeckSize () {
|
||||
let total = 0;
|
||||
for (const role of this.deck) {
|
||||
@@ -78,86 +47,67 @@ export class DeckStateManager {
|
||||
return total;
|
||||
}
|
||||
|
||||
loadCustomRolesFromCookies () {
|
||||
const customRoles = localStorage.getItem('play-werewolf-custom-roles');
|
||||
if (customRoles !== null && validateCustomRoleCookie(customRoles)) {
|
||||
this.customRoleOptions = JSON.parse(customRoles); // we know it is valid JSON from the validate function
|
||||
}
|
||||
}
|
||||
|
||||
loadCustomRolesFromFile (file, updateRoleListFunction, loadDefaultCardsFn, showIncludedCardsFn) {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = (e) => {
|
||||
toast(reader.error.message, 'error', true, true, 'medium');
|
||||
};
|
||||
reader.onload = (e) => {
|
||||
let string;
|
||||
if (typeof e.target.result !== 'string') {
|
||||
string = new TextDecoder('utf-8').decode(e.target.result);
|
||||
} else {
|
||||
string = e.target.result;
|
||||
updateDeckStatus = () => {
|
||||
document.getElementById('deck-count').innerText = this.getDeckSize() + ' Players';
|
||||
if (this.deck.length > 0) {
|
||||
if (document.getElementById('deck-list-placeholder')) {
|
||||
document.getElementById('deck-list-placeholder').remove();
|
||||
}
|
||||
if (validateCustomRoleCookie(string)) {
|
||||
this.customRoleOptions = JSON.parse(string); // we know it is valid JSON from the validate function
|
||||
ModalManager.dispelModal('upload-custom-roles-modal', 'modal-background');
|
||||
toast('Roles imported successfully', 'success', true, true, 'short');
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoleOptions));
|
||||
updateRoleListFunction(this, document.getElementById('deck-select'));
|
||||
// loadDefaultCardsFn(this);
|
||||
// showIncludedCardsFn(this);
|
||||
} else {
|
||||
toast(
|
||||
'Invalid formatting. Make sure you import the file as downloaded from this page.',
|
||||
'error',
|
||||
true,
|
||||
true,
|
||||
'medium'
|
||||
);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// via https://stackoverflow.com/a/18197341
|
||||
downloadCustomRoles (filename, text) {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
// this is user-supplied, so we should validate it fully
|
||||
function validateCustomRoleCookie (cookie) {
|
||||
const valid = false;
|
||||
if (typeof cookie === 'string' && new Blob([cookie]).size <= 1000000) {
|
||||
try {
|
||||
const cookieJSON = JSON.parse(cookie);
|
||||
if (Array.isArray(cookieJSON)) {
|
||||
for (const entry of cookieJSON) {
|
||||
if (typeof entry === 'object') {
|
||||
if (typeof entry.role !== 'string' || entry.role.length > globals.MAX_CUSTOM_ROLE_NAME_LENGTH
|
||||
|| typeof entry.team !== 'string' || (entry.team !== globals.ALIGNMENT.GOOD && entry.team !== globals.ALIGNMENT.EVIL)
|
||||
|| typeof entry.description !== 'string' || entry.description.length > globals.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const sortedDeck = this.deck.sort((a, b) => {
|
||||
if (a.team !== b.team) {
|
||||
return a.team === globals.ALIGNMENT.GOOD ? 1 : -1;
|
||||
}
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
for (let i = 0; i < sortedDeck.length; i ++) {
|
||||
const existingCardEl = document.querySelector('#deck-list [data-role-id="' + sortedDeck[i].id + '"]');
|
||||
if (sortedDeck[i].quantity > 0) {
|
||||
if (existingCardEl) {
|
||||
existingCardEl.querySelector('.role-name').innerText = sortedDeck[i].quantity + 'x ' + sortedDeck[i].role;
|
||||
} else {
|
||||
return false;
|
||||
const roleEl = document.createElement('div');
|
||||
roleEl.dataset.roleId = sortedDeck[i].id;
|
||||
roleEl.classList.add('added-role');
|
||||
roleEl.innerHTML = HTMLFragments.DECK_SELECT_ROLE_ADDED_TO_DECK;
|
||||
// roleEl.classList.add('deck-role');
|
||||
if (sortedDeck[i].team === globals.ALIGNMENT.GOOD) {
|
||||
roleEl.classList.add(globals.ALIGNMENT.GOOD);
|
||||
} else {
|
||||
roleEl.classList.add(globals.ALIGNMENT.EVIL);
|
||||
}
|
||||
roleEl.querySelector('.role-name').innerText = sortedDeck[i].quantity + 'x ' + sortedDeck[i].role;
|
||||
document.getElementById('deck-list').appendChild(roleEl);
|
||||
const minusOneHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.removeCopyOfCard(sortedDeck[i].role);
|
||||
this.updateDeckStatus();
|
||||
}
|
||||
};
|
||||
roleEl.querySelector('.role-remove').addEventListener('click', minusOneHandler);
|
||||
roleEl.querySelector('.role-remove').addEventListener('keyup', minusOneHandler);
|
||||
}
|
||||
} else {
|
||||
sortedDeck[i].markedForRemoval = true;
|
||||
if (existingCardEl) {
|
||||
existingCardEl.remove();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
this.deck = this.deck.filter((card) => {
|
||||
if (card.markedForRemoval) {
|
||||
card.markedForRemoval = false;
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.deck.length === 0) {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.setAttribute('id', 'deck-list-placeholder');
|
||||
placeholder.innerText = 'Add a card from the role box.';
|
||||
document.getElementById('deck-list').appendChild(placeholder);
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,16 +4,15 @@ import { ModalManager } from './ModalManager.js';
|
||||
import { XHRUtility } from './XHRUtility.js';
|
||||
import { globals } from '../config/globals.js';
|
||||
import { HTMLFragments } from './HTMLFragments.js';
|
||||
import { defaultCards } from '../config/defaultCards.js';
|
||||
import { UserUtility } from './UserUtility.js';
|
||||
import { RoleBox } from './RoleBox.js';
|
||||
|
||||
export class GameCreationStepManager {
|
||||
constructor (deckManager) {
|
||||
loadDefaultCards(deckManager);
|
||||
deckManager.loadCustomRolesFromCookies();
|
||||
this.step = 1;
|
||||
this.currentGame = new Game(null, null, null, null);
|
||||
this.deckManager = deckManager;
|
||||
this.roleBox = null;
|
||||
this.defaultBackHandler = () => {
|
||||
cancelCurrentToast();
|
||||
this.removeStepElementsFromDOM(this.step);
|
||||
@@ -38,7 +37,7 @@ export class GameCreationStepManager {
|
||||
title: 'Create your deck of cards:',
|
||||
forwardHandler: () => {
|
||||
if (this.deckManager.getDeckSize() >= 3 && this.deckManager.getDeckSize() <= 50) {
|
||||
this.currentGame.deck = deckManager.getCurrentDeck().filter((card) => card.quantity > 0);
|
||||
this.currentGame.deck = this.deckManager.deck.filter((card) => card.quantity > 0);
|
||||
cancelCurrentToast();
|
||||
this.removeStepElementsFromDOM(this.step);
|
||||
this.incrementStep();
|
||||
@@ -173,7 +172,7 @@ export class GameCreationStepManager {
|
||||
showButtons(false, true, this.steps[step].forwardHandler, null);
|
||||
break;
|
||||
case 2:
|
||||
renderRoleSelectionStep(this.currentGame, containerId, step, this.deckManager);
|
||||
this.renderRoleSelectionStep(this.currentGame, containerId, step, this.deckManager);
|
||||
showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler);
|
||||
break;
|
||||
case 3:
|
||||
@@ -197,6 +196,81 @@ export class GameCreationStepManager {
|
||||
removeStepElementsFromDOM (stepNumber) {
|
||||
document.getElementById('step-' + stepNumber)?.remove();
|
||||
}
|
||||
|
||||
renderRoleSelectionStep = (game, containerId, step, deckManager) => {
|
||||
const stepContainer = document.createElement('div');
|
||||
|
||||
setAttributes(stepContainer, { id: 'step-' + step, class: 'flex-row-container-left-align step' });
|
||||
|
||||
stepContainer.innerHTML += HTMLFragments.CREATE_GAME_DECK_STATUS;
|
||||
|
||||
document.getElementById(containerId).appendChild(stepContainer);
|
||||
this.roleBox = new RoleBox(stepContainer, deckManager);
|
||||
this.roleBox.loadDefaultRoles();
|
||||
this.roleBox.loadCustomRolesFromCookies();
|
||||
this.roleBox.displayDefaultRoles(document.getElementById('role-select'));
|
||||
|
||||
const exportHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.roleBox.downloadCustomRoles('play-werewolf-custom-roles', JSON.stringify(
|
||||
this.roleBox.customRoles
|
||||
.map((option) => (
|
||||
{ role: option.role, team: option.team, description: option.description, custom: option.custom }
|
||||
))
|
||||
));
|
||||
toast('Custom roles downloading', 'success', true, true);
|
||||
document.getElementById('custom-role-actions').style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('#custom-roles-export').addEventListener('click', exportHandler);
|
||||
document.querySelector('#custom-roles-export').addEventListener('keyup', exportHandler);
|
||||
|
||||
const importHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
ModalManager.displayModal('upload-custom-roles-modal', 'modal-background', 'close-upload-custom-roles-modal-button');
|
||||
}
|
||||
};
|
||||
document.querySelector('#custom-roles-import').addEventListener('click', importHandler);
|
||||
document.querySelector('#custom-roles-import').addEventListener('keyup', importHandler);
|
||||
|
||||
document.getElementById('upload-custom-roles-form').onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const fileList = document.getElementById('upload-custom-roles').files;
|
||||
if (fileList.length > 0) {
|
||||
const file = fileList[0];
|
||||
if (file.size > 1000000) {
|
||||
toast('Your file is too large (max 1MB)', 'error', true, true, 'medium');
|
||||
return;
|
||||
}
|
||||
if (file.type !== 'text/plain') {
|
||||
toast('Your file must be a text file', 'error', true, true, 'medium');
|
||||
return;
|
||||
}
|
||||
|
||||
this.roleBox.loadCustomRolesFromFile(file);
|
||||
} else {
|
||||
toast('You must upload a text file', 'error', true, true, 'medium');
|
||||
}
|
||||
};
|
||||
|
||||
const clickHandler = () => {
|
||||
const actions = document.getElementById('custom-role-actions');
|
||||
if (window.getComputedStyle(actions, null).display !== 'none') {
|
||||
actions.style.display = 'none';
|
||||
} else {
|
||||
actions.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('custom-role-hamburger').addEventListener('click', clickHandler);
|
||||
|
||||
deckManager.updateDeckStatus();
|
||||
|
||||
initializeRemainingEventListeners(deckManager, this.roleBox);
|
||||
};
|
||||
}
|
||||
|
||||
function renderNameStep (containerId, step, game, steps) {
|
||||
@@ -208,7 +282,6 @@ function renderNameStep (containerId, step, game, steps) {
|
||||
const nameInput = document.querySelector('#moderator-name');
|
||||
nameInput.value = game.moderatorName;
|
||||
nameInput.addEventListener('keyup', steps['4'].forwardHandler);
|
||||
nameInput.focus();
|
||||
}
|
||||
|
||||
function renderModerationTypeStep (game, containerId, stepNumber) {
|
||||
@@ -253,78 +326,6 @@ function renderModerationTypeStep (game, containerId, stepNumber) {
|
||||
document.getElementById(containerId).appendChild(stepContainer);
|
||||
}
|
||||
|
||||
function renderRoleSelectionStep (game, containerId, step, deckManager) {
|
||||
const stepContainer = document.createElement('div');
|
||||
setAttributes(stepContainer, { id: 'step-' + step, class: 'flex-row-container-left-align step' });
|
||||
|
||||
stepContainer.innerHTML = HTMLFragments.CREATE_GAME_CUSTOM_ROLES;
|
||||
stepContainer.innerHTML += HTMLFragments.CREATE_GAME_DECK_STATUS;
|
||||
stepContainer.innerHTML += HTMLFragments.CREATE_GAME_DECK;
|
||||
|
||||
const exportHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
deckManager.downloadCustomRoles('play-werewolf-custom-roles', JSON.stringify(
|
||||
deckManager.getCurrentCustomRoleOptions()
|
||||
.map((option) => (
|
||||
{ role: option.role, team: option.team, description: option.description, custom: option.custom }
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
document.getElementById(containerId).appendChild(stepContainer);
|
||||
document.querySelector('#custom-roles-export').addEventListener('click', exportHandler);
|
||||
document.querySelector('#custom-roles-export').addEventListener('keyup', exportHandler);
|
||||
|
||||
const importHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
ModalManager.displayModal('upload-custom-roles-modal', 'modal-background', 'close-upload-custom-roles-modal-button');
|
||||
}
|
||||
};
|
||||
document.querySelector('#custom-roles-import').addEventListener('click', importHandler);
|
||||
document.querySelector('#custom-roles-import').addEventListener('keyup', importHandler);
|
||||
|
||||
document.getElementById('upload-custom-roles-form').onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const fileList = document.getElementById('upload-custom-roles').files;
|
||||
if (fileList.length > 0) {
|
||||
const file = fileList[0];
|
||||
if (file.size > 1000000) {
|
||||
toast('Your file is too large (max 1MB)', 'error', true, true, 'medium');
|
||||
return;
|
||||
}
|
||||
if (file.type !== 'text/plain') {
|
||||
toast('Your file must be a text file', 'error', true, true, 'medium');
|
||||
return;
|
||||
}
|
||||
|
||||
deckManager.loadCustomRolesFromFile(file, updateCustomRoleOptionsList, loadDefaultCards, showIncludedCards);
|
||||
} else {
|
||||
toast('You must upload a text file', 'error', true, true, 'medium');
|
||||
}
|
||||
};
|
||||
|
||||
const clickHandler = () => {
|
||||
const actions = document.getElementById('custom-role-actions');
|
||||
if (window.getComputedStyle(actions, null).display !== 'none') {
|
||||
actions.style.display = 'none';
|
||||
} else {
|
||||
actions.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('custom-role-hamburger').addEventListener('click', clickHandler);
|
||||
|
||||
showIncludedCards(deckManager);
|
||||
|
||||
loadCustomRoles(deckManager);
|
||||
|
||||
updateDeckStatus(deckManager);
|
||||
|
||||
initializeRemainingEventListeners(deckManager);
|
||||
}
|
||||
|
||||
function renderTimerStep (containerId, stepNumber, game, steps) {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('id', 'step-' + stepNumber);
|
||||
@@ -465,119 +466,25 @@ function showButtons (back, forward, forwardHandler, backHandler, builtGame = nu
|
||||
}
|
||||
}
|
||||
|
||||
// Display a widget for each default card that allows copies of it to be added/removed. Set initial deck state.
|
||||
function showIncludedCards (deckManager) {
|
||||
document.querySelectorAll('.compact-card').forEach((el) => { el.remove(); });
|
||||
for (let i = 0; i < deckManager.getCurrentDeck().length; i ++) {
|
||||
const card = deckManager.getCurrentDeck()[i];
|
||||
const cardEl = constructCompactDeckBuilderElement(card, deckManager);
|
||||
if (card.team === globals.ALIGNMENT.GOOD) {
|
||||
document.getElementById('deck-good').appendChild(cardEl);
|
||||
} else {
|
||||
document.getElementById('deck-evil').appendChild(cardEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Display a dropdown containing all the custom roles. Adding one will add it to the game deck and
|
||||
create a widget for it */
|
||||
function loadCustomRoles (deckManager) {
|
||||
addOptionsToList(deckManager, document.getElementById('deck-select'));
|
||||
}
|
||||
|
||||
function loadDefaultCards (deckManager) {
|
||||
defaultCards.sort((a, b) => {
|
||||
if (a.team !== b.team) {
|
||||
return a.team === globals.ALIGNMENT.GOOD ? 1 : -1;
|
||||
}
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
const deck = [];
|
||||
for (let i = 0; i < defaultCards.length; i ++) {
|
||||
const card = defaultCards[i];
|
||||
card.quantity = 0;
|
||||
deck.push(card);
|
||||
}
|
||||
deckManager.deck = deck;
|
||||
}
|
||||
|
||||
function constructCompactDeckBuilderElement (card, deckManager) {
|
||||
const cardContainer = document.createElement('div');
|
||||
const alignmentClass = card.team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL;
|
||||
|
||||
cardContainer.setAttribute('class', 'compact-card ' + alignmentClass);
|
||||
|
||||
cardContainer.setAttribute('id', 'card-' + card.role.replaceAll(' ', '-'));
|
||||
|
||||
cardContainer.innerHTML =
|
||||
"<div tabindex='0' class='compact-card-left'>" +
|
||||
'<p>-</p>' +
|
||||
'</div>' +
|
||||
"<div class='compact-card-header'>" +
|
||||
"<p class='card-role'></p>" +
|
||||
"<div class='card-quantity'></div>" +
|
||||
'</div>' +
|
||||
"<div tabindex='0' class='compact-card-right'>" +
|
||||
'<p>+</p>' +
|
||||
'</div>';
|
||||
|
||||
cardContainer.querySelector('.card-role').innerText = card.role;
|
||||
cardContainer.title = card.role;
|
||||
cardContainer.querySelector('.card-quantity').innerText = card.quantity;
|
||||
|
||||
if (card.quantity > 0) {
|
||||
cardContainer.classList.add('selected-card');
|
||||
}
|
||||
|
||||
const addHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
deckManager.addCopyOfCard(card.role);
|
||||
updateDeckStatus(deckManager);
|
||||
cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity;
|
||||
if (deckManager.getCard(card.role).quantity > 0) {
|
||||
document.getElementById('card-' + card.role.replaceAll(' ', '-')).classList.add('selected-card');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
deckManager.removeCopyOfCard(card.role);
|
||||
updateDeckStatus(deckManager);
|
||||
cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity;
|
||||
if (deckManager.getCard(card.role).quantity === 0) {
|
||||
document.getElementById('card-' + card.role.replaceAll(' ', '-')).classList.remove('selected-card');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cardContainer.querySelector('.compact-card-right').addEventListener('click', addHandler);
|
||||
cardContainer.querySelector('.compact-card-right').addEventListener('keyup', addHandler);
|
||||
cardContainer.querySelector('.compact-card-left').addEventListener('click', removeHandler);
|
||||
cardContainer.querySelector('.compact-card-left').addEventListener('keyup', removeHandler);
|
||||
|
||||
return cardContainer;
|
||||
}
|
||||
|
||||
function initializeRemainingEventListeners (deckManager) {
|
||||
function initializeRemainingEventListeners (deckManager, roleBox) {
|
||||
document.getElementById('role-form').onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('role-name').value.trim();
|
||||
const description = document.getElementById('role-description').value.trim();
|
||||
const team = document.getElementById('role-alignment').value.toLowerCase().trim();
|
||||
if (deckManager.createMode) {
|
||||
if (!deckManager.getCustomRoleOption(name) && !deckManager.getCard(name)) { // confirm there is no existing custom role with the same name
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, false);
|
||||
if (roleBox.createMode) {
|
||||
if (!roleBox.getCustomRole(name) && !roleBox.getDefaultRole(name)) { // confirm there is no existing custom role with the same name
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, false, roleBox);
|
||||
} else {
|
||||
toast('There is already a role with this name', 'error', true, true, 'short');
|
||||
toast('There is already a default or custom role with this name', 'error', true, true, 'short');
|
||||
}
|
||||
} else {
|
||||
const option = deckManager.getCustomRoleOption(deckManager.currentlyEditingRoleName);
|
||||
if (name === option.role) { // did they edit the name?
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, true, option);
|
||||
const entry = roleBox.getCustomRole(roleBox.currentlyEditingRoleName);
|
||||
if (name === entry.role) { // did they edit the name?
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, true, roleBox, entry);
|
||||
} else {
|
||||
if (!deckManager.getCustomRoleOption(name) && !deckManager.getCard(name)) {
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, true, option);
|
||||
if (!roleBox.getCustomRole(name) && !roleBox.getDefaultRole(name)) {
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, true, roleBox, entry);
|
||||
} else {
|
||||
toast('There is already a role with this name', 'error', true, true, 'short');
|
||||
}
|
||||
@@ -588,8 +495,8 @@ function initializeRemainingEventListeners (deckManager) {
|
||||
'click', () => {
|
||||
const createBtn = document.getElementById('create-role-button');
|
||||
createBtn.setAttribute('value', 'Create');
|
||||
deckManager.createMode = true;
|
||||
deckManager.currentlyEditingRoleName = null;
|
||||
roleBox.createMode = true;
|
||||
roleBox.currentlyEditingRoleName = null;
|
||||
document.getElementById('role-name').value = '';
|
||||
document.getElementById('role-alignment').value = globals.ALIGNMENT.GOOD;
|
||||
document.getElementById('role-description').value = '';
|
||||
@@ -602,7 +509,7 @@ function initializeRemainingEventListeners (deckManager) {
|
||||
);
|
||||
}
|
||||
|
||||
function processNewCustomRoleSubmission (name, description, team, deckManager, isUpdate, option = null) {
|
||||
function processNewCustomRoleSubmission (name, description, team, deckManager, isUpdate, roleBox, option = null) {
|
||||
if (name.length > 40) {
|
||||
toast('Your name is too long (max 40 characters).', 'error', true);
|
||||
return;
|
||||
@@ -612,147 +519,16 @@ function processNewCustomRoleSubmission (name, description, team, deckManager, i
|
||||
return;
|
||||
}
|
||||
if (isUpdate) {
|
||||
deckManager.updateCustomRoleOption(option, name, description, team);
|
||||
roleBox.updateCustomRole(option, name, description, team);
|
||||
ModalManager.dispelModal('role-modal', 'modal-background');
|
||||
toast('Role Updated', 'success', true);
|
||||
} else {
|
||||
deckManager.addToCustomRoleOptions({ role: name, description: description, team: team, custom: true });
|
||||
roleBox.addCustomRole({ role: name, description: description, team: team, custom: true });
|
||||
ModalManager.dispelModal('role-modal', 'modal-background');
|
||||
toast('Role Created', 'success', true);
|
||||
}
|
||||
|
||||
updateCustomRoleOptionsList(deckManager, document.getElementById('deck-select'));
|
||||
}
|
||||
|
||||
function updateCustomRoleOptionsList (deckManager, selectEl) {
|
||||
document.querySelectorAll('#deck-select .deck-select-role').forEach(e => e.remove());
|
||||
addOptionsToList(deckManager, selectEl);
|
||||
}
|
||||
|
||||
function addOptionsToList (deckManager, selectEl) {
|
||||
const options = deckManager.getCurrentCustomRoleOptions();
|
||||
options.sort((a, b) => {
|
||||
if (a.team !== b.team) {
|
||||
return a.team === globals.ALIGNMENT.GOOD ? 1 : -1;
|
||||
}
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
for (let i = 0; i < options.length; i ++) {
|
||||
const optionEl = document.createElement('div');
|
||||
optionEl.innerHTML = HTMLFragments.DECK_SELECT_ROLE;
|
||||
optionEl.classList.add('deck-select-role');
|
||||
const alignmentClass = options[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL;
|
||||
optionEl.classList.add(alignmentClass);
|
||||
optionEl.querySelector('.deck-select-role-name').innerText = options[i].role;
|
||||
selectEl.appendChild(optionEl);
|
||||
}
|
||||
|
||||
addCustomRoleEventListeners(deckManager, selectEl);
|
||||
}
|
||||
|
||||
function addCustomRoleEventListeners (deckManager, select) {
|
||||
document.querySelectorAll('.deck-select-role').forEach((role) => {
|
||||
const name = role.querySelector('.deck-select-role-name').innerText;
|
||||
const includeHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (!deckManager.getCard(name)) {
|
||||
deckManager.addToDeck(name);
|
||||
const cardEl = constructCompactDeckBuilderElement(deckManager.getCard(name), deckManager);
|
||||
toast('"' + name + '" made available below.', 'success', true, true, 'medium');
|
||||
if (deckManager.getCard(name).team === globals.ALIGNMENT.GOOD) {
|
||||
document.getElementById('deck-good').appendChild(cardEl);
|
||||
} else {
|
||||
document.getElementById('deck-evil').appendChild(cardEl);
|
||||
}
|
||||
updateCustomRoleOptionsList(deckManager, select);
|
||||
} else {
|
||||
toast('"' + select.value + '" already included.', 'error', true, true, 'short');
|
||||
}
|
||||
}
|
||||
};
|
||||
role.querySelector('.deck-select-include').addEventListener('click', includeHandler);
|
||||
role.querySelector('.deck-select-include').addEventListener('keyup', includeHandler);
|
||||
|
||||
const removeHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
if (confirm("Delete the role '" + name + "'?")) {
|
||||
e.preventDefault();
|
||||
deckManager.removeFromCustomRoleOptions(name);
|
||||
updateCustomRoleOptionsList(deckManager, select);
|
||||
}
|
||||
}
|
||||
};
|
||||
role.querySelector('.deck-select-remove').addEventListener('click', removeHandler);
|
||||
role.querySelector('.deck-select-remove').addEventListener('keyup', removeHandler);
|
||||
|
||||
const infoHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
const alignmentEl = document.getElementById('custom-role-info-modal-alignment');
|
||||
alignmentEl.classList.remove(globals.ALIGNMENT.GOOD);
|
||||
alignmentEl.classList.remove(globals.ALIGNMENT.EVIL);
|
||||
e.preventDefault();
|
||||
const option = deckManager.getCustomRoleOption(name);
|
||||
document.getElementById('custom-role-info-modal-name').innerText = name;
|
||||
alignmentEl.classList.add(option.team);
|
||||
document.getElementById('custom-role-info-modal-description').innerText = option.description;
|
||||
alignmentEl.innerText = option.team;
|
||||
ModalManager.displayModal('custom-role-info-modal', 'modal-background', 'close-custom-role-info-modal-button');
|
||||
}
|
||||
};
|
||||
role.querySelector('.deck-select-info').addEventListener('click', infoHandler);
|
||||
role.querySelector('.deck-select-info').addEventListener('keyup', infoHandler);
|
||||
|
||||
const editHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
const option = deckManager.getCustomRoleOption(name);
|
||||
document.getElementById('role-name').value = option.role;
|
||||
document.getElementById('role-alignment').value = option.team;
|
||||
document.getElementById('role-description').value = option.description;
|
||||
deckManager.createMode = false;
|
||||
deckManager.currentlyEditingRoleName = option.role;
|
||||
const createBtn = document.getElementById('create-role-button');
|
||||
createBtn.setAttribute('value', 'Update');
|
||||
ModalManager.displayModal('role-modal', 'modal-background', 'close-modal-button');
|
||||
}
|
||||
};
|
||||
role.querySelector('.deck-select-edit').addEventListener('click', editHandler);
|
||||
role.querySelector('.deck-select-edit').addEventListener('keyup', editHandler);
|
||||
});
|
||||
}
|
||||
|
||||
function updateDeckStatus (deckManager) {
|
||||
document.querySelectorAll('.deck-role').forEach((el) => el.remove());
|
||||
document.getElementById('deck-count').innerText = deckManager.getDeckSize() + ' Players';
|
||||
if (deckManager.getDeckSize() === 0) {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.setAttribute('id', 'deck-list-placeholder');
|
||||
placeholder.innerText = 'Add a card from the available roles below.';
|
||||
document.getElementById('deck-list').appendChild(placeholder);
|
||||
} else {
|
||||
if (document.getElementById('deck-list-placeholder')) {
|
||||
document.getElementById('deck-list-placeholder').remove();
|
||||
}
|
||||
const sortedDeck = deckManager.getCurrentDeck().sort((a, b) => {
|
||||
if (a.team !== b.team) {
|
||||
return a.team === globals.ALIGNMENT.GOOD ? 1 : -1;
|
||||
}
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
for (const card of sortedDeck) {
|
||||
if (card.quantity > 0) {
|
||||
const roleEl = document.createElement('div');
|
||||
roleEl.classList.add('deck-role');
|
||||
if (card.team === globals.ALIGNMENT.GOOD) {
|
||||
roleEl.classList.add(globals.ALIGNMENT.GOOD);
|
||||
} else {
|
||||
roleEl.classList.add(globals.ALIGNMENT.EVIL);
|
||||
}
|
||||
roleEl.innerText = card.quantity + 'x ' + card.role;
|
||||
document.getElementById('deck-list').appendChild(roleEl);
|
||||
}
|
||||
}
|
||||
if (roleBox.category === 'custom') {
|
||||
roleBox.displayCustomRoles(document.getElementById('role-select'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -248,11 +248,15 @@ export const HTMLFragments = {
|
||||
</span>
|
||||
</button>
|
||||
<div id="custom-role-actions">
|
||||
<div tabindex="0" class="custom-role-action" id="custom-roles-export">Export</div>
|
||||
<div tabindex="0" class="custom-role-action" id="custom-roles-import">Import</div>
|
||||
<div tabindex="0" class="custom-role-action" id="custom-roles-export">Export Roles</div>
|
||||
<div tabindex="0" class="custom-role-action" id="custom-roles-import">Import Roles</div>
|
||||
</div>
|
||||
<label for="add-card-to-deck-form">Custom Role Box</label>
|
||||
<div id="deck-select"></div>
|
||||
<label for="add-card-to-deck-form">Role Box</label>
|
||||
<div id="role-category-buttons">
|
||||
<button id="role-category-default" class="role-category-button role-category-button-selected">Default Roles</button>
|
||||
<button id="role-category-custom" class="role-category-button">Custom Roles</button>
|
||||
</div>
|
||||
<div id="role-select"></div>
|
||||
<button id="custom-role-btn" class="app-button">+ Create Custom Role</button>
|
||||
</div>`,
|
||||
CREATE_GAME_DECK_STATUS:
|
||||
@@ -261,11 +265,22 @@ export const HTMLFragments = {
|
||||
<div id="deck-list"></div>
|
||||
</div>`,
|
||||
DECK_SELECT_ROLE:
|
||||
`<div class="deck-select-role-name"></div>
|
||||
<div class="deck-select-role-options">
|
||||
<img tabindex="0" class="deck-select-include" src="images/add.svg" title="make available" alt="include"/>
|
||||
<img tabindex="0" class="deck-select-info" src="images/info.svg" title="info" alt="info"/>
|
||||
<img tabindex="0" class="deck-select-edit" src="images/pencil.svg" title="edit" alt="edit"/>
|
||||
<img tabindex="0" class="deck-select-remove" src="images/delete.svg" title="remove" alt="remove"/>
|
||||
`<div class="role-name"></div>
|
||||
<div class="role-options">
|
||||
<img tabindex="0" class="role-include" src="images/add.svg" title="add one" alt="add one"/>
|
||||
<img tabindex="0" class="role-info" src="images/info.svg" title="info" alt="info"/>
|
||||
<img tabindex="0" class="role-edit" src="images/pencil.svg" title="edit" alt="edit"/>
|
||||
<img tabindex="0" class="role-remove" src="images/delete.svg" title="remove" alt="remove"/>
|
||||
</div>`,
|
||||
DECK_SELECT_ROLE_DEFAULT:
|
||||
`<div class="role-name"></div>
|
||||
<div class="role-options">
|
||||
<img tabindex="0" class="role-include" src="images/add.svg" title="add one" alt="add one"/>
|
||||
<img tabindex="0" class="role-info" src="images/info.svg" title="info" alt="info"/>
|
||||
</div>`,
|
||||
DECK_SELECT_ROLE_ADDED_TO_DECK:
|
||||
`<div class="role-name"></div>
|
||||
<div class="role-options">
|
||||
<img tabindex="0" class="role-remove" src="images/remove.svg" title="remove one" alt="remove one"/>
|
||||
</div>`
|
||||
};
|
||||
|
||||
342
client/src/modules/RoleBox.js
Normal file
@@ -0,0 +1,342 @@
|
||||
import { HTMLFragments } from './HTMLFragments.js';
|
||||
import { globals } from '../config/globals.js';
|
||||
import { defaultRoles } from '../config/defaultRoles.js';
|
||||
import { toast } from './Toast.js';
|
||||
import { ModalManager } from './ModalManager.js';
|
||||
|
||||
export class RoleBox {
|
||||
constructor (container, deckManager) {
|
||||
this.createMode = false;
|
||||
this.currentlyEditingRoleName = null;
|
||||
this.category = 'default';
|
||||
this.deckManager = deckManager;
|
||||
this.defaultRoles = [];
|
||||
console.log('hi');
|
||||
this.customRoles = [];
|
||||
container.innerHTML += HTMLFragments.CREATE_GAME_CUSTOM_ROLES;
|
||||
this.defaultButton = document.getElementById('role-category-default');
|
||||
this.customButton = document.getElementById('role-category-custom');
|
||||
this.defaultButton.addEventListener('click', () => { this.changeRoleCategory('default'); });
|
||||
this.customButton.addEventListener('click', () => { this.changeRoleCategory('custom'); });
|
||||
this.categoryTransition = document.getElementById('role-select').animate(
|
||||
[
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1 }
|
||||
], {
|
||||
fill: 'forwards',
|
||||
easing: 'linear',
|
||||
duration: 500
|
||||
});
|
||||
}
|
||||
|
||||
render = () => {
|
||||
|
||||
};
|
||||
|
||||
loadDefaultRoles = () => {
|
||||
this.defaultRoles = defaultRoles.sort((a, b) => {
|
||||
if (a.team !== b.team) {
|
||||
return a.team === globals.ALIGNMENT.GOOD ? 1 : -1;
|
||||
}
|
||||
return a.role.localeCompare(b.role);
|
||||
}).map((role) => {
|
||||
role.id = createRandomId();
|
||||
return role;
|
||||
});
|
||||
};
|
||||
|
||||
loadCustomRolesFromCookies () {
|
||||
const customRoles = localStorage.getItem('play-werewolf-custom-roles');
|
||||
if (customRoles !== null && validateCustomRoleCookie(customRoles)) {
|
||||
this.customRoles = JSON.parse(customRoles).map((role) => {
|
||||
role.id = createRandomId();
|
||||
return role;
|
||||
}); // we know it is valid JSON from the validate function
|
||||
}
|
||||
}
|
||||
|
||||
loadCustomRolesFromFile (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = (e) => {
|
||||
toast(reader.error.message, 'error', true, true, 'medium');
|
||||
};
|
||||
reader.onload = (e) => {
|
||||
let string;
|
||||
if (typeof e.target.result !== 'string') {
|
||||
string = new TextDecoder('utf-8').decode(e.target.result);
|
||||
} else {
|
||||
string = e.target.result;
|
||||
}
|
||||
if (validateCustomRoleCookie(string)) {
|
||||
this.customRoles = JSON.parse(string).map((role) => {
|
||||
role.id = createRandomId();
|
||||
return role;
|
||||
}); // we know it is valid JSON from the validate function
|
||||
const initialLength = this.customRoles.length;
|
||||
// If any imported roles match a default role, exclude them.
|
||||
this.customRoles = this.customRoles.filter((entry) => !this.defaultRoles
|
||||
.find((defaultEntry) => defaultEntry.role.toLowerCase().trim() === entry.role.toLowerCase().trim()));
|
||||
let message = this.customRoles.length === initialLength
|
||||
? 'All roles imported successfully!'
|
||||
: 'Success, but one or more roles were excluded because their names match default roles.'
|
||||
let messageType = this.customRoles.length === initialLength ? 'success' : 'warning'
|
||||
ModalManager.dispelModal('upload-custom-roles-modal', 'modal-background');
|
||||
toast(message, messageType, true, true, 'medium');
|
||||
document.getElementById('custom-role-actions').style.display = 'none';
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoles));
|
||||
this.changeRoleCategory('custom');
|
||||
this.displayCustomRoles(document.getElementById('role-select'));
|
||||
for (const card of this.deckManager.deck) {
|
||||
card.quantity = 0;
|
||||
}
|
||||
this.deckManager.updateDeckStatus();
|
||||
} else {
|
||||
toast(
|
||||
'Invalid formatting. Make sure you import the file as downloaded from this page.',
|
||||
'error',
|
||||
true,
|
||||
true,
|
||||
'medium'
|
||||
);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// via https://stackoverflow.com/a/18197341
|
||||
downloadCustomRoles = (filename, text) => {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
};
|
||||
|
||||
changeRoleCategory = (category) => {
|
||||
this.category = category;
|
||||
if (category === 'default') {
|
||||
this.displayDefaultRoles(document.getElementById('role-select'));
|
||||
if (this.defaultButton) {
|
||||
this.defaultButton.classList.add('role-category-button-selected');
|
||||
}
|
||||
if (this.customButton) {
|
||||
this.customButton.classList.remove('role-category-button-selected');
|
||||
}
|
||||
} else if (category === 'custom') {
|
||||
this.displayCustomRoles(document.getElementById('role-select'));
|
||||
if (this.customButton) {
|
||||
this.customButton.classList.add('role-category-button-selected');
|
||||
}
|
||||
if (this.defaultButton) {
|
||||
this.defaultButton.classList.remove('role-category-button-selected');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
displayDefaultRoles = (selectEl) => {
|
||||
document.querySelectorAll('#role-select .default-role, #role-select .custom-role').forEach(e => e.remove());
|
||||
this.categoryTransition.play();
|
||||
for (let i = 0; i < this.defaultRoles.length; i ++) {
|
||||
const defaultRole = document.createElement('div');
|
||||
defaultRole.innerHTML = HTMLFragments.DECK_SELECT_ROLE_DEFAULT;
|
||||
defaultRole.classList.add('default-role');
|
||||
defaultRole.dataset.roleId = this.defaultRoles[i].id;
|
||||
const alignmentClass = this.defaultRoles[i].team === globals.ALIGNMENT.GOOD
|
||||
? globals.ALIGNMENT.GOOD
|
||||
: globals.ALIGNMENT.EVIL;
|
||||
defaultRole.classList.add(alignmentClass);
|
||||
defaultRole.querySelector('.role-name').innerText = this.defaultRoles[i].role;
|
||||
selectEl.appendChild(defaultRole);
|
||||
}
|
||||
|
||||
this.addRoleEventListeners(selectEl, true, true, false, false, false);
|
||||
};
|
||||
|
||||
displayCustomRoles = (selectEl) => {
|
||||
document.querySelectorAll('#role-select .default-role, #role-select .custom-role').forEach(e => e.remove());
|
||||
this.categoryTransition.play();
|
||||
this.customRoles.sort((a, b) => {
|
||||
if (a.team !== b.team) {
|
||||
return a.team === globals.ALIGNMENT.GOOD ? 1 : -1;
|
||||
}
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
|
||||
for (let i = 0; i < this.customRoles.length; i ++) {
|
||||
const customRole = document.createElement('div');
|
||||
customRole.innerHTML = HTMLFragments.DECK_SELECT_ROLE;
|
||||
customRole.classList.add('custom-role');
|
||||
customRole.dataset.roleId = this.customRoles[i].id;
|
||||
const alignmentClass = this.customRoles[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL;
|
||||
customRole.classList.add(alignmentClass);
|
||||
customRole.querySelector('.role-name').innerText = this.customRoles[i].role;
|
||||
selectEl.appendChild(customRole);
|
||||
}
|
||||
|
||||
this.addRoleEventListeners(selectEl, true, true, true, true, true);
|
||||
};
|
||||
|
||||
addRoleEventListeners = (select, addOne, info, edit, remove, isCustom) => {
|
||||
const elements = isCustom
|
||||
? document.querySelectorAll('#role-select .custom-role')
|
||||
: document.querySelectorAll('#role-select .default-role');
|
||||
elements.forEach((role) => {
|
||||
if (addOne) {
|
||||
const plusOneHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (!this.deckManager.hasRole(name)) {
|
||||
if (isCustom) {
|
||||
this.deckManager.addToDeck(this.getCustomRole(name));
|
||||
} else {
|
||||
this.deckManager.addToDeck(this.getDefaultRole(name));
|
||||
}
|
||||
} else {
|
||||
this.deckManager.addCopyOfCard(name);
|
||||
}
|
||||
this.deckManager.updateDeckStatus();
|
||||
}
|
||||
};
|
||||
role.querySelector('.role-include').addEventListener('click', plusOneHandler);
|
||||
role.querySelector('.role-include').addEventListener('keyup', plusOneHandler);
|
||||
}
|
||||
const name = role.querySelector('.role-name').innerText;
|
||||
|
||||
if (remove) {
|
||||
const removeHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
if (confirm("Delete the role '" + name + "'?")) {
|
||||
e.preventDefault();
|
||||
this.removeFromCustomRoles(name);
|
||||
if (this.category === 'custom') {
|
||||
this.displayCustomRoles(document.getElementById('role-select'));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
role.querySelector('.role-remove').addEventListener('click', removeHandler);
|
||||
role.querySelector('.role-remove').addEventListener('keyup', removeHandler);
|
||||
}
|
||||
if (info) {
|
||||
const infoHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
const alignmentEl = document.getElementById('custom-role-info-modal-alignment');
|
||||
alignmentEl.classList.remove(globals.ALIGNMENT.GOOD);
|
||||
alignmentEl.classList.remove(globals.ALIGNMENT.EVIL);
|
||||
e.preventDefault();
|
||||
let role;
|
||||
if (isCustom) {
|
||||
role = this.getCustomRole(name);
|
||||
} else {
|
||||
role = this.getDefaultRole(name);
|
||||
}
|
||||
document.getElementById('custom-role-info-modal-name').innerText = name;
|
||||
alignmentEl.classList.add(role.team);
|
||||
document.getElementById('custom-role-info-modal-description').innerText = role.description;
|
||||
alignmentEl.innerText = role.team;
|
||||
ModalManager.displayModal('custom-role-info-modal', 'modal-background', 'close-custom-role-info-modal-button');
|
||||
}
|
||||
};
|
||||
role.querySelector('.role-info').addEventListener('click', infoHandler);
|
||||
role.querySelector('.role-info').addEventListener('keyup', infoHandler);
|
||||
}
|
||||
|
||||
if (edit) {
|
||||
const editHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
const entry = this.getCustomRole(name);
|
||||
document.getElementById('role-name').value = entry.role;
|
||||
document.getElementById('role-alignment').value = entry.team;
|
||||
document.getElementById('role-description').value = entry.description;
|
||||
this.createMode = false;
|
||||
this.currentlyEditingRoleName = entry.role;
|
||||
const createBtn = document.getElementById('create-role-button');
|
||||
createBtn.setAttribute('value', 'Update');
|
||||
ModalManager.displayModal('role-modal', 'modal-background', 'close-modal-button');
|
||||
}
|
||||
};
|
||||
role.querySelector('.role-edit').addEventListener('click', editHandler);
|
||||
role.querySelector('.role-edit').addEventListener('keyup', editHandler);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
removeFromCustomRoles = (name) => {
|
||||
const role = this.customRoles.find((entry) => entry.role === name);
|
||||
if (role) {
|
||||
this.customRoles.splice(this.customRoles.indexOf(role), 1);
|
||||
this.deckManager.removeRoleEntirelyFromDeck(role);
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoles));
|
||||
toast('"' + name + '" deleted.', 'error', true, true, 'short');
|
||||
}
|
||||
};
|
||||
|
||||
getCustomRole (name) {
|
||||
return this.customRoles.find(
|
||||
(entry) => entry.role.toLowerCase().trim() === name.toLowerCase().trim()
|
||||
);
|
||||
};
|
||||
|
||||
addCustomRole (role) {
|
||||
role.id = createRandomId();
|
||||
this.customRoles.push(role);
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoles));
|
||||
}
|
||||
|
||||
updateCustomRole (entry, name, description, team) {
|
||||
entry.role = name;
|
||||
entry.description = description;
|
||||
entry.team = team;
|
||||
this.deckManager.updateDeckStatus();
|
||||
localStorage.setItem('play-werewolf-custom-roles', JSON.stringify(this.customRoles));
|
||||
}
|
||||
|
||||
getDefaultRole (name) {
|
||||
return this.defaultRoles.find(
|
||||
(entry) => entry.role.toLowerCase().trim() === name.toLowerCase().trim()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function createRandomId () {
|
||||
let id = '';
|
||||
for (let i = 0; i < 25; i ++) {
|
||||
id += globals.CHAR_POOL[Math.floor(Math.random() * globals.CHAR_POOL.length)];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
// this is user-supplied, so we should validate it fully
|
||||
function validateCustomRoleCookie (cookie) {
|
||||
const valid = false;
|
||||
if (typeof cookie === 'string' && new Blob([cookie]).size <= 1000000) {
|
||||
try {
|
||||
const cookieJSON = JSON.parse(cookie);
|
||||
if (Array.isArray(cookieJSON)) {
|
||||
for (const entry of cookieJSON) {
|
||||
if (typeof entry === 'object') {
|
||||
if (typeof entry.role !== 'string' || entry.role.length > globals.MAX_CUSTOM_ROLE_NAME_LENGTH
|
||||
|| typeof entry.team !== 'string' || (entry.team !== globals.ALIGNMENT.GOOD && entry.team !== globals.ALIGNMENT.EVIL)
|
||||
|| typeof entry.description !== 'string' || entry.description.length > globals.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
@@ -455,7 +455,6 @@ input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.animated-placeholder {
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.role-category-button {
|
||||
background-color: transparent;
|
||||
color: #b1afcd;
|
||||
border: 1px solid #b1afcd;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
margin: 5px;
|
||||
font-family: 'signika-negative', sans-serif !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.role-category-button-selected {
|
||||
color: black;
|
||||
background-color: #b1afcd;
|
||||
}
|
||||
|
||||
#role-category-buttons {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.compact-card h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -107,7 +129,7 @@
|
||||
#deck-status-container {
|
||||
width: 20em;
|
||||
max-width: 95%;
|
||||
height: 10em;
|
||||
height: 20em;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
@@ -169,6 +191,7 @@
|
||||
display: none;
|
||||
color: #e7e7e7;
|
||||
position: absolute;
|
||||
z-index: 25;
|
||||
top: 38px;
|
||||
right: 29px;
|
||||
background-color: #333243;
|
||||
@@ -353,13 +376,13 @@ input[type="number"] {
|
||||
border: 2px solid #1c8a36;
|
||||
}
|
||||
|
||||
#deck-select {
|
||||
#role-select {
|
||||
margin: 0.5em 1em 1.5em 0;
|
||||
overflow-y: auto;
|
||||
height: 12em;
|
||||
height: 16em;
|
||||
}
|
||||
|
||||
.deck-select-role {
|
||||
.default-role, .custom-role, .added-role {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: black;
|
||||
@@ -371,23 +394,23 @@ input[type="number"] {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.deck-select-role-name {
|
||||
.role-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.deck-select-role:hover {
|
||||
.default-role:hover, .custom-role:hover, .added-role:hover {
|
||||
border: 1px solid #d7d7d7;
|
||||
}
|
||||
|
||||
.deck-select-role-options {
|
||||
.role-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#deck-select img {
|
||||
#role-select img, #deck-status-container img {
|
||||
height: 20px;
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
@@ -395,11 +418,11 @@ input[type="number"] {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#deck-select img:nth-child(4) {
|
||||
#role-select img:nth-child(4) {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
#deck-select img:hover {
|
||||
#role-select img:hover, #deck-status-container img:hover {
|
||||
filter: brightness(1.5);
|
||||
background-color: #8080804d;
|
||||
}
|
||||
@@ -540,6 +563,9 @@ input[type="number"] {
|
||||
}
|
||||
|
||||
@media(max-width: 550px) {
|
||||
#custom-roles-container, #deck-status-container {
|
||||
min-width: 90%;
|
||||
}
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
}
|
||||
@@ -558,7 +584,7 @@ input[type="number"] {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.deck-select-role-name {
|
||||
.role-name {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ button#home-create-button {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#framed-phone-screenshot {
|
||||
#framed-phone-screenshot, #framed-phone-screenshot-2 {
|
||||
max-width: 250px;
|
||||
width: 40vw;
|
||||
min-width: 175px;
|
||||
@@ -23,8 +23,16 @@ button#home-create-button {
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 0 1em;
|
||||
margin-top: 25px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#about-container > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
flex-wrap: wrap;
|
||||
padding: 25px 0;
|
||||
}
|
||||
|
||||
#join-container form {
|
||||
@@ -34,6 +42,8 @@ button#home-create-button {
|
||||
#about-container h2 {
|
||||
max-width: 17em;
|
||||
font-size: 22px;
|
||||
border-left: 1px solid #bababa;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#about-container img {
|
||||
@@ -52,14 +62,12 @@ button#home-create-button {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: #1e1b26;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@@ -43,8 +43,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="about-container">
|
||||
<img id="framed-phone-screenshot" alt="framed phone screenshot" src="../images/framed-phone-screenshot.png"/>
|
||||
<h2>Create your game, have everyone join, and deal a role to their device.</h2>
|
||||
<div>
|
||||
<img id="framed-phone-screenshot" alt="framed phone screenshot" src="../images/framed-phone-screenshot.png"/>
|
||||
<h2>Join a game and have a role dealt to your device.</h2>
|
||||
</div>
|
||||
<div>
|
||||
<img id="framed-phone-screenshot-2" alt="framed phone screenshot" src="../images/framed-phone-screenshot-2.png"/>
|
||||
<h2>Create your own game with default or custom roles.</h2>
|
||||
</div>
|
||||
</div>
|
||||
<footer id="footer">
|
||||
<a href="https://www.buymeacoffee.com/alecm33"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=alecm33&button_colour=333243&font_colour=ffffff&font_family=Lato&outline_colour=b1afcd&coffee_colour=b1afcd" /></a>
|
||||
|
||||
@@ -53,32 +53,29 @@
|
||||
to a player that is out, or to a spectator. That way, if the current dedicated moderator has to leave, or simply
|
||||
does not want to moderate anymore, they can easily delegate.
|
||||
<br><br>
|
||||
<img src="../images/tutorial/moderation-option.png" alt="moderation-option"/>
|
||||
<img src="../images/tutorial/moderation-option.png" class="tutorial-image-small" alt="moderation-option"/>
|
||||
<br><br>
|
||||
<h3>Step Two: Build your deck</h3>
|
||||
<br>
|
||||
A default set of common roles are available to you on this page. <span class="emphasized">Available Roles</span>
|
||||
are ones that have a widget where you can adjust their quantity and add them to the current game. They have been
|
||||
"taken out of the box," so to speak:
|
||||
There is a role box on this page that includes a list of <span class="emphasized">default roles</span> and a list of <span class="emphasized">custom roles</span>, which can be
|
||||
displayed by selecting the appropriate button within the box. If you want to add a certain role to the game,
|
||||
<span class="emphasized">click the green plus</span>, and one copy of it will be added to the "deck," which is the other box displaying a player
|
||||
count. Likewise, if you want to remove one copy of a given role, <span class="emphasized">click the red minus button</span> on the role in the deck box.
|
||||
<br><br>
|
||||
<img class='tutorial-image-small' src="../images/tutorial/default-roles.PNG"/>
|
||||
Here I add 3 villagers to the game, and then remove them:
|
||||
<br><br>
|
||||
You also have a <span class="emphasized">Custom Role Box</span>. These are not yet "available," in that they
|
||||
are still "in the box." If you want them in the game, click the green plus button, and they will become part of the
|
||||
available roles - a widget will be created, and you can increment or decrement the quantity of that card in the game.
|
||||
Custom roles can be created, removed, edited, and imported/exported via a formatted text file:
|
||||
<img class='tutorial-image-small' src="../images/tutorial/add-role-to-deck.gif"/>
|
||||
<br><br>
|
||||
<img class='tutorial-image-small' src="../images/tutorial/custom-roles.PNG"/>
|
||||
<span class="emphasized">You can add, edit, and remove custom roles.</span> You can also import and export them via a formatted text file. Click
|
||||
the hamburger menu on the role box to see the import/export options. Here I create a new custom role and observe
|
||||
it in the list:
|
||||
<br><br>
|
||||
Here is this in action. We add the "Clawed Minion" from the Custom Role Box to the Available Roles, and then
|
||||
we increment the quantity to 1 and see it has been added to the game:
|
||||
<br><br>
|
||||
<img class='tutorial-image-small' src="../images/tutorial/add-custom-role.gif"/>
|
||||
<img alt="create-custom-role" class='tutorial-image-small' src="../images/tutorial/create-custom-role.gif"/>
|
||||
<br><br>
|
||||
<h3>Step Three: Set an optional timer</h3>
|
||||
<br>
|
||||
If you don't fill in these fields, the game will be untimed. If you do, you can use a time between 1 minute
|
||||
and 5 hours. The timer can be played and paused by the current moderator. Importantly, when the timer expires
|
||||
If you don't fill in these fields, the game will be untimed. If you do, <span class="emphasized">you can use a time between 1 minute
|
||||
and 5 hours.</span> The timer can be played and paused by the current moderator. Importantly, when the timer expires,
|
||||
<span class="emphasized">nothing automatically happens.</span> The timer will display 0s, but the game will not
|
||||
end. Whether or not the game ends immediately after that or continues longer is up to the moderator.
|
||||
<br><br>
|
||||
@@ -103,9 +100,9 @@
|
||||
<br><br>
|
||||
<h3>Transferring your moderator powers</h3>
|
||||
<br>
|
||||
You can transfer your moderator abilities to anyone that has been removed from the game, or to anyone that happens
|
||||
to be spectating. After selecting them from the list, they will then inherit the moderator's view, and you will
|
||||
become a spectator. You can have the powers transferred back to you if you need:
|
||||
<span class="emphasized">You can transfer your moderator abilities to anyone that has been removed from the game, or to anyone that happens
|
||||
to be spectating.</span> After selecting them from the list, they will then inherit the moderator's view, and you will
|
||||
become a spectator:
|
||||
<br><br>
|
||||
<img class='tutorial-image-small' src="../images/tutorial/transfer-mod.gif"/>
|
||||
<br><br>
|
||||
|
||||