Merge branch 'main' into custom-images

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Steffen 2021-02-08 13:58:27 +01:00
commit 8bd3c9b450
5 changed files with 111 additions and 17 deletions

View File

@ -1,8 +1,13 @@
# Release Notes # Release Notes
## Version 2.7.0 ### Version 2.8.0
- added hands-off live session support for homebrew / custom characters again! - added hands-off live session support for homebrew / custom characters again!
- added custom image opt-in that will prevent any (potentially malicious / harmful) images from loading until a player manually allows them to - added custom image opt-in that will prevent any (potentially malicious / harmful) images from loading until a player manually allows them to
---
## Version 2.7.0
- added support for assigning duplicate characters to more than one player (like Legion)
- further live session bandwidth optimizations - further live session bandwidth optimizations
- sessions can now be joined by pasting the whole link into the popup (thanks @davotronic5000) - sessions can now be joined by pasting the whole link into the popup (thanks @davotronic5000)
- fabled night order bug fixed - fabled night order bug fixed

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "townsquare", "name": "townsquare",
"version": "2.6.0", "version": "2.7.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "townsquare", "name": "townsquare",
"version": "2.6.0", "version": "2.7.0",
"description": "Blood on the Clocktower Town Square", "description": "Blood on the Clocktower Town Square",
"author": "Steffen Baumgart", "author": "Steffen Baumgart",
"scripts": { "scripts": {

View File

@ -7,22 +7,38 @@
<h3>Select the characters for {{ nonTravelers }} players:</h3> <h3>Select the characters for {{ nonTravelers }} players:</h3>
<ul class="tokens" v-for="(teamRoles, team) in roleSelection" :key="team"> <ul class="tokens" v-for="(teamRoles, team) in roleSelection" :key="team">
<li class="count" :class="[team]"> <li class="count" :class="[team]">
{{ teamRoles.filter(role => role.selected).length }} / {{ teamRoles.reduce((a, { selected }) => a + selected, 0) }} /
{{ game[nonTravelers - 5][team] }} {{ game[nonTravelers - 5][team] }}
</li> </li>
<li <li
v-for="role in teamRoles" v-for="role in teamRoles"
:class="[role.team, role.selected ? 'selected' : '']" :class="[role.team, role.selected ? 'selected' : '']"
:key="role.id" :key="role.id"
@click="role.selected = !role.selected" @click="role.selected = role.selected ? 0 : 1"
> >
<Token :role="role" /> <Token :role="role" />
<div class="buttons" v-if="allowMultiple">
<font-awesome-icon
icon="minus-circle"
@click.stop="role.selected--"
/>
<span>{{ role.selected > 1 ? "x" + role.selected : "" }}</span>
<font-awesome-icon icon="plus-circle" @click.stop="role.selected++" />
</div>
</li> </li>
</ul> </ul>
<div class="warning" v-if="hasSelectedSetupRoles"> <div class="warning" v-if="hasSelectedSetupRoles">
Warning: there are characters selected that modify the game setup! The <font-awesome-icon icon="exclamation-triangle" />
randomizer does not account for these characters. <span>
Warning: there are characters selected that modify the game setup! The
randomizer does not account for these characters.
</span>
</div> </div>
<label class="multiple" :class="{ checked: allowMultiple }">
<font-awesome-icon :icon="allowMultiple ? 'check-square' : 'square'" />
<input type="checkbox" name="allow-multiple" v-model="allowMultiple" />
Allow duplicate characters
</label>
<div class="button-group"> <div class="button-group">
<div <div
class="button" class="button"
@ -58,13 +74,14 @@ export default {
data: function() { data: function() {
return { return {
roleSelection: {}, roleSelection: {},
game: gameJSON game: gameJSON,
allowMultiple: false
}; };
}, },
computed: { computed: {
selectedRoles: function() { selectedRoles: function() {
return Object.values(this.roleSelection) return Object.values(this.roleSelection)
.map(roles => roles.filter(role => role.selected).length) .map(roles => roles.reduce((a, { selected }) => a + selected, 0))
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
}, },
hasSelectedSetupRoles: function() { hasSelectedSetupRoles: function() {
@ -84,7 +101,7 @@ export default {
this.$set(this.roleSelection, role.team, []); this.$set(this.roleSelection, role.team, []);
} }
this.roleSelection[role.team].push(role); this.roleSelection[role.team].push(role);
this.$set(role, "selected", false); this.$set(role, "selected", 0);
}); });
delete this.roleSelection["traveler"]; delete this.roleSelection["traveler"];
const playerCount = Math.max(5, this.nonTravelers); const playerCount = Math.max(5, this.nonTravelers);
@ -93,10 +110,10 @@ export default {
for (let x = 0; x < composition[team]; x++) { for (let x = 0; x < composition[team]; x++) {
if (this.roleSelection[team]) { if (this.roleSelection[team]) {
const available = this.roleSelection[team].filter( const available = this.roleSelection[team].filter(
role => role.selected !== true role => !role.selected
); );
if (available.length) { if (available.length) {
randomElement(available).selected = true; randomElement(available).selected = 1;
} }
} }
} }
@ -106,7 +123,12 @@ export default {
if (this.selectedRoles <= this.nonTravelers && this.selectedRoles) { if (this.selectedRoles <= this.nonTravelers && this.selectedRoles) {
// generate list of selected roles and randomize it // generate list of selected roles and randomize it
const roles = Object.values(this.roleSelection) const roles = Object.values(this.roleSelection)
.map(roles => roles.filter(role => role.selected)) .map(roles =>
roles
// duplicate roles selected more than once and filter unselected
.reduce((a, r) => [...a, ...Array(r.selected).fill(r)], [])
)
// flatten into a single array
.reduce((a, b) => [...a, ...b], []) .reduce((a, b) => [...a, ...b], [])
.map(a => [Math.random(), a]) .map(a => [Math.random(), a])
.sort((a, b) => a[0] - b[0]) .sort((a, b) => a[0] - b[0])
@ -152,6 +174,9 @@ ul.tokens {
transition: all 250ms; transition: all 250ms;
&.selected { &.selected {
opacity: 1; opacity: 1;
.buttons {
display: flex;
}
} }
&.townsfolk { &.townsfolk {
box-shadow: 0 0 10px $townsfolk, 0 0 10px #004cff; box-shadow: 0 0 10px $townsfolk, 0 0 10px #004cff;
@ -172,6 +197,27 @@ ul.tokens {
transform: scale(1.2); transform: scale(1.2);
z-index: 10; z-index: 10;
} }
.buttons {
display: none;
position: absolute;
top: 95%;
text-align: center;
width: 100%;
z-index: 30;
font-weight: bold;
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 1));
span {
flex-grow: 1;
}
svg {
opacity: 0.25;
cursor: pointer;
&:hover {
opacity: 1;
color: red;
}
}
}
} }
.count { .count {
opacity: 1; opacity: 1;
@ -203,9 +249,51 @@ ul.tokens {
} }
} }
.roles .modal .warning { .roles .modal {
color: red; .multiple {
text-align: center; display: block;
margin: auto; text-align: center;
cursor: pointer;
&.checked,
&:hover {
color: red;
}
&.checked {
margin-top: 10px;
}
svg {
margin-right: 5px;
}
input {
display: none;
}
}
.warning {
color: red;
position: absolute;
bottom: 20px;
right: 20px;
z-index: 10;
svg {
font-size: 150%;
vertical-align: middle;
}
span {
display: none;
text-align: center;
position: absolute;
right: -20px;
bottom: 30px;
width: 420px;
background: rgba(0, 0, 0, 0.75);
padding: 5px;
border-radius: 10px;
border: 2px solid black;
}
&:hover span {
display: block;
}
}
} }
</style> </style>

View File

@ -18,6 +18,7 @@ const faIcons = [
"Dice", "Dice",
"Dragon", "Dragon",
"ExchangeAlt", "ExchangeAlt",
"ExclamationTriangle",
"FileCode", "FileCode",
"FileUpload", "FileUpload",
"HandPaper", "HandPaper",