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

21975
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -7,7 +7,7 @@ const client = require("prom-client");
const register = new client.Registry();
// Add a default label which is added to all metrics
register.setDefaultLabels({
app: "clocktower-online"
app: "clocktower-online",
});
const PING_INTERVAL = 30000; // 30 seconds
@ -22,11 +22,9 @@ if (process.env.NODE_ENV !== "development") {
const server = https.createServer(options);
const wss = new WebSocket.Server({
...(process.env.NODE_ENV === "development" ? { port: 8081 } : { server }),
verifyClient: info =>
verifyClient: (info) =>
info.origin &&
!!info.origin.match(
/^https?:\/\/([^.]+\.github\.io|localhost|clocktower\.online|eddbra1nprivatetownsquare\.xyz)/i
)
!!info.origin.match(/^https?:\/\/(townsquare.clocktower.guru|localhost)/i),
});
function noop() {}
@ -48,14 +46,14 @@ const metrics = {
help: "Concurrent Players",
collect() {
this.set(wss.clients.size);
}
},
}),
channels_concurrent: new client.Gauge({
name: "channels_concurrent",
help: "Concurrent Channels",
collect() {
this.set(Object.keys(channels).length);
}
},
}),
channels_list: new client.Gauge({
name: "channel_players",
@ -66,35 +64,35 @@ const metrics = {
this.set(
{ name: channel },
channels[channel].filter(
ws =>
(ws) =>
ws &&
(ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING)
).length
);
}
}
},
}),
messages_incoming: new client.Counter({
name: "messages_incoming",
help: "Incoming messages"
help: "Incoming messages",
}),
messages_outgoing: new client.Counter({
name: "messages_outgoing",
help: "Outgoing messages"
help: "Outgoing messages",
}),
connection_terminated_host: new client.Counter({
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({
name: "connection_terminated_spam",
help: "Terminated connection due to message spam"
help: "Terminated connection due to message spam",
}),
connection_terminated_timeout: new client.Counter({
name: "connection_terminated_timeout",
help: "Terminated connection due to timeout"
})
help: "Terminated connection due to timeout",
}),
};
// register metrics
@ -113,7 +111,7 @@ wss.on("connection", function connection(ws, req) {
ws.playerId === "host" &&
channels[ws.channel] &&
channels[ws.channel].some(
client =>
(client) =>
client !== ws &&
client.readyState === WebSocket.OPEN &&
client.playerId === "host"
@ -149,11 +147,7 @@ wss.on("connection", function connection(ws, req) {
metrics.connection_terminated_spam.inc();
return;
}
const messageType = data
.toLocaleLowerCase()
.substr(1)
.split(",", 1)
.pop();
const messageType = data.toLocaleLowerCase().substr(1).split(",", 1).pop();
switch (messageType) {
case '"ping"':
// ping messages will only be sent host -> all or all -> host
@ -232,7 +226,7 @@ const interval = setInterval(function ping() {
if (
!channels[channel].length ||
!channels[channel].some(
ws =>
(ws) =>
ws &&
(ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING)
@ -255,6 +249,6 @@ if (process.env.NODE_ENV !== "development") {
server.listen(8080);
server.on("request", (req, res) => {
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>
<div
id="app"
id="townsquare-app"
@keyup="keyup"
tabindex="-1"
:class="{
night: grimoire.isNight,
static: grimoire.isStatic
static: grimoire.isStatic,
}"
:style="{
backgroundImage: grimoire.background
? `url('${grimoire.background}')`
: ''
: '',
}"
>
<video
@ -20,29 +20,42 @@
autoplay
loop
></video>
<div class="backdrop"></div>
<transition name="blur">
<Intro v-if="!players.length"></Intro>
<TownInfo v-if="players.length && !session.nomination"></TownInfo>
<Vote v-if="session.nomination"></Vote>
</transition>
<Intro v-if="!players.length"></Intro>
<TownInfo v-if="players.length && !session.nomination"></TownInfo>
<Vote v-if="session.nomination"></Vote>
<TownSquare></TownSquare>
<Menu ref="menu"></Menu>
<EditionModal />
<FabledModal />
<RolesModal />
<ReferenceModal />
<NightOrderModal />
<VoteHistoryModal />
<GameStateModal />
<Gradients />
<span id="version">v{{ version }}</span>
</div>
</template>
<script>
import { mapState } from "vuex";
import { version } from "../package.json";
import Package from "../package.json";
import TownSquare from "./components/TownSquare";
import TownInfo from "./components/TownInfo";
import Menu from "./components/Menu";
@ -71,15 +84,15 @@ export default {
Menu,
EditionModal,
RolesModal,
Gradients
Gradients,
},
computed: {
...mapState(["grimoire", "session"]),
...mapState("players", ["players"])
...mapState("players", ["players"]),
},
data() {
return {
version
version: Package.version,
};
},
methods: {
@ -124,8 +137,8 @@ export default {
case "escape":
this.$store.commit("toggleModal");
}
}
}
},
},
};
</script>
@ -198,6 +211,10 @@ ul {
}
#app {
height: 100%;
width: 100%;
}
#townsquare-app {
height: 100%;
background-position: center center;
background-size: cover;
@ -326,7 +343,7 @@ video#background {
}
/* Night phase backdrop */
#app > .backdrop {
#townsquare-app > .backdrop {
position: absolute;
left: 0;
right: 0;
@ -364,7 +381,7 @@ video#background {
}
}
#app.night > .backdrop {
#townsquare-app.night > .backdrop {
opacity: 0.5;
}
</style>

View file

@ -2,13 +2,11 @@
<div id="controls">
<span
class="nomlog-summary"
v-show="session.voteHistory.length && session.sessionId"
v-show="session.sessionId"
@click="toggleModal('voteHistory')"
:title="
`${session.voteHistory.length} recent ${
session.voteHistory.length == 1 ? 'nomination' : 'nominations'
}`
"
:title="`${session.voteHistory.length} recent ${
session.voteHistory.length == 1 ? 'nomination' : 'nominations'
}`"
>
<font-awesome-icon icon="book-dead" />
{{ session.voteHistory.length }}
@ -17,32 +15,39 @@
class="session"
:class="{
spectator: session.isSpectator,
reconnecting: session.isReconnecting
reconnecting: session.isReconnecting,
}"
v-if="session.sessionId"
@click="leaveSession"
:title="
`${session.playerCount} other players in this session${
session.ping ? ' (' + session.ping + 'ms latency)' : ''
}`
"
:title="`${session.playerCount} other players in this session${
session.ping ? ' (' + session.ping + 'ms latency)' : ''
}`"
>
<font-awesome-icon icon="broadcast-tower" />
{{ session.playerCount }}
</span>
<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>
<li class="tabs" :class="tab">
<font-awesome-icon icon="book-open" @click="tab = 'grimoire'" />
<font-awesome-icon icon="broadcast-tower" @click="tab = 'session'" />
<font-awesome-icon
icon="users"
v-if="!session.isSpectator"
@click="tab = 'players'"
/>
<font-awesome-icon icon="theater-masks" @click="tab = 'characters'" />
<font-awesome-icon icon="question" @click="tab = 'help'" />
<span @click="tab = 'grimoire'">
<font-awesome-icon icon="book-open" />
</span>
<span @click="tab = 'session'">
<font-awesome-icon icon="broadcast-tower" />
</span>
<span @click="tab = 'players'">
<font-awesome-icon icon="users" v-if="!session.isSpectator" />
</span>
<span @click="tab = 'characters'">
<font-awesome-icon icon="theater-masks" />
</span>
<span @click="tab = 'help'">
<font-awesome-icon icon="question" />
</span>
</li>
<template v-if="tab === 'grimoire'">
@ -64,7 +69,7 @@
<font-awesome-icon
:icon="[
'fas',
grimoire.isNightOrder ? 'check-square' : 'square'
grimoire.isNightOrder ? 'check-square' : 'square',
]"
/>
</em>
@ -72,20 +77,18 @@
<li v-if="players.length">
Zoom
<em>
<font-awesome-icon
@click="setZoom(grimoire.zoom - 1)"
icon="search-minus"
/>
<span @click="setZoom(grimoire.zoom - 1)" class="zoom">
<font-awesome-icon icon="search-minus" />
</span>
{{ Math.round(100 + grimoire.zoom * 10) }}%
<font-awesome-icon
@click="setZoom(grimoire.zoom + 1)"
icon="search-plus"
/>
<span @click="setZoom(grimoire.zoom + 1)" class="zoom">
<font-awesome-icon icon="search-plus" />
</span>
</em>
</li>
<li @click="setBackground">
Background image
<em><font-awesome-icon icon="image"/></em>
<em><font-awesome-icon icon="image" /></em>
</li>
<li v-if="!edition.isOfficial" @click="imageOptIn">
<small>Show Custom Images</small>
@ -93,7 +96,7 @@
><font-awesome-icon
:icon="[
'fas',
grimoire.isImageOptIn ? 'check-square' : 'square'
grimoire.isImageOptIn ? 'check-square' : 'square',
]"
/></em>
</li>
@ -118,9 +121,7 @@
<li class="headline" v-if="session.sessionId">
{{ session.isSpectator ? "Playing" : "Hosting" }}
</li>
<li class="headline" v-else>
Live Session
</li>
<li class="headline" v-else>Live Session</li>
<template v-if="!session.sessionId">
<li @click="hostSession">Host (Storyteller)<em>[H]</em></li>
<li @click="joinSession">Join (Player)<em>[J]</em></li>
@ -132,11 +133,11 @@
</li>
<li @click="copySessionUrl">
Copy player link
<em><font-awesome-icon icon="copy"/></em>
<em><font-awesome-icon icon="copy" /></em>
</li>
<li v-if="!session.isSpectator" @click="distributeRoles">
Send Characters
<em><font-awesome-icon icon="theater-masks"/></em>
<em><font-awesome-icon icon="theater-masks" /></em>
</li>
<li
v-if="session.voteHistory.length || !session.isSpectator"
@ -157,11 +158,11 @@
<li @click="addPlayer" v-if="players.length < 20">Add<em>[A]</em></li>
<li @click="randomizeSeatings" v-if="players.length > 2">
Randomize
<em><font-awesome-icon icon="dice"/></em>
<em><font-awesome-icon icon="dice" /></em>
</li>
<li @click="clearPlayers" v-if="players.length">
Remove all
<em><font-awesome-icon icon="trash-alt"/></em>
<em><font-awesome-icon icon="trash-alt" /></em>
</li>
</template>
@ -181,11 +182,11 @@
</li>
<li v-if="!session.isSpectator" @click="toggleModal('fabled')">
Add Fabled
<em><font-awesome-icon icon="dragon"/></em>
<em><font-awesome-icon icon="dragon" /></em>
</li>
<li @click="clearRoles" v-if="players.length">
Remove all
<em><font-awesome-icon icon="trash-alt"/></em>
<em><font-awesome-icon icon="trash-alt" /></em>
</li>
</template>
@ -202,7 +203,7 @@
</li>
<li @click="toggleModal('gameState')">
Game State JSON
<em><font-awesome-icon icon="file-code"/></em>
<em><font-awesome-icon icon="file-code" /></em>
</li>
<li>
<a href="https://discord.gg/Gd7ybwWbFk" target="_blank">
@ -236,11 +237,11 @@ import { mapMutations, mapState } from "vuex";
export default {
computed: {
...mapState(["grimoire", "session", "edition"]),
...mapState("players", ["players"])
...mapState("players", ["players"]),
},
data() {
return {
tab: "grimoire"
tab: "grimoire",
};
},
methods: {
@ -353,9 +354,9 @@ export default {
"toggleNightOrder",
"toggleStatic",
"setZoom",
"toggleModal"
])
}
"toggleModal",
]),
},
};
</script>
@ -389,7 +390,7 @@ export default {
}
}
> span {
span {
display: inline-block;
cursor: pointer;
z-index: 5;
@ -432,7 +433,7 @@ export default {
transform: rotate(0deg);
}
> svg {
span .menuCog {
cursor: pointer;
background: rgba(0, 0, 0, 0.5);
border: 3px solid black;
@ -442,6 +443,7 @@ export default {
border-bottom: 0;
border-radius: 10px 10px 0 0;
padding: 5px 5px 15px;
box-sizing: border-box;
}
a {
@ -473,10 +475,10 @@ export default {
justify-content: space-between;
min-height: 30px;
&.tabs {
tabs {
display: flex;
padding: 0;
svg {
span svg {
flex-grow: 1;
flex-shrink: 0;
height: 35px;
@ -492,11 +494,11 @@ export default {
border-right: 0;
}
}
&.grimoire .fa-book-open,
&.players .fa-users,
&.characters .fa-theater-masks,
&.session .fa-broadcast-tower,
&.help .fa-question {
&.grimoire span .fa-book-open,
&.players span .fa-users,
&.characters span .fa-theater-masks,
&.session span .fa-broadcast-tower,
&.help span .fa-question {
background: linear-gradient(
to bottom,
$townsfolk 0%,

View file

@ -47,41 +47,33 @@
/>
<!-- Overlay icons -->
<div class="overlay">
<font-awesome-icon
icon="hand-paper"
class="vote"
title="Hand UP"
@click="vote()"
/>
<font-awesome-icon
icon="times"
class="vote"
title="Hand DOWN"
@click="vote()"
/>
<font-awesome-icon
icon="times-circle"
class="cancel"
title="Cancel"
@click="cancel()"
/>
<div class="overlay" @click="vote()">
<font-awesome-icon icon="hand-paper" class="vote" title="Hand UP" />
</div>
<div class="overlay" @click="vote()">
<font-awesome-icon icon="times" class="vote" title="Hand DOWN" />
</div>
<div class="overlay" @click="cancel()">
<font-awesome-icon icon="times-circle" class="cancel" title="Cancel" />
</div>
<div class="overlay" @click="swapPlayer(player)">
<font-awesome-icon
icon="exchange-alt"
class="swap"
@click="swapPlayer(player)"
title="Swap seats with this player"
/>
</div>
<div class="overlay" @click="movePlayer(player)">
<font-awesome-icon
icon="redo-alt"
class="move"
@click="movePlayer(player)"
title="Move player to this seat"
/>
</div>
<div class="overlay" @click="nominatePlayer(player)">
<font-awesome-icon
icon="hand-point-right"
class="nominate"
@click="nominatePlayer(player)"
title="Nominate this player"
/>
</div>
@ -95,13 +87,14 @@
/>
<!-- Ghost vote icon -->
<font-awesome-icon
icon="vote-yea"
class="has-vote"
v-if="player.isDead && !player.isVoteless"
@click="updatePlayer('isVoteless', true)"
title="Ghost vote"
/>
<div @click="updatePlayer('isVoteless', true)">
<font-awesome-icon
icon="vote-yea"
class="has-vote"
v-if="player.isDead && !player.isVoteless"
title="Ghost vote"
/>
</div>
<!-- On block icon -->
<div class="marked">
@ -125,7 +118,7 @@
@click="changePronouns"
v-if="
!session.isSpectator ||
(session.isSpectator && player.id === session.playerId)
(session.isSpectator && player.id === session.playerId)
"
>
<font-awesome-icon icon="venus-mars" />Change Pronouns
@ -166,9 +159,7 @@
:class="{ disabled: player.id && player.id !== session.playerId }"
>
<font-awesome-icon icon="chair" />
<template v-if="!player.id">
Claim seat
</template>
<template v-if="!player.id"> Claim seat </template>
<template v-else-if="player.id === session.playerId">
Vacate seat
</template>
@ -226,10 +217,10 @@ export default {
...mapState("players", ["players"]),
...mapState(["grimoire", "session"]),
...mapGetters({ nightOrder: "players/nightOrder" }),
index: function() {
index: function () {
return this.players.indexOf(this.player);
},
voteLocked: function() {
voteLocked: function () {
const session = this.session;
const players = this.players.length;
if (!session.nomination) return false;
@ -237,7 +228,7 @@ export default {
(this.index - 1 + players - session.nomination[1]) % players;
return indexAdjusted < session.lockedVote - 1;
},
zoom: function() {
zoom: function () {
const unit = window.innerWidth > window.innerHeight ? "vh" : "vw";
if (this.players.length < 7) {
return { width: 18 + this.grimoire.zoom + unit };
@ -554,7 +545,7 @@ export default {
fill: url(#default);
}
&:hover *,
&.fa-hand-paper * {
&.fa-hand * {
fill: url(#demon);
}
&.fa-times * {
@ -564,14 +555,14 @@ export default {
}
// 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;
transform: scale(1);
}
// 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.vote-lock.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,
#townsquare.vote .player.vote-lock:not(.vote-yes) .overlay svg.vote.fa-times {
opacity: 1;
transform: scale(1);

View file

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

View file

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

View file

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

View file

@ -23,20 +23,19 @@
<div
v-if="
session.isVoteWatchingAllowed &&
!session.isVoteInProgress &&
session.lockedVote < 1
!session.isVoteInProgress &&
session.lockedVote < 1
"
>
Time per player:
<font-awesome-icon
@mousedown.prevent="setVotingSpeed(-500)"
icon="minus-circle"
/>
<span @mousedown.prevent="setVotingSpeed(-500)">
<font-awesome-icon icon="minus-circle"
/></span>
{{ session.votingSpeed / 1000 }}s
<font-awesome-icon
@mousedown.prevent="setVotingSpeed(500)"
icon="plus-circle"
/>
<span @mousedown.prevent="setVotingSpeed(500)">
<font-awesome-icon icon="plus-circle" />
</span>
</div>
<div class="button-group">
<div
@ -71,9 +70,7 @@
>
Mark for execution
</div>
<div class="button" @click="removeMarked">
Clear mark
</div>
<div class="button" @click="removeMarked">Clear mark</div>
</div>
</template>
<template v-else-if="canVote">
@ -97,9 +94,7 @@
</div>
</div>
</template>
<div v-else-if="!player">
Please claim a seat to vote.
</div>
<div v-else-if="!player">Please claim a seat to vote.</div>
</div>
<transition name="blur">
<div
@ -128,10 +123,10 @@ export default {
...mapState("players", ["players"]),
...mapState(["session", "grimoire"]),
...mapGetters({ alive: "players/alive" }),
nominator: function() {
nominator: function () {
return this.players[this.session.nomination[0]];
},
nominatorStyle: function() {
nominatorStyle: function () {
const players = this.players.length;
const nomination = this.session.nomination[0];
return {
@ -139,10 +134,10 @@ export default {
transitionDuration: this.session.votingSpeed - 100 + "ms",
};
},
nominee: function() {
nominee: function () {
return this.players[this.session.nomination[1]];
},
nomineeStyle: function() {
nomineeStyle: function () {
const players = this.players.length;
const nomination = this.session.nomination[1];
const lock = this.session.lockedVote;
@ -152,16 +147,16 @@ export default {
transitionDuration: this.session.votingSpeed - 100 + "ms",
};
},
player: function() {
player: function () {
return this.players.find((p) => p.id === this.session.playerId);
},
currentVote: function() {
currentVote: function () {
const index = this.players.findIndex(
(p) => p.id === this.session.playerId
);
return index >= 0 ? !!this.session.votes[index] : undefined;
},
canVote: function() {
canVote: function () {
if (!this.player) return false;
if (this.player.isVoteless && this.nominee.role.team !== "traveler")
return false;
@ -172,7 +167,7 @@ export default {
(index - 1 + players - session.nomination[1]) % players;
return indexAdjusted >= session.lockedVote - 1;
},
voters: function() {
voters: function () {
const nomination = this.session.nomination[1];
const voters = Array(this.players.length)
.fill("")
@ -183,9 +178,10 @@ export default {
...voters.slice(nomination + 1),
...voters.slice(0, nomination + 1),
];
return (this.session.lockedVote
? reorder.slice(0, this.session.lockedVote - 1)
: reorder
return (
this.session.lockedVote
? reorder.slice(0, this.session.lockedVote - 1)
: reorder
).filter((n) => !!n);
},
},

View file

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

View file

@ -4,12 +4,14 @@
@close="toggleModal('nightOrder')"
v-if="modals.nightOrder && roles.size"
>
<font-awesome-icon
@click="toggleModal('reference')"
icon="address-card"
class="toggle"
title="Show Character Reference"
/>
<div @click="toggleModal('reference')">
<font-awesome-icon
icon="address-card"
class="toggle"
title="Show Character Reference"
/>
</div>
<h3>
Night Order
<font-awesome-icon icon="cloud-moon" />
@ -47,7 +49,7 @@
: require('../../assets/icons/' +
(role.imageAlt || role.id) +
'.png')
})`
})`,
}"
></span>
<span class="reminder" v-if="role.firstNightReminder">
@ -72,7 +74,7 @@
: require('../../assets/icons/' +
(role.imageAlt || role.id) +
'.png')
})`
})`,
}"
></span>
<span class="name">
@ -104,10 +106,10 @@ import { mapMutations, mapState } from "vuex";
export default {
components: {
Modal
Modal,
},
computed: {
rolesFirstNight: function() {
rolesFirstNight: function () {
const rolesFirstNight = [];
// add minion / demon infos to night order sheet
if (this.players.length > 6) {
@ -117,60 +119,60 @@ export default {
name: "Minion info",
firstNight: 5,
team: "minion",
players: this.players.filter(p => p.role.team === "minion"),
players: this.players.filter((p) => p.role.team === "minion"),
firstNightReminder:
"• 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",
name: "Demon info & bluffs",
firstNight: 8,
team: "demon",
players: this.players.filter(p => p.role.team === "demon"),
players: this.players.filter((p) => p.role.team === "demon"),
firstNightReminder:
"• 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 " +
"characters not in play."
"characters not in play.",
}
);
}
this.roles.forEach(role => {
const players = this.players.filter(p => p.role.id === role.id);
this.roles.forEach((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
.filter(({ firstNight }) => firstNight)
.forEach(fabled => {
.forEach((fabled) => {
rolesFirstNight.push(Object.assign({ players: [] }, fabled));
});
rolesFirstNight.sort((a, b) => a.firstNight - b.firstNight);
return rolesFirstNight;
},
rolesOtherNight: function() {
rolesOtherNight: function () {
const rolesOtherNight = [];
this.roles.forEach(role => {
const players = this.players.filter(p => p.role.id === role.id);
this.roles.forEach((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
.filter(({ otherNight }) => otherNight)
.forEach(fabled => {
.forEach((fabled) => {
rolesOtherNight.push(Object.assign({ players: [] }, fabled));
});
rolesOtherNight.sort((a, b) => a.otherNight - b.otherNight);
return rolesOtherNight;
},
...mapState(["roles", "modals", "edition", "grimoire"]),
...mapState("players", ["players", "fabled"])
...mapState("players", ["players", "fabled"]),
},
methods: {
...mapMutations(["toggleModal"])
}
...mapMutations(["toggleModal"]),
},
};
</script>

View file

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

View file

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

View file

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

View file

@ -1,5 +1,20 @@
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" ? "/townsquare/" : "/"
publicPath: process.env.NODE_ENV === "production" ? "/" : "/"
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
chainWebpack: (config) => {
config.resolve.alias.set("vue", "@vue/compat");
config.module
.rule("vue")
.use("vue-loader")
.tap((options) => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 3,
},
},
};
});
},
};