This commit is contained in:
nicfreeman1209 2021-01-29 22:33:53 +00:00
commit f60ed34ee3
25 changed files with 1028 additions and 988 deletions

View File

@ -18,6 +18,9 @@ on:
push:
branches-ignore:
- 'gh-pages'
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
###############
# Set the Job #

View File

@ -1,10 +1,57 @@
# Release Notes
## Version 2.6.0
- night mode can be toggeled with [S] now (thanks @davotronic5000)
- night order shows which players are dead
---
## Version 2.5.0
- all travelers from the base editions are now optionally available (thanks @davotronic5000)
- night order shows player names near roles now
---
## Version 2.4.0
- added spoiler role (Pixie!)
- fixed bug with ST sending out roles that are not part of the current edition / script (ie. travelers or base set roles)
- better Lycanthrope icon (thanks @AWConant)
---
## Version 2.3.1
- better vote history design and added timestamps
- adjusted player menu styling on smaller screens
- improved CONTRIBUTING.md description of hosting your own copy
---
## Version 2.3.0
- added spoiler role (Lycanthrope!)
- fixed copy to clipboard in Firefox
- fixed non-countdown votes still playing countdown sound for a split second
---
## Version 2.2.1
- clearing players / roles now also clears Fabled
- fix list of locked votes showing unlocked votes sometimes
---
## Version 2.2.0
- added [V] hotkey to open nomination history (thanks @lilserf)
- updated roles according to official Wiki changes
- adjusted roles night order
---
## Version 2.1.1
- show vote results at the end of a vote
- fixed global reminders not showing up anymore when the associated role is assigned to a player
- adjusted backend metrics
---
## Version 2.1.0

View File

