Merge pull request #7 from davotronic5000/develop

upgraded vue to latest version and fixed bugs resulting from it.  Sti…
This commit is contained in:
Dave 2023-05-27 11:48:58 +01:00 committed by GitHub
commit b01b61ad47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 4842 additions and 17938 deletions

21951
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,26 +11,26 @@
}, },
"main": "App.vue", "main": "App.vue",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^5.15.1", "@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^0.1.10", "@fortawesome/vue-fontawesome": "^3.0.3",
"@vue/cli-service": "^4.5.9", "@vue/cli-service": "^5.0.8",
"prom-client": "^13.0.0", "prom-client": "^13.0.0",
"sass": "^1.30.0", "sass": "^1.30.0",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"vue": "^2.6.12", "vue": "^3.3.4",
"vue-template-compiler": "^2.6.12", "@vue/compat": "^3.3.4",
"vuex": "^3.6.0", "vuex": "^4.1.0",
"ws": "^7.4.6" "ws": "^7.4.6",
"vue-loader": "^17.1.1"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-eslint": "^4.5.9", "@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-prettier": "^6.0.0", "eslint": "^8.41.0",
"eslint": "^6.7.2", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-prettier": "^3.2.0", "eslint-plugin-vue": "^9.14.0",
"eslint-plugin-vue": "^6.2.2", "prettier": "^2.8.8"
"prettier": "^1.19.1"
}, },
"keywords": [ "keywords": [
"botc", "botc",

View file

@ -7,7 +7,7 @@ const client = require("prom-client");
const register = new client.Registry(); const register = new client.Registry();
// Add a default label which is added to all metrics // Add a default label which is added to all metrics
register.setDefaultLabels({ register.setDefaultLabels({
app: "clocktower-online" app: "clocktower-online",
}); });
const PING_INTERVAL = 30000; // 30 seconds const PING_INTERVAL = 30000; // 30 seconds
@ -22,11 +22,9 @@ if (process.env.NODE_ENV !== "development") {
const server = https.createServer(options); const server = https.createServer(options);
const wss = new WebSocket.Server({ const wss = new WebSocket.Server({
...(process.env.NODE_ENV === "development" ? { port: 8081 } : { server }), ...(process.env.NODE_ENV === "development" ? { port: 8081 } : { server }),
verifyClient: info => verifyClient: (info) =>
info.origin && info.origin &&
!!info.origin.match( !!info.origin.match(/^https?:\/\/(townsquare.clocktower.guru|localhost)/i),
/^https?:\/\/([^.]+\.github\.io|localhost|clocktower\.online|eddbra1nprivatetownsquare\.xyz)/i
)
}); });
function noop() {} function noop() {}
@ -48,14 +46,14 @@ const metrics = {
help: "Concurrent Players", help: "Concurrent Players",
collect() { collect() {
this.set(wss.clients.size); this.set(wss.clients.size);
} },
}), }),
channels_concurrent: new client.Gauge({ channels_concurrent: new client.Gauge({
name: "channels_concurrent", name: "channels_concurrent",
help: "Concurrent Channels", help: "Concurrent Channels",
collect() { collect() {
this.set(Object.keys(channels).length); this.set(Object.keys(channels).length);
} },
}), }),
channels_list: new client.Gauge({ channels_list: new client.Gauge({
name: "channel_players", name: "channel_players",
@ -66,35 +64,35 @@ const metrics = {
this.set( this.set(
{ name: channel }, { name: channel },
channels[channel].filter( channels[channel].filter(
ws => (ws) =>
ws && ws &&
(ws.readyState === WebSocket.OPEN || (ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING) ws.readyState === WebSocket.CONNECTING)
).length ).length
); );
} }
} },
}), }),
messages_incoming: new client.Counter({ messages_incoming: new client.Counter({
name: "messages_incoming", name: "messages_incoming",
help: "Incoming messages" help: "Incoming messages",
}), }),
messages_outgoing: new client.Counter({ messages_outgoing: new client.Counter({
name: "messages_outgoing", name: "messages_outgoing",
help: "Outgoing messages" help: "Outgoing messages",
}), }),
connection_terminated_host: new client.Counter({ connection_terminated_host: new client.Counter({
name: "connection_terminated_host", name: "connection_terminated_host",
help: "Terminated connection due to host already present" help: "Terminated connection due to host already present",
}), }),
connection_terminated_spam: new client.Counter({ connection_terminated_spam: new client.Counter({
name: "connection_terminated_spam", name: "connection_terminated_spam",
help: "Terminated connection due to message spam" help: "Terminated connection due to message spam",
}), }),
connection_terminated_timeout: new client.Counter({ connection_terminated_timeout: new client.Counter({
name: "connection_terminated_timeout", name: "connection_terminated_timeout",
help: "Terminated connection due to timeout" help: "Terminated connection due to timeout",
}) }),
}; };
// register metrics // register metrics
@ -113,7 +111,7 @@ wss.on("connection", function connection(ws, req) {
ws.playerId === "host" && ws.playerId === "host" &&
channels[ws.channel] && channels[ws.channel] &&
channels[ws.channel].some( channels[ws.channel].some(
client => (client) =>
client !== ws && client !== ws &&
client.readyState === WebSocket.OPEN && client.readyState === WebSocket.OPEN &&
client.playerId === "host" client.playerId === "host"
@ -149,11 +147,7 @@ wss.on("connection", function connection(ws, req) {
metrics.connection_terminated_spam.inc(); metrics.connection_terminated_spam.inc();
return; return;
} }
const messageType = data const messageType = data.toLocaleLowerCase().substr(1).split(",", 1).pop();
.toLocaleLowerCase()
.substr(1)
.split(",", 1)
.pop();
switch (messageType) { switch (messageType) {
case '"ping"': case '"ping"':
// ping messages will only be sent host -> all or all -> host // ping messages will only be sent host -> all or all -> host
@ -232,7 +226,7 @@ const interval = setInterval(function ping() {
if ( if (
!channels[channel].length || !channels[channel].length ||
!channels[channel].some( !channels[channel].some(
ws => (ws) =>
ws && ws &&
(ws.readyState === WebSocket.OPEN || (ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING) ws.readyState === WebSocket.CONNECTING)
@ -255,6 +249,6 @@ if (process.env.NODE_ENV !== "development") {
server.listen(8080); server.listen(8080);
server.on("request", (req, res) => { server.on("request", (req, res) => {
res.setHeader("Content-Type", register.contentType); res.setHeader("Content-Type", register.contentType);
register.metrics().then(out => res.end(out)); register.metrics().then((out) => res.end(out));
}); });
} }

View file

@ -1,16 +1,16 @@
<template> <template>
<div <div
id="app" id="townsquare-app"
@keyup="keyup" @keyup="keyup"
tabindex="-1" tabindex="-1"
:class="{ :class="{
night: grimoire.isNight, night: grimoire.isNight,
static: grimoire.isStatic static: grimoire.isStatic,
}" }"
:style="{ :style="{
backgroundImage: grimoire.background backgroundImage: grimoire.background
? `url('${grimoire.background}')` ? `url('${grimoire.background}')`
: '' : '',
}" }"
> >
<video <video
@ -20,29 +20,42 @@
autoplay autoplay
loop loop
></video> ></video>
<div class="backdrop"></div> <div class="backdrop"></div>
<transition name="blur">
<Intro v-if="!players.length"></Intro> <Intro v-if="!players.length"></Intro>
<TownInfo v-if="players.length && !session.nomination"></TownInfo> <TownInfo v-if="players.length && !session.nomination"></TownInfo>
<Vote v-if="session.nomination"></Vote> <Vote v-if="session.nomination"></Vote>
</transition>
<TownSquare></TownSquare> <TownSquare></TownSquare>
<Menu ref="menu"></Menu> <Menu ref="menu"></Menu>
<EditionModal /> <EditionModal />
<FabledModal /> <FabledModal />
<RolesModal /> <RolesModal />
<ReferenceModal /> <ReferenceModal />
<NightOrderModal /> <NightOrderModal />
<VoteHistoryModal /> <VoteHistoryModal />
<GameStateModal /> <GameStateModal />
<Gradients /> <Gradients />
<span id="version">v{{ version }}</span> <span id="version">v{{ version }}</span>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapState } from "vuex";
import { version } from "../package.json"; import Package from "../package.json";
import TownSquare from "./components/TownSquare"; import TownSquare from "./components/TownSquare";
import TownInfo from "./components/TownInfo"; import TownInfo from "./components/TownInfo";
import Menu from "./components/Menu"; import Menu from "./components/Menu";
@ -71,15 +84,15 @@ export default {
Menu, Menu,
EditionModal, EditionModal,
RolesModal, RolesModal,
Gradients Gradients,
}, },
computed: { computed: {
...mapState(["grimoire", "session"]), ...mapState(["grimoire", "session"]),
...mapState("players", ["players"]) ...mapState("players", ["players"]),
}, },
data() { data() {
return { return {
version version: Package.version,
}; };
}, },
methods: { methods: {
@ -124,8 +137,8 @@ export default {
case "escape": case "escape":
this.$store.commit("toggleModal"); this.$store.commit("toggleModal");
} }
} },
} },
}; };
</script> </script>
@ -198,6 +211,10 @@ ul {
} }
#app { #app {
height: 100%;
width: 100%;
}
#townsquare-app {
height: 100%; height: 100%;
background-position: center center; background-position: center center;
background-size: cover; background-size: cover;
@ -326,7 +343,7 @@ video#background {
} }
/* Night phase backdrop */ /* Night phase backdrop */
#app > .backdrop { #townsquare-app > .backdrop {
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
@ -364,7 +381,7 @@ video#background {
} }
} }
#app.night > .backdrop { #townsquare-app.night > .backdrop {
opacity: 0.5; opacity: 0.5;
} }
</style> </style>

