mirror of
				https://github.com/bra1n/townsquare.git
				synced 2025-10-21 16:55:12 +00:00 
			
		
		
		
	
						commit
						ef4241308e
					
				
					 25 changed files with 1028 additions and 988 deletions
				
			
		
							
								
								
									
										3
									
								
								.github/workflows/linter.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/linter.yml
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -18,6 +18,9 @@ on:
 | 
			
		|||
  push:
 | 
			
		||||
    branches-ignore:
 | 
			
		||||
      - 'gh-pages'
 | 
			
		||||
  pull_request:
 | 
			
		||||
    # The branches below must be a subset of the branches above
 | 
			
		||||
    branches: [ main ]
 | 
			
		||||
 | 
			
		||||
###############
 | 
			
		||||
# Set the Job #
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										47
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										47
									
								
								CHANGELOG.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,10 +1,57 @@
 | 
			
		|||
# Release Notes
 | 
			
		||||
 | 
			
		||||
## Version 2.6.0
 | 
			
		||||
- night mode can be toggeled with [S] now (thanks @davotronic5000)
 | 
			
		||||
- night order shows which players are dead
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Version 2.5.0
 | 
			
		||||
- all travelers from the base editions are now optionally available (thanks @davotronic5000)
 | 
			
		||||
- night order shows player names near roles now
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Version 2.4.0
 | 
			
		||||
- added spoiler role (Pixie!)
 | 
			
		||||
- fixed bug with ST sending out roles that are not part of the current edition / script (ie. travelers or base set roles)
 | 
			
		||||
- better Lycanthrope icon (thanks @AWConant)
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Version 2.3.1
 | 
			
		||||
- better vote history design and added timestamps
 | 
			
		||||
- adjusted player menu styling on smaller screens
 | 
			
		||||
- improved CONTRIBUTING.md description of hosting your own copy
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Version 2.3.0
 | 
			
		||||
- added spoiler role (Lycanthrope!)
 | 
			
		||||
- fixed copy to clipboard in Firefox
 | 
			
		||||
- fixed non-countdown votes still playing countdown sound for a split second
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Version 2.2.1
 | 
			
		||||
- clearing players / roles now also clears Fabled
 | 
			
		||||
- fix list of locked votes showing unlocked votes sometimes
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Version 2.2.0
 | 
			
		||||
- added [V] hotkey to open nomination history (thanks @lilserf)
 | 
			
		||||
- updated roles according to official Wiki changes
 | 
			
		||||
- adjusted roles night order
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Version 2.1.1
 | 
			
		||||
- show vote results at the end of a vote
 | 
			
		||||
- fixed global reminders not showing up anymore when the associated role is assigned to a player
 | 
			
		||||
- adjusted backend metrics
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Version 2.1.0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,20 @@ $ npm install
 | 
			
		|||
 | 
			
		||||
The development server can be started with `npm run serve`.
 | 
			
		||||
 | 
			
		||||
### Deploying to GitHub pages or with a non-root path
 | 
			
		||||
 | 
			
		||||
Deploying a forked version to GitHub Pages or running your local
 | 
			
		||||
development copy in a sub-path (instead of the web root) will require you to modify
 | 
			
		||||
the `vue.config.js` and configure the path at which the website will be served.
 | 
			
		||||
 | 
			
		||||
For example, deploying your forked `townsquare` project to GitHub pages would need the following
 | 
			
		||||
`vue.config.js` changes: 
 | 
			
		||||
