import { HTMLFragments } from '../front_end_components/HTMLFragments.js'; import { ALIGNMENT, PRIMITIVES } from '../../config/globals.js'; import { defaultRoles } from '../../config/defaultRoles.js'; import { toast } from '../front_end_components/Toast.js'; import { ModalManager } from '../front_end_components/ModalManager.js'; import { Confirmation } from '../front_end_components/Confirmation.js'; export class RoleBox { constructor (container, deckManager) { this.createMode = false; this.category = 'default'; this.deckManager = deckManager; this.defaultRoles = []; 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 }); } loadDefaultRoles = () => { this.defaultRoles = defaultRoles.sort((a, b) => { if (a.team !== b.team) { return a.team === 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((roleObj) => { return { id: createRandomId(), role: roleObj.role, team: roleObj.team, description: roleObj.description, custom: true }; }); // 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((roleObj) => { return { id: createRandomId(), role: roleObj.role, team: roleObj.team, description: roleObj.description, custom: true }; }); // 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())); const message = this.customRoles.length === initialLength ? 'All roles imported successfully!' : 'Success, but one or more roles were excluded because their names match default roles.'; const 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); } loadSelectedRolesFromCurrentGame = (game) => { for (const card of game.deck) { if (card.quantity) { for (let i = 0; i < card.quantity; i ++) { if (!this.deckManager.hasRole(card.role)) { const role = this.getDefaultRole(card.role) ? this.getDefaultRole(card.role) : this.getCustomRole(card.role); if (role) { role.id = card.id; this.deckManager.addToDeck(role); } } else { this.deckManager.addCopyOfCard(card.role); } } } } this.deckManager.updateDeckStatus(); } // 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.querySelector('#custom-role-placeholder')?.remove(); 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 === ALIGNMENT.GOOD ? ALIGNMENT.GOOD : 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); }; displayCustomRolePlaceHolder = () => { const placeholder = document.createElement('div'); placeholder.setAttribute('id', 'custom-role-placeholder'); placeholder.innerText = 'Create a role with the button below.'; document.getElementById('role-select').appendChild(placeholder); }; displayCustomRoles = (selectEl) => { document.querySelector('#custom-role-placeholder')?.remove(); document.querySelectorAll('#role-select .default-role, #role-select .custom-role').forEach(e => e.remove()); this.categoryTransition.play(); if (this.customRoles.length === 0) { this.displayCustomRolePlaceHolder(); return; } this.customRoles.sort((a, b) => { if (a.team !== b.team) { return a.team === 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 === ALIGNMENT.GOOD ? ALIGNMENT.GOOD : 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) => { const name = role.querySelector('.role-name').innerText; 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); } toast(name + ' ', 'neutral', true, true, 'short', () => { const toastEl = document.createElement('span'); toastEl.innerHTML = `+1 `; toastEl.querySelector('.toast-plus-role-quantity').innerText = ' (' + this.deckManager.getQuantityOfRole(name) + ')'; return toastEl; }); this.deckManager.updateDeckStatus(); } }; role.querySelector('.role-include').addEventListener('click', plusOneHandler); role.querySelector('.role-include').addEventListener('keyup', plusOneHandler); } if (remove) { const removeHandler = (e) => { if (e.type === 'click' || e.code === 'Enter') { Confirmation("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) => { document.querySelector('#custom-role-info-modal-image')?.remove(); if (e.type === 'click' || e.code === 'Enter') { const alignmentEl = document.getElementById('custom-role-info-modal-alignment'); const nameEl = document.getElementById('custom-role-info-modal-name'); alignmentEl.classList.remove(ALIGNMENT.GOOD); alignmentEl.classList.remove(ALIGNMENT.EVIL); nameEl.classList.remove(ALIGNMENT.GOOD); nameEl.classList.remove(ALIGNMENT.EVIL); e.preventDefault(); let role; if (isCustom) { document.getElementById('custom-role-info-modal-image-placeholder').style.display = 'none'; role = this.getCustomRole(name); } else { document.getElementById('custom-role-info-modal-image-placeholder').style.display = 'flex'; role = this.getDefaultRole(name); const roleImg = new Image(); roleImg.id = 'custom-role-info-modal-image'; roleImg.onload = () => { document.getElementById('custom-role-info-modal-image-placeholder').appendChild(roleImg); }; if (name.toLowerCase() === 'villager') { roleImg.src = '../images/roles/Villager' + Math.ceil(Math.random() * 2) + '.png'; } else { roleImg.src = '../images/roles/' + name.replaceAll(' ', '') + '.png'; } } nameEl.innerText = name; nameEl.classList.add(role.team); 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 < 50; i ++) { id += PRIMITIVES.CHAR_POOL[Math.floor(Math.random() * PRIMITIVES.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 (entry !== null && typeof entry === 'object') { if (typeof entry.role !== 'string' || entry.role.length > PRIMITIVES.MAX_CUSTOM_ROLE_NAME_LENGTH || typeof entry.team !== 'string' || (entry.team !== ALIGNMENT.GOOD && entry.team !== ALIGNMENT.EVIL) || typeof entry.description !== 'string' || entry.description.length > PRIMITIVES.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH ) { return false; } } else { return false; } } return true; } } catch (e) { return false; } } return valid; }