View file

@ -2,13 +2,11 @@
<div id="controls"> <div id="controls">
<span <span
class="nomlog-summary" class="nomlog-summary"
v-show="session.voteHistory.length && session.sessionId" v-show="session.sessionId"
@click="toggleModal('voteHistory')" @click="toggleModal('voteHistory')"
:title=" :title="`${session.voteHistory.length} recent ${
`${session.voteHistory.length} recent ${
session.voteHistory.length == 1 ? 'nomination' : 'nominations' session.voteHistory.length == 1 ? 'nomination' : 'nominations'
}` }`"
"
> >
<font-awesome-icon icon="book-dead" /> <font-awesome-icon icon="book-dead" />
{{ session.voteHistory.length }} {{ session.voteHistory.length }}
@ -17,32 +15,39 @@
class="session" class="session"
:class="{ :class="{
spectator: session.isSpectator, spectator: session.isSpectator,
reconnecting: session.isReconnecting reconnecting: session.isReconnecting,
}" }"
v-if="session.sessionId" v-if="session.sessionId"
@click="leaveSession" @click="leaveSession"
:title=" :title="`${session.playerCount} other players in this session${
`${session.playerCount} other players in this session${
session.ping ? ' (' + session.ping + 'ms latency)' : '' session.ping ? ' (' + session.ping + 'ms latency)' : ''
}` }`"
"
> >
<font-awesome-icon icon="broadcast-tower" /> <font-awesome-icon icon="broadcast-tower" />
{{ session.playerCount }} {{ session.playerCount }}
</span> </span>
<div class="menu" :class="{ open: grimoire.isMenuOpen }"> <div class="menu" :class="{ open: grimoire.isMenuOpen }">
<font-awesome-icon icon="cog" @click="toggleMenu" /> <span @click="toggleMenu">
<font-awesome-icon icon="cog" class="menuCog" />
</span>
<ul> <ul>
<li class="tabs" :class="tab"> <li class="tabs" :class="tab">
<font-awesome-icon icon="book-open" @click="tab = 'grimoire'" /> <span @click="tab = 'grimoire'">
<font-awesome-icon icon="broadcast-tower" @click="tab = 'session'" /> <font-awesome-icon icon="book-open" />
<font-awesome-icon </span>
icon="users" <span @click="tab = 'session'">
v-if="!session.isSpectator" <font-awesome-icon icon="broadcast-tower" />
@click="tab = 'players'" </span>
/> <span @click="tab = 'players'">
<font-awesome-icon icon="theater-masks" @click="tab = 'characters'" /> <font-awesome-icon icon="users" v-if="!session.isSpectator" />
<font-awesome-icon icon="question" @click="tab = 'help'" /> </span>
<span @click="tab = 'characters'">
<font-awesome-icon icon="theater-masks" />
</span>
<span @click="tab = 'help'">
<font-awesome-icon icon="question" />
</span>
</li> </li>
<template v-if="tab === 'grimoire'"> <template v-if="tab === 'grimoire'">
@ -64,7 +69,7 @@
<font-awesome-icon <font-awesome-icon
:icon="[ :icon="[
'fas', 'fas',
grimoire.isNightOrder ? 'check-square' : 'square' grimoire.isNightOrder ? 'check-square' : 'square',
]" ]"
/> />
</em> </em>
@ -72,15 +77,13 @@
<li v-if="players.length"> <li v-if="players.length">
Zoom Zoom
<em> <em>
<font-awesome-icon <span @click="setZoom(grimoire.zoom - 1)" class="zoom">
@click="setZoom(grimoire.zoom - 1)" <font-awesome-icon icon="search-minus" />
icon="search-minus" </span>
/>
{{ Math.round(100 + grimoire.zoom * 10) }}% {{ Math.round(100 + grimoire.zoom * 10) }}%
<font-awesome-icon <span @click="setZoom(grimoire.zoom + 1)" class="zoom">
@click="setZoom(grimoire.zoom + 1)" <font-awesome-icon icon="search-plus" />
icon="search-plus" </span>
/>
</em> </em>
</li> </li>
<li @click="setBackground"> <li @click="setBackground">
@ -93,7 +96,7 @@
><font-awesome-icon ><font-awesome-icon
:icon="[ :icon="[
'fas', 'fas',
grimoire.isImageOptIn ? 'check-square' : 'square' grimoire.isImageOptIn ? 'check-square' : 'square',
]" ]"
/></em> /></em>
</li> </li>
@ -118,9 +121,7 @@
<li class="headline" v-if="session.sessionId"> <li class="headline" v-if="session.sessionId">
{{ session.isSpectator ? "Playing" : "Hosting" }} {{ session.isSpectator ? "Playing" : "Hosting" }}
</li> </li>
<li class="headline" v-else> <li class="headline" v-else>Live Session</li>
Live Session
</li>
<template v-if="!session.sessionId"> <template v-if="!session.sessionId">
<li @click="hostSession">Host (Storyteller)<em>[H]</em></li> <li @click="hostSession">Host (Storyteller)<em>[H]</em></li>
<li @click="joinSession">Join (Player)<em>[J]</em></li> <li @click="joinSession">Join (Player)<em>[J]</em></li>
@ -236,11 +237,11 @@ import { mapMutations, mapState } from "vuex";
export default { export default {
computed: { computed: {
...mapState(["grimoire", "session", "edition"]), ...mapState(["grimoire", "session", "edition"]),
...mapState("players", ["players"]) ...mapState("players", ["players"]),
}, },
data() { data() {
return { return {
tab: "grimoire" tab: "grimoire",
}; };
}, },
methods: { methods: {
@ -353,9 +354,9 @@ export default {
"toggleNightOrder", "toggleNightOrder",
"toggleStatic", "toggleStatic",
"setZoom", "setZoom",
"toggleModal" "toggleModal",
]) ]),
} },
}; };
</script> </script>
@ -389,7 +390,7 @@ export default {
} }
} }
> span { span {
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
z-index: 5; z-index: 5;
@ -432,7 +433,7 @@ export default {
transform: rotate(0deg); transform: rotate(0deg);
} }
> svg { span .menuCog {
cursor: pointer; cursor: pointer;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border: 3px solid black; border: 3px solid black;
@ -442,6 +443,7 @@ export default {
border-bottom: 0; border-bottom: 0;
border-radius: 10px 10px 0 0; border-radius: 10px 10px 0 0;
padding: 5px 5px 15px; padding: 5px 5px 15px;
box-sizing: border-box;
} }
a { a {
@ -473,10 +475,10 @@ export default {
justify-content: space-between; justify-content: space-between;
min-height: 30px; min-height: 30px;
&.tabs { tabs {
display: flex; display: flex;
padding: 0; padding: 0;
svg { span svg {
flex-grow: 1; flex-grow: 1;
flex-shrink: 0; flex-shrink: 0;
height: 35px; height: 35px;
@ -492,11 +494,11 @@ export default {
border-right: 0; border-right: 0;
} }
} }
&.grimoire .fa-book-open, &.grimoire span .fa-book-open,
&.players .fa-users, &.players span .fa-users,
&.characters .fa-theater-masks, &.characters span .fa-theater-masks,
&.session .fa-broadcast-tower, &.session span .fa-broadcast-tower,
&.help .fa-question { &.help span .fa-question {
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
$townsfolk 0%, $townsfolk 0%,

View file

@ -47,41 +47,33 @@
/> />
<!-- Overlay icons --> <!-- Overlay icons -->
<div class="overlay"> <div class="overlay" @click="vote()">
<font-awesome-icon <font-awesome-icon icon="hand-paper" class="vote" title="Hand UP" />
icon="hand-paper" </div>
class="vote" <div class="overlay" @click="vote()">
title="Hand UP" <font-awesome-icon icon="times" class="vote" title="Hand DOWN" />
@click="vote()" </div>
/> <div class="overlay" @click="cancel()">
<font-awesome-icon <font-awesome-icon icon="times-circle" class="cancel" title="Cancel" />
icon="times" </div>
class="vote" <div class="overlay" @click="swapPlayer(player)">
title="Hand DOWN"
@click="vote()"
/>
<font-awesome-icon
icon="times-circle"
class="cancel"
title="Cancel"
@click="cancel()"
/>
<font-awesome-icon <font-awesome-icon
icon="exchange-alt" icon="exchange-alt"
class="swap" class="swap"
@click="swapPlayer(player)"
title="Swap seats with this player" title="Swap seats with this player"
/> />
</div>
<div class="overlay" @click="movePlayer(player)">
<font-awesome-icon <font-awesome-icon
icon="redo-alt" icon="redo-alt"
class="move" class="move"
@click="movePlayer(player)"
title="Move player to this seat" title="Move player to this seat"
/> />
</div>
<div class="overlay" @click="nominatePlayer(player)">
<font-awesome-icon <font-awesome-icon
icon="hand-point-right" icon="hand-point-right"
class="nominate" class="nominate"
@click="nominatePlayer(player)"
title="Nominate this player" title="Nominate this player"
/> />
</div> </div>
@ -95,13 +87,14 @@
/> />
<!-- Ghost vote icon --> <!-- Ghost vote icon -->
<div @click="updatePlayer('isVoteless', true)">
<font-awesome-icon <font-awesome-icon
icon="vote-yea" icon="vote-yea"
class="has-vote" class="has-vote"
v-if="player.isDead && !player.isVoteless" v-if="player.isDead && !player.isVoteless"
@click="updatePlayer('isVoteless', true)"
title="Ghost vote" title="Ghost vote"
/> />
</div>
<!-- On block icon --> <!-- On block icon -->
<div class="marked"> <div class="marked">
@ -166,9 +159,7 @@
:class="{ disabled: player.id && player.id !== session.playerId }" :class="{ disabled: player.id && player.id !== session.playerId }"
> >
<font-awesome-icon icon="chair" /> <font-awesome-icon icon="chair" />
<template v-if="!player.id"> <template v-if="!player.id"> Claim seat </template>
Claim seat
</template>
<template v-else-if="player.id === session.playerId"> <template v-else-if="player.id === session.playerId">
Vacate seat Vacate seat
</template> </template>
@ -554,7 +545,7 @@ export default {
fill: url(#default); fill: url(#default);
} }
&:hover *, &:hover *,
&.fa-hand-paper * { &.fa-hand * {
fill: url(#demon); fill: url(#demon);
} }
&.fa-times * { &.fa-times * {
@ -564,14 +555,14 @@ export default {
} }
// other player voted yes, but is not locked yet // other player voted yes, but is not locked yet
#townsquare.vote .player.vote-yes .overlay svg.vote.fa-hand-paper { #townsquare.vote .player.vote-yes .overlay svg.vote.fa-hand {
opacity: 0.5; opacity: 0.5;
transform: scale(1); transform: scale(1);
} }
// you voted yes | a locked vote yes | a locked vote no // you voted yes | a locked vote yes | a locked vote no
#townsquare.vote .player.you.vote-yes .overlay svg.vote.fa-hand-paper, #townsquare.vote .player.you.vote-yes .overlay svg.vote.fa-hand,
#townsquare.vote .player.vote-lock.vote-yes .overlay svg.vote.fa-hand-paper, #townsquare.vote .player.vote-lock.vote-yes .overlay svg.vote.fa-hand,
#townsquare.vote .player.vote-lock:not(.vote-yes) .overlay svg.vote.fa-times { #townsquare.vote .player.vote-lock:not(.vote-yes) .overlay svg.vote.fa-times {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);

View file

@ -1,5 +1,7 @@
<template> <template>
<div class="token" @click="setRole" :class="[role.id]"> <div class="token" @click="setRole" :class="[role.id]">
<span <span
class="icon" class="icon"
v-if="role.id" v-if="role.id"
@ -8,42 +10,52 @@
role.image && grimoire.isImageOptIn role.image && grimoire.isImageOptIn
? role.image ? role.image
: require('../assets/icons/' + (role.imageAlt || role.id) + '.png') : require('../assets/icons/' + (role.imageAlt || role.id) + '.png')
})` })`,
}" }"
></span> ></span>
<span <span
class="leaf-left" class="leaf-left"
v-if="role.firstNight || role.firstNightReminder" v-if="role.firstNight || role.firstNightReminder"
></span> ></span>
<span <span
class="leaf-right" class="leaf-right"
v-if="role.otherNight || role.otherNightReminder" v-if="role.otherNight || role.otherNightReminder"
></span> ></span>
<span v-if="reminderLeaves" :class="['leaf-top' + reminderLeaves]"></span> <span v-if="reminderLeaves" :class="['leaf-top' + reminderLeaves]"></span>
<span class="leaf-orange" v-if="role.setup"></span> <span class="leaf-orange" v-if="role.setup"></span>
<svg viewBox="0 0 150 150" class="name"> <svg viewBox="0 0 150 150" class="name">
<path <path
d="M 13 75 C 13 160, 138 160, 138 75" d="M 13 75 C 13 160, 138 160, 138 75"
id="curve" id="curve"
fill="transparent" fill="transparent"
/> />
<text <text
width="150" width="150"
x="66.6%" x="66.6%"
text-anchor="middle" text-anchor="middle"
class="label mozilla" class="label mozilla"
:font-size="role.name | nameToFontSize" :font-size="nameToFontSize(role.name)"
> >
<textPath xlink:href="#curve">
{{ role.name }} <textPath xlink:href="#curve"> {{ role.name }} </textPath>
</textPath>
</text> </text>
</svg> </svg>
<div class="edition" :class="[`edition-${role.edition}`, role.team]"></div> <div class="edition" :class="[`edition-${role.edition}`, role.team]"></div>
<div class="ability" v-if="role.ability">
{{ role.ability }} <div class="ability" v-if="role.ability"> {{ role.ability }} </div>
</div>
</div> </div>
</template> </template>
<script> <script>
@ -54,8 +66,8 @@ export default {
props: { props: {
role: { role: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
} },
}, },
computed: { computed: {
reminderLeaves: function() { reminderLeaves: function() {
@ -64,19 +76,19 @@ export default {
(this.role.remindersGlobal || []).length (this.role.remindersGlobal || []).length
); );
}, },
...mapState(["grimoire"]) ...mapState(["grimoire"]),
}, },
data() { data() {
return {}; return {};
}, },
filters: {
nameToFontSize: name => (name && name.length > 10 ? "90%" : "110%")
},
methods: { methods: {
nameToFontSize(name) {
name && name.length > 10 ? "90%" : "110%";
},
setRole() { setRole() {
this.$emit("set-role"); this.$emit("set-role");
} },
} },
}; };
</script> </script>
@ -233,3 +245,4 @@ export default {
} }
} }
</style> </style>