```js
 | 
			
		||||
module.exports = {
 | 
			
		||||
  publicPath: process.env.NODE_ENV === "production" ? "/townsquare/" : "/"
 | 
			
		||||
};
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +78,10 @@ $ npm run lint
 | 
			
		|||
 | 
			
		||||
- **`dist`**: contains built files for distribution.
 | 
			
		||||
 | 
			
		||||
- **`public`**: static assets and templates that don't need to be built dynamically.
 | 
			
		||||
  
 | 
			
		||||
- **`server`**: NodeJS code for the live session backend server.
 | 
			
		||||
 | 
			
		||||
- **`src`**: contains the source code. The codebase is written in ES2015.
 | 
			
		||||
 | 
			
		||||
  - **`assets`**: contains all graphical assets like images, fonts, icons, etc.
 | 
			
		||||
| 
						 | 
				
			
			@ -73,9 +91,13 @@ $ npm run lint
 | 
			
		|||
    - **`fonts`**: webfonts used on the page
 | 
			
		||||
 | 
			
		||||
    - **`icons`**: character token icons
 | 
			
		||||
      
 | 
			
		||||
    - **`sounds`**: sound effects used on the page
 | 
			
		||||
 | 
			
		||||
  - **`components`**: the internal components used in the project
 | 
			
		||||
  
 | 
			
		||||
    - **`modals`**: the modals have a separate subfolder
 | 
			
		||||
 | 
			
		||||
  - **`store`**: the VueX data store and modules
 | 
			
		||||
    
 | 
			
		||||
    - **`modules`**: VueX modules that live in their own namespace
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "townsquare",
 | 
			
		||||
  "version": "2.1.1",
 | 
			
		||||
  "version": "2.6.0",
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "townsquare",
 | 
			
		||||
  "version": "2.1.1",
 | 
			
		||||
  "version": "2.6.0",
 | 
			
		||||
  "description": "Blood on the Clocktower Town Square",
 | 
			
		||||
  "author": "Steffen Baumgart",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -102,6 +102,15 @@ export default {
 | 
			
		|||
          if (this.session.isSpectator) return;
 | 
			
		||||
          this.$store.commit("toggleModal", "roles");
 | 
			
		||||
          break;
 | 
			
		||||
        case "v":
 | 
			
		||||
          if (this.session.voteHistory.length) {
 | 
			
		||||
            this.$store.commit("toggleModal", "voteHistory");
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case "s":
 | 
			
		||||
          if (this.session.isSpectator) return;
 | 
			
		||||
          this.$store.commit("toggleNight");
 | 
			
		||||
          break;
 | 
			
		||||
        case "escape":
 | 
			
		||||
          this.$store.commit("toggleModal");
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 95 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/icons/lycanthrope.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/icons/lycanthrope.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 86 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/icons/pixie.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/icons/pixie.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 151 KiB  | 
| 
						 | 
				
			
			@ -43,10 +43,7 @@
 | 
			
		|||
          <li @click="toggleNight" v-if="!session.isSpectator">
 | 
			
		||||
            <template v-if="!grimoire.isNight">Switch to Night</template>
 | 
			
		||||
            <template v-if="grimoire.isNight">Switch to Day</template>
 | 
			
		||||
            <em
 | 
			
		||||
              ><font-awesome-icon
 | 
			
		||||
                :icon="['fas', grimoire.isNight ? 'sun' : 'cloud-moon']"
 | 
			
		||||
            /></em>
 | 
			
		||||
            <em>[S]</em>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li @click="toggleNightOrder" v-if="players.length">
 | 
			
		||||
            Night order
 | 
			
		||||
| 
						 | 
				
			
			@ -114,8 +111,7 @@
 | 
			
		|||
            v-if="session.voteHistory.length"
 | 
			
		||||
            @click="toggleModal('voteHistory')"
 | 
			
		||||
          >
 | 
			
		||||
            Nomination history
 | 
			
		||||
            <em><font-awesome-icon icon="hand-paper"/></em>
 | 
			
		||||
            Nomination history<em>[V]</em>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li @click="leaveSession" v-if="session.sessionId">
 | 
			
		||||
            Leave Session
 | 
			
		||||
| 
						 | 
				
			
			@ -239,16 +235,9 @@ export default {
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    copySessionUrl() {
 | 
			
		||||
      // check for clipboard permissions
 | 
			
		||||
      navigator.permissions
 | 
			
		||||
        .query({ name: "clipboard-write" })
 | 
			
		||||
        .then(({ state }) => {
 | 
			
		||||
          if (state === "granted" || state === "prompt") {
 | 
			
		||||
            const url = window.location.href.split("#")[0];
 | 
			
		||||
            const link = url + "#" + this.session.sessionId;
 | 
			
		||||
            navigator.clipboard.writeText(link);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      const url = window.location.href.split("#")[0];
 | 
			
		||||
      const link = url + "#" + this.session.sessionId;
 | 
			
		||||
      navigator.clipboard.writeText(link);
 | 
			
		||||
    },
 | 
			
		||||
    distributeRoles() {
 | 
			
		||||
      if (this.session.isSpectator) return;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -675,7 +675,7 @@ li.move:not(.from) .player .overlay svg.move {
 | 
			
		|||
    border: 10px solid transparent;
 | 
			
		||||
    border-right-color: black;
 | 
			
		||||
    right: 100%;
 | 
			
		||||
    bottom: 7px;
 | 
			
		||||
    bottom: 5px;
 | 
			
		||||
    margin-right: 2px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -161,11 +161,13 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    voters: function() {
 | 
			
		||||
      const nomination = this.session.nomination[1];
 | 
			
		||||
      const voters = this.session.votes.map((vote, index) =>
 | 
			
		||||
        vote ? this.players[index].name : ""
 | 
			
		||||
      );
 | 
			
		||||
      const voters = Array(this.players.length)
 | 
			
		||||
        .fill("")
 | 
			
		||||
        .map((x, index) =>
 | 
			
		||||
          this.session.votes[index] ? this.players[index].name : ""
 | 
			
		||||
        );
 | 
			
		||||
      const reorder = [
 | 
			
		||||
        ...voters.slice(nomination + 1, this.players.length),
 | 
			
		||||
        ...voters.slice(nomination + 1),
 | 
			
		||||
        ...voters.slice(0, nomination + 1)
 | 
			
		||||
      ];
 | 
			
		||||
      return reorder.slice(0, this.session.lockedVote - 1).filter(n => !!n);
 | 
			
		||||
| 
						 | 
				
			
			@ -178,15 +180,15 @@ export default {
 | 
			
		|||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    countdown() {
 | 
			
		||||
      this.$store.commit("session/setVoteInProgress", true);
 | 
			
		||||
      this.$store.commit("session/lockVote", 0);
 | 
			
		||||
      this.$store.commit("session/setVoteInProgress", true);
 | 
			
		||||
      this.voteTimer = setInterval(() => {
 | 
			
		||||
        this.start();
 | 
			
		||||
      }, 4000);
 | 
			
		||||
    },
 | 
			
		||||
    start() {
 | 
			
		||||
      this.$store.commit("session/setVoteInProgress", true);
 | 
			
		||||
      this.$store.commit("session/lockVote", 1);
 | 
			
		||||
      this.$store.commit("session/setVoteInProgress", true);
 | 
			
		||||
      clearInterval(this.voteTimer);
 | 
			
		||||
      this.voteTimer = setInterval(() => {
 | 
			
		||||
        this.$store.commit("session/lockVote");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,14 +54,7 @@ export default {
 | 
			
		|||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    copy: function() {
 | 
			
		||||
      // check for clipboard permissions
 | 
			
		||||
      navigator.permissions
 | 
			
		||||
        .query({ name: "clipboard-write" })
 | 
			
		||||
        .then(({ state }) => {
 | 
			
		||||
          if (state === "granted" || state === "prompt") {
 | 
			
		||||
            navigator.clipboard.writeText(this.input || this.gamestate);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      navigator.clipboard.writeText(this.input || this.gamestate);
 | 
			
		||||
    },
 | 
			
		||||
    load: function() {
 | 
			
		||||
      if (this.session.isSpectator) return;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,17 @@
 | 
			
		|||
        >
 | 
			
		||||
          <span class="name">
 | 
			
		||||
            {{ role.name }}
 | 
			
		||||
            <template v-if="role.players.length">
 | 
			
		||||
              <br />
 | 
			
		||||
              <small
 | 
			
		||||
                v-for="(player, index) in role.players"
 | 
			
		||||
                :class="{ dead: player.isDead }"
 | 
			
		||||
                :key="index"
 | 
			
		||||
                >{{
 | 
			
		||||
                  player.name + (role.players.length > index + 1 ? "," : "")
 | 
			
		||||
                }}</small
 | 
			
		||||
              >
 | 
			
		||||
            </template>
 | 
			
		||||
          </span>
 | 
			
		||||
          <span
 | 
			
		||||
            class="icon"
 | 
			
		||||
| 
						 | 
				
			
			@ -53,6 +64,17 @@
 | 
			
		|||
          ></span>
 | 
			
		||||
          <span class="name">
 | 
			
		||||
            {{ role.name }}
 | 
			
		||||
            <template v-if="role.players.length">
 | 
			
		||||
              <br />
 | 
			
		||||
              <small
 | 
			
		||||
                v-for="(player, index) in role.players"
 | 
			
		||||
                :class="{ dead: player.isDead }"
 | 
			
		||||
                :key="index"
 | 
			
		||||
                >{{
 | 
			
		||||
                  player.name + (role.players.length > index + 1 ? "," : "")
 | 
			
		||||
                }}</small
 | 
			
		||||
              >
 | 
			
		||||
            </template>
 | 
			
		||||
          </span>
 | 
			
		||||
        </li>
 | 
			
		||||
      </ul>
 | 
			
		||||
| 
						 | 
				
			
			@ -68,11 +90,6 @@ export default {
 | 
			
		|||
  components: {
 | 
			
		||||
    Modal
 | 
			
		||||
  },
 | 
			
		||||
  data: function() {
 | 
			
		||||
    return {
 | 
			
		||||
      roleSelection: {}
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    rolesFirstNight: function() {
 | 
			
		||||
      const rolesFirstNight = [];
 | 
			
		||||
| 
						 | 
				
			
			@ -83,23 +100,22 @@ export default {
 | 
			
		|||
            id: "evil",
 | 
			
		||||
            name: "Minion info",
 | 
			
		||||
            firstNight: 2,
 | 
			
		||||
            team: "minion"
 | 
			
		||||
            team: "minion",
 | 
			
		||||
            players: this.players.filter(p => p.role.team === "minion")
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: "evil",
 | 
			
		||||
            name: "Demon info & bluffs",
 | 
			
		||||
            firstNight: 4,
 | 
			
		||||
            team: "demon"
 | 
			
		||||
            team: "demon",
 | 
			
		||||
            players: this.players.filter(p => p.role.team === "demon")
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      this.roles.forEach(role => {
 | 
			
		||||
        if (
 | 
			
		||||
          role.firstNight &&
 | 
			
		||||
          (role.team !== "traveler" ||
 | 
			
		||||
            this.players.some(p => p.role.id === role.id))
 | 
			
		||||
        ) {
 | 
			
		||||
          rolesFirstNight.push(role);
 | 
			
		||||
        const players = this.players.filter(p => p.role.id === role.id);
 | 
			
		||||
        if (role.firstNight && (role.team !== "traveler" || players.length)) {
 | 
			
		||||
          rolesFirstNight.push(Object.assign({ players }, role));
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      this.fabled
 | 
			
		||||
| 
						 | 
				
			
			@ -113,12 +129,9 @@ export default {
 | 
			
		|||
    rolesOtherNight: function() {
 | 
			
		||||
      const rolesOtherNight = [];
 | 
			
		||||
      this.roles.forEach(role => {
 | 
			
		||||
        if (
 | 
			
		||||
          role.otherNight &&
 | 
			
		||||
          (role.team !== "traveler" ||
 | 
			
		||||
            this.players.some(p => p.role.id === role.id))
 | 
			
		||||
        ) {
 | 
			
		||||
          rolesOtherNight.push(role);
 | 
			
		||||
        const players = this.players.filter(p => p.role.id === role.id);
 | 
			
		||||
        if (role.otherNight && (role.team !== "traveler" || players.length)) {
 | 
			
		||||
          rolesOtherNight.push(Object.assign({ players }, role));
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      this.fabled
 | 
			
		||||
| 
						 | 
				
			
			@ -179,57 +192,42 @@ h4 {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.fabled {
 | 
			
		||||
  .name,
 | 
			
		||||
  .player,
 | 
			
		||||
  h4 {
 | 
			
		||||
    color: $fabled;
 | 
			
		||||
    &:before,
 | 
			
		||||
    &:after {
 | 
			
		||||
      background-color: $fabled;
 | 
			
		||||
  .name {
 | 
			
		||||
    background: linear-gradient(90deg, $fabled, transparent 35%);
 | 
			
		||||
    .night .other & {
 | 
			
		||||
      background: linear-gradient(-90deg, $fabled, transparent 35%);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.townsfolk {
 | 
			
		||||
  .name,
 | 
			
		||||
  .player,
 | 
			
		||||
  h4 {
 | 
			
		||||
    color: $townsfolk;
 | 
			
		||||
    &:before,
 | 
			
		||||
    &:after {
 | 
			
		||||
      background-color: $townsfolk;
 | 
			
		||||
  .name {
 | 
			
		||||
    background: linear-gradient(90deg, $townsfolk, transparent 35%);
 | 
			
		||||
    .night .other & {
 | 
			
		||||
      background: linear-gradient(-90deg, $townsfolk, transparent 35%);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.outsider {
 | 
			
		||||
  .name,
 | 
			
		||||
  .player,
 | 
			
		||||
  h4 {
 | 
			
		||||
    color: $outsider;
 | 
			
		||||
    &:before,
 | 
			
		||||
    &:after {
 | 
			
		||||
      background-color: $outsider;
 | 
			
		||||
  .name {
 | 
			
		||||
    background: linear-gradient(90deg, $outsider, transparent 35%);
 | 
			
		||||
    .night .other & {
 | 
			
		||||
      background: linear-gradient(-90deg, $outsider, transparent 35%);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.minion {
 | 
			
		||||
  .name,
 | 
			
		||||
  .player,
 | 
			
		||||
  h4 {
 | 
			
		||||
    color: $minion;
 | 
			
		||||
    &:before,
 | 
			
		||||
    &:after {
 | 
			
		||||
      background-color: $minion;
 | 
			
		||||
  .name {
 | 
			
		||||
    background: linear-gradient(90deg, $minion, transparent 35%);
 | 
			
		||||
    .night .other & {
 | 
			
		||||
      background: linear-gradient(-90deg, $minion, transparent 35%);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.demon {
 | 
			
		||||
  .name,
 | 
			
		||||
  .player,
 | 
			
		||||
  h4 {
 | 
			
		||||
    color: $demon;
 | 
			
		||||
    &:before,
 | 
			
		||||
    &:after {
 | 
			
		||||
      background-color: $demon;
 | 
			
		||||
  .name {
 | 
			
		||||
    background: linear-gradient(90deg, $demon, transparent 35%);
 | 
			
		||||
    .night .other & {
 | 
			
		||||
      background: linear-gradient(-90deg, $demon, transparent 35%);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -237,20 +235,15 @@ ul {
 | 
			
		|||
  li {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    align-content: center;
 | 
			
		||||
    /*background: linear-gradient(0deg, #ffffff0f, transparent);*/
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    margin-bottom: 3px;
 | 
			
		||||
    .icon {
 | 
			
		||||
      width: 6vh;
 | 
			
		||||
      background-size: cover;
 | 
			
		||||
      background-position: 0 -5px;
 | 
			
		||||
      background-position: 0 0;
 | 
			
		||||
      flex-grow: 0;
 | 
			
		||||
      flex-shrink: 0;
 | 
			
		||||
      margin: 0 10px;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      border-left: 1px solid #ffffff1f;
 | 
			
		||||
      border-right: 1px solid #ffffff1f;
 | 
			
		||||
      margin: 0 2px;
 | 
			
		||||
      &:after {
 | 
			
		||||
        content: " ";
 | 
			
		||||
        display: block;
 | 
			
		||||
| 
						 | 
				
			
			@ -261,19 +254,18 @@ ul {
 | 
			
		|||
      flex-grow: 0;
 | 
			
		||||
      flex-shrink: 0;
 | 
			
		||||
      width: 15%;
 | 
			
		||||
      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;
 | 
			
		||||
      padding: 5px;
 | 
			
		||||
      border-left: 1px solid rgba(255, 255, 255, 0.4);
 | 
			
		||||
      border-right: 1px solid rgba(255, 255, 255, 0.4);
 | 
			
		||||
      small {
 | 
			
		||||
        color: #888;
 | 
			
		||||
        margin-right: 5px;
 | 
			
		||||
        &.dead {
 | 
			
		||||
          text-decoration: line-through;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &.legend {
 | 
			
		||||
| 
						 | 
				
			
			@ -307,28 +299,23 @@ ul {
 | 
			
		|||
  .headline {
 | 
			
		||||
    display: block;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    border-bottom: 1px solid white;
 | 
			
		||||
    border-bottom: 1px solid rgba(255, 255, 255, 0.4);
 | 
			
		||||
    padding: 5px 10px;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
  .icon {
 | 
			
		||||
    border-color: white;
 | 
			
		||||
  }
 | 
			
		||||
  .name {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
  }
 | 
			
		||||
  .first {
 | 
			
		||||
    .icon {
 | 
			
		||||
      border-right: 0;
 | 
			
		||||
    .name {
 | 
			
		||||
      border-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .other {
 | 
			
		||||
    li .name {
 | 
			
		||||
      text-align: left;
 | 
			
		||||
    }
 | 
			
		||||
    .icon {
 | 
			
		||||
      border-left: 0;
 | 
			
		||||
      border-right: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,11 +56,6 @@ export default {
 | 
			
		|||
  components: {
 | 
			
		||||
    Modal
 | 
			
		||||
  },
 | 
			
		||||
  data: function() {
 | 
			
		||||
    return {
 | 
			
		||||
      roleSelection: {}
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    rolesGrouped: function() {
 | 
			
		||||
      const rolesGrouped = {};
 | 
			
		||||
| 
						 | 
				
			
			@ -136,7 +131,6 @@ h4 {
 | 
			
		|||
 | 
			
		||||
.townsfolk {
 | 
			
		||||
  .name,
 | 
			
		||||
  .player,
 | 
			
		||||
  h4 {
 | 
			
		||||
    color: $townsfolk;
 | 
			
		||||
    &:before,
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +141,6 @@ h4 {
 | 
			
		|||
}
 | 
			
		||||
.outsider {
 | 
			
		||||
  .name,
 | 
			
		||||
  .player,
 | 
			
		||||
  h4 {
 | 
			
		||||
    color: $outsider;
 | 
			
		||||
    &:before,
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +151,6 @@ h4 {
 | 
			
		|||
}
 | 
			
		||||
.minion {
 | 
			
		||||
  .name,
 | 
			
		||||
  .player,
 | 
			
		||||
  h4 {
 | 
			
		||||
    color: $minion;
 | 
			
		||||
    &:before,
 | 
			
		||||
| 
						 | 
				
			
			@ -169,7 +161,6 @@ h4 {
 | 
			
		|||
}
 | 
			
		||||
.demon {
 | 
			
		||||
  .name,
 | 
			
		||||
  .player,
 | 
			
		||||
  h4 {
 | 
			
		||||
    color: $demon;
 | 
			
		||||
    &:before,
 | 
			
		||||
| 
						 | 
				
			
			@ -208,7 +199,6 @@ ul {
 | 
			
		|||
      width: 15%;
 | 
			
		||||
      font-weight: bold;
 | 
			
		||||
      text-align: right;
 | 
			
		||||
      font-family: "Papyrus", sans-serif;
 | 
			
		||||
      font-size: 110%;
 | 
			
		||||
    }
 | 
			
		||||
    .player {
 | 
			
		||||
| 
						 | 
				
			
			@ -216,6 +206,8 @@ ul {
 | 
			
		|||
      flex-shrink: 1;
 | 
			
		||||
      text-align: right;
 | 
			
		||||
      margin: 0 10px;
 | 
			
		||||
      color: #888;
 | 
			
		||||
      font-size: smaller;
 | 
			
		||||
    }
 | 
			
		||||
    .ability {
 | 
			
		||||
      flex-grow: 1;
 | 
			
		||||
| 
						 | 
				
			
			@ -230,6 +222,7 @@ ul {
 | 
			
		|||
      height: auto;
 | 
			
		||||
      font-family: inherit;
 | 
			
		||||
      font-size: inherit;
 | 
			
		||||
      color: #fff;
 | 
			
		||||
    }
 | 
			
		||||
    .icon:after {
 | 
			
		||||
      padding-top: 0;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,6 +82,21 @@ export default {
 | 
			
		|||
          }))
 | 
			
		||||
        ];
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // add out of script traveler reminders
 | 
			
		||||
      this.$store.state.otherTravelers.forEach(role => {
 | 
			
		||||
        if (players.some(p => p.role.id === role.id)) {
 | 
			
		||||
          reminders = [
 | 
			
		||||
            ...reminders,
 | 
			
		||||
            ...role.reminders.map(name => ({
 | 
			
		||||
              role: role.id,
 | 
			
		||||
              image: role.image,
 | 
			
		||||
              name
 | 
			
		||||
            }))
 | 
			
		||||
          ];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      reminders.push({ role: "good", name: "Good" });
 | 
			
		||||
      reminders.push({ role: "evil", name: "Evil" });
 | 
			
		||||
      reminders.push({ role: "custom", name: "Custom note" });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <Modal
 | 
			
		||||
    v-if="modals.role && availableRoles.length"
 | 
			
		||||
    @close="toggleModal('role')"
 | 
			
		||||
  >
 | 
			
		||||
  <Modal v-if="modals.role && availableRoles.length" @close="close">
 | 
			
		||||
    <h3>
 | 
			
		||||
      Choose a new character for
 | 
			
		||||
      {{
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +8,7 @@
 | 
			
		|||
          : "bluffing"
 | 
			
		||||
      }}
 | 
			
		||||
    </h3>
 | 
			
		||||
    <ul class="tokens">
 | 
			
		||||
    <ul class="tokens" v-if="tab === 'editionRoles' || !otherTravelers.size">
 | 
			
		||||
      <li
 | 
			
		||||
        v-for="role in availableRoles"
 | 
			
		||||
        :class="[role.team]"
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +18,33 @@
 | 
			
		|||
        <Token :role="role" />
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <ul class="tokens" v-if="tab === 'otherTravelers' && otherTravelers.size">
 | 
			
		||||
      <li
 | 
			
		||||
        v-for="role in otherTravelers.values()"
 | 
			
		||||
        :class="[role.team]"
 | 
			
		||||
        :key="role.id"
 | 
			
		||||
        @click="setRole(role)"
 | 
			
		||||
      >
 | 
			
		||||
        <Token :role="role" />
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <div
 | 
			
		||||
      class="button-group"
 | 
			
		||||
      v-if="playerIndex >= 0 && otherTravelers.size && !session.isSpectator"
 | 
			
		||||
    >
 | 
			
		||||
      <span
 | 
			
		||||
        class="button"
 | 
			
		||||
        :class="{ townsfolk: tab === 'editionRoles' }"
 | 
			
		||||
        @click="tab = 'editionRoles'"
 | 
			
		||||
        >Edtition Roles</span
 | 
			
		||||
      >
 | 
			
		||||
      <span
 | 
			
		||||
        class="button"
 | 
			
		||||
        :class="{ townsfolk: tab === 'otherTravelers' }"
 | 
			
		||||
        @click="tab = 'otherTravelers'"
 | 
			
		||||
        >Other Travelers</span
 | 
			
		||||
      >
 | 
			
		||||
    </div>
 | 
			
		||||
  </Modal>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +74,13 @@ export default {
 | 
			
		|||
      return availableRoles;
 | 
			
		||||
    },
 | 
			
		||||
    ...mapState(["modals", "roles", "session"]),
 | 
			
		||||
    ...mapState("players", ["players"])
 | 
			
		||||
    ...mapState("players", ["players"]),
 | 
			
		||||
    ...mapState(["otherTravelers"])
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      tab: "editionRoles"
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    setRole(role) {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +102,10 @@ export default {
 | 
			
		|||
      }
 | 
			
		||||
      this.$store.commit("toggleModal", "role");
 | 
			
		||||
    },
 | 
			
		||||
    close() {
 | 
			
		||||
      this.tab = "editionRoles";
 | 
			
		||||
      this.toggleModal("role");
 | 
			
		||||
    },
 | 
			
		||||
    ...mapMutations(["toggleModal"])
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,22 +15,50 @@
 | 
			
		|||
    <table>
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
          <td>Time</td>
 | 
			
		||||
          <td>Nominator</td>
 | 
			
		||||
          <td>Nominee</td>
 | 
			
		||||
          <td>Type</td>
 | 
			
		||||
          <td>Votes</td>
 | 
			
		||||
          <td>Majority</td>
 | 
			
		||||
          <td><font-awesome-icon icon="hand-paper" /> Hand up</td>
 | 
			
		||||
          <td>
 | 
			
		||||
            <font-awesome-icon icon="user-friends" />
 | 
			
		||||
            Voters
 | 
			
		||||
          </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody>
 | 
			
		||||
        <tr v-for="(vote, index) in session.voteHistory" :key="index">
 | 
			
		||||
          <td>
 | 
			
		||||
            {{
 | 
			
		||||
              vote.timestamp
 | 
			
		||||
                .getHours()
 | 
			
		||||
                .toString()
 | 
			
		||||
                .padStart(2, "0")
 | 
			
		||||
            }}:{{
 | 
			
		||||
              vote.timestamp
 | 
			
		||||
                .getMinutes()
 | 
			
		||||
                .toString()
 | 
			
		||||
                .padStart(2, "0")
 | 
			
		||||
            }}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td>{{ vote.nominator }}</td>
 | 
			
		||||
          <td>{{ vote.nominee }}</td>
 | 
			
		||||
          <td>{{ vote.type }}</td>
 | 
			
		||||
          <td>{{ vote.majority }}</td>
 | 
			
		||||
          <td>
 | 
			
		||||
            {{ vote.votes.length }}
 | 
			
		||||
            <font-awesome-icon icon="user-friends" />
 | 
			
		||||
            <font-awesome-icon icon="hand-paper" />
 | 
			
		||||
          </td>
 | 
			
		||||
          <td>
 | 
			
		||||
            {{ vote.majority }}
 | 
			
		||||
            <font-awesome-icon
 | 
			
		||||
              :icon="[
 | 
			
		||||
                'fas',
 | 
			
		||||
                vote.votes.length >= vote.majority ? 'check-square' : 'square'
 | 
			
		||||
              ]"
 | 
			
		||||
            />
 | 
			
		||||
          </td>
 | 
			
		||||
          <td>
 | 
			
		||||
            {{ vote.votes.join(", ") }}
 | 
			
		||||
          </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
| 
						 | 
				
			
			@ -89,13 +117,16 @@ thead td {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
tbody {
 | 
			
		||||
  td:nth-child(1) {
 | 
			
		||||
  td:nth-child(2) {
 | 
			
		||||
    color: $townsfolk;
 | 
			
		||||
  }
 | 
			
		||||
  td:nth-child(2) {
 | 
			
		||||
  td:nth-child(3) {
 | 
			
		||||
    color: $demon;
 | 
			
		||||
  }
 | 
			
		||||
  td:nth-child(4) {
 | 
			
		||||
  td:nth-child(5) {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
  td:nth-child(6) {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,7 +34,6 @@ const faIcons = [
 | 
			
		|||
  "SearchMinus",
 | 
			
		||||
  "SearchPlus",
 | 
			
		||||
  "Square",
 | 
			
		||||
  "Sun",
 | 
			
		||||
  "TheaterMasks",
 | 
			
		||||
  "Times",
 | 
			
		||||
  "TimesCircle",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,12 @@
 | 
			
		|||
  .player > .name {
 | 
			
		||||
    top: 0;
 | 
			
		||||
  }
 | 
			
		||||
  .player > .menu {
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    &:before {
 | 
			
		||||
      bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Old phones
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1578
									
								
								src/roles.json
									
										
									
									
									
								
							
							
						
						
									
										1578
									
								
								src/roles.json
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
				
			
			@ -25,6 +25,19 @@ const getRolesByEdition = (edition = editionJSON[0]) => {
 | 
			
		|||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getTravelersNotInEdition = (edition = editionJSON[0]) => {
 | 
			
		||||
  return new Map(
 | 
			
		||||
    rolesJSON
 | 
			
		||||
      .filter(
 | 
			
		||||
        r =>
 | 
			
		||||
          r.team === "traveler" &&
 | 
			
		||||
          r.edition !== edition.id &&
 | 
			
		||||
          !edition.roles.includes(r.id)
 | 
			
		||||
      )
 | 
			
		||||
      .map(role => [role.id, role])
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// base definition for custom roles
 | 
			
		||||
const imageBase =
 | 
			
		||||
  "https://raw.githubusercontent.com/bra1n/townsquare/main/src/assets/icons/";
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +83,7 @@ export default new Vuex.Store({
 | 
			
		|||
    },
 | 
			
		||||
    edition: editionJSONbyId.get("tb"),
 | 
			
		||||
    roles: getRolesByEdition(),
 | 
			
		||||
    otherTravelers: getTravelersNotInEdition(),
 | 
			
		||||
    fabled
 | 
			
		||||
  },
 | 
			
		||||
  getters: {
 | 
			
		||||
| 
						 | 
				
			
			@ -180,11 +194,18 @@ export default new Vuex.Store({
 | 
			
		|||
          // convert to Map
 | 
			
		||||
          .map(role => [role.id, role])
 | 
			
		||||
      );
 | 
			
		||||
      // update extraTravelers map to only show travelers not in this script
 | 
			
		||||
      state.otherTravelers = new Map(
 | 
			
		||||
        rolesJSON
 | 
			
		||||
          .filter(r => r.team === "traveler" && !roles.some(i => i.id === r.id))
 | 
			
		||||
          .map(role => [role.id, role])
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    setEdition(state, edition) {
 | 
			
		||||
      if (editionJSONbyId.has(edition.id)) {
 | 
			
		||||
        state.edition = editionJSONbyId.get(edition.id);
 | 
			
		||||
        state.roles = getRolesByEdition(state.edition);
 | 
			
		||||
        state.otherTravelers = getTravelersNotInEdition(state.edition);
 | 
			
		||||
      } else {
 | 
			
		||||
        state.edition = edition;
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -84,6 +84,7 @@ const actions = {
 | 
			
		|||
        name,
 | 
			
		||||
        id
 | 
			
		||||
      }));
 | 
			
		||||
      commit("setFabled", { fabled: [] });
 | 
			
		||||
    }
 | 
			
		||||
    commit("set", players);
 | 
			
		||||
    commit("setBluff");
 | 
			
		||||
| 
						 | 
				
			
			@ -94,6 +95,7 @@ const mutations = {
 | 
			
		|||
  clear(state) {
 | 
			
		||||
    state.players = [];
 | 
			
		||||
    state.bluffs = [];
 | 
			
		||||
    state.fabled = [];
 | 
			
		||||
  },
 | 
			
		||||
  set(state, players = []) {
 | 
			
		||||
    state.players = players;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,8 +72,8 @@ const mutations = {
 | 
			
		|||
  addHistory(state, players) {
 | 
			
		||||
    if (!state.nomination || state.lockedVote <= players.length) return;
 | 
			
		||||
    const isBanishment = players[state.nomination[1]].role.team === "traveler";
 | 
			
		||||
    console.log(isBanishment);
 | 
			
		||||
    state.voteHistory.push({
 | 
			
		||||
      timestamp: new Date(),
 | 
			
		||||
      nominator: players[state.nomination[0]].name,
 | 
			
		||||
      nominee: players[state.nomination[1]].name,
 | 
			
		||||
      type: isBanishment ? "Banishment" : "Execution",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -443,8 +443,11 @@ class LiveSession {
 | 
			
		|||
          value: {}
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        // load role
 | 
			
		||||
        const role = this._store.state.roles.get(value);
 | 
			
		||||
        // load role, first from session, the global, then fail gracefully
 | 
			
		||||
        const role =
 | 
			
		||||
          this._store.state.roles.get(value) ||
 | 
			
		||||
          this._store.getters.rolesJSONbyId.get(value) ||
 | 
			
		||||
          {};
 | 
			
		||||
        this._store.commit("players/update", {
 | 
			
		||||
          player,
 | 
			
		||||
          property: "role",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue