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)
|
[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
|
## 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/)
|
* Night reminders and other auxiliary text written by [Ben Finney](http://bignose.whitetree.org/projects/botc/diy/)
|
||||||
* Iconography by [Font Awesome](https://fontawesome.com/)
|
* Iconography by [Font Awesome](https://fontawesome.com/)
|
||||||
* Background image by [Ryan Maloney](https://www.artstation.com/maloney94)
|
* 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/)
|
* 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>
|
<TownInfo v-if="players.length"></TownInfo>
|
||||||
<TownSquare @screenshot="takeScreenshot"></TownSquare>
|
<TownSquare @screenshot="takeScreenshot"></TownSquare>
|
||||||
<Menu ref="menu"></Menu>
|
<Menu ref="menu"></Menu>
|
||||||
<EditionModal></EditionModal>
|
<EditionModal />
|
||||||
<RolesModal></RolesModal>
|
<RolesModal />
|
||||||
|
<ReferenceModal />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -27,9 +28,11 @@ import Menu from "./components/Menu";
|
||||||
import RolesModal from "./components/modals/RolesModal";
|
import RolesModal from "./components/modals/RolesModal";
|
||||||
import EditionModal from "./components/modals/EditionModal";
|
import EditionModal from "./components/modals/EditionModal";
|
||||||
import Intro from "./components/Intro";
|
import Intro from "./components/Intro";
|
||||||
|
import ReferenceModal from "./components/modals/ReferenceModal";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ReferenceModal,
|
||||||
Intro,
|
Intro,
|
||||||
TownInfo,
|
TownInfo,
|
||||||
TownSquare,
|
TownSquare,
|
||||||
|
@ -46,7 +49,7 @@ export default {
|
||||||
this.$refs.menu.takeScreenshot(dimensions);
|
this.$refs.menu.takeScreenshot(dimensions);
|
||||||
},
|
},
|
||||||
keyup({ key }) {
|
keyup({ key }) {
|
||||||
switch (key) {
|
switch (key.toLocaleLowerCase()) {
|
||||||
case "g":
|
case "g":
|
||||||
this.$store.commit("toggleGrimoire");
|
this.$store.commit("toggleGrimoire");
|
||||||
break;
|
break;
|
||||||
|
@ -54,7 +57,7 @@ export default {
|
||||||
this.$refs.menu.addPlayer();
|
this.$refs.menu.addPlayer();
|
||||||
break;
|
break;
|
||||||
case "r":
|
case "r":
|
||||||
this.$refs.menu.randomizeSeatings();
|
this.$store.commit("toggleModal", "reference");
|
||||||
break;
|
break;
|
||||||
case "e":
|
case "e":
|
||||||
if (this.session.isSpectator) return;
|
if (this.session.isSpectator) return;
|
||||||
|
@ -86,6 +89,12 @@ export default {
|
||||||
url("assets/fonts/papyrus.svg#PapyrusW01") format("svg"); /* iOS 4.1- */
|
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,
|
html,
|
||||||
body {
|
body {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
@ -121,6 +130,9 @@ h4,
|
||||||
h5 {
|
h5 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-family: PiratesBay, sans-serif;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
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>
|
<template>
|
||||||
<div class="intro" >
|
<div class="intro">
|
||||||
<img src="static/apple-icon.png" alt="" />
|
<img src="static/apple-icon.png" alt="" />
|
||||||
Welcome to the (unofficial)
|
Welcome to the (unofficial)
|
||||||
<b> Virtual Blood on the Clocktower Town Square</b>!<br />
|
<b> Virtual Blood on the Clocktower Town Square</b>!<br />
|
||||||
Please add more players through the
|
Please add more players through the
|
||||||
<span class="button">
|
<span class="button" @click="toggleMenu">
|
||||||
<font-awesome-icon icon="cog" @click="toggleMenu" /> Menu
|
<font-awesome-icon icon="cog" /> Menu
|
||||||
</span>
|
</span>
|
||||||
on the top right or by pressing <b>[A]</b>.<br />
|
on the top right or by pressing <b>[A]</b>.<br />
|
||||||
This project is free and open source and can be found on
|
This project is free and open source and can be found on
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="controls">
|
<div id="controls">
|
||||||
<Screenshot ref="screenshot"></Screenshot>
|
<Screenshot ref="screenshot"></Screenshot>
|
||||||
<font-awesome-icon
|
<span
|
||||||
@click="leaveSession"
|
class="session"
|
||||||
icon="broadcast-tower"
|
:class="{ spectator: session.isSpectator }"
|
||||||
v-if="session.sessionId"
|
v-if="session.sessionId"
|
||||||
v-bind:class="{ spectator: session.isSpectator }"
|
@click="leaveSession"
|
||||||
title="You're currently in a live game!"
|
: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
|
<font-awesome-icon
|
||||||
icon="camera"
|
icon="camera"
|
||||||
@click="takeScreenshot()"
|
@click="takeScreenshot()"
|
||||||
title="Take a screenshot"
|
title="Take a screenshot"
|
||||||
v-bind:class="{ success: grimoire.isScreenshotSuccess }"
|
:class="{ success: grimoire.isScreenshotSuccess }"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
<div class="menu" v-bind:class="{ open: grimoire.isMenuOpen }">
|
<div class="menu" v-bind:class="{ open: grimoire.isMenuOpen }">
|
||||||
<font-awesome-icon icon="cog" @click="toggleMenu" />
|
<font-awesome-icon icon="cog" @click="toggleMenu" />
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -79,12 +86,18 @@
|
||||||
<li @click="clearPlayers" v-if="players.length">
|
<li @click="clearPlayers" v-if="players.length">
|
||||||
Remove all
|
Remove all
|
||||||
</li>
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Characters -->
|
<!-- Characters -->
|
||||||
<li class="headline">
|
<li class="headline">
|
||||||
<font-awesome-icon icon="theater-masks" />
|
<font-awesome-icon icon="theater-masks" />
|
||||||
Characters
|
Characters
|
||||||
</li>
|
</li>
|
||||||
|
<li @click="toggleModal('reference')">
|
||||||
|
<em>[R]</em>
|
||||||
|
Reference Sheet
|
||||||
|
</li>
|
||||||
|
<template v-if="!session.isSpectator">
|
||||||
<li @click="toggleModal('edition')">
|
<li @click="toggleModal('edition')">
|
||||||
<em>[E]</em>
|
<em>[E]</em>
|
||||||
Select Edition
|
Select Edition
|
||||||
|
@ -120,10 +133,10 @@ export default {
|
||||||
this.$refs.screenshot.capture(dimensions);
|
this.$refs.screenshot.capture(dimensions);
|
||||||
},
|
},
|
||||||
setBackground() {
|
setBackground() {
|
||||||
this.$store.commit(
|
const background = prompt("Enter custom background URL");
|
||||||
"setBackground",
|
if (background || background === "") {
|
||||||
prompt("Enter custom background URL")
|
this.$store.commit("setBackground", background);
|
||||||
);
|
}
|
||||||
},
|
},
|
||||||
hostSession() {
|
hostSession() {
|
||||||
const sessionId = prompt(
|
const sessionId = prompt(
|
||||||
|
@ -241,14 +254,15 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> svg {
|
> span {
|
||||||
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
margin-top: 10px;
|
margin-top: 7px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .fa-broadcast-tower {
|
.session {
|
||||||
color: $demon;
|
color: $demon;
|
||||||
&.spectator {
|
&.spectator {
|
||||||
color: $townsfolk;
|
color: $townsfolk;
|
||||||
|
@ -317,9 +331,10 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.headline {
|
.headline {
|
||||||
|
font-family: PiratesBay, sans-serif;
|
||||||
|
letter-spacing: 1px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
$townsfolk 0%,
|
$townsfolk 0%,
|
||||||
|
|
|
@ -37,14 +37,20 @@
|
||||||
icon="times-circle"
|
icon="times-circle"
|
||||||
class="cancel"
|
class="cancel"
|
||||||
title="Cancel"
|
title="Cancel"
|
||||||
@click="doSwap(true)"
|
@click="cancel()"
|
||||||
/>
|
/>
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
icon="exchange-alt"
|
icon="exchange-alt"
|
||||||
class="swap"
|
class="swap"
|
||||||
@click="doSwap()"
|
@click="swapPlayer(player)"
|
||||||
title="Swap seats with this 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
|
<font-awesome-icon
|
||||||
icon="vote-yea"
|
icon="vote-yea"
|
||||||
|
@ -65,14 +71,17 @@
|
||||||
<transition name="fold">
|
<transition name="fold">
|
||||||
<ul class="menu" v-if="isMenuOpen && !session.isSpectator">
|
<ul class="menu" v-if="isMenuOpen && !session.isSpectator">
|
||||||
<li @click="changeName">
|
<li @click="changeName">
|
||||||
<font-awesome-icon icon="user-edit" />
|
<font-awesome-icon icon="user-edit" />Rename
|
||||||
Rename
|
|
||||||
</li>
|
</li>
|
||||||
<!--<li @click="nomination">
|
<!--<li @click="nomination">
|
||||||
<font-awesome-icon icon="hand-point-right" />
|
<font-awesome-icon icon="hand-point-right" />
|
||||||
Nomination
|
Nomination
|
||||||
</li>-->
|
</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" />
|
<font-awesome-icon icon="exchange-alt" />
|
||||||
Swap seats
|
Swap seats
|
||||||
</li>
|
</li>
|
||||||
|
@ -182,12 +191,16 @@ export default {
|
||||||
value
|
value
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
initSwap() {
|
swapPlayer(player) {
|
||||||
this.isMenuOpen = false;
|
this.isMenuOpen = false;
|
||||||
this.$emit("swap-seats");
|
this.$emit("swap-player", player);
|
||||||
},
|
},
|
||||||
doSwap(cancel) {
|
movePlayer(player) {
|
||||||
this.$emit("swap-seats", cancel ? false : this.player);
|
this.isMenuOpen = false;
|
||||||
|
this.$emit("move-player", player);
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.$emit("cancel");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -243,6 +256,10 @@ export default {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#townsquare.spectator & {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
#townsquare:not(.spectator) &:hover:before {
|
#townsquare:not(.spectator) &:hover:before {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
top: -10px;
|
top: -10px;
|
||||||
|
@ -349,6 +366,7 @@ export default {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&.swap,
|
&.swap,
|
||||||
|
&.move,
|
||||||
&.cancel {
|
&.cancel {
|
||||||
top: 9%;
|
top: 9%;
|
||||||
left: 20%;
|
left: 20%;
|
||||||
|
@ -364,13 +382,14 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
li.swap-from .player > svg.cancel {
|
li.from .player > svg.cancel {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
pointer-events: all;
|
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;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
@ -603,7 +622,7 @@ li.swap:not(.swap-from) .player > svg.swap {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 5px ($token / -4) 0;
|
margin: 5px ($token / -4) 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: ($token * 0.3 + 2px) 5px 0;
|
padding: ($token * 0.3 + 5px) 5px 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
line-height: 90%;
|
line-height: 90%;
|
||||||
border: 3px solid black;
|
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 {
|
&:hover:before {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,15 @@
|
||||||
@add-reminder="openReminderModal(index)"
|
@add-reminder="openReminderModal(index)"
|
||||||
@set-role="openRoleModal(index)"
|
@set-role="openRoleModal(index)"
|
||||||
@remove-player="removePlayer(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)"
|
@screenshot="$emit('screenshot', $event)"
|
||||||
v-bind:class="{
|
v-bind:class="{
|
||||||
'swap-from': swapFrom === index,
|
from: Math.max(swap, move, nominate) === index,
|
||||||
swap: swapFrom > -1
|
swap: swap > -1,
|
||||||
|
move: move > -1,
|
||||||
|
nominate: nominate > -1
|
||||||
}"
|
}"
|
||||||
></Player>
|
></Player>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -66,7 +70,9 @@ export default {
|
||||||
return {
|
return {
|
||||||
selectedPlayer: 0,
|
selectedPlayer: 0,
|
||||||
bluffs: 3,
|
bluffs: 3,
|
||||||
swapFrom: -1
|
swap: -1,
|
||||||
|
move: -1,
|
||||||
|
nominate: -1
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -80,7 +86,8 @@ export default {
|
||||||
},
|
},
|
||||||
openRoleModal(playerIndex) {
|
openRoleModal(playerIndex) {
|
||||||
const player = this.players[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.selectedPlayer = playerIndex;
|
||||||
this.$store.commit("toggleModal", "role");
|
this.$store.commit("toggleModal", "role");
|
||||||
},
|
},
|
||||||
|
@ -94,18 +101,34 @@ export default {
|
||||||
this.$store.commit("players/remove", playerIndex);
|
this.$store.commit("players/remove", playerIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
swapSeats(from, to) {
|
swapPlayer(from, to) {
|
||||||
if (to === undefined) {
|
if (to === undefined) {
|
||||||
this.swapFrom = from;
|
this.cancel();
|
||||||
} else if (to === false) {
|
this.swap = from;
|
||||||
this.swapFrom = -1;
|
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit("players/swap", [
|
this.$store.commit("players/swap", [
|
||||||
this.swapFrom,
|
this.swap,
|
||||||
this.players.indexOf(to)
|
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"
|
v-show="modals.edition"
|
||||||
@close="toggleModal('edition')"
|
@close="toggleModal('edition')"
|
||||||
>
|
>
|
||||||
|
<div v-if="!isCustom">
|
||||||
<h3>Select an edition:</h3>
|
<h3>Select an edition:</h3>
|
||||||
<ul class="editions">
|
<ul class="editions">
|
||||||
<li
|
<li
|
||||||
|
@ -15,7 +16,48 @@
|
||||||
>
|
>
|
||||||
{{ edition.name }}
|
{{ edition.name }}
|
||||||
</li>
|
</li>
|
||||||
|
<li class="edition edition-custom" @click="isCustom = true">
|
||||||
|
Custom Script
|
||||||
|
</li>
|
||||||
</ul>
|
</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>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -30,11 +72,61 @@ export default {
|
||||||
},
|
},
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
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"]),
|
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>
|
</script>
|
||||||
|
|
||||||
|
@ -63,15 +155,16 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.editions .edition {
|
ul.editions .edition {
|
||||||
|
font-family: PiratesBay, sans-serif;
|
||||||
|
letter-spacing: 1px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 100px;
|
padding-top: 100px;
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
background-size: 100% auto;
|
background-size: 100% auto;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
width: 200px;
|
width: 30%;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
font-weight: bold;
|
|
||||||
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000,
|
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);
|
1px 1px 0 #000, 0 0 5px rgba(0, 0, 0, 0.75);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -79,4 +172,23 @@ ul.editions .edition {
|
||||||
color: red;
|
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>
|
</style>
|
||||||
|
|
|
@ -48,6 +48,11 @@ export default {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 60%;
|
max-width: 60%;
|
||||||
|
|
||||||
|
.characters & {
|
||||||
|
max-height: 80%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 0;
|
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
|
<li
|
||||||
v-for="reminder in availableReminders"
|
v-for="reminder in availableReminders"
|
||||||
class="reminder"
|
class="reminder"
|
||||||
v-bind:class="[reminder.role]"
|
:class="[reminder.role]"
|
||||||
v-bind:key="reminder.role + ' ' + reminder.name"
|
:key="reminder.role + ' ' + reminder.name"
|
||||||
@click="addReminder(reminder)"
|
@click="addReminder(reminder)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon"
|
class="icon"
|
||||||
v-bind:style="{
|
:style="{
|
||||||
backgroundImage: `url(${require('../../assets/icons/' +
|
backgroundImage: `url(${require('../../assets/icons/' +
|
||||||
reminder.role +
|
reminder.role +
|
||||||
'.png')})`
|
'.png')})`
|
||||||
|
@ -48,6 +48,7 @@ export default {
|
||||||
});
|
});
|
||||||
reminders.push({ role: "good", name: "Good" });
|
reminders.push({ role: "good", name: "Good" });
|
||||||
reminders.push({ role: "evil", name: "Evil" });
|
reminders.push({ role: "evil", name: "Evil" });
|
||||||
|
reminders.push({ role: "custom", name: "Custom note" });
|
||||||
return reminders;
|
return reminders;
|
||||||
},
|
},
|
||||||
...mapState(["modals"]),
|
...mapState(["modals"]),
|
||||||
|
@ -56,7 +57,14 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
addReminder(reminder) {
|
addReminder(reminder) {
|
||||||
const player = this.$store.state.players.players[this.playerIndex];
|
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", {
|
this.$store.commit("players/update", {
|
||||||
player,
|
player,
|
||||||
property: "reminders",
|
property: "reminders",
|
||||||
|
@ -85,7 +93,7 @@ ul.reminders .reminder {
|
||||||
border: 3px solid black;
|
border: 3px solid black;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 65px 9px 0;
|
padding: 70px 9px 0;
|
||||||
line-height: 100%;
|
line-height: 100%;
|
||||||
transition: transform 500ms ease;
|
transition: transform 500ms ease;
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default {
|
||||||
availableRoles.push({});
|
availableRoles.push({});
|
||||||
return availableRoles;
|
return availableRoles;
|
||||||
},
|
},
|
||||||
...mapState(["modals", "roles"]),
|
...mapState(["modals", "roles", "session"]),
|
||||||
...mapState("players", ["players"])
|
...mapState("players", ["players"])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -61,6 +61,7 @@ export default {
|
||||||
role
|
role
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (this.session.isSpectator && role.team === "traveler") return;
|
||||||
// assign to player
|
// assign to player
|
||||||
const player = this.$store.state.players.players[this.playerIndex];
|
const player = this.$store.state.players.players[this.playerIndex];
|
||||||
this.$store.commit("players/update", {
|
this.$store.commit("players/update", {
|
||||||
|
@ -105,4 +106,8 @@ ul.tokens li {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#townsquare.spectator ul.tokens li.traveler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
77
src/main.js
77
src/main.js
|
@ -2,57 +2,38 @@ import Vue from "vue";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||||
import {
|
import { fas } from "@fortawesome/free-solid-svg-icons";
|
||||||
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 { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
|
|
||||||
library.add(
|
const faIcons = [
|
||||||
faBookOpen,
|
"BookOpen",
|
||||||
faCamera,
|
"BroadcastTower",
|
||||||
faCog,
|
"Camera",
|
||||||
faHeartbeat,
|
"CheckSquare",
|
||||||
faSearchMinus,
|
"Cog",
|
||||||
faSearchPlus,
|
"Copy",
|
||||||
faTheaterMasks,
|
"ExchangeAlt",
|
||||||
faTimesCircle,
|
"FileUpload",
|
||||||
faUser,
|
"HandPointRight",
|
||||||
faUserEdit,
|
"Heartbeat",
|
||||||
faUserFriends,
|
"Link",
|
||||||
faUsers,
|
"PeopleArrows",
|
||||||
faVoteYea,
|
"Random",
|
||||||
faCheckSquare,
|
"RedoAlt",
|
||||||
faSquare,
|
"SearchMinus",
|
||||||
faRandom,
|
"SearchPlus",
|
||||||
faPeopleArrows,
|
"Square",
|
||||||
faBroadcastTower,
|
"TheaterMasks",
|
||||||
faCopy,
|
"TimesCircle",
|
||||||
faExchangeAlt,
|
"Undo",
|
||||||
faHandPointRight
|
"User",
|
||||||
);
|
"UserEdit",
|
||||||
|
"UserFriends",
|
||||||
|
"Users",
|
||||||
|
"VoteYea"
|
||||||
|
];
|
||||||
|
library.add(...faIcons.map(i => fas["fa" + i]));
|
||||||
Vue.component("font-awesome-icon", FontAwesomeIcon);
|
Vue.component("font-awesome-icon", FontAwesomeIcon);
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
|
|
|
@ -38,9 +38,11 @@ export default new Vuex.Store({
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
sessionId: "",
|
sessionId: "",
|
||||||
isSpectator: false
|
isSpectator: false,
|
||||||
|
playerCount: 0
|
||||||
},
|
},
|
||||||
modals: {
|
modals: {
|
||||||
|
reference: false,
|
||||||
edition: false,
|
edition: false,
|
||||||
roles: false,
|
roles: false,
|
||||||
role: false,
|
role: false,
|
||||||
|
@ -78,6 +80,9 @@ export default new Vuex.Store({
|
||||||
setSpectator({ session }, spectator) {
|
setSpectator({ session }, spectator) {
|
||||||
session.isSpectator = spectator;
|
session.isSpectator = spectator;
|
||||||
},
|
},
|
||||||
|
setPlayerCount({ session }, playerCount) {
|
||||||
|
session.playerCount = playerCount;
|
||||||
|
},
|
||||||
setBluff({ grimoire }, { index, role } = {}) {
|
setBluff({ grimoire }, { index, role } = {}) {
|
||||||
if (index !== undefined) {
|
if (index !== undefined) {
|
||||||
grimoire.bluffs.splice(index, 1, role);
|
grimoire.bluffs.splice(index, 1, role);
|
||||||
|
@ -87,6 +92,12 @@ export default new Vuex.Store({
|
||||||
},
|
},
|
||||||
toggleModal({ modals }, name) {
|
toggleModal({ modals }, name) {
|
||||||
modals[name] = !modals[name];
|
modals[name] = !modals[name];
|
||||||
|
if (modals[name]) {
|
||||||
|
for (let modal in modals) {
|
||||||
|
if (modal === name) continue;
|
||||||
|
modals[modal] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateScreenshot({ grimoire }, status) {
|
updateScreenshot({ grimoire }, status) {
|
||||||
if (status !== true && status !== false) {
|
if (status !== true && status !== false) {
|
||||||
|
@ -97,11 +108,21 @@ export default new Vuex.Store({
|
||||||
grimoire.isScreenshot = false;
|
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) {
|
setEdition(state, edition) {
|
||||||
state.edition = edition;
|
state.edition = edition;
|
||||||
state.modals.edition = false;
|
state.modals.edition = false;
|
||||||
|
if (edition !== "custom") {
|
||||||
state.roles = getRolesByEdition(edition);
|
state.roles = getRolesByEdition(edition);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
plugins: [persistence, session]
|
plugins: [persistence, session]
|
||||||
});
|
});
|
||||||
|
|
|
@ -84,6 +84,9 @@ const mutations = {
|
||||||
state.players[to],
|
state.players[to],
|
||||||
state.players[from]
|
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!
|
// this will initialize state.roles!
|
||||||
store.commit("setEdition", localStorage.edition);
|
store.commit("setEdition", localStorage.edition);
|
||||||
}
|
}
|
||||||
|
if (localStorage.roles !== undefined) {
|
||||||
|
store.commit("setRoles", JSON.parse(localStorage.roles));
|
||||||
|
}
|
||||||
if (localStorage.bluffs !== undefined) {
|
if (localStorage.bluffs !== undefined) {
|
||||||
JSON.parse(localStorage.bluffs).forEach((role, index) => {
|
JSON.parse(localStorage.bluffs).forEach((role, index) => {
|
||||||
store.commit("setBluff", {
|
store.commit("setBluff", {
|
||||||
|
@ -50,7 +53,19 @@ module.exports = store => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "setEdition":
|
case "setEdition":
|
||||||
|
if (payload === "custom") {
|
||||||
|
localStorage.removeItem("edition");
|
||||||
|
} else {
|
||||||
localStorage.setItem("edition", payload);
|
localStorage.setItem("edition", payload);
|
||||||
|
localStorage.removeItem("roles");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setRoles":
|
||||||
|
if (!payload.length) {
|
||||||
|
localStorage.removeItem("roles");
|
||||||
|
} else {
|
||||||
|
localStorage.setItem("roles", JSON.stringify(payload));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "setBluff":
|
case "setBluff":
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
|
@ -74,6 +89,7 @@ module.exports = store => {
|
||||||
case "players/clear":
|
case "players/clear":
|
||||||
case "players/set":
|
case "players/set":
|
||||||
case "players/swap":
|
case "players/swap":
|
||||||
|
case "players/move":
|
||||||
if (state.players.players.length) {
|
if (state.players.players.length) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"players",
|
"players",
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
class LiveSession {
|
class LiveSession {
|
||||||
constructor(store) {
|
constructor(store) {
|
||||||
this.wss = "wss://connect.websocket.in/v3/";
|
this._wss = "wss://connect.websocket.in/v3/";
|
||||||
this.key = "zXzDomOphNQ94tWXrHfT8E8gkxjUMSXOQt0ypZetKoFsIUiEBegqWNAlExyd";
|
this._key = "zXzDomOphNQ94tWXrHfT8E8gkxjUMSXOQt0ypZetKoFsIUiEBegqWNAlExyd";
|
||||||
this.socket = null;
|
this._socket = null;
|
||||||
this.isSpectator = true;
|
this._isSpectator = true;
|
||||||
this.gamestate = [];
|
this._gamestate = [];
|
||||||
this.store = store;
|
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) {
|
_open(channel) {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
this.socket = new WebSocket(this.wss + channel + "?apiKey=" + this.key);
|
this._socket = new WebSocket(this._wss + channel + "?apiKey=" + this._key);
|
||||||
this.socket.addEventListener("message", this._handleMessage.bind(this));
|
this._socket.addEventListener("message", this._handleMessage.bind(this));
|
||||||
this.socket.onopen = this._onOpen.bind(this);
|
this._socket.onopen = this._onOpen.bind(this);
|
||||||
this.socket.onclose = () => {
|
this._socket.onclose = () => {
|
||||||
this.socket = null;
|
this._socket = null;
|
||||||
this.store.commit("setSessionId", "");
|
this._store.commit("setSessionId", "");
|
||||||
|
clearInterval(this._pingTimer);
|
||||||
|
this._pingTimer = null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +44,8 @@ class LiveSession {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_send(command, params) {
|
_send(command, params) {
|
||||||
if (this.socket) {
|
if (this._socket && this._socket.readyState === 1) {
|
||||||
this.socket.send(JSON.stringify([command, params]));
|
this._socket.send(JSON.stringify([command, params]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,11 +54,22 @@ class LiveSession {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_onOpen() {
|
_onOpen() {
|
||||||
if (this.isSpectator) {
|
if (this._isSpectator) {
|
||||||
this._send("req", "gs");
|
this._send("req", "gs");
|
||||||
} else {
|
} else {
|
||||||
this.sendGamestate();
|
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;
|
break;
|
||||||
case "player":
|
case "player":
|
||||||
this._updatePlayer(params);
|
this._updatePlayer(params);
|
||||||
|
break;
|
||||||
|
case "ping":
|
||||||
|
this._handlePing(params);
|
||||||
|
break;
|
||||||
|
case "bye":
|
||||||
|
this._handleBye(params);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +110,8 @@ class LiveSession {
|
||||||
* @param channel
|
* @param channel
|
||||||
*/
|
*/
|
||||||
connect(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);
|
this._open(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,9 +119,11 @@ class LiveSession {
|
||||||
* Close the current session, if any.
|
* Close the current session, if any.
|
||||||
*/
|
*/
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.socket) {
|
this._store.commit("setPlayerCount", 0);
|
||||||
this.socket.close();
|
if (this._socket) {
|
||||||
this.socket = null;
|
this._send("bye", this._playerId);
|
||||||
|
this._socket.close();
|
||||||
|
this._socket = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,8 +131,8 @@ class LiveSession {
|
||||||
* Publish the current gamestate.
|
* Publish the current gamestate.
|
||||||
*/
|
*/
|
||||||
sendGamestate() {
|
sendGamestate() {
|
||||||
if (this.isSpectator) return;
|
if (this._isSpectator) return;
|
||||||
this.gamestate = this.store.state.players.players.map(player => ({
|
this._gamestate = this._store.state.players.players.map(player => ({
|
||||||
name: player.name,
|
name: player.name,
|
||||||
isDead: player.isDead,
|
isDead: player.isDead,
|
||||||
isVoteless: player.isVoteless,
|
isVoteless: player.isVoteless,
|
||||||
|
@ -113,8 +147,8 @@ class LiveSession {
|
||||||
: {})
|
: {})
|
||||||
}));
|
}));
|
||||||
this._send("gs", {
|
this._send("gs", {
|
||||||
gamestate: this.gamestate,
|
gamestate: this._gamestate,
|
||||||
edition: this.store.state.edition
|
edition: this._store.state.edition
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,16 +159,16 @@ class LiveSession {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateGamestate({ gamestate, edition }) {
|
_updateGamestate({ gamestate, edition }) {
|
||||||
this.store.commit("setEdition", edition);
|
this._store.commit("setEdition", edition);
|
||||||
const players = this.store.state.players.players;
|
const players = this._store.state.players.players;
|
||||||
// adjust number of players
|
// adjust number of players
|
||||||
if (players.length < gamestate.length) {
|
if (players.length < gamestate.length) {
|
||||||
for (let x = players.length; x < gamestate.length; x++) {
|
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) {
|
} else if (players.length > gamestate.length) {
|
||||||
for (let x = players.length; x > gamestate.length; x--) {
|
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
|
// update status for each player
|
||||||
|
@ -142,34 +176,34 @@ class LiveSession {
|
||||||
const player = players[x];
|
const player = players[x];
|
||||||
const { name, isDead, isVoteless, role } = state;
|
const { name, isDead, isVoteless, role } = state;
|
||||||
if (player.name !== name) {
|
if (player.name !== name) {
|
||||||
this.store.commit("players/update", {
|
this._store.commit("players/update", {
|
||||||
player,
|
player,
|
||||||
property: "name",
|
property: "name",
|
||||||
value: name
|
value: name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (player.isDead !== isDead) {
|
if (player.isDead !== isDead) {
|
||||||
this.store.commit("players/update", {
|
this._store.commit("players/update", {
|
||||||
player,
|
player,
|
||||||
property: "isDead",
|
property: "isDead",
|
||||||
value: isDead
|
value: isDead
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (player.isVoteless !== isVoteless) {
|
if (player.isVoteless !== isVoteless) {
|
||||||
this.store.commit("players/update", {
|
this._store.commit("players/update", {
|
||||||
player,
|
player,
|
||||||
property: "isVoteless",
|
property: "isVoteless",
|
||||||
value: isVoteless
|
value: isVoteless
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (role && player.role.id !== role.id) {
|
if (role && player.role.id !== role.id) {
|
||||||
this.store.commit("players/update", {
|
this._store.commit("players/update", {
|
||||||
player,
|
player,
|
||||||
property: "role",
|
property: "role",
|
||||||
value: role
|
value: role
|
||||||
});
|
});
|
||||||
} else if (!role && player.role.team === "traveler") {
|
} else if (!role && player.role.team === "traveler") {
|
||||||
this.store.commit("players/update", {
|
this._store.commit("players/update", {
|
||||||
player,
|
player,
|
||||||
property: "role",
|
property: "role",
|
||||||
value: {}
|
value: {}
|
||||||
|
@ -185,12 +219,12 @@ class LiveSession {
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
sendPlayer({ player, property, value }) {
|
sendPlayer({ player, property, value }) {
|
||||||
if (this.isSpectator || property === "reminders") return;
|
if (this._isSpectator || property === "reminders") return;
|
||||||
const index = this.store.state.players.players.indexOf(player);
|
const index = this._store.state.players.players.indexOf(player);
|
||||||
if (property === "role") {
|
if (property === "role") {
|
||||||
if (value.team && value.team === "traveler") {
|
if (value.team && value.team === "traveler") {
|
||||||
// update local gamestate to remember this player as a traveler
|
// update local gamestate to remember this player as a traveler
|
||||||
this.gamestate[index].role = {
|
this._gamestate[index].role = {
|
||||||
id: player.role.id,
|
id: player.role.id,
|
||||||
team: "traveler",
|
team: "traveler",
|
||||||
name: player.role.name
|
name: player.role.name
|
||||||
|
@ -198,10 +232,10 @@ class LiveSession {
|
||||||
this._send("player", {
|
this._send("player", {
|
||||||
index,
|
index,
|
||||||
property,
|
property,
|
||||||
value: this.gamestate[index].role
|
value: this._gamestate[index].role
|
||||||
});
|
});
|
||||||
} else if (this.gamestate[index].role) {
|
} else if (this._gamestate[index].role) {
|
||||||
delete this.gamestate[index].role;
|
delete this._gamestate[index].role;
|
||||||
this._send("player", { index, property, value: {} });
|
this._send("player", { index, property, value: {} });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -217,7 +251,7 @@ class LiveSession {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updatePlayer({ index, property, value }) {
|
_updatePlayer({ index, property, value }) {
|
||||||
const player = this.store.state.players.players[index];
|
const player = this._store.state.players.players[index];
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
// special case where a player stops being a traveler
|
// special case where a player stops being a traveler
|
||||||
if (
|
if (
|
||||||
|
@ -226,16 +260,47 @@ class LiveSession {
|
||||||
player.role.team === "traveler"
|
player.role.team === "traveler"
|
||||||
) {
|
) {
|
||||||
// reset to an unknown role
|
// reset to an unknown role
|
||||||
this.store.commit("players/update", {
|
this._store.commit("players/update", {
|
||||||
player,
|
player,
|
||||||
property: "role",
|
property: "role",
|
||||||
value: {}
|
value: {}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// just update the player otherwise
|
// 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 => {
|
module.exports = store => {
|
||||||
|
@ -255,6 +320,7 @@ module.exports = store => {
|
||||||
break;
|
break;
|
||||||
case "players/set":
|
case "players/set":
|
||||||
case "players/swap":
|
case "players/swap":
|
||||||
|
case "players/move":
|
||||||
case "players/clear":
|
case "players/clear":
|
||||||
case "players/remove":
|
case "players/remove":
|
||||||
case "players/add":
|
case "players/add":
|
||||||
|
|
|
@ -9,5 +9,6 @@ $editions:
|
||||||
'tb',
|
'tb',
|
||||||
'bmr',
|
'bmr',
|
||||||
'snv',
|
'snv',
|
||||||
'luf' true
|
'luf' true,
|
||||||
|
'custom' true
|
||||||
;
|
;
|
||||||
|
|
Loading…
Reference in New Issue