mirror of
https://github.com/AlecM33/Werewolf.git
synced 2025-12-26 15:57:50 +01:00
stricter content security policy
This commit is contained in:
@@ -36,7 +36,7 @@ export class DeckStateManager {
|
||||
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, 3);
|
||||
toast('"' + name + '" deleted.', 'error', true, true, 'short');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export class DeckStateManager {
|
||||
loadCustomRolesFromFile (file, updateRoleListFunction, loadDefaultCardsFn, showIncludedCardsFn) {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = (e) => {
|
||||
toast(reader.error.message, 'error', true, true, 5);
|
||||
toast(reader.error.message, 'error', true, true, 'medium');
|
||||
};
|
||||
reader.onload = (e) => {
|
||||
let string;
|
||||
@@ -100,13 +100,19 @@ export class DeckStateManager {
|
||||
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, 3);
|
||||
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, 5);
|
||||
toast(
|
||||
'Invalid formatting. Make sure you import the file as downloaded from this page.',
|
||||
'error',
|
||||
true,
|
||||
true,
|
||||
'medium'
|
||||
);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
@@ -132,7 +132,7 @@ export class GameCreationStepManager {
|
||||
button.classList.remove('submitted');
|
||||
button.addEventListener('click', this.steps['4'].forwardHandler);
|
||||
if (e.status === 429) {
|
||||
toast('You\'ve sent this request too many times.', 'error', true, true, 6);
|
||||
toast('You\'ve sent this request too many times.', 'error', true, true, 'medium');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -280,23 +280,23 @@ function renderRoleSelectionStep (game, containerId, step, deckManager) {
|
||||
if (fileList.length > 0) {
|
||||
const file = fileList[0];
|
||||
if (file.size > 1000000) {
|
||||
toast('Your file is too large (max 1MB)', 'error', true, true, 5);
|
||||
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, 5);
|
||||
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, 5);
|
||||
toast('You must upload a text file', 'error', true, true, 'medium');
|
||||
}
|
||||
};
|
||||
|
||||
const clickHandler = () => {
|
||||
const actions = document.getElementById('custom-role-actions');
|
||||
if (actions.style.display !== 'none') {
|
||||
if (window.getComputedStyle(actions, null).display !== 'none') {
|
||||
actions.style.display = 'none';
|
||||
} else {
|
||||
actions.style.display = 'block';
|
||||
@@ -563,7 +563,7 @@ function initializeRemainingEventListeners (deckManager) {
|
||||
if (!deckManager.getCustomRoleOption(name) && !deckManager.getCard(name)) { // confirm there is no existing custom role with the same name
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, false);
|
||||
} else {
|
||||
toast('There is already a role with this name', 'error', true, true, 3);
|
||||
toast('There is already a role with this name', 'error', true, true, 'short');
|
||||
}
|
||||
} else {
|
||||
const option = deckManager.getCustomRoleOption(deckManager.currentlyEditingRoleName);
|
||||
@@ -573,7 +573,7 @@ function initializeRemainingEventListeners (deckManager) {
|
||||
if (!deckManager.getCustomRoleOption(name) && !deckManager.getCard(name)) {
|
||||
processNewCustomRoleSubmission(name, description, team, deckManager, true, option);
|
||||
} else {
|
||||
toast('There is already a role with this name', 'error', true, true, 3);
|
||||
toast('There is already a role with this name', 'error', true, true, 'short');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -653,7 +653,7 @@ function addCustomRoleEventListeners (deckManager, select) {
|
||||
if (!deckManager.getCard(name)) {
|
||||
deckManager.addToDeck(name);
|
||||
const cardEl = constructCompactDeckBuilderElement(deckManager.getCard(name), deckManager);
|
||||
toast('"' + name + '" made available below.', 'success', true, true, 4);
|
||||
toast('"' + name + '" made available below.', 'success', true, true, 'medium');
|
||||
if (deckManager.getCard(name).team === globals.ALIGNMENT.GOOD) {
|
||||
document.getElementById('deck-good').appendChild(cardEl);
|
||||
} else {
|
||||
@@ -661,7 +661,7 @@ function addCustomRoleEventListeners (deckManager, select) {
|
||||
}
|
||||
updateCustomRoleOptionsList(deckManager, select);
|
||||
} else {
|
||||
toast('"' + select.value + '" already included.', 'error', true, true, 3);
|
||||
toast('"' + select.value + '" already included.', 'error', true, true, 'short');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ export const HTMLFragments = {
|
||||
<button id='role-info-button' class='app-button'>View Role Info <img src='/images/info.svg'/></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id='game-role' style='display:none'>
|
||||
<div id='game-role'>
|
||||
<h4 id='role-name'></h4>
|
||||
<img alt='role' id='role-image'/>
|
||||
<p id='role-description'></p>
|
||||
@@ -81,8 +81,8 @@ export const HTMLFragments = {
|
||||
<div id='game-player-list'></div>
|
||||
</div>`,
|
||||
MODERATOR_GAME_VIEW:
|
||||
`<div id='transfer-mod-modal-background' class='modal-background' style='display: none'></div>
|
||||
<div tabindex='-1' id='transfer-mod-modal' class='modal' style='display: none'>
|
||||
`<div id='transfer-mod-modal-background' class='modal-background'></div>
|
||||
<div tabindex='-1' id='transfer-mod-modal' class='modal'>
|
||||
<h3>Transfer Mod Powers 👑</h3>
|
||||
<div id='transfer-mod-modal-content'></div>
|
||||
<div class='modal-button-container'>
|
||||
@@ -116,8 +116,8 @@ export const HTMLFragments = {
|
||||
</div>
|
||||
</div>`,
|
||||
TEMP_MOD_GAME_VIEW:
|
||||
`<div id='transfer-mod-modal-background' class='modal-background' style='display: none'></div>
|
||||
<div id='transfer-mod-modal' class='modal' style='display: none'>
|
||||
`<div id='transfer-mod-modal-background' class='modal-background'></div>
|
||||
<div id='transfer-mod-modal' class='modal'>
|
||||
<form id='transfer-mod-form'>
|
||||
<div id='transfer-mod-form-content'>
|
||||
<h3>Transfer Mod Powers 👑</h3>
|
||||
@@ -139,7 +139,7 @@ export const HTMLFragments = {
|
||||
<button id='role-info-button' class='app-button'>View Role Info <img src='/images/info.svg'/></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id='game-role' style='display:none'>
|
||||
<div id='game-role'>
|
||||
<h4 id='role-name'></h4>
|
||||
<img alt='role' id='role-image'/>
|
||||
<p id='role-description'></p>
|
||||
@@ -249,7 +249,7 @@ export const HTMLFragments = {
|
||||
<span class="hamburger-inner"></span>
|
||||
</span>
|
||||
</button>
|
||||
<div id="custom-role-actions" style="display:none">
|
||||
<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>
|
||||
|
||||
@@ -8,35 +8,49 @@ export const toast = (message, type, positionAtTop = true, dispelAutomatically =
|
||||
|
||||
function buildAndInsertMessageElement (message, type, positionAtTop, dispelAutomatically, duration) {
|
||||
cancelCurrentToast();
|
||||
let backgroundColor, border;
|
||||
const position = positionAtTop ? 'top:2rem;' : 'bottom: 90px;';
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.classList.add('info-message');
|
||||
const positionClass = positionAtTop ? 'toast-top' : 'toast-bottom';
|
||||
messageEl.classList.add(positionClass);
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
backgroundColor = '#fff5b1';
|
||||
border = '3px solid #c7c28a';
|
||||
messageEl.classList.add('toast-warning');
|
||||
break;
|
||||
case 'error':
|
||||
backgroundColor = '#fdaeb7';
|
||||
border = '3px solid #c78a8a';
|
||||
messageEl.classList.add('toast-error');
|
||||
break;
|
||||
case 'success':
|
||||
backgroundColor = '#bef5cb';
|
||||
border = '3px solid #8ac78a;';
|
||||
messageEl.classList.add('toast-success');
|
||||
break;
|
||||
}
|
||||
|
||||
const durationInSeconds = duration ? duration + 's' : globals.TOAST_DURATION_DEFAULT + 's';
|
||||
let animation = '';
|
||||
if (dispelAutomatically) {
|
||||
animation = 'animation:fade-in-slide-down-then-exit ' + durationInSeconds + ' ease normal forwards';
|
||||
} else {
|
||||
animation = 'animation:fade-in-slide-down ' + durationInSeconds + ' ease normal forwards';
|
||||
switch (duration) {
|
||||
case null:
|
||||
case undefined:
|
||||
messageEl.classList.add('toast-medium');
|
||||
break;
|
||||
case 'short':
|
||||
messageEl.classList.add('toast-short');
|
||||
break;
|
||||
case 'medium':
|
||||
messageEl.classList.add('toast-medium');
|
||||
break;
|
||||
case 'long':
|
||||
messageEl.classList.add('toast-long');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const messageEl = document.createElement('div');
|
||||
|
||||
if (dispelAutomatically) {
|
||||
messageEl.classList.add('toast-dispel-automatically');
|
||||
} else {
|
||||
messageEl.classList.add('toast-not-dispelled-automatically');
|
||||
}
|
||||
|
||||
messageEl.setAttribute('id', 'current-info-message');
|
||||
messageEl.setAttribute('style', 'background-color:' + backgroundColor + ';' + 'border:' + border + ';' + position + animation);
|
||||
messageEl.setAttribute('class', 'info-message');
|
||||
messageEl.innerText = message;
|
||||
|
||||
document.body.prepend(messageEl);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ function syncWithGame (stateBucket, gameTimerManager, gameStateRenderer, timerWo
|
||||
document.querySelector('.spinner-container')?.remove();
|
||||
document.querySelector('.spinner-background')?.remove();
|
||||
document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM;
|
||||
toast('You are connected.', 'success', true, true, 2);
|
||||
toast('You are connected.', 'success', true, true, 'short');
|
||||
processGameState(stateBucket.currentGameState, cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
}
|
||||
});
|
||||
@@ -142,7 +142,7 @@ function displayClientInfo (name, userType) {
|
||||
|
||||
function setClientSocketHandlers (stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager) {
|
||||
socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => {
|
||||
toast(player.name + ' joined!', 'success', false, true, 3);
|
||||
toast(player.name + ' joined!', 'success', false, true, 'short');
|
||||
stateBucket.currentGameState.people.push(player);
|
||||
stateBucket.currentGameState.isFull = gameIsFull;
|
||||
gameStateRenderer.renderLobbyPlayers();
|
||||
@@ -163,7 +163,7 @@ function setClientSocketHandlers (stateBucket, gameStateRenderer, socket, timerW
|
||||
|
||||
socket.on(globals.EVENTS.PLAYER_LEFT, (player) => {
|
||||
removeStartGameFunctionalityIfPresent(gameStateRenderer);
|
||||
toast(player.name + ' has left!', 'error', false, true, 3);
|
||||
toast(player.name + ' has left!', 'error', false, true, 'short');
|
||||
const index = stateBucket.currentGameState.people.findIndex(person => person.id === player.id);
|
||||
if (index >= 0) {
|
||||
stateBucket.currentGameState.people.splice(
|
||||
@@ -221,7 +221,7 @@ function setClientSocketHandlers (stateBucket, gameStateRenderer, socket, timerW
|
||||
if (killedPerson) {
|
||||
killedPerson.out = true;
|
||||
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
|
||||
toast(killedPerson.name + ' killed.', 'success', true, true, 6);
|
||||
toast(killedPerson.name + ' killed.', 'success', true, true, 'medium');
|
||||
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(stateBucket.currentGameState.status === globals.STATUS.ENDED);
|
||||
} else {
|
||||
if (killedPerson.id === stateBucket.currentGameState.client.id) {
|
||||
@@ -230,9 +230,9 @@ function setClientSocketHandlers (stateBucket, gameStateRenderer, socket, timerW
|
||||
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
|
||||
}
|
||||
gameStateRenderer.updatePlayerCardToKilledState();
|
||||
toast('You have been killed!', 'warning', true, true, 6);
|
||||
toast('You have been killed!', 'warning', true, true, 'medium');
|
||||
} else {
|
||||
toast(killedPerson.name + ' was killed!', 'warning', true, true, 6);
|
||||
toast(killedPerson.name + ' was killed!', 'warning', true, true, 'medium');
|
||||
}
|
||||
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
|
||||
@@ -250,13 +250,13 @@ function setClientSocketHandlers (stateBucket, gameStateRenderer, socket, timerW
|
||||
revealedPerson.gameRole = revealData.gameRole;
|
||||
revealedPerson.alignment = revealData.alignment;
|
||||
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
|
||||
toast(revealedPerson.name + ' revealed.', 'success', true, true, 6);
|
||||
toast(revealedPerson.name + ' revealed.', 'success', true, true, 'medium');
|
||||
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(stateBucket.currentGameState.status === globals.STATUS.ENDED);
|
||||
} else {
|
||||
if (revealedPerson.id === stateBucket.currentGameState.client.id) {
|
||||
toast('Your role has been revealed!', 'warning', true, true, 6);
|
||||
toast('Your role has been revealed!', 'warning', true, true, 'medium');
|
||||
} else {
|
||||
toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', true, true, 6);
|
||||
toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', true, true, 'medium');
|
||||
}
|
||||
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
|
||||
|
||||
@@ -51,9 +51,9 @@ const joinHandler = (e) => {
|
||||
document.getElementById('submit-new-name').classList.remove('submitted');
|
||||
document.getElementById('submit-new-name').setAttribute('value', 'Join Game');
|
||||
if (res.status === 404) {
|
||||
toast('This game was not found.', 'error', true, true, 8);
|
||||
toast('This game was not found.', 'error', true, true, 'long');
|
||||
} else if (res.status === 400) {
|
||||
toast('This name is already taken.', 'error', true, true, 8);
|
||||
toast('This name is already taken.', 'error', true, true, 'long');
|
||||
} else if (res.status >= 500) {
|
||||
toast(
|
||||
'The server is experiencing problems. Please try again later',
|
||||
@@ -63,7 +63,7 @@ const joinHandler = (e) => {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toast('Name must be between 1 and 30 characters.', 'error', true, true, 8);
|
||||
toast('Name must be between 1 and 30 characters.', 'error', true, true, 'long');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -65,6 +65,53 @@ h3 {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.toast-top {
|
||||
top: 2rem;
|
||||
}
|
||||
|
||||
.toast-bottom {
|
||||
bottom: 90px;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background-color: #bef5cb;
|
||||
border: 3px solid #8ac78a;
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
background-color: #fff5b1;
|
||||
border: 3px solid #c7c28a;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background-color: #fdaeb7;
|
||||
border: 3px solid #c78a8a;
|
||||
}
|
||||
|
||||
.toast-dispel-automatically {
|
||||
animation: fade-in-slide-down-then-exit ease normal forwards;
|
||||
}
|
||||
|
||||
.toast-not-dispelled-automatically {
|
||||
animation: fade-in-slide-down ease normal forwards;
|
||||
}
|
||||
|
||||
.toast-short {
|
||||
animation-duration: 3s;
|
||||
}
|
||||
|
||||
.toast-medium {
|
||||
animation-duration: 5s;
|
||||
}
|
||||
|
||||
.toast-long {
|
||||
animation-duration: 8s;
|
||||
}
|
||||
|
||||
#footer {
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
@@ -78,10 +125,14 @@ h3 {
|
||||
margin-top: 4em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
#footer a img {
|
||||
#footer a:not([href='https://www.buymeacoffee.com/alecm33']) img {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
#footer a[href='https://www.buymeacoffee.com/alecm33'] img {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#footer a {
|
||||
color: #f7f7f7;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
}
|
||||
|
||||
#custom-role-actions {
|
||||
display: none;
|
||||
color: whitesmoke;
|
||||
position: absolute;
|
||||
top: 38px;
|
||||
|
||||
@@ -219,10 +219,10 @@ h1 {
|
||||
}
|
||||
|
||||
#game-role {
|
||||
display: none;
|
||||
position: relative;
|
||||
border: 5px solid transparent;
|
||||
background-color: #e7e7e7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
border-left: 5px solid #b1afcd;
|
||||
animation: entrance 0.5s forwards;
|
||||
transform-origin: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#player-new-name {
|
||||
max-width: 17em;
|
||||
}
|
||||
|
||||
#game-parameters div:nth-child(1) {
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
font-family: sans-serif;
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-background {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -50,7 +52,6 @@
|
||||
}
|
||||
|
||||
#custom-role-info-modal {
|
||||
display: flex;
|
||||
color: #d7d7d7;
|
||||
text-align: left;
|
||||
font-family: signika-negative, sans-serif;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const template =
|
||||
`<div id="modal-background" class="modal-background" style="display: none"></div>
|
||||
<div tabindex="-1" id="role-modal" class="modal" style="display: none">
|
||||
`<div id="modal-background" class="modal-background"></div>
|
||||
<div tabindex="-1" id="role-modal" class="modal">
|
||||
<form id="role-form">
|
||||
<div>
|
||||
<label for="role-name">Role Name</label>
|
||||
@@ -15,7 +15,7 @@ const template =
|
||||
</div>
|
||||
<div>
|
||||
<label for="role-description">Description</label>
|
||||
<textarea style="resize:none" id="role-description" rows="4" cols="1" placeholder="Describe your role..." required></textarea>
|
||||
<textarea id="role-description" rows="4" cols="1" placeholder="Describe your role..." required></textarea>
|
||||
</div>
|
||||
<div class="modal-button-container">
|
||||
<button id="close-modal-button" class="cancel app-button">Close</button>
|
||||
@@ -23,7 +23,7 @@ const template =
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div tabindex="-1" id="upload-custom-roles-modal" class="modal" style="display:none">
|
||||
<div tabindex="-1" id="upload-custom-roles-modal" class="modal">
|
||||
<h3>Import Custom Roles</h3>
|
||||
<form id="upload-custom-roles-form">
|
||||
<input type="file" id="upload-custom-roles" name="Upload Custom Roles" accept="text/plain"/>
|
||||
@@ -33,7 +33,7 @@ const template =
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div tabindex="-1" id="custom-role-info-modal" class="modal" style="display:none">
|
||||
<div tabindex="-1" id="custom-role-info-modal" class="modal">
|
||||
<h3 id="custom-role-info-modal-name"></h3>
|
||||
<div id="custom-role-info-modal-alignment"></div>
|
||||
<div id="custom-role-info-modal-description"></div>
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
</form>
|
||||
</div>
|
||||
<footer id="footer">
|
||||
<script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" data-slug="alecm33" data-color="#333243" data-emoji="" data-font="Lato" data-text="Buy me a coffee" data-outline-color="#ffffff" data-font-color="#ffffff" data-coffee-color="#FFDD00" ></script>
|
||||
|
||||
|
||||
<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>
|
||||
<div>
|
||||
<a href="https://github.com/AlecM33/Werewolf"><img src='/images/GitHub-Mark-Light-120px-plus.png'/></a>
|
||||
<a href="mailto:play.werewolf.contact@gmail.com?Subject=Werewolf App" target="_top"><img src='/images/email.svg'/></a>
|
||||
|
||||
Reference in New Issue
Block a user