@ -43,6 +43,20 @@ $ npm install
The development server can be started with `npm run serve`.
### Deploying to GitHub pages or with a non-root path
Deploying a forked version to GitHub Pages or running your local
development copy in a sub-path (instead of the web root) will require you to modify
the `vue.config.js` and configure the path at which the website will be served.
For example, deploying your forked `townsquare` project to GitHub pages would need the following
`vue.config.js` changes:
```js
module.exports = {
publicPath: process.env.NODE_ENV === "production" ? "/townsquare/" : "/"
};
```
### Committing Changes
Commit messages should be verbose enough to allow someone else to follow your changes and should include references to issues that are being worked on.
@ -64,6 +78,10 @@ $ npm run lint
- **`dist`**: contains built files for distribution.
- **`public`**: static assets and templates that don't need to be built dynamically.
- **`server`**: NodeJS code for the live session backend server.
- **`src`**: contains the source code. The codebase is written in ES2015.
- **`assets`**: contains all graphical assets like images, fonts, icons, etc.
@ -74,8 +92,12 @@ $ npm run lint
- **`icons`**: character token icons
- **`sounds`**: sound effects used on the page
- **`components`**: the internal components used in the project
- **`modals`**: the modals have a separate subfolder
- **`store`**: the VueX data store and modules
- **`modules`**: VueX modules that live in their own namespace

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "townsquare",
"version": "2.1.1",
"version": "2.6.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "townsquare",
"version": "2.1.1",
"version": "2.6.0",
"description": "Blood on the Clocktower Town Square",
"author": "Steffen Baumgart",
"scripts": {

View File

@ -102,6 +102,15 @@ export default {
if (this.session.isSpectator) return;
this.$store.commit("toggleModal", "roles");
break;
case "v":
if (this.session.voteHistory.length) {
this.$store.commit("toggleModal", "voteHistory");
}
break;
case "s":
if (this.session.isSpectator) return;
this.$store.commit("toggleNight");
break;
case "escape":
this.$store.commit("toggleModal");
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
src/assets/icons/pixie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@ -43,10 +43,7 @@
<li @click="toggleNight" v-if="!session.isSpectator">
<template v-if="!grimoire.isNight">Switch to Night</template>
<template v-if="grimoire.isNight">Switch to Day</template>
<em
><font-awesome-icon
:icon="['fas', grimoire.isNight ? 'sun' : 'cloud-moon']"
/></em>
<em>[S]</em>
</li>
<li @click="toggleNightOrder" v-if="players.length">
Night order
@ -114,8 +111,7 @@
v-if="session.voteHistory.length"
@click="toggleModal('voteHistory')"
>
Nomination history
<em><font-awesome-icon icon="hand-paper"/></em>
Nomination history<em>[V]</em>
</li>
<li @click="leaveSession" v-if="session.sessionId">
Leave Session
@ -239,16 +235,9 @@ export default {
}
},
copySessionUrl() {
// check for clipboard permissions
navigator.permissions
.query({ name: "clipboard-write" })
.then(({ state }) => {
if (state === "granted" || state === "prompt") {
const url = window.location.href.split("#")[0];
const link = url + "#" + this.session.sessionId;
navigator.clipboard.writeText(link);
}
});
const url = window.location.href.split("#")[0];
const link = url + "#" + this.session.sessionId;
navigator.clipboard.writeText(link);
},
distributeRoles() {
if (this.session.isSpectator) return;

View File

@ -675,7 +675,7 @@ li.move:not(.from) .player .overlay svg.move {
border: 10px solid transparent;
border-right-color: black;
right: 100%;
bottom: 7px;
bottom: 5px;
margin-right: 2px;
}

View File

@ -161,11 +161,13 @@ export default {
},
voters: function() {
const nomination = this.session.nomination[1];
const voters = this.session.votes.map((vote, index) =>
vote ? this.players[index].name : ""
);
const voters = Array(this.players.length)
.fill("")
.map((x, index) =>
this.session.votes[index] ? this.players[index].name : ""
);
const reorder = [
...voters.slice(nomination + 1, this.players.length),
...voters.slice(nomination + 1),
...voters.slice(0, nomination + 1)
];
return reorder.slice(0, this.session.lockedVote - 1).filter(n => !!n);
@ -178,15 +180,15 @@ export default {
},
methods: {
countdown() {
this.$store.commit("session/setVoteInProgress", true);
this.$store.commit("session/lockVote", 0);
this.$store.commit("session/setVoteInProgress", true);
this.voteTimer = setInterval(() => {
this.start();
}, 4000);
},
start() {
this.$store.commit("session/setVoteInProgress", true);
this.$store.commit("session/lockVote", 1);
this.$store.commit("session/setVoteInProgress", true);
clearInterval(this.voteTimer);
this.voteTimer = setInterval(() => {
this.$store.commit("session/lockVote");

View File

@ -54,14 +54,7 @@ export default {
},
methods: {
copy: function() {
// check for clipboard permissions
navigator.permissions
.query({ name: "clipboard-write" })
.then(({ state }) => {
if (state === "granted" || state === "prompt") {
navigator.clipboard.writeText(this.input || this.gamestate);
}
});
navigator.clipboard.writeText(this.input || this.gamestate);
},
load: function() {
if (this.session.isSpectator) return;

View File

@ -25,6 +25,17 @@
>
<span class="name">
{{ role.name }}
<template v-if="role.players.length">
<br />
<small
v-for="(player, index) in role.players"
:class="{ dead: player.isDead }"
:key="index"
>{{
player.name + (role.players.length > index + 1 ? "," : "")
}}</small
>
</template>
</span>
<span
class="icon"
@ -53,6 +64,17 @@
></span>
<span class="name">
{{ role.name }}
<template v-if="role.players.length">
<br />
<small
v-for="(player, index) in role.players"
:class="{ dead: player.isDead }"
:key="index"
>{{
player.name + (role.players.length > index + 1 ? "," : "")
}}</small
>
</template>
</span>
</li>
</ul>
@ -68,11 +90,6 @@ export default {
components: {
Modal
},
data: function() {
return {
roleSelection: {}
};
},
computed: {
rolesFirstNight: function() {
const rolesFirstNight = [];
@ -83,23 +100,22 @@ export default {
id: "evil",
name: "Minion info",
firstNight: 2,
team: "minion"
team: "minion",
players: this.players.filter(p => p.role.team === "minion")
},
{
id: "evil",
name: "Demon info & bluffs",
firstNight: 4,
team: "demon"
team: "demon",
players: this.players.filter(p => p.role.team === "demon")
}
);
}
this.roles.forEach(role => {
if (
role.firstNight &&
(role.team !== "traveler" ||
this.players.some(p => p.role.id === role.id))
) {
rolesFirstNight.push(role);
const players = this.players.filter(p => p.role.id === role.id);
if (role.firstNight && (role.team !== "traveler" || players.length)) {
rolesFirstNight.push(Object.assign({ players }, role));
}
});
this.fabled
@ -113,12 +129,9 @@ export default {
rolesOtherNight: function() {
const rolesOtherNight = [];
this.roles.forEach(role => {
if (
role.otherNight &&
(role.team !== "traveler" ||
this.players.some(p => p.role.id === role.id))
) {
rolesOtherNight.push(role);
const players = this.players.filter(p => p.role.id === role.id);
if (role.otherNight && (role.team !== "traveler" || players.length)) {
rolesOtherNight.push(Object.assign({ players }, role));
}
});
this.fabled
@ -179,57 +192,42 @@ h4 {
}
.fabled {
.name,
.player,
h4 {
color: $fabled;
&:before,
&:after {
background-color: $fabled;
.name {
background: linear-gradient(90deg, $fabled, transparent 35%);
.night .other & {
background: linear-gradient(-90deg, $fabled, transparent 35%);
}
}
}
.townsfolk {
.name,
.player,
h4 {
color: $townsfolk;
&:before,
&:after {
background-color: $townsfolk;
.name {
background: linear-gradient(90deg, $townsfolk, transparent 35%);
.night .other & {
background: linear-gradient(-90deg, $townsfolk, transparent 35%);
}
}
}
.outsider {
.name,
.player,
h4 {
color: $outsider;
&:before,
&:after {
background-color: $outsider;
.name {
background: linear-gradient(90deg, $outsider, transparent 35%);
.night .other & {
background: linear-gradient(-90deg, $outsider, transparent 35%);
}
}
}
.minion {
.name,
.player,
h4 {
color: $minion;
&:before,
&:after {
background-color: $minion;
.name {
background: linear-gradient(90deg, $minion, transparent 35%);
.night .other & {
background: linear-gradient(-90deg, $minion, transparent 35%);
}
}
}
.demon {
.name,
.player,
h4 {
color: $demon;
&:before,
&:after {
background-color: $demon;
.name {
background: linear-gradient(90deg, $demon, transparent 35%);
.night .other & {
background: linear-gradient(-90deg, $demon, transparent 35%);
}
}
}
@ -237,20 +235,15 @@ ul {
li {
display: flex;
width: 100%;
align-items: center;
align-content: center;
/*background: linear-gradient(0deg, #ffffff0f, transparent);*/
border-radius: 10px;
margin-bottom: 3px;
.icon {
width: 6vh;
background-size: cover;
background-position: 0 -5px;
background-position: 0 0;
flex-grow: 0;
flex-shrink: 0;
margin: 0 10px;
text-align: center;
border-left: 1px solid #ffffff1f;
border-right: 1px solid #ffffff1f;
margin: 0 2px;
&:after {
content: " ";
display: block;
@ -261,19 +254,18 @@ ul {
flex-grow: 0;
flex-shrink: 0;
width: 15%;
font-weight: bold;
text-align: right;
font-family: "Papyrus", sans-serif;
font-size: 110%;
}
.player {
flex-grow: 0;
flex-shrink: 1;
text-align: right;
margin: 0 10px;
}
.ability {
flex-grow: 1;
padding: 5px;
border-left: 1px solid rgba(255, 255, 255, 0.4);
border-right: 1px solid rgba(255, 255, 255, 0.4);
small {
color: #888;
margin-right: 5px;
&.dead {
text-decoration: line-through;
}
}
}
}
&.legend {
@ -307,28 +299,23 @@ ul {
.headline {
display: block;
font-weight: bold;
border-bottom: 1px solid white;
border-bottom: 1px solid rgba(255, 255, 255, 0.4);
padding: 5px 10px;
border-radius: 0;
text-align: center;
}
.icon {
border-color: white;
}
.name {
flex-grow: 1;
}
.first {
.icon {
border-right: 0;
.name {
border-left: 0;
}
}
.other {
li .name {
text-align: left;
}
.icon {
border-left: 0;
border-right: 0;
}
}
}

View File

@ -56,11 +56,6 @@ export default {
components: {
Modal
},
data: function() {
return {
roleSelection: {}
};
},
computed: {
rolesGrouped: function() {
const rolesGrouped = {};
@ -136,7 +131,6 @@ h4 {
.townsfolk {
.name,
.player,
h4 {
color: $townsfolk;
&:before,
@ -147,7 +141,6 @@ h4 {
}
.outsider {
.name,
.player,
h4 {
color: $outsider;
&:before,
@ -158,7 +151,6 @@ h4 {
}
.minion {
.name,
.player,
h4 {
color: $minion;
&:before,
@ -169,7 +161,6 @@ h4 {
}
.demon {
.name,
.player,
h4 {
color: $demon;
&:before,
@ -208,7 +199,6 @@ ul {
width: 15%;
font-weight: bold;
text-align: right;
font-family: "Papyrus", sans-serif;
font-size: 110%;
}
.player {
@ -216,6 +206,8 @@ ul {
flex-shrink: 1;
text-align: right;
margin: 0 10px;
color: #888;
font-size: smaller;
}
.ability {
flex-grow: 1;
@ -230,6 +222,7 @@ ul {
height: auto;
font-family: inherit;
font-size: inherit;
color: #fff;
}
.icon:after {
padding-top: 0;

View File

@ -82,6 +82,21 @@ export default {
}))
];
});
// add out of script traveler reminders
this.$store.state.otherTravelers.forEach(role => {
if (players.some(p => p.role.id === role.id)) {
reminders = [
...reminders,
...role.reminders.map(name => ({
role: role.id,
image: role.image,
name
}))
];
}
});
reminders.push({ role: "good", name: "Good" });
reminders.push({ role: "evil", name: "Evil" });
reminders.push({ role: "custom", name: "Custom note" });

View File

@ -1,8 +1,5 @@
<template>
<Modal
v-if="modals.role && availableRoles.length"
@close="toggleModal('role')"
>
<Modal v-if="modals.role && availableRoles.length" @close="close">
<h3>
Choose a new character for
{{
@ -11,7 +8,7 @@
: "bluffing"
}}
</h3>
<ul class="tokens">
<ul class="tokens" v-if="tab === 'editionRoles' || !otherTravelers.size">
<li
v-for="role in availableRoles"
:class="[role.team]"
@ -21,6 +18,33 @@
<Token :role="role" />
</li>
</ul>
<ul class="tokens" v-if="tab === 'otherTravelers' && otherTravelers.size">
<li
v-for="role in otherTravelers.values()"
:class="[role.team]"
:key="role.id"
@click="setRole(role)"
>
<Token :role="role" />
</li>
</ul>
<div
class="button-group"
v-if="playerIndex >= 0 && otherTravelers.size && !session.isSpectator"
>
<span
class="button"
:class="{ townsfolk: tab === 'editionRoles' }"
@click="tab = 'editionRoles'"
>Edtition Roles</span
>
<span
class="button"
:class="{ townsfolk: tab === 'otherTravelers' }"
@click="tab = 'otherTravelers'"
>Other Travelers</span
>
</div>
</Modal>
</template>
@ -50,7 +74,13 @@ export default {
return availableRoles;
},
...mapState(["modals", "roles", "session"]),
...mapState("players", ["players"])
...mapState("players", ["players"]),
...mapState(["otherTravelers"])
},
data() {
return {
tab: "editionRoles"
};
},
methods: {
setRole(role) {
@ -72,6 +102,10 @@ export default {
}
this.$store.commit("toggleModal", "role");
},
close() {
this.tab = "editionRoles";
this.toggleModal("role");
},
...mapMutations(["toggleModal"])
}
};

View File

@ -15,22 +15,50 @@
<table>
<thead>
<tr>
<td>Time</td>
<td>Nominator</td>
<td>Nominee</td>
<td>Type</td>
<td>Votes</td>
<td>Majority</td>
<td><font-awesome-icon icon="hand-paper" /> Hand up</td>
<td>
<font-awesome-icon icon="user-friends" />
Voters
</td>
</tr>
</thead>
<tbody>
<tr v-for="(vote, index) in session.voteHistory" :key="index">
<td>
{{
vote.timestamp
.getHours()
.toString()
.padStart(2, "0")
}}:{{
vote.timestamp
.getMinutes()
.toString()
.padStart(2, "0")
}}
</td>
<td>{{ vote.nominator }}</td>
<td>{{ vote.nominee }}</td>
<td>{{ vote.type }}</td>
<td>{{ vote.majority }}</td>
<td>
{{ vote.votes.length }}
<font-awesome-icon icon="user-friends" />
<font-awesome-icon icon="hand-paper" />
</td>
<td>
{{ vote.majority }}
<font-awesome-icon
:icon="[
'fas',
vote.votes.length >= vote.majority ? 'check-square' : 'square'
]"
/>
</td>
<td>
{{ vote.votes.join(", ") }}
</td>
</tr>
@ -89,13 +117,16 @@ thead td {
}
tbody {
td:nth-child(1) {
td:nth-child(2) {
color: $townsfolk;
}
td:nth-child(2) {
td:nth-child(3) {
color: $demon;
}
td:nth-child(4) {
td:nth-child(5) {
text-align: center;
}
td:nth-child(6) {
text-align: center;
}
}

View File

@ -34,7 +34,6 @@ const faIcons = [
"SearchMinus",
"SearchPlus",
"Square",
"Sun",
"TheaterMasks",
"Times",
"TimesCircle",

View File

@ -44,6 +44,12 @@
.player > .name {
top: 0;
}
.player > .menu {
bottom: 0;
&:before {
bottom: 0;
}
}
}
// Old phones

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,19 @@ const getRolesByEdition = (edition = editionJSON[0]) => {
);
};
const getTravelersNotInEdition = (edition = editionJSON[0]) => {
return new Map(
rolesJSON
.filter(
r =>
r.team === "traveler" &&
r.edition !== edition.id &&
!edition.roles.includes(r.id)
)
.map(role => [role.id, role])
);
};
// base definition for custom roles
const imageBase =
"https://raw.githubusercontent.com/bra1n/townsquare/main/src/assets/icons/";
@ -70,6 +83,7 @@ export default new Vuex.Store({
},
edition: editionJSONbyId.get("tb"),
roles: getRolesByEdition(),
otherTravelers: getTravelersNotInEdition(),
fabled
},
getters: {
@ -180,11 +194,18 @@ export default new Vuex.Store({
// convert to Map
.map(role => [role.id, role])
);
// update extraTravelers map to only show travelers not in this script
state.otherTravelers = new Map(
rolesJSON
.filter(r => r.team === "traveler" && !roles.some(i => i.id === r.id))
.map(role => [role.id, role])
);
},
setEdition(state, edition) {
if (editionJSONbyId.has(edition.id)) {
state.edition = editionJSONbyId.get(edition.id);
state.roles = getRolesByEdition(state.edition);
state.otherTravelers = getTravelersNotInEdition(state.edition);
} else {
state.edition = edition;
}

View File

@ -84,6 +84,7 @@ const actions = {
name,
id
}));
commit("setFabled", { fabled: [] });
}
commit("set", players);
commit("setBluff");
@ -94,6 +95,7 @@ const mutations = {
clear(state) {
state.players = [];
state.bluffs = [];
state.fabled = [];
},
set(state, players = []) {
state.players = players;

View File

@ -72,8 +72,8 @@ const mutations = {
addHistory(state, players) {
if (!state.nomination || state.lockedVote <= players.length) return;
const isBanishment = players[state.nomination[1]].role.team === "traveler";
console.log(isBanishment);
state.voteHistory.push({
timestamp: new Date(),
nominator: players[state.nomination[0]].name,
nominee: players[state.nomination[1]].name,
type: isBanishment ? "Banishment" : "Execution",

View File

@ -443,8 +443,11 @@ class LiveSession {
value: {}
});
} else {
// load role
const role = this._store.state.roles.get(value);
// load role, first from session, the global, then fail gracefully
const role =
this._store.state.roles.get(value) ||
this._store.getters.rolesJSONbyId.get(value) ||
{};
this._store.commit("players/update", {
player,
property: "role",