mirror of https://github.com/bra1n/townsquare.git
Merge branch 'master' into voting
This commit is contained in:
commit
2aeb9f66c6
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "Bug: "
|
||||
labels: bug
|
||||
assignees: bra1n
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: "Feature Request: "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
|
@ -0,0 +1,76 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at steffen@baumgart.biz. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
|
@ -0,0 +1,81 @@
|
|||
# Contributing Guide
|
||||
|
||||
Hi! I'm really excited that you are interested in contributing to this project.
|
||||
Before submitting your contribution, please make sure to take a moment and read through the following guidelines:
|
||||
|
||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
- [Issue Reporting Guidelines](#issue-reporting-guidelines)
|
||||
- [Pull Request Guidelines](#pull-request-guidelines)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Project Structure](#project-structure)
|
||||
|
||||
## Issue Reporting Guidelines
|
||||
|
||||
- Report a new issue either through [GitHub](https://github.com/bra1n/townsquare/issues/new/choose) or [Email](mailto:steffen@baumgart.biz)
|
||||
- Please include with your issue the browser and operating system you're using as well as steps necessary to reproduce the bug, if any
|
||||
- Be patient, I'm working on this project in my spare time :-)
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
- The `master` branch is what is currently deployed to the website. All development should be done in dedicated branches.
|
||||
|
||||
- Work in the `src` folder and **DO NOT** checkin `dist` in the commits.
|
||||
|
||||
- It's OK to have multiple small commits as you work on the PR - GitHub will automatically squash it before merging.
|
||||
|
||||
- If adding a new feature:
|
||||
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first before working on it.
|
||||
- Feel free to write a test for it, but so far I didn't have time for that.
|
||||
|
||||
- If fixing a bug:
|
||||
- If you are resolving a special issue, add `(fix #xxxx[,#xxxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`.
|
||||
- Provide a detailed description of the bug in the PR. Live demo preferred.
|
||||
|
||||
## Development Setup
|
||||
|
||||
You will need [Node.js](http://nodejs.org) **version 8+** and a Chrome browser.
|
||||
|
||||
After cloning the repo, run:
|
||||
|
||||
``` bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
The development server can be started with `npm run serve`.
|
||||
|
||||
### Committing Changes
|
||||
|
||||
Commit messages should be verbose enough to allow someone else to follow your changes and should include references to issues that are being worked on.
|
||||
|
||||
### Commonly used NPM scripts
|
||||
|
||||
``` bash
|
||||
# watch and auto re-build dist/
|
||||
$ npm run serve
|
||||
|
||||
# build all dist files, including npm packages
|
||||
$ npm run build
|
||||
|
||||
# run linting scripts
|
||||
$ npm run link
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
- **`dist`**: contains built files for distribution.
|
||||
|
||||
- **`src`**: contains the source code. The codebase is written in ES2015.
|
||||
|
||||
- **`assets`**: contains all graphical assets like images, fonts, icons, etc.
|
||||
|
||||
- **`editions`**: edition logos and icons
|
||||
|
||||
- **`fonts`**: webfonts used on the page
|
||||
|
||||
- **`icons`**: character token icons
|
||||
|
||||
- **`components`**: the internal components used in the project
|
||||
|
||||
- **`modals`**: the modals have a separate subfolder
|
||||
|
||||
- **`store`**: the VueX data store and modules
|
18
README.md
18
README.md
|
@ -7,14 +7,26 @@ It is supposed to aid storytellers and allow them to quickly set up and capture
|
|||
|
||||
[You can try it online!](https://bra1n.github.io/townsquare)
|
||||
|
||||
WORK IN PROGRESS
|
||||
### Features
|
||||
|
||||
- Public Town Square and Storyteller Grimoire (toggle with **shortcut \[G\]**)
|
||||
- Supports custom script JSON generated by the [Script Tool](https://bloodontheclocktower.com/script)
|
||||
- Live Session for Storyteller / Players
|
||||
- Includes all 3 base editions and travelers
|
||||
- Night sheet and reminder text for each character ability to help storytellers
|
||||
- Many other customization options!
|
||||
|
||||
## [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
|
||||
## [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## Acknowledgements and Copyrights
|
||||
|
||||
* [Blood on the Clocktower](https://bloodontheclocktower.com/) images, names and abilities by [The Pandemonium Institute](https://www.thepandemoniuminstitute.com/)
|
||||
* [Blood on the Clocktower](https://bloodontheclocktower.com/) names and abilities by [The Pandemonium Institute](https://www.thepandemoniuminstitute.com/)
|
||||
* Night reminders and other auxiliary text written by [Ben Finney](http://bignose.whitetree.org/projects/botc/diy/)
|
||||
* Iconography by [Font Awesome](https://fontawesome.com/)
|
||||
* Background image by [Ryan Maloney](https://www.artstation.com/maloney94)
|
||||
* Webfonts by [Google Fonts](https://fonts.google.com/) and [Online Web Fonts](https://www.onlinewebfonts.com/)
|
||||
* All other images and icons are copyright to their respective owners
|
||||
|
||||
This project and its website are not affiliated with The Pandemonium Institute in any way.
|
||||
This project and its website are provided free of charge and not affiliated with The Pandemonium Institute in any way.
|
||||
|
|
20
src/App.vue
20
src/App.vue
|
@ -14,8 +14,9 @@
|
|||
<TownInfo v-if="players.length"></TownInfo>
|
||||
<TownSquare @screenshot="takeScreenshot"></TownSquare>
|
||||
<Menu ref="menu"></Menu>
|
||||
<EditionModal></EditionModal>
|
||||
<RolesModal></RolesModal>
|
||||
<EditionModal />
|
||||
<RolesModal />
|
||||
<ReferenceModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -27,9 +28,11 @@ import Menu from "./components/Menu";
|
|||
import RolesModal from "./components/modals/RolesModal";
|
||||
import EditionModal from "./components/modals/EditionModal";
|
||||
import Intro from "./components/Intro";
|
||||
import ReferenceModal from "./components/modals/ReferenceModal";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ReferenceModal,
|
||||
Intro,
|
||||
TownInfo,
|
||||
TownSquare,
|
||||
|
@ -46,7 +49,7 @@ export default {
|
|||
this.$refs.menu.takeScreenshot(dimensions);
|
||||
},
|
||||
keyup({ key }) {
|
||||
switch (key) {
|
||||
switch (key.toLocaleLowerCase()) {
|
||||
case "g":
|
||||
this.$store.commit("toggleGrimoire");
|
||||
break;
|
||||
|
@ -54,7 +57,7 @@ export default {
|
|||
this.$refs.menu.addPlayer();
|
||||
break;
|
||||
case "r":
|
||||
this.$refs.menu.randomizeSeatings();
|
||||
this.$store.commit("toggleModal", "reference");
|
||||
break;
|
||||
case "e":
|
||||
if (this.session.isSpectator) return;
|
||||
|
@ -86,6 +89,12 @@ export default {
|
|||
url("assets/fonts/papyrus.svg#PapyrusW01") format("svg"); /* iOS 4.1- */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: PiratesBay;
|
||||
src: url("assets/fonts/piratesbay.ttf");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-size: 1.2em;
|
||||
|
@ -121,6 +130,9 @@ h4,
|
|||
h5 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-family: PiratesBay, sans-serif;
|
||||
letter-spacing: 1px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ul {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 438 KiB |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="intro" >
|
||||
<div class="intro">
|
||||
<img src="static/apple-icon.png" alt="" />
|
||||
Welcome to the (unofficial)
|
||||
<b> Virtual Blood on the Clocktower Town Square</b>!<br />
|
||||
Please add more players through the
|
||||
<span class="button">
|
||||
<font-awesome-icon icon="cog" @click="toggleMenu" /> Menu
|
||||
<span class="button" @click="toggleMenu">
|
||||
<font-awesome-icon icon="cog" /> Menu
|
||||
</span>
|
||||
on the top right or by pressing <b>[A]</b>.<br />
|
||||
This project is free and open source and can be found on
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
<template>
|
||||
<div id="controls">
|
||||
<Screenshot ref="screenshot"></Screenshot>
|
||||
<font-awesome-icon
|
||||
@click="leaveSession"
|
||||
icon="broadcast-tower"
|
||||
<span
|
||||
class="session"
|
||||
:class="{ spectator: session.isSpectator }"
|
||||
v-if="session.sessionId"
|
||||
v-bind:class="{ spectator: session.isSpectator }"
|
||||
title="You're currently in a live game!"
|
||||
/>
|
||||
@click="leaveSession"
|
||||
:title="
|
||||
`You're currently in a live game with ${session.playerCount} other players!`
|
||||
"
|
||||
>
|
||||
<font-awesome-icon icon="broadcast-tower" />
|
||||
{{ session.playerCount }}
|
||||
</span>
|
||||
<span class="camera">
|
||||
<font-awesome-icon
|
||||
icon="camera"
|
||||
@click="takeScreenshot()"
|
||||
title="Take a screenshot"
|
||||
v-bind:class="{ success: grimoire.isScreenshotSuccess }"
|
||||
:class="{ success: grimoire.isScreenshotSuccess }"
|
||||
/>
|
||||
</span>
|
||||
<div class="menu" v-bind:class="{ open: grimoire.isMenuOpen }">
|
||||
<font-awesome-icon icon="cog" @click="toggleMenu" />
|
||||
<ul>
|
||||
|
@ -79,12 +86,18 @@
|
|||
<li @click="clearPlayers" v-if="players.length">
|
||||
Remove all
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- Characters -->
|
||||
<li class="headline">
|
||||
<font-awesome-icon icon="theater-masks" />
|
||||
Characters
|
||||
</li>
|
||||
<li @click="toggleModal('reference')">
|
||||
<em>[R]</em>
|
||||
Reference Sheet
|
||||
</li>
|
||||
<template v-if="!session.isSpectator">
|
||||
<li @click="toggleModal('edition')">
|
||||
<em>[E]</em>
|
||||
Select Edition
|
||||
|
@ -120,10 +133,10 @@ export default {
|
|||
this.$refs.screenshot.capture(dimensions);
|
||||
},
|
||||
setBackground() {
|
||||
this.$store.commit(
|
||||
"setBackground",
|
||||
prompt("Enter custom background URL")
|
||||
);
|
||||
const background = prompt("Enter custom background URL");
|
||||
if (background || background === "") {
|
||||
this.$store.commit("setBackground", background);
|
||||
}
|
||||
},
|
||||
hostSession() {
|
||||
const sessionId = prompt(
|
||||
|
@ -241,14 +254,15 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
> svg {
|
||||
> span {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
z-index: 5;
|
||||
margin-top: 10px;
|
||||
margin-top: 7px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
> .fa-broadcast-tower {
|
||||
.session {
|
||||
color: $demon;
|
||||
&.spectator {
|
||||
color: $townsfolk;
|
||||
|
@ -317,9 +331,10 @@ export default {
|
|||
}
|
||||
|
||||
.headline {
|
||||
font-family: PiratesBay, sans-serif;
|
||||
letter-spacing: 1px;
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
$townsfolk 0%,
|
||||
|
|
|
@ -37,14 +37,20 @@
|
|||
icon="times-circle"
|
||||
class="cancel"
|
||||
title="Cancel"
|
||||
@click="doSwap(true)"
|
||||
@click="cancel()"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
icon="exchange-alt"
|
||||
class="swap"
|
||||
@click="doSwap()"
|
||||
@click="swapPlayer(player)"
|
||||
title="Swap seats with this player"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
icon="redo-alt"
|
||||
class="move"
|
||||
@click="movePlayer(player)"
|
||||
title="Move player to this seat"
|
||||
/>
|
||||
|
||||
<font-awesome-icon
|
||||
icon="vote-yea"
|
||||
|
@ -65,14 +71,17 @@
|
|||
<transition name="fold">
|
||||
<ul class="menu" v-if="isMenuOpen && !session.isSpectator">
|
||||
<li @click="changeName">
|
||||
<font-awesome-icon icon="user-edit" />
|
||||
Rename
|
||||
<font-awesome-icon icon="user-edit" />Rename
|
||||
</li>
|
||||
<!--<li @click="nomination">
|
||||
<font-awesome-icon icon="hand-point-right" />
|
||||
Nomination
|
||||
</li>-->
|
||||
<li @click="initSwap">
|
||||
<li @click="movePlayer()">
|
||||
<font-awesome-icon icon="redo-alt" />
|
||||
Move player
|
||||
</li>
|
||||
<li @click="swapPlayer()">
|
||||
<font-awesome-icon icon="exchange-alt" />
|
||||
Swap seats
|
||||
</li>
|
||||
|
@ -182,12 +191,16 @@ export default {
|
|||
value
|
||||
});
|
||||
},
|
||||
initSwap() {
|
||||
swapPlayer(player) {
|
||||
this.isMenuOpen = false;
|
||||
this.$emit("swap-seats");
|
||||
this.$emit("swap-player", player);
|
||||
},
|
||||
doSwap(cancel) {
|
||||
this.$emit("swap-seats", cancel ? false : this.player);
|
||||
movePlayer(player) {
|
||||
this.isMenuOpen = false;
|
||||
this.$emit("move-player", player);
|
||||
},
|
||||
cancel() {
|
||||
this.$emit("cancel");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -243,6 +256,10 @@ export default {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
#townsquare.spectator & {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#townsquare:not(.spectator) &:hover:before {
|
||||
opacity: 0.5;
|
||||
top: -10px;
|
||||
|
@ -349,6 +366,7 @@ export default {
|
|||
z-index: 2;
|
||||
cursor: pointer;
|
||||
&.swap,
|
||||
&.move,
|
||||
&.cancel {
|
||||
top: 9%;
|
||||
left: 20%;
|
||||
|
@ -364,13 +382,14 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
li.swap-from .player > svg.cancel {
|
||||
li.from .player > svg.cancel {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
li.swap:not(.swap-from) .player > svg.swap {
|
||||
li.swap:not(.from) .player > svg.swap,
|
||||
li.move:not(.from) .player > svg.move {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
pointer-events: all;
|
||||
|
@ -603,7 +622,7 @@ li.swap:not(.swap-from) .player > svg.swap {
|
|||
display: block;
|
||||
margin: 5px ($token / -4) 0;
|
||||
text-align: center;
|
||||
padding: ($token * 0.3 + 2px) 5px 0;
|
||||
padding: ($token * 0.3 + 5px) 5px 0;
|
||||
border-radius: 50%;
|
||||
line-height: 90%;
|
||||
border: 3px solid black;
|
||||
|
@ -639,6 +658,19 @@ li.swap:not(.swap-from) .player > svg.swap {
|
|||
}
|
||||
}
|
||||
|
||||
&.custom {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
font-size: 70%;
|
||||
word-break: break-word;
|
||||
.icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
@ -16,11 +16,15 @@
|
|||
@add-reminder="openReminderModal(index)"
|
||||
@set-role="openRoleModal(index)"
|
||||
@remove-player="removePlayer(index)"
|
||||
@swap-seats="swapSeats(index, $event)"
|
||||
@cancel="cancel(index)"
|
||||
@swap-player="swapPlayer(index, $event)"
|
||||
@move-player="movePlayer(index, $event)"
|
||||
@screenshot="$emit('screenshot', $event)"
|
||||
v-bind:class="{
|
||||
'swap-from': swapFrom === index,
|
||||
swap: swapFrom > -1
|
||||
from: Math.max(swap, move, nominate) === index,
|
||||
swap: swap > -1,
|
||||
move: move > -1,
|
||||
nominate: nominate > -1
|
||||
}"
|
||||
></Player>
|
||||
</ul>
|
||||
|
@ -66,7 +70,9 @@ export default {
|
|||
return {
|
||||
selectedPlayer: 0,
|
||||
bluffs: 3,
|
||||
swapFrom: -1
|
||||
swap: -1,
|
||||
move: -1,
|
||||
nominate: -1
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -80,7 +86,8 @@ export default {
|
|||
},
|
||||
openRoleModal(playerIndex) {
|
||||
const player = this.players[playerIndex];
|
||||
if (this.session.isSpectator && player.role.team === "traveler") return;
|
||||
if (this.session.isSpectator && player && player.role.team === "traveler")
|
||||
return;
|
||||
this.selectedPlayer = playerIndex;
|
||||
this.$store.commit("toggleModal", "role");
|
||||
},
|
||||
|
@ -94,18 +101,34 @@ export default {
|
|||
this.$store.commit("players/remove", playerIndex);
|
||||
}
|
||||
},
|
||||
swapSeats(from, to) {
|
||||
swapPlayer(from, to) {
|
||||
if (to === undefined) {
|
||||
this.swapFrom = from;
|
||||
} else if (to === false) {
|
||||
this.swapFrom = -1;
|
||||
this.cancel();
|
||||
this.swap = from;
|
||||
} else {
|
||||
this.$store.commit("players/swap", [
|
||||
this.swapFrom,
|
||||
this.swap,
|
||||
this.players.indexOf(to)
|
||||
]);
|
||||
this.swapFrom = -1;
|
||||
this.cancel();
|
||||
}
|
||||
},
|
||||
movePlayer(from, to) {
|
||||
if (to === undefined) {
|
||||
this.cancel();
|
||||
this.move = from;
|
||||
} else {
|
||||
this.$store.commit("players/move", [
|
||||
this.move,
|
||||
this.players.indexOf(to)
|
||||
]);
|
||||
this.cancel();
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.move = -1;
|
||||
this.swap = -1;
|
||||
this.nominate = -1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
v-show="modals.edition"
|
||||
@close="toggleModal('edition')"
|
||||
>
|
||||
<div v-if="!isCustom">
|
||||
<h3>Select an edition:</h3>
|
||||
<ul class="editions">
|
||||
<li
|
||||
|
@ -15,7 +16,48 @@
|
|||
>
|
||||
{{ edition.name }}
|
||||
</li>
|
||||
<li class="edition edition-custom" @click="isCustom = true">
|
||||
Custom Script
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="custom" v-else>
|
||||
<h3>Upload a custom script</h3>
|
||||
To play with a custom script, you need to select the characters you want
|
||||
to play with in the official
|
||||
<a href="https://bloodontheclocktower.com/script-tool/" target="_blank"
|
||||
>Script Tool</a
|
||||
>
|
||||
and then upload the generated "custom-list.json" either directly here or
|
||||
provide a URL to such a hosted JSON file.<br />
|
||||
<h3>Some custom Teensyville scripts:</h3>
|
||||
<ul class="scripts">
|
||||
<li
|
||||
v-for="(script, index) in scripts"
|
||||
v-bind:key="index"
|
||||
@click="handleURL(script[1])"
|
||||
>
|
||||
{{ script[0] }}
|
||||
</li>
|
||||
</ul>
|
||||
<input
|
||||
type="file"
|
||||
ref="upload"
|
||||
accept="application/json"
|
||||
@change="handleUpload"
|
||||
/>
|
||||
<div class="button-group">
|
||||
<div class="button" @click="openUpload">
|
||||
<font-awesome-icon icon="file-upload" /> Upload JSON
|
||||
</div>
|
||||
<div class="button" @click="promptURL">
|
||||
<font-awesome-icon icon="link" /> Enter URL
|
||||
</div>
|
||||
<div class="button" @click="isCustom = false">
|
||||
<font-awesome-icon icon="undo" /> Back
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
|
@ -30,11 +72,61 @@ export default {
|
|||
},
|
||||
data: function() {
|
||||
return {
|
||||
editions: editionJSON
|
||||
editions: editionJSON,
|
||||
isCustom: false,
|
||||
scripts: [
|
||||
[
|
||||
"On Thin Ice",
|
||||
"https://gist.githubusercontent.com/bra1n/8dacd9f2abc6f428331ea1213ab153f5/raw/9758aff4b59965dc7a094db549d950be5a26b571/custom-script.json"
|
||||
],
|
||||
[
|
||||
"Race To The Bottom",
|
||||
"https://gist.githubusercontent.com/bra1n/63e1354cb3dc9d4032bcd0623dc48888/raw/5be4df8386ec61e3a98c32be77f8cac3f8414379/custom-script.json"
|
||||
],
|
||||
[
|
||||
"Frankenstein's Mayor by Ted",
|
||||
"https://gist.githubusercontent.com/bra1n/32c52b422cc01b934a4291eeb81dbcee/raw/3ca5a043c41141ac40667dc15097deb327263268/Frankensteins_Mayor_by_Ted.json"
|
||||
]
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: mapState(["modals"]),
|
||||
methods: mapMutations(["toggleModal", "setEdition"])
|
||||
methods: {
|
||||
openUpload() {
|
||||
this.$refs.upload.click();
|
||||
},
|
||||
handleUpload() {
|
||||
const file = this.$refs.upload.files[0];
|
||||
if (file && file.size) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () => {
|
||||
this.parseScript(JSON.parse(reader.result));
|
||||
});
|
||||
reader.readAsText(file);
|
||||
}
|
||||
},
|
||||
promptURL() {
|
||||
const url = prompt("Enter URL to a custom-script.json file");
|
||||
if (url) {
|
||||
this.handleURL(url);
|
||||
}
|
||||
},
|
||||
async handleURL(url) {
|
||||
const res = await fetch(url);
|
||||
if (res && res.json) {
|
||||
const script = await res.json();
|
||||
this.parseScript(script);
|
||||
}
|
||||
},
|
||||
parseScript(script) {
|
||||
if (!script || !script.length) return;
|
||||
const roles = script.map(({ id }) => id.replace(/[^a-z]/g, ""));
|
||||
this.$store.commit("setRoles", roles);
|
||||
this.$store.commit("setEdition", "custom");
|
||||
this.isCustom = false;
|
||||
},
|
||||
...mapMutations(["toggleModal", "setEdition"])
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -63,15 +155,16 @@ export default {
|
|||
}
|
||||
|
||||
ul.editions .edition {
|
||||
font-family: PiratesBay, sans-serif;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
padding-top: 100px;
|
||||
background-position: center center;
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
width: 200px;
|
||||
width: 30%;
|
||||
margin: 5px;
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000,
|
||||
1px 1px 0 #000, 0 0 5px rgba(0, 0, 0, 0.75);
|
||||
cursor: pointer;
|
||||
|
@ -79,4 +172,23 @@ ul.editions .edition {
|
|||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.custom {
|
||||
text-align: center;
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
.scripts {
|
||||
list-style-type: disc;
|
||||
font-size: 120%;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 50%;
|
||||
text-align: left;
|
||||
margin: 10px auto;
|
||||
li:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -48,6 +48,11 @@ export default {
|
|||
flex-direction: column;
|
||||
max-width: 60%;
|
||||
|
||||
.characters & {
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
<template>
|
||||
<Modal
|
||||
class="characters"
|
||||
v-show="modals.reference"
|
||||
@close="toggleModal('reference')"
|
||||
v-if="roles.size"
|
||||
>
|
||||
<h3>{{ editionName }} Character Reference</h3>
|
||||
<ul class="legend">
|
||||
<li>
|
||||
<span class="name">Name</span>
|
||||
<span class="icon">Icon</span>
|
||||
<span class="ability">Ability</span>
|
||||
<span class="player" v-if="Object.keys(playersByRole).length">
|
||||
Player
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-for="(teamRoles, team) in rolesGrouped" :key="team" :class="[team]">
|
||||
<h4>{{ team }}</h4>
|
||||
<ul>
|
||||
<li v-for="role in teamRoles" :class="[team]" :key="role.id">
|
||||
<span class="name">{{ role.name }}</span>
|
||||
<span
|
||||
class="icon"
|
||||
v-if="role.id"
|
||||
v-bind:style="{
|
||||
backgroundImage: `url(${require('../../assets/icons/' +
|
||||
role.id +
|
||||
'.png')})`
|
||||
}"
|
||||
></span>
|
||||
<span class="ability">{{ role.ability }}</span>
|
||||
<span class="player" v-if="Object.keys(playersByRole).length">{{
|
||||
playersByRole[role.id] ? playersByRole[role.id].join(", ") : ""
|
||||
}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "./Modal";
|
||||
import editionJSON from "./../../editions.json";
|
||||
import { mapMutations, mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
roleSelection: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
editionName: function() {
|
||||
const edition = editionJSON.find(({ id }) => id === this.edition);
|
||||
return edition ? edition.name : "Custom script";
|
||||
},
|
||||
rolesGrouped: function() {
|
||||
const rolesGrouped = {};
|
||||
this.roles.forEach(role => {
|
||||
if (!rolesGrouped[role.team]) {
|
||||
rolesGrouped[role.team] = [];
|
||||
}
|
||||
rolesGrouped[role.team].push(role);
|
||||
});
|
||||
delete rolesGrouped["traveler"];
|
||||
return rolesGrouped;
|
||||
},
|
||||
playersByRole: function() {
|
||||
const players = {};
|
||||
this.players.forEach(({ name, role }) => {
|
||||
if (role && role.id && role.team !== "traveler") {
|
||||
if (!players[role.id]) {
|
||||
players[role.id] = [];
|
||||
}
|
||||
players[role.id].push(name);
|
||||
}
|
||||
});
|
||||
return players;
|
||||
},
|
||||
...mapState(["roles", "modals", "edition"]),
|
||||
...mapState("players", ["players"])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["toggleModal"])
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../vars.scss";
|
||||
|
||||
h4 {
|
||||
text-transform: capitalize;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
&:before,
|
||||
&:after {
|
||||
content: " ";
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
&:before {
|
||||
margin-right: 15px;
|
||||
}
|
||||
&:after {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.townsfolk {
|
||||
.name,
|
||||
.player,
|
||||
h4 {
|
||||
color: $townsfolk;
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $townsfolk;
|
||||
}
|
||||
}
|
||||
}
|
||||
.outsider {
|
||||
.name,
|
||||
.player,
|
||||
h4 {
|
||||
color: $outsider;
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $outsider;
|
||||
}
|
||||
}
|
||||
}
|
||||
.minion {
|
||||
.name,
|
||||
.player,
|
||||
h4 {
|
||||
color: $minion;
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $minion;
|
||||
}
|
||||
}
|
||||
}
|
||||
.demon {
|
||||
.name,
|
||||
.player,
|
||||
h4 {
|
||||
color: $demon;
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $demon;
|
||||
}
|
||||
}
|
||||
}
|
||||
ul {
|
||||
li {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
/*background: linear-gradient(0deg, #ffffff0f, transparent);*/
|
||||
border-radius: 10px;
|
||||
.icon {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
background-size: cover;
|
||||
background-position: 0 -5px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
margin: 0 10px;
|
||||
text-align: center;
|
||||
border-left: 1px solid #ffffff1f;
|
||||
border-right: 1px solid #ffffff1f;
|
||||
}
|
||||
.name {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 150px;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
font-family: "Papyrus", sans-serif;
|
||||
font-size: 110%;
|
||||
}
|
||||
.player {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
text-align: right;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.ability {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
&.legend {
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
margin-top: 10px;
|
||||
li span {
|
||||
background: none;
|
||||
height: auto;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -9,13 +9,13 @@
|
|||
<li
|
||||
v-for="reminder in availableReminders"
|
||||
class="reminder"
|
||||
v-bind:class="[reminder.role]"
|
||||
v-bind:key="reminder.role + ' ' + reminder.name"
|
||||
:class="[reminder.role]"
|
||||
:key="reminder.role + ' ' + reminder.name"
|
||||
@click="addReminder(reminder)"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
v-bind:style="{
|
||||
:style="{
|
||||
backgroundImage: `url(${require('../../assets/icons/' +
|
||||
reminder.role +
|
||||
'.png')})`
|
||||
|
@ -48,6 +48,7 @@ export default {
|
|||
});
|
||||
reminders.push({ role: "good", name: "Good" });
|
||||
reminders.push({ role: "evil", name: "Evil" });
|
||||
reminders.push({ role: "custom", name: "Custom note" });
|
||||
return reminders;
|
||||
},
|
||||
...mapState(["modals"]),
|
||||
|
@ -56,7 +57,14 @@ export default {
|
|||
methods: {
|
||||
addReminder(reminder) {
|
||||
const player = this.$store.state.players.players[this.playerIndex];
|
||||
const value = [...player.reminders, reminder];
|
||||
let value;
|
||||
if (reminder.role === "custom") {
|
||||
const name = prompt("Add a custom reminder note");
|
||||
if (!name) return;
|
||||
value = [...player.reminders, { role: "custom", name }];
|
||||
} else {
|
||||
value = [...player.reminders, reminder];
|
||||
}
|
||||
this.$store.commit("players/update", {
|
||||
player,
|
||||
property: "reminders",
|
||||
|
@ -85,7 +93,7 @@ ul.reminders .reminder {
|
|||
border: 3px solid black;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
padding: 65px 9px 0;
|
||||
padding: 70px 9px 0;
|
||||
line-height: 100%;
|
||||
transition: transform 500ms ease;
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export default {
|
|||
availableRoles.push({});
|
||||
return availableRoles;
|
||||
},
|
||||
...mapState(["modals", "roles"]),
|
||||
...mapState(["modals", "roles", "session"]),
|
||||
...mapState("players", ["players"])
|
||||
},
|
||||
methods: {
|
||||
|
@ -61,6 +61,7 @@ export default {
|
|||
role
|
||||
});
|
||||
} else {
|
||||
if (this.session.isSpectator && role.team === "traveler") return;
|
||||
// assign to player
|
||||
const player = this.$store.state.players.players[this.playerIndex];
|
||||
this.$store.commit("players/update", {
|
||||
|
@ -105,4 +106,8 @@ ul.tokens li {
|
|||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
#townsquare.spectator ul.tokens li.traveler {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
77
src/main.js
77
src/main.js
|
@ -2,57 +2,38 @@ import Vue from "vue";
|
|||
import App from "./App";
|
||||
import store from "./store";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faBookOpen,
|
||||
faCamera,
|
||||
faCog,
|
||||
faHeartbeat,
|
||||
faSearchMinus,
|
||||
faSearchPlus,
|
||||
faTheaterMasks,
|
||||
faTimesCircle,
|
||||
faUser,
|
||||
faUserEdit,
|
||||
faUserFriends,
|
||||
faUsers,
|
||||
faVoteYea,
|
||||
faCheckSquare,
|
||||
faSquare,
|
||||
faRandom,
|
||||
faPeopleArrows,
|
||||
faBroadcastTower,
|
||||
faCopy,
|
||||
faExchangeAlt,
|
||||
faHandPointRight
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { fas } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
library.add(
|
||||
faBookOpen,
|
||||
faCamera,
|
||||
faCog,
|
||||
faHeartbeat,
|
||||
faSearchMinus,
|
||||
faSearchPlus,
|
||||
faTheaterMasks,
|
||||
faTimesCircle,
|
||||
faUser,
|
||||
faUserEdit,
|
||||
faUserFriends,
|
||||
faUsers,
|
||||
faVoteYea,
|
||||
faCheckSquare,
|
||||
faSquare,
|
||||
faRandom,
|
||||
faPeopleArrows,
|
||||
faBroadcastTower,
|
||||
faCopy,
|
||||
faExchangeAlt,
|
||||
faHandPointRight
|
||||
);
|
||||
|
||||
const faIcons = [
|
||||
"BookOpen",
|
||||
"BroadcastTower",
|
||||
"Camera",
|
||||
"CheckSquare",
|
||||
"Cog",
|
||||
"Copy",
|
||||
"ExchangeAlt",
|
||||
"FileUpload",
|
||||
"HandPointRight",
|
||||
"Heartbeat",
|
||||
"Link",
|
||||
"PeopleArrows",
|
||||
"Random",
|
||||
"RedoAlt",
|
||||
"SearchMinus",
|
||||
"SearchPlus",
|
||||
"Square",
|
||||
"TheaterMasks",
|
||||
"TimesCircle",
|
||||
"Undo",
|
||||
"User",
|
||||
"UserEdit",
|
||||
"UserFriends",
|
||||
"Users",
|
||||
"VoteYea"
|
||||
];
|
||||
library.add(...faIcons.map(i => fas["fa" + i]));
|
||||
Vue.component("font-awesome-icon", FontAwesomeIcon);
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
new Vue({
|
||||
|
|
|
@ -38,9 +38,11 @@ export default new Vuex.Store({
|
|||
},
|
||||
session: {
|
||||
sessionId: "",
|
||||
isSpectator: false
|
||||
isSpectator: false,
|
||||
playerCount: 0
|
||||
},
|
||||
modals: {
|
||||
reference: false,
|
||||
edition: false,
|
||||
roles: false,
|
||||
role: false,
|
||||
|
@ -78,6 +80,9 @@ export default new Vuex.Store({
|
|||
setSpectator({ session }, spectator) {
|
||||
session.isSpectator = spectator;
|
||||
},
|
||||
setPlayerCount({ session }, playerCount) {
|
||||
session.playerCount = playerCount;
|
||||
},
|
||||
setBluff({ grimoire }, { index, role } = {}) {
|
||||
if (index !== undefined) {
|
||||
grimoire.bluffs.splice(index, 1, role);
|
||||
|
@ -87,6 +92,12 @@ export default new Vuex.Store({
|
|||
},
|
||||
toggleModal({ modals }, name) {
|
||||
modals[name] = !modals[name];
|
||||
if (modals[name]) {
|
||||
for (let modal in modals) {
|
||||
if (modal === name) continue;
|
||||
modals[modal] = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateScreenshot({ grimoire }, status) {
|
||||
if (status !== true && status !== false) {
|
||||
|
@ -97,11 +108,21 @@ export default new Vuex.Store({
|
|||
grimoire.isScreenshot = false;
|
||||
}
|
||||
},
|
||||
setRoles(state, roles) {
|
||||
state.roles = new Map(
|
||||
rolesJSON
|
||||
.filter(r => roles.includes(r.id))
|
||||
.sort((a, b) => b.team.localeCompare(a.team))
|
||||
.map(role => [role.id, role])
|
||||
);
|
||||
},
|
||||
setEdition(state, edition) {
|
||||
state.edition = edition;
|
||||
state.modals.edition = false;
|
||||
if (edition !== "custom") {
|
||||
state.roles = getRolesByEdition(edition);
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [persistence, session]
|
||||
});
|
||||
|
|
|
@ -84,6 +84,9 @@ const mutations = {
|
|||
state.players[to],
|
||||
state.players[from]
|
||||
];
|
||||
},
|
||||
move(state, [from, to]) {
|
||||
state.players.splice(to, 0, state.players.splice(from, 1)[0]);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,9 @@ module.exports = store => {
|
|||
// this will initialize state.roles!
|
||||
store.commit("setEdition", localStorage.edition);
|
||||
}
|
||||
if (localStorage.roles !== undefined) {
|
||||
store.commit("setRoles", JSON.parse(localStorage.roles));
|
||||
}
|
||||
if (localStorage.bluffs !== undefined) {
|
||||
JSON.parse(localStorage.bluffs).forEach((role, index) => {
|
||||
store.commit("setBluff", {
|
||||
|
@ -50,7 +53,19 @@ module.exports = store => {
|
|||
}
|
||||
break;
|
||||
case "setEdition":
|
||||
if (payload === "custom") {
|
||||
localStorage.removeItem("edition");
|
||||
} else {
|
||||
localStorage.setItem("edition", payload);
|
||||
localStorage.removeItem("roles");
|
||||
}
|
||||
break;
|
||||
case "setRoles":
|
||||
if (!payload.length) {
|
||||
localStorage.removeItem("roles");
|
||||
} else {
|
||||
localStorage.setItem("roles", JSON.stringify(payload));
|
||||
}
|
||||
break;
|
||||
case "setBluff":
|
||||
localStorage.setItem(
|
||||
|
@ -74,6 +89,7 @@ module.exports = store => {
|
|||
case "players/clear":
|
||||
case "players/set":
|
||||
case "players/swap":
|
||||
case "players/move":
|
||||
if (state.players.players.length) {
|
||||
localStorage.setItem(
|
||||
"players",
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
class LiveSession {
|
||||
constructor(store) {
|
||||
this.wss = "wss://connect.websocket.in/v3/";
|
||||
this.key = "zXzDomOphNQ94tWXrHfT8E8gkxjUMSXOQt0ypZetKoFsIUiEBegqWNAlExyd";
|
||||
this.socket = null;
|
||||
this.isSpectator = true;
|
||||
this.gamestate = [];
|
||||
this.store = store;
|
||||
this._wss = "wss://connect.websocket.in/v3/";
|
||||
this._key = "zXzDomOphNQ94tWXrHfT8E8gkxjUMSXOQt0ypZetKoFsIUiEBegqWNAlExyd";
|
||||
this._socket = null;
|
||||
this._isSpectator = true;
|
||||
this._gamestate = [];
|
||||
this._store = store;
|
||||
this._pingInterval = 30 * 1000; // 30 seconds between pings
|
||||
this._pingTimer = null;
|
||||
this._players = {}; // map of players connected to a session
|
||||
this._playerId = Math.random()
|
||||
.toString(36)
|
||||
.substr(2);
|
||||
|
||||
// reconnect to previous session
|
||||
if (this._store.state.session.sessionId) {
|
||||
this.connect(this._store.state.session.sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,12 +26,14 @@ class LiveSession {
|
|||
*/
|
||||
_open(channel) {
|
||||
this.disconnect();
|
||||
this.socket = new WebSocket(this.wss + channel + "?apiKey=" + this.key);
|
||||
this.socket.addEventListener("message", this._handleMessage.bind(this));
|
||||
this.socket.onopen = this._onOpen.bind(this);
|
||||
this.socket.onclose = () => {
|
||||
this.socket = null;
|
||||
this.store.commit("setSessionId", "");
|
||||
this._socket = new WebSocket(this._wss + channel + "?apiKey=" + this._key);
|
||||
this._socket.addEventListener("message", this._handleMessage.bind(this));
|
||||
this._socket.onopen = this._onOpen.bind(this);
|
||||
this._socket.onclose = () => {
|
||||
this._socket = null;
|
||||
this._store.commit("setSessionId", "");
|
||||
clearInterval(this._pingTimer);
|
||||
this._pingTimer = null;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -31,8 +44,8 @@ class LiveSession {
|
|||
* @private
|
||||
*/
|
||||
_send(command, params) {
|
||||
if (this.socket) {
|
||||
this.socket.send(JSON.stringify([command, params]));
|
||||
if (this._socket && this._socket.readyState === 1) {
|
||||
this._socket.send(JSON.stringify([command, params]));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,11 +54,22 @@ class LiveSession {
|
|||
* @private
|
||||
*/
|
||||
_onOpen() {
|
||||
if (this.isSpectator) {
|
||||
if (this._isSpectator) {
|
||||
this._send("req", "gs");
|
||||
} else {
|
||||
this.sendGamestate();
|
||||
}
|
||||
this._ping();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a ping message with player ID and ST flag.
|
||||
* @private
|
||||
*/
|
||||
_ping() {
|
||||
this._send("ping", [this._isSpectator, this._playerId]);
|
||||
clearTimeout(this._pingTimer);
|
||||
this._pingTimer = setTimeout(this._ping.bind(this), this._pingInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,6 +95,13 @@ class LiveSession {
|
|||
break;
|
||||
case "player":
|
||||
this._updatePlayer(params);
|
||||
break;
|
||||
case "ping":
|
||||
this._handlePing(params);
|
||||
break;
|
||||
case "bye":
|
||||
this._handleBye(params);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +110,8 @@ class LiveSession {
|
|||
* @param channel
|
||||
*/
|
||||
connect(channel) {
|
||||
this.isSpectator = this.store.state.session.isSpectator;
|
||||
this._store.commit("setPlayerCount", 0);
|
||||
this._isSpectator = this._store.state.session.isSpectator;
|
||||
this._open(channel);
|
||||
}
|
||||
|
||||
|
@ -87,9 +119,11 @@ class LiveSession {
|
|||
* Close the current session, if any.
|
||||
*/
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
this._store.commit("setPlayerCount", 0);
|
||||
if (this._socket) {
|
||||
this._send("bye", this._playerId);
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,8 +131,8 @@ class LiveSession {
|
|||
* Publish the current gamestate.
|
||||
*/
|
||||
sendGamestate() {
|
||||
if (this.isSpectator) return;
|
||||
this.gamestate = this.store.state.players.players.map(player => ({
|
||||
if (this._isSpectator) return;
|
||||
this._gamestate = this._store.state.players.players.map(player => ({
|
||||
name: player.name,
|
||||
isDead: player.isDead,
|
||||
isVoteless: player.isVoteless,
|
||||
|
@ -113,8 +147,8 @@ class LiveSession {
|
|||
: {})
|
||||
}));
|
||||
this._send("gs", {
|
||||
gamestate: this.gamestate,
|
||||
edition: this.store.state.edition
|
||||
gamestate: this._gamestate,
|
||||
edition: this._store.state.edition
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -125,16 +159,16 @@ class LiveSession {
|
|||
* @private
|
||||
*/
|
||||
_updateGamestate({ gamestate, edition }) {
|
||||
this.store.commit("setEdition", edition);
|
||||
const players = this.store.state.players.players;
|
||||
this._store.commit("setEdition", edition);
|
||||
const players = this._store.state.players.players;
|
||||
// adjust number of players
|
||||
if (players.length < gamestate.length) {
|
||||
for (let x = players.length; x < gamestate.length; x++) {
|
||||
this.store.commit("players/add", gamestate[x].name);
|
||||
this._store.commit("players/add", gamestate[x].name);
|
||||
}
|
||||
} else if (players.length > gamestate.length) {
|
||||
for (let x = players.length; x > gamestate.length; x--) {
|
||||
this.store.commit("players/remove", x - 1);
|
||||
this._store.commit("players/remove", x - 1);
|
||||
}
|
||||
}
|
||||
// update status for each player
|
||||
|
@ -142,34 +176,34 @@ class LiveSession {
|
|||
const player = players[x];
|
||||
const { name, isDead, isVoteless, role } = state;
|
||||
if (player.name !== name) {
|
||||
this.store.commit("players/update", {
|
||||
this._store.commit("players/update", {
|
||||
player,
|
||||
property: "name",
|
||||
value: name
|
||||
});
|
||||
}
|
||||
if (player.isDead !== isDead) {
|
||||
this.store.commit("players/update", {
|
||||
this._store.commit("players/update", {
|
||||
player,
|
||||
property: "isDead",
|
||||
value: isDead
|
||||
});
|
||||
}
|
||||
if (player.isVoteless !== isVoteless) {
|
||||
this.store.commit("players/update", {
|
||||
this._store.commit("players/update", {
|
||||
player,
|
||||
property: "isVoteless",
|
||||
value: isVoteless
|
||||
});
|
||||
}
|
||||
if (role && player.role.id !== role.id) {
|
||||
this.store.commit("players/update", {
|
||||
this._store.commit("players/update", {
|
||||
player,
|
||||
property: "role",
|
||||
value: role
|
||||
});
|
||||
} else if (!role && player.role.team === "traveler") {
|
||||
this.store.commit("players/update", {
|
||||
this._store.commit("players/update", {
|
||||
player,
|
||||
property: "role",
|
||||
value: {}
|
||||
|
@ -185,12 +219,12 @@ class LiveSession {
|
|||
* @param value
|
||||
*/
|
||||
sendPlayer({ player, property, value }) {
|
||||
if (this.isSpectator || property === "reminders") return;
|
||||
const index = this.store.state.players.players.indexOf(player);
|
||||
if (this._isSpectator || property === "reminders") return;
|
||||
const index = this._store.state.players.players.indexOf(player);
|
||||
if (property === "role") {
|
||||
if (value.team && value.team === "traveler") {
|
||||
// update local gamestate to remember this player as a traveler
|
||||
this.gamestate[index].role = {
|
||||
this._gamestate[index].role = {
|
||||
id: player.role.id,
|
||||
team: "traveler",
|
||||
name: player.role.name
|
||||
|
@ -198,10 +232,10 @@ class LiveSession {
|
|||
this._send("player", {
|
||||
index,
|
||||
property,
|
||||
value: this.gamestate[index].role
|
||||
value: this._gamestate[index].role
|
||||
});
|
||||
} else if (this.gamestate[index].role) {
|
||||
delete this.gamestate[index].role;
|
||||
} else if (this._gamestate[index].role) {
|
||||
delete this._gamestate[index].role;
|
||||
this._send("player", { index, property, value: {} });
|
||||
}
|
||||
} else {
|
||||
|
@ -217,7 +251,7 @@ class LiveSession {
|
|||
* @private
|
||||
*/
|
||||
_updatePlayer({ index, property, value }) {
|
||||
const player = this.store.state.players.players[index];
|
||||
const player = this._store.state.players.players[index];
|
||||
if (!player) return;
|
||||
// special case where a player stops being a traveler
|
||||
if (
|
||||
|
@ -226,16 +260,47 @@ class LiveSession {
|
|||
player.role.team === "traveler"
|
||||
) {
|
||||
// reset to an unknown role
|
||||
this.store.commit("players/update", {
|
||||
this._store.commit("players/update", {
|
||||
player,
|
||||
property: "role",
|
||||
value: {}
|
||||
});
|
||||
} else {
|
||||
// just update the player otherwise
|
||||
this.store.commit("players/update", { player, property, value });
|
||||
this._store.commit("players/update", { player, property, value });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a ping message by another player / storyteller
|
||||
* @param isSpectator
|
||||
* @param playerId
|
||||
* @private
|
||||
*/
|
||||
_handlePing([isSpectator, playerId]) {
|
||||
const now = new Date().getTime();
|
||||
this._players[playerId] = now;
|
||||
// remove players that haven't sent a ping in twice the timespan
|
||||
for (let player in this._players) {
|
||||
if (now - this._players[player] > this._pingInterval * 2) {
|
||||
delete this._players[player];
|
||||
}
|
||||
}
|
||||
this._store.commit("setPlayerCount", Object.keys(this._players).length);
|
||||
if (!this._isSpectator && !isSpectator) {
|
||||
alert("Another storyteller joined the session!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a player leaving the sessions
|
||||
* @param playerId
|
||||
* @private
|
||||
*/
|
||||
_handleBye(playerId) {
|
||||
delete this._players[playerId];
|
||||
this._store.commit("setPlayerCount", Object.keys(this._players).length);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = store => {
|
||||
|
@ -255,6 +320,7 @@ module.exports = store => {
|
|||
break;
|
||||
case "players/set":
|
||||
case "players/swap":
|
||||
case "players/move":
|
||||
case "players/clear":
|
||||
case "players/remove":
|
||||
case "players/add":
|
||||
|
|
|
@ -9,5 +9,6 @@ $editions:
|
|||
'tb',
|
||||
'bmr',
|
||||
'snv',
|
||||
'luf' true
|
||||
'luf' true,
|
||||
'custom' true
|
||||
;
|
||||
|
|
Loading…
Reference in New Issue