rework of deck builder page

This commit is contained in:
AlecM33
2022-06-11 16:59:50 -04:00
parent b2505b7488
commit acf3b645cb
24 changed files with 674 additions and 536 deletions

View File

@@ -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.'
},

View File

@@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 882 KiB

After

Width:  |  Height:  |  Size: 775 KiB

View File

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

View File

@@ -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'));
}
}

View File

@@ -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>`
};

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

View File

@@ -455,7 +455,6 @@ input {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.animated-placeholder {

View File

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

View File

@@ -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;

View File

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

View File

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