mirror of https://github.com/bra1n/townsquare.git
516 lines
14 KiB
Vue
516 lines
14 KiB
Vue
<template>
|
|
<div id="controls">
|
|
<Screenshot ref="screenshot"></Screenshot>
|
|
<span
|
|
class="session"
|
|
:class="{
|
|
spectator: session.isSpectator,
|
|
reconnecting: session.isReconnecting
|
|
}"
|
|
v-if="session.sessionId"
|
|
@click="leaveSession"
|
|
:title="
|
|
`${session.playerCount} other players in this session${
|
|
session.ping ? ' (' + session.ping + 'ms latency)' : ''
|
|
}`
|
|
"
|
|
>
|
|
<font-awesome-icon icon="broadcast-tower" />
|
|
{{ session.playerCount }}
|
|
</span>
|
|
<span class="camera">
|
|
<font-awesome-icon
|
|
icon="camera"
|
|
@click="takeScreenshot()"
|
|
title="Take a screenshot"
|
|
:class="{ success: grimoire.isScreenshotSuccess }"
|
|
/>
|
|
</span>
|
|
<div class="menu" :class="{ open: grimoire.isMenuOpen }">
|
|
<font-awesome-icon icon="cog" @click="toggleMenu" />
|
|
<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'" />
|
|
</li>
|
|
|
|
<template v-if="tab === 'grimoire'">
|
|
<!-- Grimoire -->
|
|
<li class="headline">Grimoire</li>
|
|
<li @click="toggleGrimoire" v-if="players.length">
|
|
<template v-if="!grimoire.isPublic">Hide</template>
|
|
<template v-if="grimoire.isPublic">Show</template>
|
|
<em>[G]</em>
|
|
</li>
|
|
<li @click="toggleNight" v-if="!session.isSpectator">
|
|
<template v-if="!grimoire.isNight">Switch to Night</template>
|
|
<template v-if="grimoire.isNight">Switch to Day</template>
|
|
<em
|
|
><font-awesome-icon
|
|
:icon="['fas', grimoire.isNight ? 'sun' : 'cloud-moon']"
|
|
/></em>
|
|
</li>
|
|
<li @click="toggleNightOrder" v-if="players.length">
|
|
Night order
|
|
<em
|
|
><font-awesome-icon
|
|
:icon="[
|
|
'fas',
|
|
grimoire.isNightOrder ? 'check-square' : 'square'
|
|
]"
|
|
/></em>
|
|
</li>
|
|
<li v-if="players.length">
|
|
Zoom
|
|
<em>
|
|
<font-awesome-icon
|
|
@click="setZoom(grimoire.zoom - 1)"
|
|
icon="search-minus"
|
|
/>
|
|
{{ Math.round(100 + grimoire.zoom * 10) }}%
|
|
<font-awesome-icon
|
|
@click="setZoom(grimoire.zoom + 1)"
|
|
icon="search-plus"
|
|
/>
|
|
</em>
|
|
</li>
|
|
<li @click="setBackground">
|
|
Background image
|
|
<em><font-awesome-icon icon="image"/></em>
|
|
</li>
|
|
</template>
|
|
|
|
<template v-if="tab === 'session'">
|
|
<li class="headline" v-if="session.sessionId">
|
|
{{ session.isSpectator ? "Playing" : "Hosting" }}
|
|
</li>
|
|
<li class="headline" v-else>
|
|
Live Session
|
|
</li>
|
|
<li @click="hostSession" v-if="!session.sessionId">
|
|
Host (Storyteller)<em>[H]</em>
|
|
</li>
|
|
<li @click="joinSession" v-if="!session.sessionId">
|
|
Join (Player)<em>[J]</em>
|
|
</li>
|
|
<li v-if="session.sessionId && session.ping">
|
|
Delay to {{ session.isSpectator ? "host" : "players" }}
|
|
<em>{{ session.ping }}ms</em>
|
|
</li>
|
|
<li v-if="session.sessionId" @click="copySessionUrl">
|
|
Copy player link
|
|
<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>
|
|
</li>
|
|
<li
|
|
v-if="session.voteHistory.length"
|
|
@click="toggleModal('voteHistory')"
|
|
>
|
|
Nomination history
|
|
<em><font-awesome-icon icon="hand-paper"/></em>
|
|
</li>
|
|
<li @click="leaveSession" v-if="session.sessionId">
|
|
Leave Session
|
|
<em>{{ session.sessionId }}</em>
|
|
</li>
|
|
</template>
|
|
|
|
<template v-if="tab === 'players' && !session.isSpectator">
|
|
<!-- Users -->
|
|
<li class="headline">Players</li>
|
|
<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>
|
|
</li>
|
|
<li @click="clearPlayers" v-if="players.length">
|
|
Remove all
|
|
<em><font-awesome-icon icon="trash-alt"/></em>
|
|
</li>
|
|
</template>
|
|
|
|
<template v-if="tab === 'characters'">
|
|
<!-- Characters -->
|
|
<li class="headline">Characters</li>
|
|
<li v-if="!session.isSpectator" @click="toggleModal('edition')">
|
|
Select Edition
|
|
<em>[E]</em>
|
|
</li>
|
|
<li
|
|
@click="toggleModal('roles')"
|
|
v-if="!session.isSpectator && players.length > 4"
|
|
>
|
|
Choose & Assign
|
|
<em>[C]</em>
|
|
</li>
|
|
<li v-if="!session.isSpectator" @click="toggleModal('fabled')">
|
|
Add Fabled
|
|
<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>
|
|
</li>
|
|
</template>
|
|
|
|
<template v-if="tab === 'help'">
|
|
<!-- Help -->
|
|
<li class="headline">Help</li>
|
|
<li @click="toggleModal('reference')">
|
|
Reference Sheet
|
|
<em>[R]</em>
|
|
</li>
|
|
<li @click="toggleModal('nightOrder')">
|
|
Night Order Sheet
|
|
<em>[N]</em>
|
|
</li>
|
|
<li @click="toggleModal('gameState')">
|
|
Game State JSON
|
|
<em><font-awesome-icon icon="file-code"/></em>
|
|
</li>
|
|
<li>
|
|
<a href="https://discord.gg/Gd7ybwWbFk" target="_blank">
|
|
Join Discord
|
|
</a>
|
|
<em>
|
|
<a href="https://discord.gg/Gd7ybwWbFk" target="_blank">
|
|
<font-awesome-icon :icon="['fab', 'discord']" />
|
|
</a>
|
|
</em>
|
|
</li>
|
|
<li>
|
|
<a href="https://github.com/bra1n/townsquare" target="_blank">
|
|
Source code
|
|
</a>
|
|
<em>
|
|
<a href="https://github.com/bra1n/townsquare" target="_blank">
|
|
<font-awesome-icon :icon="['fab', 'github']" />
|
|
</a>
|
|
</em>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapMutations, mapState } from "vuex";
|
|
import Screenshot from "./Screenshot";
|
|
|
|
export default {
|
|
components: {
|
|
Screenshot
|
|
},
|
|
computed: {
|
|
...mapState(["grimoire", "session"]),
|
|
...mapState("players", ["players"])
|
|
},
|
|
data() {
|
|
return {
|
|
tab: "grimoire"
|
|
};
|
|
},
|
|
methods: {
|
|
takeScreenshot(dimensions = {}) {
|
|
this.$store.commit("updateScreenshot");
|
|
this.$refs.screenshot.capture(dimensions);
|
|
},
|
|
setBackground() {
|
|
const background = prompt("Enter custom background URL");
|
|
if (background || background === "") {
|
|
this.$store.commit("setBackground", background);
|
|
}
|
|
},
|
|
hostSession() {
|
|
const sessionId = prompt(
|
|
"Enter a channel number / name for your session",
|
|
Math.round(Math.random() * 10000)
|
|
);
|
|
if (sessionId) {
|
|
this.$store.commit("session/clearVoteHistory");
|
|
this.$store.commit("session/setSpectator", false);
|
|
this.$store.commit(
|
|
"session/setSessionId",
|
|
sessionId
|
|
.toLocaleLowerCase()
|
|
.replace(/[^0-9a-z]/g, "")
|
|
.substr(0, 10)
|
|
);
|
|
this.copySessionUrl();
|
|
}
|
|
},
|
|
copySessionUrl() {
|
|
// check for clipboard permissions
|
|
navigator.permissions
|
|
.query({ name: "clipboard-write" })
|
|
.then(({ state }) => {
|
|
if (state === "granted" || state === "prompt") {
|
|
const url = window.location.href.split("#")[0];
|
|
const link = url + "#" + this.session.sessionId;
|
|
navigator.clipboard.writeText(link);
|
|
}
|
|
});
|
|
},
|
|
distributeRoles() {
|
|
if (this.session.isSpectator) return;
|
|
const popup =
|
|
"Do you want to distribute assigned characters to all SEATED players?";
|
|
if (confirm(popup)) {
|
|
this.$store.commit("session/distributeRoles", true);
|
|
setTimeout(
|
|
(() => {
|
|
this.$store.commit("session/distributeRoles", false);
|
|
}).bind(this),
|
|
2000
|
|
);
|
|
}
|
|
},
|
|
joinSession() {
|
|
const sessionId = prompt(
|
|
"Enter the channel number / name of the session you want to join"
|
|
);
|
|
if (sessionId) {
|
|
this.$store.commit("session/clearVoteHistory");
|
|
this.$store.commit("session/setSpectator", true);
|
|
this.$store.commit("toggleGrimoire", false);
|
|
this.$store.commit(
|
|
"session/setSessionId",
|
|
sessionId
|
|
.toLocaleLowerCase()
|
|
.replace(/[^0-9a-z]/g, "")
|
|
.substr(0, 10)
|
|
);
|
|
}
|
|
},
|
|
leaveSession() {
|
|
if (confirm("Are you sure you want to leave the active live game?")) {
|
|
this.$store.commit("session/setSpectator", false);
|
|
this.$store.commit("session/setSessionId", "");
|
|
}
|
|
},
|
|
addPlayer() {
|
|
if (this.session.isSpectator) return;
|
|
if (this.players.length >= 20) return;
|
|
const name = prompt("Player name");
|
|
if (name) {
|
|
this.$store.commit("players/add", name);
|
|
}
|
|
},
|
|
randomizeSeatings() {
|
|
if (this.session.isSpectator) return;
|
|
if (confirm("Are you sure you want to randomize seatings?")) {
|
|
this.$store.dispatch("players/randomize");
|
|
}
|
|
},
|
|
clearPlayers() {
|
|
if (this.session.isSpectator) return;
|
|
if (confirm("Are you sure you want to remove all players?")) {
|
|
this.$store.commit("players/clear");
|
|
}
|
|
},
|
|
clearRoles() {
|
|
if (confirm("Are you sure you want to remove all player roles?")) {
|
|
this.$store.dispatch("players/clearRoles");
|
|
}
|
|
},
|
|
...mapMutations([
|
|
"toggleGrimoire",
|
|
"toggleMenu",
|
|
"toggleNight",
|
|
"toggleNightOrder",
|
|
"updateScreenshot",
|
|
"setZoom",
|
|
"toggleModal"
|
|
])
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
@import "../vars.scss";
|
|
|
|
// success animation
|
|
@keyframes greenToWhite {
|
|
from {
|
|
color: green;
|
|
}
|
|
to {
|
|
color: white;
|
|
}
|
|
}
|
|
|
|
// Controls
|
|
#controls {
|
|
position: absolute;
|
|
right: 3px;
|
|
top: 3px;
|
|
text-align: right;
|
|
padding-right: 50px;
|
|
z-index: 200;
|
|
|
|
#app.screenshot & {
|
|
display: none;
|
|
}
|
|
|
|
svg {
|
|
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 1));
|
|
&.success {
|
|
animation: greenToWhite 1s normal forwards;
|
|
animation-iteration-count: 1;
|
|
}
|
|
}
|
|
|
|
> span {
|
|
display: inline-block;
|
|
cursor: pointer;
|
|
z-index: 5;
|
|
margin-top: 7px;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
span.session {
|
|
color: $demon;
|
|
&.spectator {
|
|
color: $townsfolk;
|
|
}
|
|
&.reconnecting {
|
|
animation: blink 1s infinite;
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes blink {
|
|
50% {
|
|
opacity: 0.5;
|
|
color: gray;
|
|
}
|
|
}
|
|
|
|
.menu {
|
|
width: 220px;
|
|
transform-origin: 200px 22px;
|
|
transition: transform 500ms cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
|
transform: rotate(-90deg);
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
|
|
&.open {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
> svg {
|
|
cursor: pointer;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border: 3px solid black;
|
|
width: 40px;
|
|
height: 50px;
|
|
margin-bottom: -8px;
|
|
border-bottom: 0;
|
|
border-radius: 10px 10px 0 0;
|
|
padding: 5px 5px 15px;
|
|
}
|
|
|
|
a {
|
|
color: white;
|
|
text-decoration: none;
|
|
&:hover {
|
|
color: red;
|
|
}
|
|
}
|
|
|
|
ul {
|
|
display: flex;
|
|
list-style-type: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
box-shadow: 0 0 10px black;
|
|
border: 3px solid black;
|
|
border-radius: 10px 0 10px 10px;
|
|
|
|
li {
|
|
padding: 2px 5px;
|
|
color: white;
|
|
text-align: left;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
min-height: 30px;
|
|
|
|
&.tabs {
|
|
display: flex;
|
|
padding: 0;
|
|
svg {
|
|
flex-grow: 1;
|
|
flex-shrink: 0;
|
|
height: 35px;
|
|
border-bottom: 3px solid black;
|
|
border-right: 3px solid black;
|
|
padding: 5px 0;
|
|
cursor: pointer;
|
|
transition: color 250ms;
|
|
&:hover {
|
|
color: red;
|
|
}
|
|
&:last-child {
|
|
border-right: 0;
|
|
}
|
|
}
|
|
&.grimoire .fa-book-open,
|
|
&.players .fa-users,
|
|
&.characters .fa-theater-masks,
|
|
&.session .fa-broadcast-tower,
|
|
&.help .fa-question {
|
|
background: linear-gradient(
|
|
to bottom,
|
|
$townsfolk 0%,
|
|
rgba(0, 0, 0, 0.5) 100%
|
|
);
|
|
}
|
|
}
|
|
|
|
&:not(.headline):not(.tabs):hover {
|
|
cursor: pointer;
|
|
color: red;
|
|
}
|
|
|
|
em {
|
|
flex-grow: 0;
|
|
font-style: normal;
|
|
margin-left: 10px;
|
|
font-size: 80%;
|
|
}
|
|
}
|
|
|
|
.headline {
|
|
font-family: PiratesBay, sans-serif;
|
|
letter-spacing: 1px;
|
|
padding: 0 10px;
|
|
text-align: center;
|
|
justify-content: center;
|
|
background: linear-gradient(
|
|
to right,
|
|
$townsfolk 0%,
|
|
rgba(0, 0, 0, 0.5) 20%,
|
|
rgba(0, 0, 0, 0.5) 80%,
|
|
$demon 100%
|
|
);
|
|
}
|
|
}
|
|
}
|
|
</style>
|