View file

@ -1,5 +1,7 @@
<template> <template>
<ul class="info"> <ul class="info">
<li <li
class="edition" class="edition"
:class="['edition-' + edition.id]" :class="['edition-' + edition.id]"
@ -8,67 +10,94 @@
edition.logo && grimoire.isImageOptIn edition.logo && grimoire.isImageOptIn
? edition.logo ? edition.logo
: require('../assets/editions/' + edition.id + '.png') : require('../assets/editions/' + edition.id + '.png')
})` })`,
}" }"
></li> ></li>
<li v-if="players.length - teams.traveler < 5"> <li v-if="players.length - teams.traveler < 5">
Please add more players! Please add more players!
</li> </li>
<li> <li>
<span class="meta" v-if="!edition.isOfficial"> <span class="meta" v-if="!edition.isOfficial">
{{ edition.name }} {{ edition.name }} {{ edition.author ? "by " + edition.author : "" }}
{{ edition.author ? "by " + edition.author : "" }}
</span> </span>
<span> <span>
{{ players.length }} <font-awesome-icon class="players" icon="users" /> {{ players.length }}
<font-awesome-icon class="players" icon="users" />
</span> </span>
<span> <span>
{{ teams.alive }} {{ teams.alive }}
<font-awesome-icon class="alive" icon="heartbeat" /> <font-awesome-icon class="alive" icon="heartbeat" />
</span> </span>
<span> <span>
{{ teams.votes }} <font-awesome-icon class="votes" icon="vote-yea" /> {{ teams.votes }}
<font-awesome-icon class="votes" icon="vote-yea" />
</span> </span>
</li> </li>
<li v-if="players.length - teams.traveler >= 5"> <li v-if="players.length - teams.traveler >= 5">
<span> <span>
{{ teams.townsfolk }} {{ teams.townsfolk }}
<font-awesome-icon class="townsfolk" icon="user-friends" /> <font-awesome-icon class="townsfolk" icon="user-friends" />
</span> </span>
<span> <span>
{{ teams.outsider }} {{ teams.outsider }}
<font-awesome-icon <font-awesome-icon
class="outsider" class="outsider"
:icon="teams.outsider > 1 ? 'user-friends' : 'user'" :icon="teams.outsider > 1 ? 'user-friends' : 'user'"
/> />
</span> </span>
<span> <span>
{{ teams.minion }} {{ teams.minion }}
<font-awesome-icon <font-awesome-icon
class="minion" class="minion"
:icon="teams.minion > 1 ? 'user-friends' : 'user'" :icon="teams.minion > 1 ? 'user-friends' : 'user'"
/> />
</span> </span>
<span> <span>
{{ teams.demon }} {{ teams.demon }}
<font-awesome-icon <font-awesome-icon
class="demon" class="demon"
:icon="teams.demon > 1 ? 'user-friends' : 'user'" :icon="teams.demon > 1 ? 'user-friends' : 'user'"
/> />
</span> </span>
<span v-if="teams.traveler"> <span v-if="teams.traveler">
{{ teams.traveler }} {{ teams.traveler }}
<font-awesome-icon <font-awesome-icon
class="traveler" class="traveler"
:icon="teams.traveler > 1 ? 'user-friends' : 'user'" :icon="teams.traveler > 1 ? 'user-friends' : 'user'"
/> />
</span> </span>
<span v-if="grimoire.isNight"> <span v-if="grimoire.isNight">
Night phase Night phase
<font-awesome-icon :icon="['fas', 'cloud-moon']" /> <font-awesome-icon :icon="['fas', 'cloud-moon']" />
</span> </span>
</li> </li>
</ul> </ul>
</template> </template>
<script> <script>
@ -80,7 +109,7 @@ export default {
teams: function() { teams: function() {
const { players } = this.$store.state.players; const { players } = this.$store.state.players;
const nonTravelers = this.$store.getters["players/nonTravelers"]; const nonTravelers = this.$store.getters["players/nonTravelers"];
const alive = players.filter(player => player.isDead !== true).length; const alive = players.filter((player) => player.isDead !== true).length;
return { return {
...gameJSON[nonTravelers - 5], ...gameJSON[nonTravelers - 5],
traveler: players.length - nonTravelers, traveler: players.length - nonTravelers,
@ -88,13 +117,13 @@ export default {
votes: votes:
alive + alive +
players.filter( players.filter(
player => player.isDead === true && player.isVoteless !== true (player) => player.isDead === true && player.isVoteless !== true
).length ).length,
}; };
}, },
...mapState(["edition", "grimoire"]), ...mapState(["edition", "grimoire"]),
...mapState("players", ["players"]) ...mapState("players", ["players"]),
} },
}; };
</script> </script>
@ -178,3 +207,4 @@ export default {
} }
} }
</style> </style>

