Ability to add custom roles to the deck for a single game

This commit is contained in:
Alec Maier
2020-04-06 00:07:08 -04:00
parent dfa7657212
commit dcf0a8e3fa
11 changed files with 361 additions and 38 deletions

107
assets/images/custom.svg Normal file
View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="119.66505mm"
height="109.59733mm"
viewBox="0 0 119.66505 109.59733"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="host.svg">
<defs
id="defs2">
<inkscape:path-effect
effect="bspline"
id="path-effect4526"
is_visible="true"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect4526-4"
is_visible="true"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="-232.43275"
inkscape:cy="116.16088"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-44.488903,-69.97024)">
<circle
style="opacity:0.95999995;fill:none;fill-opacity:1;stroke:whitesmoke;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="path4518"
cx="104.32143"
cy="101.96429"
r="30.994047" />
<g
id="g4548"
transform="translate(0.75595639,-10.583334)">
<path
inkscape:original-d="m 44.60119,189.65476 c 6.047883,-10.5836 12.095501,-21.16693 18.142858,-31.75 13.607002,-2.6e-4 27.214549,-0.50423 40.821422,-0.75595"
inkscape:path-effect="#path-effect4526"
inkscape:connector-curvature="0"
id="path4524"
d="m 44.60119,189.65476 c 6.047799,-10.58365 12.095417,-21.16698 21.920308,-26.58444 9.824892,-5.41745 23.437104,-5.66953 37.043972,-5.92151"
style="fill:none;stroke:whitesmoke;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:original-d="m 44.60119,189.65476 c 6.047883,-10.5836 12.095501,-21.16693 18.142858,-31.75 13.607002,-2.6e-4 27.214549,-0.50423 40.821422,-0.75595"
inkscape:path-effect="#path-effect4526-4"
inkscape:connector-curvature="0"
id="path4524-1"
d="m 44.60119,189.65476 c 6.047799,-10.58365 12.095417,-21.16698 21.920308,-26.58444 9.824892,-5.41745 23.437104,-5.66953 37.043972,-5.92151"
style="fill:none;stroke:whitesmoke;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(-1,0,0,1,207.13094,0)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
assets/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -172,7 +172,7 @@ io.on('connection', function(socket) {
game.killedPlayer = player.name;
game.lastKilled = player.id;
game.killedRole = player.card.role;
game.message = player.name + ", a " + player.card.role + ", was killed!"
game.message = player.name + ", a " + player.card.role + ", was killed!";
const winCheck = teamWon(game);
if (winCheck === "wolf") {
game.winningTeam = "wolf";

View File

@@ -2,7 +2,7 @@ import {utility} from './util.js'
const socket = io();
const finishedArtArray = ["Villager", "Werewolf", "Seer", "Shadow", "Hunter", "Mason", "Minion", "Sorcerer"];
const standardRoles = ["Villager", "Werewolf", "Seer", "Shadow", "Hunter", "Mason", "Minion", "Sorcerer"];
let clock;
let currentGame = null;
let cardFlippedOver = false;
@@ -19,13 +19,13 @@ window.onblur = function() { // pause animations if the window is not in focus
this.document.querySelector("#overlay").style.animationPlayState = 'paused';
this.document.querySelector("#killed-role").style.animationPlayState = 'paused';
this.document.querySelector("#killed-name").style.animationPlayState = 'paused';
}
};
window.onfocus = function() { // play animations when window is focused
this.document.querySelector("#overlay").style.animationPlayState = 'running';
this.document.querySelector("#killed-role").style.animationPlayState = 'running';
this.document.querySelector("#killed-name").style.animationPlayState = 'running';
}
};
function buildGameBasedOnState() {
switch(currentGame.state) {
@@ -44,7 +44,7 @@ function buildGameBasedOnState() {
}
function hideAfterExit(e) {
e.target.style.display = 'none'
e.target.style.display = 'none';
e.target.classList.remove(e.target.exitClass);
}
@@ -66,7 +66,7 @@ function triggerEntranceAnimation(selector, entranceClass, exitClass, image) {
transitionEl.entranceClass = entranceClass;
transitionEl.exitClass = exitClass;
transitionEl.offsetWidth;
if (image) {
if (image && standardRoles.includes(currentGame.killedRole)) {
transitionEl.setAttribute("src", "../assets/images/roles/" + currentGame.killedRole + ".png");
}
transitionEl.classList.add(entranceClass);
@@ -183,9 +183,9 @@ function renderGame() {
function renderPlayerCard(player) {
const card = player.card;
const cardArt = finishedArtArray.includes(card.role) ?
const cardArt = standardRoles.includes(card.role) ?
"<img alt='" + card.role + "' src='../assets/images/roles/" + card.role + ".png' />"
: "<div class='placeholder'>Art coming soon.</div>";
: "<div class='placeholder'>Custom Role</div>";
const cardClass = player.card.team === "good" ? "game-card-inner village" : "game-card-inner wolf";
const playerCard = document.createElement("div");
playerCard.setAttribute("id", "game-card");

View File

@@ -36,27 +36,41 @@ let atLeastOnePlayer = false;
// register event listeners on buttons
document.getElementById("reset-btn").addEventListener("click", resetCardQuantities);
document.getElementById("create-btn").addEventListener("click", createGame);
document.getElementById("role-btn").addEventListener("click", displayRoleModal);
document.getElementById("close").addEventListener("click", closeModal);
document.getElementById("role-btn").addEventListener("click", function() { displayModal("role-modal") });
document.getElementById("custom-role-form").addEventListener("submit", function(e) {
addCustomCardToRoles(e);
});
Array.from(document.getElementsByClassName("close")).forEach(function(element) {
element.addEventListener('click', closeModal);
});
// render all of the available cards to the user
window.onload = function() {
renderAvailableCards();
};
function renderAvailableCards() {
for (let i = 0; i < cards.length; i ++) {
const newCard = new Card(cards[i].role, cards[i].team, cards[i].description, cards[i].powerRole);
// put card info in the informational role description modal
const modalRole = document.createElement("div");
modalRole.setAttribute("class", "modal-role");
const roleClass = cards[i].team === "good" ? "role-village" : "role-wolf";
const roleImage = finishedArtArray.includes(cards[i].role) ?
"<img alt='No art' src='/assets/images/roles-small/" + cards[i].role + ".png' />"
: "<span>Art soon.</span>";
let roleImage;
if (cards[i].custom === true) {
roleImage = "<img alt='No art' class='card-image-custom' src='/assets/images/custom.svg' />";
} else {
roleImage = finishedArtArray.includes(cards[i].role) ?
"<img alt='No art' src='/assets/images/roles-small/" + cards[i].role + ".png' />"
: "<span>Art soon.</span>";
}
modalRole.innerHTML =
"<div>" +
roleImage +
"<div>" +
"<h2 class='" + roleClass + "'>" + cards[i].role + "</h2>" +
"<p>" + cards[i].team + "</p>" +
"</div>" +
roleImage +
"<div>" +
"<h2 class='" + roleClass + "'>" + cards[i].role + "</h2>" +
"<p>" + cards[i].team + "</p>" +
"</div>" +
"</div>" +
"<p>" + cards[i].description + "</p>";
@@ -70,20 +84,23 @@ window.onload = function() {
cardContainer.setAttribute("class", "card");
cardContainer.setAttribute("id", "card-" + i);
cardContainer.innerHTML =
cardContainer.innerHTML =
"<div class='card-top'>" +
"<div class='card-header'>" +
"<div>" +
"<p class='card-role'>" + newCard.role + "</p>" +
"<div class='" + quantityClass + "'>" + newCard.quantity + "</div>" +
"</div>" +
"<p>+</p>" +
"</div>" +
"<div class='card-header'>" +
"<div>" +
"<p class='card-role'>" + newCard.role + "</p>" +
"<div class='" + quantityClass + "'>" + newCard.quantity + "</div>" +
"</div>" +
"<img class='card-image' src='../assets/images/roles-small/" + newCard.role + ".png' alt='" + newCard.role + "'/>" +
"<p>+</p>" +
"</div>" +
"</div>";
cardContainer.innerHTML = cards[i].custom
? cardContainer.innerHTML += "<img class='card-image card-image-custom' src='../assets/images/custom.svg' alt='" + newCard.role + "'/>"
: cardContainer.innerHTML +="<img class='card-image' src='../assets/images/roles-small/" + newCard.role + ".png' alt='" + newCard.role + "'/>";
cardContainer.innerHTML +=
"<div class='card-bottom'>" +
"<p>-</p>" +
"</div>"
"<p>-</p>" +
"</div>";
document.getElementById("card-select").appendChild(cardContainer);
let cardTop = document.getElementById("card-" + i).getElementsByClassName("card-top")[0];
let cardQuantity = document.getElementById("card-" + i).getElementsByClassName("card-quantity")[0];
@@ -95,7 +112,45 @@ window.onload = function() {
cardBottom.card = newCard;
cardBottom.quantityEl = cardQuantity;
}
};
renderCustomCard();
}
function renderCustomCard() {
let customCard = document.createElement("div");
customCard.classList.add("card", "custom-card");
customCard.setAttribute("id", "custom");
let cardHeader = document.createElement("h1");
cardHeader.innerText = "Add Custom Role";
let cardBody = document.createElement("div");
cardBody.innerText = "+";
customCard.appendChild(cardHeader);
customCard.appendChild(cardBody);
document.getElementById("card-select").appendChild(customCard);
customCard.addEventListener("click", function() {
displayModal("custom-card-modal");
});
}
function addCustomCardToRoles(e) {
e.preventDefault();
let newCard = {
role: document.getElementById("custom-role-name").value,
team: document.getElementById("custom-role-team").value,
description: document.getElementById("custom-role-desc").value,
powerRole: document.getElementById("custom-role-power").checked,
custom: true
};
cards.push(newCard);
document.getElementById("card-select").innerHTML = "";
document.getElementById("roles").innerHTML = "";
renderAvailableCards();
closeModal();
}
function incrementCardQuantity(e) {
if(e.target.card.quantity < 25) {
@@ -133,13 +188,14 @@ function resetCardQuantities() {
});
}
function displayRoleModal() {
document.getElementById("role-modal").classList.remove("hidden");
function displayModal(modalId) {
document.getElementById(modalId).classList.remove("hidden");
document.getElementById("app-content").classList.add("hidden");
}
function closeModal() {
document.getElementById("role-modal").classList.add("hidden");
document.getElementById("custom-card-modal").classList.add("hidden");
document.getElementById("app-content").classList.remove("hidden");
}

View File

@@ -29,6 +29,10 @@
left: 14%;
}
.disclaimer, .custom-card h1 {
font-size: 15px;
}
.modal {
width: 92%;
}
@@ -62,10 +66,6 @@
justify-content: center;
}
#card-select {
justify-content: center;
}
#join-game-container {
display: flex;
flex-direction: column
@@ -127,6 +127,10 @@
margin: 0.5em;
}
.disclaimer, .custom-card h1 {
font-size: 18px;
}
h3 {
font-size: 30px;
}
@@ -151,6 +155,10 @@
width: 90%;
}
#custom-card-modal .modal-content {
width: 50%;
}
.modal-header {
padding: 0 3em;
}
@@ -209,6 +217,12 @@
}
@media(max-width: 1225px) and (min-width: 750.01px) {
#custom-card-modal .modal-content {
width: 75%;
}
}
@font-face {
font-family: 'diavlo';
src: url("../assets/fonts/Diavlo_LIGHT_II_37.otf") format("opentype");
@@ -385,6 +399,20 @@ button {
user-select: none;
}
.custom-card {
border: 1px dashed whitesmoke;
background-color: transparent;
}
.custom-card div {
font-size: 40px;
color: whitesmoke;
}
.custom-card h1 {
color: whitesmoke;
}
.card:hover {
background-color: #55565c;
}
@@ -439,6 +467,12 @@ button {
pointer-events: none;
}
.card-image-custom {
width: 50px;
height: 50px;
padding: 25px;
}
.card-quantity {
pointer-events: none;
font-weight: bold;
@@ -645,6 +679,99 @@ input[type=text] {
font-size: 1.1em;
}
textarea {
resize: none;
background-color: transparent;
border: 1px solid #464552;
caret-color: gray;
font-family: inherit;
margin: 0.5em 0 1em 0;
color: gray;
padding: 0.9em;
border-radius: 5px;
font-size: 1.1em;
}
.checkbox label::before{
content: "";
display: inline-block;
height: 30px;
left: -42px;
width: 30px;
border: 1px solid whitesmoke;
border-radius: 3px;
}
.checkbox input[type=checkbox] {
opacity: 0;
}
.checkbox label::after {
display: inline-block;
height: 8px;
width: 16px;
left: -35px;
top: 8px;
border-left: 3px solid;
border-bottom: 3px solid;
transform: rotate(-55deg);
}
.checkbox {
display: flex;
width: 100%;
margin: 1em 0;
}
.checkbox label {
position: relative;
margin: 1em 0 1em 1.5em;
}
.checkbox label::before,
.checkbox label::after {
position: absolute;
}
.checkbox input[type="checkbox"] + label::after {
content: none;
}
.checkbox input[type="checkbox"]:checked + label::after {
content: "";
}
.checkbox input[type="checkbox"]:focus + label::before {
outline: rgb(59, 153, 252) auto 5px;
}
.checkbox label:hover::before {
border: 1px solid #333243 !important;
cursor: pointer;
}
.checkbox label:hover {
cursor: pointer;
}
.disclaimer {
color: #bd2a2a;
}
select {
border-radius: 5px;
padding: 10px 20px;
font-size: 20px;
font-family: inherit;
background-color: transparent;
color: whitesmoke;
}
select option {
background: rgb(35,40,43);
}
input[type=number] {
background-color: transparent;
border: 1px solid #464552;

View File

@@ -5,18 +5,47 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel = "stylesheet" type = "text/css" href = "../static/styles.css" />
<link rel="shortcut icon" type="image/png" href="../assets/images/favicon.ico"/>
<script src="../node_modules/socket.io-client/dist/socket.io.js"></script>
</head>
<body>
<div class="modal hidden" id="role-modal">
<div class="modal-content">
<div class="modal-header">
<span id="close" class="close">&times;</span>
<span class="close" class="close">&times;</span>
</div>
<div id="modal-body" class="modal-body">
<div class="modal-body">
<div id="roles"></div>
</div>
</div>
</div>
<div class="modal hidden" id="custom-card-modal">
<div class="modal-content">
<div class="modal-header">
<span class="close" class="close">&times;</span>
</div>
<div class="modal-body">
<h2>Add a role for this game only.</h2>
<h3 class="disclaimer">Warning: If your role changes the victory condition (like the Hunter), the game will not detect the new condition.</h3>
<form id="custom-role-form">
<label for="custom-role-name">Name</label>
<input id="custom-role-name" type="text" required/>
<label for="custom-role-desc">Description</label>
<textarea rows="3" id="custom-role-desc" required></textarea>
<label for="custom-role-team">Team</label>
<select id="custom-role-team">
<option value="good">Good</option>
<option value="evil">Evil</option>
</select>
<div class="checkbox">
<input type="checkbox" id="custom-role-power">
<label for="custom-role-power">Power Role?</label>
</div>
<br><br>
<input type="submit" class="app-btn" value="Add Role">
</form>
</div>
</div>
</div>
<div id="app-content">
<div id="create-game-container">

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel = "stylesheet" type = "text/css" href = "../static/styles.css" />
<link rel="shortcut icon" type="image/png" href="../assets/images/favicon.ico"/>
<script src="../node_modules/socket.io-client/dist/socket.io.js"></script>
</head>
<body>

View File

@@ -4,6 +4,7 @@
<title>Werewolf</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel = "stylesheet" type = "text/css" href = "../static/styles.css" />
<link rel="shortcut icon" type="image/png" href="../assets/images/favicon.ico"/>
<script src="../node_modules/socket.io-client/dist/socket.io.js"></script>
</head>
<body>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel = "stylesheet" type = "text/css" href = "static/styles.css" />
<link rel="shortcut icon" type="image/png" href="../assets/images/favicon.ico"/>
<script src="../node_modules/socket.io-client/dist/socket.io.js"></script>
</head>
<body>

View File

@@ -6,6 +6,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel = "stylesheet" type = "text/css" href = "../static/styles.css" />
<link rel="shortcut icon" type="image/png" href="../assets/images/favicon.ico"/>
</head>
<body>
<div id="learn-container">