View file

@ -5,7 +5,7 @@
:class="{ :class="{
public: grimoire.isPublic, public: grimoire.isPublic,
spectator: session.isSpectator, spectator: session.isSpectator,
vote: session.nomination vote: session.nomination,
}" }"
> >
<ul class="circle" :class="['size-' + players.length]"> <ul class="circle" :class="['size-' + players.length]">
@ -18,7 +18,7 @@
from: Math.max(swap, move, nominate) === index, from: Math.max(swap, move, nominate) === index,
swap: swap > -1, swap: swap > -1,
move: move > -1, move: move > -1,
nominate: nominate > -1 nominate: nominate > -1,
}" }"
></Player> ></Player>
</ul> </ul>
@ -31,10 +31,16 @@
> >
<h3> <h3>
<span v-if="session.isSpectator">Other characters</span> <span v-if="session.isSpectator">Other characters</span>
<span v-else>Demon bluffs</span> <span v-else>Demon bluffs</span>
<font-awesome-icon icon="times-circle" @click.stop="toggleBluffs" />
<font-awesome-icon icon="plus-circle" @click.stop="toggleBluffs" /> <span @click.stop="toggleBluffs">
<font-awesome-icon
:icon="['fas', isBluffsOpen ? 'minus-circle' : 'plus-circle']"
/>
</span>
</h3> </h3>
<ul> <ul>
<li <li
v-for="index in bluffSize" v-for="index in bluffSize"
@ -49,9 +55,13 @@
<div class="fabled" :class="{ closed: !isFabledOpen }" v-if="fabled.length"> <div class="fabled" :class="{ closed: !isFabledOpen }" v-if="fabled.length">
<h3> <h3>
<span>Fabled</span> <span>Fabled</span>
<font-awesome-icon icon="times-circle" @click.stop="toggleFabled" /> <span @click.stop="toggleFabled">
<font-awesome-icon icon="plus-circle" @click.stop="toggleFabled" /> <font-awesome-icon
:icon="['fas', isFabledOpen ? 'minus-circle' : 'plus-circle']"
/>
</span>
</h3> </h3>
<ul> <ul>
<li <li
v-for="(role, index) in fabled" v-for="(role, index) in fabled"
@ -63,25 +73,30 @@
v-if="nightOrder.get(role).first && grimoire.isNightOrder" v-if="nightOrder.get(role).first && grimoire.isNightOrder"
> >
<em>{{ nightOrder.get(role).first }}.</em> <em>{{ nightOrder.get(role).first }}.</em>
<span v-if="role.firstNightReminder">{{
role.firstNightReminder <span v-if="role.firstNightReminder">
}}</span> {{ role.firstNightReminder }}
</span>
</div> </div>
<div <div
class="night-order other" class="night-order other"
v-if="nightOrder.get(role).other && grimoire.isNightOrder" v-if="nightOrder.get(role).other && grimoire.isNightOrder"
> >
<em>{{ nightOrder.get(role).other }}.</em> <em>{{ nightOrder.get(role).other }}.</em>
<span v-if="role.otherNightReminder">{{
role.otherNightReminder <span v-if="role.otherNightReminder">
}}</span> {{ role.otherNightReminder }}
</span>
</div> </div>
<Token :role="role"></Token> <Token :role="role"></Token>
</li> </li>
</ul> </ul>
</div> </div>
<ReminderModal :player-index="selectedPlayer"></ReminderModal> <ReminderModal :player-index="selectedPlayer"></ReminderModal>
<RoleModal :player-index="selectedPlayer"></RoleModal> <RoleModal :player-index="selectedPlayer"></RoleModal>
</div> </div>
</template> </template>
@ -98,12 +113,12 @@ export default {
Player, Player,
Token, Token,
RoleModal, RoleModal,
ReminderModal ReminderModal,
}, },
computed: { computed: {
...mapGetters({ nightOrder: "players/nightOrder" }), ...mapGetters({ nightOrder: "players/nightOrder" }),
...mapState(["grimoire", "roles", "session"]), ...mapState(["grimoire", "roles", "session"]),
...mapState("players", ["players", "bluffs", "fabled"]) ...mapState("players", ["players", "bluffs", "fabled"]),
}, },
data() { data() {
return { return {
@ -113,7 +128,7 @@ export default {
move: -1, move: -1,
nominate: -1, nominate: -1,
isBluffsOpen: true, isBluffsOpen: true,
isFabledOpen: true isFabledOpen: true,
}; };
}, },
methods: { methods: {
@ -170,7 +185,7 @@ export default {
// update nomination array if removed player has lower index // update nomination array if removed player has lower index
this.$store.commit("session/setNomination", [ this.$store.commit("session/setNomination", [
nomination[0] > playerIndex ? nomination[0] - 1 : nomination[0], nomination[0] > playerIndex ? nomination[0] - 1 : nomination[0],
nomination[1] > playerIndex ? nomination[1] - 1 : nomination[1] nomination[1] > playerIndex ? nomination[1] - 1 : nomination[1],
]); ]);
} }
} }
@ -186,7 +201,7 @@ export default {
if (this.session.nomination) { if (this.session.nomination) {
// update nomination if one of the involved players is swapped // update nomination if one of the involved players is swapped
const swapTo = this.players.indexOf(to); const swapTo = this.players.indexOf(to);
const updatedNomination = this.session.nomination.map(nom => { const updatedNomination = this.session.nomination.map((nom) => {
if (nom === this.swap) return swapTo; if (nom === this.swap) return swapTo;
if (nom === swapTo) return this.swap; if (nom === swapTo) return this.swap;
return nom; return nom;
@ -200,7 +215,7 @@ export default {
} }
this.$store.commit("players/swap", [ this.$store.commit("players/swap", [
this.swap, this.swap,
this.players.indexOf(to) this.players.indexOf(to),
]); ]);
this.cancel(); this.cancel();
} }
@ -214,7 +229,7 @@ export default {
if (this.session.nomination) { if (this.session.nomination) {
// update nomination if it is affected by the move // update nomination if it is affected by the move
const moveTo = this.players.indexOf(to); const moveTo = this.players.indexOf(to);
const updatedNomination = this.session.nomination.map(nom => { const updatedNomination = this.session.nomination.map((nom) => {
if (nom === this.move) return moveTo; if (nom === this.move) return moveTo;
if (nom > this.move && nom <= moveTo) return nom - 1; if (nom > this.move && nom <= moveTo) return nom - 1;
if (nom < this.move && nom >= moveTo) return nom + 1; if (nom < this.move && nom >= moveTo) return nom + 1;
@ -229,7 +244,7 @@ export default {
} }
this.$store.commit("players/move", [ this.$store.commit("players/move", [
this.move, this.move,
this.players.indexOf(to) this.players.indexOf(to),
]); ]);
this.cancel(); this.cancel();
} }
@ -251,8 +266,8 @@ export default {
this.move = -1; this.move = -1;
this.swap = -1; this.swap = -1;
this.nominate = -1; this.nominate = -1;
} },
} },
}; };
</script> </script>
@ -301,14 +316,14 @@ export default {
} }
@mixin on-circle($item-count) { @mixin on-circle($item-count) {
$angle: (360 / $item-count); $angle: calc(360 / $item-count);
$rot: 0; $rot: 0;
// rotation and tooltip placement // rotation and tooltip placement
@for $i from 1 through $item-count { @for $i from 1 through $item-count {
&:nth-child(#{$i}) { &:nth-child(#{$i}) {
transform: rotate($rot * 1deg); transform: rotate($rot * 1deg);
@if $i - 1 <= $item-count / 2 { @if $i - 1 <= calc($item-count / 2) {
// first half of players // first half of players
z-index: $item-count - $i + 1; z-index: $item-count - $i + 1;
// open menu on the left // open menu on the left
@ -372,15 +387,15 @@ export default {
} }
// move reminders closer to the sides of the circle // move reminders closer to the sides of the circle
$q: $item-count / 4; $q: calc($item-count / 4);
$x: $i - 1; $x: $i - 1;
@if $x < $q or ($x >= $item-count / 2 and $x < $q * 3) { @if $x < $q or ($x >= calc($item-count / 2) and $x < $q * 3) {
.player { .player {
margin-bottom: -10% + 20% * (1 - ($x % $q / $q)); margin-bottom: -10% + 20% * (1 - ($x % $q / $q));
} }
} @else { } @else {
.player { .player {
margin-bottom: -10% + 20% * ($x % $q / $q); margin-bottom: -10% + 20% * ($x % calc($q / $q));
} }
} }
} }

View file

@ -28,15 +28,14 @@
" "
> >
Time per player: Time per player:
<font-awesome-icon <span @mousedown.prevent="setVotingSpeed(-500)">
@mousedown.prevent="setVotingSpeed(-500)" <font-awesome-icon icon="minus-circle"
icon="minus-circle" /></span>
/>
{{ session.votingSpeed / 1000 }}s {{ session.votingSpeed / 1000 }}s
<font-awesome-icon <span @mousedown.prevent="setVotingSpeed(500)">
@mousedown.prevent="setVotingSpeed(500)" <font-awesome-icon icon="plus-circle" />
icon="plus-circle" </span>
/>
</div> </div>
<div class="button-group"> <div class="button-group">
<div <div
@ -71,9 +70,7 @@
> >
Mark for execution Mark for execution
</div> </div>
<div class="button" @click="removeMarked"> <div class="button" @click="removeMarked">Clear mark</div>
Clear mark
</div>
</div> </div>
</template> </template>
<template v-else-if="canVote"> <template v-else-if="canVote">
@ -97,9 +94,7 @@
</div> </div>
</div> </div>
</template> </template>
<div v-else-if="!player"> <div v-else-if="!player">Please claim a seat to vote.</div>
Please claim a seat to vote.
</div>
</div> </div>
<transition name="blur"> <transition name="blur">
<div <div
@ -183,7 +178,8 @@ export default {
...voters.slice(nomination + 1), ...voters.slice(nomination + 1),
...voters.slice(0, nomination + 1), ...voters.slice(0, nomination + 1),
]; ];
return (this.session.lockedVote return (
this.session.lockedVote
? reorder.slice(0, this.session.lockedVote - 1) ? reorder.slice(0, this.session.lockedVote - 1)
: reorder : reorder
).filter((n) => !!n); ).filter((n) => !!n);

View file

@ -10,17 +10,22 @@
@click.stop="" @click.stop=""
> >
<div class="top-right-buttons"> <div class="top-right-buttons">
<font-awesome-icon <div
@click="isMaximized = !isMaximized" @click="isMaximized = !isMaximized"
class="top-right-button" style="display: inline-block"
:icon="['fas', isMaximized ? 'window-minimize' : 'window-maximize']" >
/>
<font-awesome-icon <font-awesome-icon
@click="close"
class="top-right-button" class="top-right-button"
icon="times-circle" :icon="[
'fas',
isMaximized ? 'window-minimize' : 'window-maximize',
]"
/> />
</div> </div>
<div @click="close" style="display: inline-block">
<font-awesome-icon class="top-right-button" icon="times-circle" />
</div>
</div>
<div class="slot"> <div class="slot">
<slot></slot> <slot></slot>
</div> </div>
@ -31,16 +36,17 @@
<script> <script>
export default { export default {
emits: ["close"],
data: function () { data: function () {
return { return {
isMaximized: false isMaximized: false,
}; };
}, },
methods: { methods: {
close() { close() {
this.$emit("close"); this.$emit("close");
} },
} },
}; };
</script> </script>

View file

@ -4,12 +4,14 @@
@close="toggleModal('nightOrder')" @close="toggleModal('nightOrder')"
v-if="modals.nightOrder && roles.size" v-if="modals.nightOrder && roles.size"
> >
<div @click="toggleModal('reference')">
<font-awesome-icon <font-awesome-icon
@click="toggleModal('reference')"
icon="address-card" icon="address-card"
class="toggle" class="toggle"
title="Show Character Reference" title="Show Character Reference"
/> />
</div>
<h3> <h3>
Night Order Night Order
<font-awesome-icon icon="cloud-moon" /> <font-awesome-icon icon="cloud-moon" />
@ -47,7 +49,7 @@
: require('../../assets/icons/' + : require('../../assets/icons/' +
(role.imageAlt || role.id) + (role.imageAlt || role.id) +
'.png') '.png')
})` })`,
}" }"
></span> ></span>
<span class="reminder" v-if="role.firstNightReminder"> <span class="reminder" v-if="role.firstNightReminder">
@ -72,7 +74,7 @@
: require('../../assets/icons/' + : require('../../assets/icons/' +
(role.imageAlt || role.id) + (role.imageAlt || role.id) +
'.png') '.png')
})` })`,
}" }"
></span> ></span>
<span class="name"> <span class="name">
@ -104,7 +106,7 @@ import { mapMutations, mapState } from "vuex";
export default { export default {
components: { components: {
Modal Modal,
}, },
computed: { computed: {
rolesFirstNight: function () { rolesFirstNight: function () {
@ -117,33 +119,33 @@ export default {
name: "Minion info", name: "Minion info",
firstNight: 5, firstNight: 5,
team: "minion", team: "minion",
players: this.players.filter(p => p.role.team === "minion"), players: this.players.filter((p) => p.role.team === "minion"),
firstNightReminder: firstNightReminder:
"• If more than one Minion, they all make eye contact with each other. " + "• If more than one Minion, they all make eye contact with each other. " +
"• Show the “This is the Demon” card. Point to the Demon." "• Show the “This is the Demon” card. Point to the Demon.",
}, },
{ {
id: "evil", id: "evil",
name: "Demon info & bluffs", name: "Demon info & bluffs",
firstNight: 8, firstNight: 8,
team: "demon", team: "demon",
players: this.players.filter(p => p.role.team === "demon"), players: this.players.filter((p) => p.role.team === "demon"),
firstNightReminder: firstNightReminder:
"• Show the “These are your minions” card. Point to each Minion. " + "• Show the “These are your minions” card. Point to each Minion. " +
"• Show the “These characters are not in play” card. Show 3 character tokens of good " + "• Show the “These characters are not in play” card. Show 3 character tokens of good " +
"characters not in play." "characters not in play.",
} }
); );
} }
this.roles.forEach(role => { this.roles.forEach((role) => {
const players = this.players.filter(p => p.role.id === role.id); const players = this.players.filter((p) => p.role.id === role.id);
if (role.firstNight && (role.team !== "traveler" || players.length)) { if (role.firstNight && (role.team !== "traveler" || players.length)) {
rolesFirstNight.push(Object.assign({ players }, role)); rolesFirstNight.push(Object.assign({ players }, role));
} }
}); });
this.fabled this.fabled
.filter(({ firstNight }) => firstNight) .filter(({ firstNight }) => firstNight)
.forEach(fabled => { .forEach((fabled) => {
rolesFirstNight.push(Object.assign({ players: [] }, fabled)); rolesFirstNight.push(Object.assign({ players: [] }, fabled));
}); });
rolesFirstNight.sort((a, b) => a.firstNight - b.firstNight); rolesFirstNight.sort((a, b) => a.firstNight - b.firstNight);
@ -151,26 +153,26 @@ export default {
}, },
rolesOtherNight: function () { rolesOtherNight: function () {
const rolesOtherNight = []; const rolesOtherNight = [];
this.roles.forEach(role => { this.roles.forEach((role) => {
const players = this.players.filter(p => p.role.id === role.id); const players = this.players.filter((p) => p.role.id === role.id);
if (role.otherNight && (role.team !== "traveler" || players.length)) { if (role.otherNight && (role.team !== "traveler" || players.length)) {
rolesOtherNight.push(Object.assign({ players }, role)); rolesOtherNight.push(Object.assign({ players }, role));
} }
}); });
this.fabled this.fabled
.filter(({ otherNight }) => otherNight) .filter(({ otherNight }) => otherNight)
.forEach(fabled => { .forEach((fabled) => {
rolesOtherNight.push(Object.assign({ players: [] }, fabled)); rolesOtherNight.push(Object.assign({ players: [] }, fabled));
}); });
rolesOtherNight.sort((a, b) => a.otherNight - b.otherNight); rolesOtherNight.sort((a, b) => a.otherNight - b.otherNight);
return rolesOtherNight; return rolesOtherNight;
}, },
...mapState(["roles", "modals", "edition", "grimoire"]), ...mapState(["roles", "modals", "edition", "grimoire"]),
...mapState("players", ["players", "fabled"]) ...mapState("players", ["players", "fabled"]),
}, },
methods: { methods: {
...mapMutations(["toggleModal"]) ...mapMutations(["toggleModal"]),
} },
}; };
</script> </script>

View file

@ -4,12 +4,14 @@
@close="toggleModal('reference')" @close="toggleModal('reference')"
v-if="modals.reference && roles.size" v-if="modals.reference && roles.size"
> >
<div @click="toggleModal('nightOrder')">
<font-awesome-icon <font-awesome-icon
@click="toggleModal('nightOrder')"
icon="cloud-moon" icon="cloud-moon"
class="toggle" class="toggle"
title="Show Night Order" title="Show Night Order"
/> />
</div>
<h3> <h3>
Character Reference Character Reference
<font-awesome-icon icon="address-card" /> <font-awesome-icon icon="address-card" />
@ -35,7 +37,7 @@
: require('../../assets/icons/' + : require('../../assets/icons/' +
(role.imageAlt || role.id) + (role.imageAlt || role.id) +
'.png') '.png')
})` })`,
}" }"
></span> ></span>
<div class="role"> <div class="role">
@ -62,7 +64,7 @@
:style="{ :style="{
backgroundImage: `url(${require('../../assets/icons/' + backgroundImage: `url(${require('../../assets/icons/' +
jinx.first.id + jinx.first.id +
'.png')})` '.png')})`,
}" }"
></span> ></span>
<span <span
@ -70,7 +72,7 @@
:style="{ :style="{
backgroundImage: `url(${require('../../assets/icons/' + backgroundImage: `url(${require('../../assets/icons/' +
jinx.second.id + jinx.second.id +
'.png')})` '.png')})`,
}" }"
></span> ></span>
<div class="role"> <div class="role">
@ -93,7 +95,7 @@ import { mapMutations, mapState } from "vuex";
export default { export default {
components: { components: {
Modal Modal,
}, },
computed: { computed: {
/** /**
@ -102,14 +104,14 @@ export default {
*/ */
jinxed: function () { jinxed: function () {
const jinxed = []; const jinxed = [];
this.roles.forEach(role => { this.roles.forEach((role) => {
if (this.jinxes.get(role.id)) { if (this.jinxes.get(role.id)) {
this.jinxes.get(role.id).forEach((reason, second) => { this.jinxes.get(role.id).forEach((reason, second) => {
if (this.roles.get(second)) { if (this.roles.get(second)) {
jinxed.push({ jinxed.push({
first: role, first: role,
second: this.roles.get(second), second: this.roles.get(second),
reason reason,
}); });
} }
}); });
@ -119,7 +121,7 @@ export default {
}, },
rolesGrouped: function () { rolesGrouped: function () {
const rolesGrouped = {}; const rolesGrouped = {};
this.roles.forEach(role => { this.roles.forEach((role) => {
if (!rolesGrouped[role.team]) { if (!rolesGrouped[role.team]) {
rolesGrouped[role.team] = []; rolesGrouped[role.team] = [];
} }
@ -141,11 +143,11 @@ export default {
return players; return players;
}, },
...mapState(["roles", "modals", "edition", "grimoire", "jinxes"]), ...mapState(["roles", "modals", "edition", "grimoire", "jinxes"]),
...mapState("players", ["players"]) ...mapState("players", ["players"]),
}, },
methods: { methods: {
...mapMutations(["toggleModal"]) ...mapMutations(["toggleModal"]),
} },
}; };
</script> </script>

View file

@ -5,11 +5,13 @@
@close="toggleModal('roles')" @close="toggleModal('roles')"
> >
<h3>Select the characters for {{ nonTravelers }} players:</h3> <h3>Select the characters for {{ nonTravelers }} players:</h3>
<ul class="tokens" v-for="(teamRoles, team) in roleSelection" :key="team"> <ul class="tokens" v-for="(teamRoles, team) in roleSelection" :key="team">
<li class="count" :class="[team]"> <li class="count" :class="[team]">
{{ teamRoles.reduce((a, { selected }) => a + selected, 0) }} / {{ teamRoles.reduce((a, { selected }) => a + selected, 0) }} /
{{ game[nonTravelers - 5][team] }} {{ game[nonTravelers - 5][team] }}
</li> </li>
<li <li
v-for="role in teamRoles" v-for="role in teamRoles"
:class="[role.team, role.selected ? 'selected' : '']" :class="[role.team, role.selected ? 'selected' : '']"
@ -17,40 +19,51 @@
@click="role.selected = role.selected ? 0 : 1" @click="role.selected = role.selected ? 0 : 1"
> >
<Token :role="role" /> <Token :role="role" />
<font-awesome-icon icon="exclamation-triangle" v-if="role.setup" /> <font-awesome-icon icon="exclamation-triangle" v-if="role.setup" />
<div class="buttons" v-if="allowMultiple"> <div class="buttons" v-if="allowMultiple">
<font-awesome-icon <div @click.stop="role.selected--">
icon="minus-circle" <font-awesome-icon icon="minus-circle" />
@click.stop="role.selected--" </div>
/>
<span>{{ role.selected > 1 ? "x" + role.selected : "" }}</span> <span>{{ role.selected > 1 ? "x" + role.selected : "" }}</span>
<font-awesome-icon icon="plus-circle" @click.stop="role.selected++" />
<div @click.stop="role.selected++">
<font-awesome-icon icon="plus-circle" />
</div>
</div> </div>
</li> </li>
</ul> </ul>
<div class="warning" v-if="hasSelectedSetupRoles"> <div class="warning" v-if="hasSelectedSetupRoles">
<font-awesome-icon icon="exclamation-triangle" /> <font-awesome-icon icon="exclamation-triangle" />
<span> <span>
Warning: there are characters selected that modify the game setup! The Warning: there are characters selected that modify the game setup! The
randomizer does not account for these characters. randomizer does not account for these characters.
</span> </span>
</div> </div>
<label class="multiple" :class="{ checked: allowMultiple }"> <label class="multiple" :class="{ checked: allowMultiple }">
<font-awesome-icon :icon="allowMultiple ? 'check-square' : 'square'" /> <font-awesome-icon :icon="allowMultiple ? 'check-square' : 'square'" />
<input type="checkbox" name="allow-multiple" v-model="allowMultiple" /> <input type="checkbox" name="allow-multiple" v-model="allowMultiple" />
Allow duplicate characters Allow duplicate characters
</label> </label>
<div class="button-group"> <div class="button-group">
<div <div
class="button" class="button"
@click="assignRoles" @click="assignRoles"
:class="{ :class="{
disabled: selectedRoles > nonTravelers || !selectedRoles disabled: selectedRoles > nonTravelers || !selectedRoles,
}" }"
> >
<font-awesome-icon icon="people-arrows" /> <font-awesome-icon icon="people-arrows" />
Assign {{ selectedRoles }} characters randomly Assign {{ selectedRoles }} characters randomly
</div> </div>
<div class="button" @click="selectRandomRoles"> <div class="button" @click="selectRandomRoles">
<font-awesome-icon icon="random" /> <font-awesome-icon icon="random" />
Shuffle characters Shuffle characters
@ -65,53 +78,55 @@ import gameJSON from "./../../game";
import Token from "./../Token"; import Token from "./../Token";
import { mapGetters, mapMutations, mapState } from "vuex"; import { mapGetters, mapMutations, mapState } from "vuex";
const randomElement = arr => arr[Math.floor(Math.random() * arr.length)]; const randomElement = (arr) => arr[Math.floor(Math.random() * arr.length)];
export default { export default {
components: { components: {
Token, Token,
Modal Modal,
}, },
data: function () { data: function () {
return { return {
roleSelection: {}, roleSelection: {},
game: gameJSON, game: gameJSON,
allowMultiple: false allowMultiple: false,
}; };
}, },
computed: { computed: {
selectedRoles: function () { selectedRoles: function () {
return Object.values(this.roleSelection) return Object.values(this.roleSelection)
.map(roles => roles.reduce((a, { selected }) => a + selected, 0)) .map((roles) => roles.reduce((a, { selected }) => a + selected, 0))
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
}, },
hasSelectedSetupRoles: function () { hasSelectedSetupRoles: function () {
return Object.values(this.roleSelection).some(roles => return Object.values(this.roleSelection).some((roles) =>
roles.some(role => role.selected && role.setup) roles.some((role) => role.selected && role.setup)
); );
}, },
...mapState(["roles", "modals"]), ...mapState(["roles", "modals"]),
...mapState("players", ["players"]), ...mapState("players", ["players"]),
...mapGetters({ nonTravelers: "players/nonTravelers" }) ...mapGetters({ nonTravelers: "players/nonTravelers" }),
}, },
methods: { methods: {
selectRandomRoles() { selectRandomRoles() {
this.roleSelection = {}; this.roleSelection = {};
this.roles.forEach(role => { this.roles.forEach((role) => {
if (!this.roleSelection[role.team]) { if (!this.roleSelection[role.team]) {
this.$set(this.roleSelection, role.team, []); this.roleSelection[role.team] = [];
//this.$set(this.roleSelection, role.team, []);
} }
this.roleSelection[role.team].push(role); this.roleSelection[role.team].push(role);
this.$set(role, "selected", 0); role["selected"] = 0;
//this.$set(role, "selected", 0);
}); });
delete this.roleSelection["traveler"]; delete this.roleSelection["traveler"];
const playerCount = Math.max(5, this.nonTravelers); const playerCount = Math.max(5, this.nonTravelers);
const composition = this.game[playerCount - 5]; const composition = this.game[playerCount - 5];
Object.keys(composition).forEach(team => { Object.keys(composition).forEach((team) => {
for (let x = 0; x < composition[team]; x++) { for (let x = 0; x < composition[team]; x++) {
if (this.roleSelection[team]) { if (this.roleSelection[team]) {
const available = this.roleSelection[team].filter( const available = this.roleSelection[team].filter(
role => !role.selected (role) => !role.selected
); );
if (available.length) { if (available.length) {
randomElement(available).selected = 1; randomElement(available).selected = 1;
@ -124,30 +139,30 @@ export default {
if (this.selectedRoles <= this.nonTravelers && this.selectedRoles) { if (this.selectedRoles <= this.nonTravelers && this.selectedRoles) {
// generate list of selected roles and randomize it // generate list of selected roles and randomize it
const roles = Object.values(this.roleSelection) const roles = Object.values(this.roleSelection)
.map(roles => .map((roles) =>
roles roles
// duplicate roles selected more than once and filter unselected // duplicate roles selected more than once and filter unselected
.reduce((a, r) => [...a, ...Array(r.selected).fill(r)], []) .reduce((a, r) => [...a, ...Array(r.selected).fill(r)], [])
) )
// flatten into a single array // flatten into a single array
.reduce((a, b) => [...a, ...b], []) .reduce((a, b) => [...a, ...b], [])
.map(a => [Math.random(), a]) .map((a) => [Math.random(), a])
.sort((a, b) => a[0] - b[0]) .sort((a, b) => a[0] - b[0])
.map(a => a[1]); .map((a) => a[1]);
this.players.forEach(player => { this.players.forEach((player) => {
if (player.role.team !== "traveler" && roles.length) { if (player.role.team !== "traveler" && roles.length) {
const value = roles.pop(); const value = roles.pop();
this.$store.commit("players/update", { this.$store.commit("players/update", {
player, player,
property: "role", property: "role",
value value,
}); });
} }
}); });
this.$store.commit("toggleModal", "roles"); this.$store.commit("toggleModal", "roles");
} }
}, },
...mapMutations(["toggleModal"]) ...mapMutations(["toggleModal"]),
}, },
mounted: function () { mounted: function () {
if (!Object.keys(this.roleSelection).length) { if (!Object.keys(this.roleSelection).length) {
@ -157,8 +172,8 @@ export default {
watch: { watch: {
roles() { roles() {
this.selectRandomRoles(); this.selectRandomRoles();
} },
} },
}; };
</script> </script>
@ -178,7 +193,7 @@ ul.tokens {
.buttons { .buttons {
display: flex; display: flex;
} }
.fa-exclamation-triangle { .fa-triangle-exclamation {
display: block; display: block;
} }
} }
@ -201,7 +216,7 @@ ul.tokens {
transform: scale(1.2); transform: scale(1.2);
z-index: 10; z-index: 10;
} }
.fa-exclamation-triangle { .fa-triangle-exclamation {
position: absolute; position: absolute;
color: red; color: red;
filter: drop-shadow(0 0 3px black) drop-shadow(0 0 3px black); filter: drop-shadow(0 0 3px black) drop-shadow(0 0 3px black);

View file

@ -4,13 +4,14 @@
v-if="modals.voteHistory && (session.voteHistory || !session.isSpectator)" v-if="modals.voteHistory && (session.voteHistory || !session.isSpectator)"
@close="toggleModal('voteHistory')" @close="toggleModal('voteHistory')"
> >
<div @click="clearVoteHistory">
<font-awesome-icon <font-awesome-icon
@click="clearVoteHistory"
icon="trash-alt" icon="trash-alt"
class="clear" class="clear"
title="Clear vote history" title="Clear vote history"
v-if="session.isSpectator" v-if="session.isSpectator"
/> />
</div>
<h3>Vote history</h3> <h3>Vote history</h3>
@ -58,16 +59,8 @@
<tbody> <tbody>
<tr v-for="(vote, index) in session.voteHistory" :key="index"> <tr v-for="(vote, index) in session.voteHistory" :key="index">
<td> <td>
{{ {{ vote.timestamp.getHours().toString().padStart(2, "0") }}:{{
vote.timestamp vote.timestamp.getMinutes().toString().padStart(2, "0")
.getHours()
.toString()
.padStart(2, "0")
}}:{{
vote.timestamp
.getMinutes()
.toString()
.padStart(2, "0")
}} }}
</td> </td>
<td>{{ vote.nominator }}</td> <td>{{ vote.nominator }}</td>

View file

@ -1,5 +1,5 @@
import Vue from "vue"; import { createApp } from "vue";
import App from "./App"; import App from "./App.vue";
import store from "./store"; import store from "./store";
import { library } from "@fortawesome/fontawesome-svg-core"; import { library } from "@fortawesome/fontawesome-svg-core";
import { fas } from "@fortawesome/free-solid-svg-icons"; import { fas } from "@fortawesome/free-solid-svg-icons";
@ -52,17 +52,15 @@ const faIcons = [
"VolumeMute", "VolumeMute",
"VoteYea", "VoteYea",
"WindowMaximize", "WindowMaximize",
"WindowMinimize" "WindowMinimize",
]; ];
const fabIcons = ["Github", "Discord"]; const fabIcons = ["Github", "Discord"];
library.add( library.add(
...faIcons.map(i => fas["fa" + i]), ...faIcons.map((i) => fas["fa" + i]),
...fabIcons.map(i => fab["fa" + i]) ...fabIcons.map((i) => fab["fa" + i])
); );
Vue.component("font-awesome-icon", FontAwesomeIcon);
Vue.config.productionTip = false;
new Vue({ createApp(App)
render: h => h(App), .component("font-awesome-icon", FontAwesomeIcon)
store .use(store)
}).$mount("#app"); .mount("#app");

View file

@ -1,5 +1,20 @@
module.exports = { module.exports = {
// if the app is supposed to run on Github Pages in a subfolder, use the following config: publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
// publicPath: process.env.NODE_ENV === "production" ? "/townsquare/" : "/" chainWebpack: (config) => {
publicPath: process.env.NODE_ENV === "production" ? "/" : "/" config.resolve.alias.set("vue", "@vue/compat");
config.module
.rule("vue")
.use("vue-loader")
.tap((options) => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 3,
},
},
};
});
},
}; };