Localization + French translation

This commit is contained in:
Pingumask 2022-10-27 22:36:19 +02:00
parent 02dc543751
commit fdb208c03a
28 changed files with 3119 additions and 193 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
.idea
.vscode
node_modules
dist
*.pem

View file

@ -2,27 +2,18 @@
<div class="intro">
<img src="static/apple-icon.png" alt="" class="logo" />
<div>
Welcome to the (unofficial)
<b>Virtual Town Square and Grimoire</b> for Blood on the Clocktower!
Please add more players through the
{{ locale.intro.header }}
<span class="button" @click="toggleMenu">
<font-awesome-icon icon="cog" /> Menu
<font-awesome-icon icon="cog" /> {{ locale.intro.menu }}
</span>
on the top right or by pressing <b>[A]</b>. You can also join a game
session by pressing <b>[J]</b>.<br />
{{ locale.intro.body }}
<div class="footer">
This project is free and open source and can be found on
<a href="https://github.com/bra1n/townsquare" target="_blank">GitHub</a
>. It is not affiliated with The Pandemonium Institute. "Blood on the
Clocktower" is a trademark of Steven Medway and The Pandemonium
Institute.
{{ locale.intro.footerStart }}
<a href="https://github.com/bra1n/townsquare" target="_blank">GitHub</a>
{{ locale.intro.footerEnd }}
</div>
</div>
<a
class="redirect"
v-if="language === 'zh-CN'"
href="https://clocktower.gstonegames.com"
>
<a class="redirect" v-if="language === 'zh-CN'" href="https://clocktower.gstonegames.com">
<img src="../assets/gstone.png" class="gstone" alt="" />
你想使用中文版魔典吗
</a>
@ -30,9 +21,13 @@
</template>
<script>
import { mapMutations } from "vuex";
import { mapMutations, mapState, mapGetters } from "vuex";
export default {
computed: {
...mapState(["locale"]),
...mapGetters({ nightOrder: "players/nightOrder" })
},
data() {
return {
language: window.navigator.userLanguage || window.navigator.language

View file

@ -47,19 +47,19 @@
<template v-if="tab === 'grimoire'">
<!-- Grimoire -->
<li class="headline">Grimoire</li>
<li class="headline">{{ locale.menu.grimoire.title }}</li>
<li @click="toggleGrimoire" v-if="players.length">
<template v-if="!grimoire.isPublic">Hide</template>
<template v-if="grimoire.isPublic">Show</template>
<template v-if="!grimoire.isPublic">{{ locale.menu.grimoire.hide }}</template>
<template v-if="grimoire.isPublic">{{ locale.menu.grimoire.show }}</template>
<em>[G]</em>
</li>
<li @click="toggleNight" v-if="!session.isSpectator">
<template v-if="!grimoire.isNight">Switch to Night</template>
<template v-if="grimoire.isNight">Switch to Day</template>
<template v-if="!grimoire.isNight">{{ locale.menu.grimoire.nightSwitch }}</template>
<template v-if="grimoire.isNight">{{ locale.menu.grimoire.daySwitch }}</template>
<em>[S]</em>
</li>
<li @click="toggleNightOrder" v-if="players.length">
Night order
{{ locale.menu.grimoire.order }}
<em>
<font-awesome-icon
:icon="[
@ -70,7 +70,7 @@
</em>
</li>
<li v-if="players.length">
Zoom
{{ locale.menu.grimoire.zoom }}
<em>
<font-awesome-icon
@click="setZoom(grimoire.zoom - 1)"
@ -84,11 +84,11 @@
</em>
</li>
<li @click="setBackground">
Background image
{{ locale.menu.grimoire.background }}
<em><font-awesome-icon icon="image"/></em>
</li>
<li v-if="!edition.isOfficial" @click="imageOptIn">
<small>Show Custom Images</small>
<small>{{ locale.menu.grimoire.customImages }}</small>
<em
><font-awesome-icon
:icon="[
@ -98,14 +98,14 @@
/></em>
</li>
<li @click="toggleStatic">
Disable Animations
{{ locale.menu.grimoire.animations }}
<em
><font-awesome-icon
:icon="['fas', grimoire.isStatic ? 'check-square' : 'square']"
/></em>
</li>
<li @click="toggleMuted">
Mute Sounds
{{ locale.menu.grimoire.mute }}
<em
><font-awesome-icon
:icon="['fas', grimoire.isMuted ? 'volume-mute' : 'volume-up']"
@ -116,36 +116,36 @@
<template v-if="tab === 'session'">
<!-- Session -->
<li class="headline" v-if="session.sessionId">
{{ session.isSpectator ? "Playing" : "Hosting" }}
{{ session.isSpectator ? locale.menu.session.title.player : locale.menu.session.title.host }}
</li>
<li class="headline" v-else>
Live Session
{{ locale.menu.session.title.create }}
</li>
<template v-if="!session.sessionId">
<li @click="hostSession">Host (Storyteller)<em>[H]</em></li>
<li @click="joinSession">Join (Player)<em>[J]</em></li>
<li @click="hostSession">{{ locale.menu.session.storyteller }}<em>[H]</em></li>
<li @click="joinSession">{{ locale.menu.session.player }}<em>[J]</em></li>
</template>
<template v-else>
<li v-if="session.ping">
Delay to {{ session.isSpectator ? "host" : "players" }}
{{ locale.menu.session.delay }} {{ session.isSpectator ? locale.menu.session.host : locale.menu.session.players }}
<em>{{ session.ping }}ms</em>
</li>
<li @click="copySessionUrl">
Copy player link
{{ locale.menu.session.link }}
<em><font-awesome-icon icon="copy"/></em>
</li>
<li v-if="!session.isSpectator" @click="distributeRoles">
Send Characters
{{ locale.menu.session.sendRoles }}
<em><font-awesome-icon icon="theater-masks"/></em>
</li>
<li
v-if="session.voteHistory.length || !session.isSpectator"
@click="toggleModal('voteHistory')"
>
Vote history<em>[V]</em>
{{ locale.menu.session.voteHistory }}<em>[V]</em>
</li>
<li @click="leaveSession">
Leave Session
{{ locale.menu.session.leave }}
<em>{{ session.sessionId }}</em>
</li>
</template>
@ -153,60 +153,60 @@
<template v-if="tab === 'players' && !session.isSpectator">
<!-- Users -->
<li class="headline">Players</li>
<li @click="addPlayer" v-if="players.length < 20">Add<em>[A]</em></li>
<li class="headline">{{ locale.menu.players.title }}</li>
<li @click="addPlayer" v-if="players.length < 20">{{ locale.menu.players.add }}<em>[A]</em></li>
<li @click="randomizeSeatings" v-if="players.length > 2">
Randomize
{{ locale.menu.players.randomize }}
<em><font-awesome-icon icon="dice"/></em>
</li>
<li @click="clearPlayers" v-if="players.length">
Remove all
{{ locale.menu.players.removeAll }}
<em><font-awesome-icon icon="trash-alt"/></em>
</li>
</template>
<template v-if="tab === 'characters'">
<!-- Characters -->
<li class="headline">Characters</li>
<li class="headline">{{ locale.menu.characters.title }}</li>
<li v-if="!session.isSpectator" @click="toggleModal('edition')">
Select Edition
{{ locale.menu.characters.selectEdition }}
<em>[E]</em>
</li>
<li
@click="toggleModal('roles')"
v-if="!session.isSpectator && players.length > 4"
>
Choose & Assign
{{ locale.menu.characters.assign }}
<em>[C]</em>
</li>
<li v-if="!session.isSpectator" @click="toggleModal('fabled')">
Add Fabled
{{ locale.menu.characters.addFabled }}
<em><font-awesome-icon icon="dragon"/></em>
</li>
<li @click="clearRoles" v-if="players.length">
Remove all
{{ locale.menu.characters.removeAll }}
<em><font-awesome-icon icon="trash-alt"/></em>
</li>
</template>
<template v-if="tab === 'help'">
<!-- Help -->
<li class="headline">Help</li>
<li class="headline">{{ locale.menu.help.title }}</li>
<li @click="toggleModal('reference')">
Reference Sheet
{{ locale.menu.help.reference }}
<em>[R]</em>
</li>
<li @click="toggleModal('nightOrder')">
Night Order Sheet
{{ locale.menu.help.nightOrder }}
<em>[N]</em>
</li>
<li @click="toggleModal('gameState')">
Game State JSON
{{ locale.menu.help.gameState }}
<em><font-awesome-icon icon="file-code"/></em>
</li>
<li>
<a href="https://discord.gg/Gd7ybwWbFk" target="_blank">
Join Discord
{{ locale.menu.help.discord }}
</a>
<em>
<a href="https://discord.gg/Gd7ybwWbFk" target="_blank">
@ -216,7 +216,7 @@
</li>
<li>
<a href="https://github.com/bra1n/townsquare" target="_blank">
Source code
{{ locale.menu.help.source }}
</a>
<em>
<a href="https://github.com/bra1n/townsquare" target="_blank">
@ -235,7 +235,7 @@ import { mapMutations, mapState } from "vuex";
export default {
computed: {
...mapState(["grimoire", "session", "edition"]),
...mapState(["grimoire", "session", "edition", "locale"]),
...mapState("players", ["players"])
},
data() {
@ -245,7 +245,7 @@ export default {
},
methods: {
setBackground() {
const background = prompt("Enter custom background URL");
const background = prompt(this.locale.prompt.background);
if (background || background === "") {
this.$store.commit("setBackground", background);
}
@ -253,13 +253,14 @@ export default {
hostSession() {
if (this.session.sessionId) return;
const sessionId = prompt(
"Enter a channel number / name for your session",
this.locale.prompt.createSession,
Math.round(Math.random() * 10000)
);
if (sessionId) {
this.$store.commit("session/clearVoteHistory");
this.$store.commit("session/setSpectator", false);
this.$store.commit("session/setSessionId", sessionId);
this.$store.commit("toggleGrimoire", false);
this.copySessionUrl();
}
},
@ -270,8 +271,7 @@ export default {
},
distributeRoles() {
if (this.session.isSpectator) return;
const popup =
"Do you want to distribute assigned characters to all SEATED players?";
const popup = this.locale.prompt.sendRoles;
if (confirm(popup)) {
this.$store.commit("session/distributeRoles", true);
setTimeout(
@ -283,8 +283,7 @@ export default {
}
},
imageOptIn() {
const popup =
"Are you sure you want to allow custom images? A malicious script file author might track your IP address this way.";
const popup = this.locale.prompt.imageOptIn;
if (this.grimoire.isImageOptIn || confirm(popup)) {
this.toggleImageOptIn();
}
@ -292,7 +291,7 @@ export default {
joinSession() {
if (this.session.sessionId) return this.leaveSession();
let sessionId = prompt(
"Enter the channel number / name of the session you want to join"
this.locale.prompt.joinSession
);
if (sessionId.match(/^https?:\/\//i)) {
sessionId = sessionId.split("#").pop();
@ -305,7 +304,7 @@ export default {
}
},
leaveSession() {
if (confirm("Are you sure you want to leave the active live game?")) {
if (confirm(this.locale.prompt.leaveSession)) {
this.$store.commit("session/setSpectator", false);
this.$store.commit("session/setSessionId", "");
}
@ -313,20 +312,20 @@ export default {
addPlayer() {
if (this.session.isSpectator) return;
if (this.players.length >= 20) return;
const name = prompt("Player name");
const name = prompt(this.locale.prompt.addPlayer);
if (name) {
this.$store.commit("players/add", name);
}
},
randomizeSeatings() {
if (this.session.isSpectator) return;
if (confirm("Are you sure you want to randomize seatings?")) {
if (confirm(this.locale.prompt.randomizeSeatings)) {
this.$store.dispatch("players/randomize");
}
},
clearPlayers() {
if (this.session.isSpectator) return;
if (confirm("Are you sure you want to remove all players?")) {
if (confirm(this.locale.prompt.clearPlayers)) {
// abort vote if in progress
if (this.session.nomination) {
this.$store.commit("session/nomination");
@ -335,7 +334,7 @@ export default {
}
},
clearRoles() {
if (confirm("Are you sure you want to remove all player roles?")) {
if (confirm(this.locale.prompt.clearRoles)) {
this.$store.dispatch("players/clearRoles");
}
},

View file

@ -47,38 +47,38 @@
<font-awesome-icon
icon="hand-paper"
class="vote"
title="Hand UP"
:title="locale.player.handUp"
@click="vote()"
/>
<font-awesome-icon
icon="times"
class="vote"
title="Hand DOWN"
:title="locale.player.handDown"
@click="vote()"
/>
<font-awesome-icon
icon="times-circle"
class="cancel"
title="Cancel"
:title="locale.player.cancel"
@click="cancel()"
/>
<font-awesome-icon
icon="exchange-alt"
class="swap"
@click="swapPlayer(player)"
title="Swap seats with this player"
:title="locale.player.swap"
/>
<font-awesome-icon
icon="redo-alt"
class="move"
@click="movePlayer(player)"
title="Move player to this seat"
:title="locale.player.move"
/>
<font-awesome-icon
icon="hand-point-right"
class="nominate"
@click="nominatePlayer(player)"
title="Nominate this player"
:title="locale.player.nominate"
/>
</div>
@ -96,7 +96,7 @@
class="has-vote"
v-if="player.isDead && !player.isVoteless"
@click="updatePlayer('isVoteless', true)"
title="Ghost vote"
:title="locale.player.ghostVote"
/>
<!-- On block icon -->
@ -124,35 +124,35 @@
(session.isSpectator && player.id === session.playerId)
"
>
<font-awesome-icon icon="venus-mars" />Change Pronouns
<font-awesome-icon icon="venus-mars" />{{ locale.player.changePronouns }}
</li>
<template v-if="!session.isSpectator">
<li @click="changeName">
<font-awesome-icon icon="user-edit" />Rename
<font-awesome-icon icon="user-edit" />{{ locale.player.changeName }}
</li>
<li @click="movePlayer()" :class="{ disabled: session.lockedVote }">
<font-awesome-icon icon="redo-alt" />
Move player
{{ locale.player.movePlayer }}
</li>
<li @click="swapPlayer()" :class="{ disabled: session.lockedVote }">
<font-awesome-icon icon="exchange-alt" />
Swap seats
{{ locale.player.swapPlayers }}
</li>
<li @click="removePlayer" :class="{ disabled: session.lockedVote }">
<font-awesome-icon icon="times-circle" />
Remove
{{ locale.player.removePlayer }}
</li>
<li
@click="updatePlayer('id', '', true)"
v-if="player.id && session.sessionId"
>
<font-awesome-icon icon="chair" />
Empty seat
{{ locale.player.emptySeat }}
</li>
<template v-if="!session.nomination">
<li @click="nominatePlayer()">
<font-awesome-icon icon="hand-point-right" />
Nomination
{{ locale.player.nomination }}
</li>
</template>
</template>
@ -163,12 +163,12 @@
>
<font-awesome-icon icon="chair" />
<template v-if="!player.id">
Claim seat
{{ locale.player.claimSeat }}
</template>
<template v-else-if="player.id === session.playerId">
Vacate seat
{{ locale.player.vacateSeat }}
</template>
<template v-else> Seat occupied</template>
<template v-else> {{ locale.player.occupiedSeat }}</template>
</li>
</ul>
</transition>
@ -220,7 +220,7 @@ export default {
},
computed: {
...mapState("players", ["players"]),
...mapState(["grimoire", "session"]),
...mapState(["grimoire", "session", "locale"]),
...mapGetters({ nightOrder: "players/nightOrder" }),
index: function() {
return this.players.indexOf(this.player);

View file

@ -12,12 +12,12 @@
}"
></li>
<li v-if="players.length - teams.traveler < 5">
Please add more players!
{{ locale.towninfo.addPlayers }}
</li>
<li>
<span class="meta" v-if="!edition.isOfficial">
{{ edition.name }}
{{ edition.author ? "by " + edition.author : "" }}
{{ edition.author ? " ©" + edition.author : "" }}
</span>
<span>
{{ players.length }} <font-awesome-icon class="players" icon="users" />

View file

@ -30,8 +30,8 @@
:class="{ closed: !isBluffsOpen }"
>
<h3>
<span v-if="session.isSpectator">Other characters</span>
<span v-else>Demon bluffs</span>
<span v-if="session.isSpectator">{{ locale.townsquare.others }}</span>
<span v-else>{{ locale.townsquare.bluffs }}</span>
<font-awesome-icon icon="times-circle" @click.stop="toggleBluffs" />
<font-awesome-icon icon="plus-circle" @click.stop="toggleBluffs" />
</h3>
@ -48,7 +48,7 @@
<div class="fabled" :class="{ closed: !isFabledOpen }" v-if="fabled.length">
<h3>
<span>Fabled</span>
<span>{{ locale.townsquare.fabled }}</span>
<font-awesome-icon icon="times-circle" @click.stop="toggleFabled" />
<font-awesome-icon icon="plus-circle" @click.stop="toggleFabled" />
</h3>
@ -102,7 +102,7 @@ export default {
},
computed: {
...mapGetters({ nightOrder: "players/nightOrder" }),
...mapState(["grimoire", "roles", "session"]),
...mapState(["grimoire", "roles", "session", "locale"]),
...mapState("players", ["players", "bluffs", "fabled"])
},
data() {

View file

@ -6,22 +6,22 @@
</div>
<div class="overlay">
<audio src="../assets/sounds/countdown.mp3" preload="auto"></audio>
<em class="blue">{{ nominator.name }}</em> nominated
<em class="blue">{{ nominator.name }}</em> {{ locale.vote.nominated }}
<em>{{ nominee.name }}</em
>!
<br />
<em class="blue">
{{ voters.length }} vote{{ voters.length !== 1 ? "s" : "" }}
{{ voters.length }} {{ locale.vote.votes }}
</em>
in favor
{{ locale.vote.inFavor }}
<em v-if="nominee.role.team !== 'traveler'">
(majority is {{ Math.ceil(alive / 2) }})
({{ locale.vote.majorityIs }} {{ Math.ceil(alive / 2) }})
</em>
<em v-else>(majority is {{ Math.ceil(players.length / 2) }})</em>
<em v-else>({{ locale.vote.majorityIs }} {{ Math.ceil(players.length / 2) }})</em>
<template v-if="!session.isSpectator">
<div v-if="!session.isVoteInProgress && session.lockedVote < 1">
Time per player:
{{ locale.vote.timePerPlayer }}
<font-awesome-icon
@mousedown.prevent="setVotingSpeed(-500)"
icon="minus-circle"
@ -38,10 +38,10 @@
v-if="!session.isVoteInProgress"
@click="countdown"
>
Countdown
{{ locale.vote.countdown }}
</div>
<div class="button" v-if="!session.isVoteInProgress" @click="start">
{{ session.lockedVote ? "Restart" : "Start" }}
{{ session.lockedVote ? locale.vote.restart : locale.vote.start }}
</div>
<template v-else>
<div
@ -49,11 +49,11 @@
:class="{ disabled: !session.lockedVote }"
@click="pause"
>
{{ voteTimer ? "Pause" : "Resume" }}
{{ voteTimer ? locale.vote.pause : locale.vote.resume }}
</div>
<div class="button" @click="stop">Reset</div>
<div class="button" @click="stop">{{ locale.vote.reset }}</div>
</template>
<div class="button demon" @click="finish">Close</div>
<div class="button demon" @click="finish">{{ locale.vote.close }}</div>
</div>
<div class="button-group mark" v-if="nominee.role.team !== 'traveler'">
<div
@ -63,16 +63,16 @@
}"
@click="setMarked"
>
Mark for execution
{{ locale.vote.setMarked }}
</div>
<div class="button" @click="removeMarked">
Clear mark
{{ locale.vote.removeMarked }}
</div>
</div>
</template>
<template v-else-if="canVote">
<div v-if="!session.isVoteInProgress">
{{ session.votingSpeed / 1000 }} seconds between votes
{{ session.votingSpeed / 1000 }} {{ locale.vote.secondsBetweenVotes }}
</div>
<div class="button-group">
<div
@ -80,19 +80,19 @@
@click="vote(false)"
:class="{ disabled: !currentVote }"
>
Hand DOWN
{{ locale.vote.handDown }}
</div>
<div
class="button demon"
@click="vote(true)"
:class="{ disabled: currentVote }"
>
Hand UP
{{ locale.vote.handUp }}
</div>
</div>
</template>
<div v-else-if="!player">
Please claim a seat to vote.
{{ locale.vote.seatToVote }}
</div>
</div>
<transition name="blur">
@ -103,7 +103,7 @@
<span>3</span>
<span>2</span>
<span>1</span>
<span>GO</span>
<span>{{ locale.vote.doVote }}</span>
<audio
:autoplay="!grimoire.isMuted"
src="../assets/sounds/countdown.mp3"
@ -120,7 +120,7 @@ import { mapGetters, mapState } from "vuex";
export default {
computed: {
...mapState("players", ["players"]),
...mapState(["session", "grimoire"]),
...mapState(["session", "grimoire", "locale"]),
...mapGetters({ alive: "players/alive" }),
nominator: function() {
return this.players[this.session.nomination[0]];

View file

@ -1,7 +1,7 @@
<template>
<Modal class="editions" v-if="modals.edition" @close="toggleModal('edition')">
<div v-if="!isCustom">
<h3>Select an edition:</h3>
<h3>{{ locale.modal.edition.title }}</h3>
<ul class="editions">
<li
v-for="edition in editions"
@ -24,29 +24,27 @@
backgroundImage: `url(${require('../../assets/editions/custom.png')})`
}"
>
Custom Script / Characters
{{ locale.modal.edition.custom.button }}
</li>
</ul>
</div>
<div class="custom" v-else>
<h3>Load custom script / characters</h3>
To play with a custom script, you need to select the characters you want
to play with in the official
<h3>{{ locale.modal.edition.custom.title }}</h3>
{{ locale.modal.edition.custom.introStart }}
<a href="https://script.bloodontheclocktower.com/" target="_blank"
>Script Tool</a
>{{ locale.modal.edition.custom.scriptTool }}</a
>
and then upload the generated "custom-list.json" either directly here or
provide a URL to such a hosted JSON file.<br />
{{ locale.modal.edition.custom.introEnd }}.<br />
<br />
To play with custom characters, please read
{{ locale.modal.edition.custom.instructionsStart }}
<a
href="https://github.com/bra1n/townsquare#custom-characters"
target="_blank"
>the documentation</a
>{{ locale.modal.edition.custom.documentation }}n</a
>
on how to write a custom character definition file.
<b>Only load custom JSON files from sources that you trust!</b>
<h3>Some popular custom scripts:</h3>
{{ locale.modal.edition.custom.instructionsEnd }}<br/>
<b>{{ locale.modal.edition.custom.warning }}</b>
<h3>{{ locale.modal.edition.popularScripts }}</h3>
<ul class="scripts">
<li
v-for="(script, index) in scripts"
@ -64,16 +62,20 @@
/>
<div class="button-group">
<div class="button" @click="openUpload">
<font-awesome-icon icon="file-upload" /> Upload JSON
<font-awesome-icon icon="file-upload" />
{{ locale.modal.edition.custom.upload }}
</div>
<div class="button" @click="promptURL">
<font-awesome-icon icon="link" /> Enter URL
<font-awesome-icon icon="link" />
{{ locale.modal.edition.custom.url }}
</div>
<div class="button" @click="readFromClipboard">
<font-awesome-icon icon="clipboard" /> Use JSON from Clipboard
<font-awesome-icon icon="clipboard" />
{{ locale.modal.edition.custom.clipboard }}
</div>
<div class="button" @click="isCustom = false">
<font-awesome-icon icon="undo" /> Back
<font-awesome-icon icon="undo" />
{{ locale.modal.edition.custom.back }}
</div>
</div>
</div>
@ -81,7 +83,6 @@
</template>
<script>
import editionJSON from "../../editions";
import { mapMutations, mapState } from "vuex";
import Modal from "./Modal";
@ -91,7 +92,7 @@ export default {
},
data: function() {
return {
editions: editionJSON,
editions: this.$store.state.editions,
isCustom: false,
scripts: [
[
@ -121,7 +122,9 @@ export default {
]
};
},
computed: mapState(["modals"]),
computed: {
...mapState(["modals", "locale", "editions"])
},
methods: {
openUpload() {
this.$refs.upload.click();
@ -143,7 +146,7 @@ export default {
}
},
promptURL() {
const url = prompt("Enter URL to a custom-script.json file");
const url = prompt(this.locale.prompt.customUrl);
if (url) {
this.handleURL(url);
}
@ -155,7 +158,7 @@ export default {
const script = await res.json();
this.parseRoles(script);
} catch (e) {
alert("Error loading custom script: " + e.message);
alert(this.locale.prompt.customError + ": " + e.message);
}
}
},

View file

@ -1,8 +1,6 @@
<template>
<Modal v-if="modals.fabled && fabled.length" @close="toggleModal('fabled')">
<h3>
Choose a fabled character to add to the game
</h3>
<h3>{{ locale.modal.fabled.title }}</h3>
<ul class="tokens">
<li v-for="role in fabled" :key="role.id" @click="setFabled(role)">
<Token :role="role" />
@ -19,7 +17,7 @@ import Token from "../Token";
export default {
components: { Token, Modal },
computed: {
...mapState(["modals", "fabled", "grimoire"]),
...mapState(["modals", "fabled", "grimoire", "locale"]),
fabled() {
const fabled = [];
this.$store.state.fabled.forEach(role => {

View file

@ -4,7 +4,7 @@
v-if="modals.gameState"
@close="toggleModal('gameState')"
>
<h3>Current Game State</h3>
<h3>{{ locale.modal.gameState.title }}</h3>
<textarea
:value="gamestate"
@input.stop="input = $event.target.value"
@ -13,10 +13,10 @@
></textarea>
<div class="button-group">
<div class="button townsfolk" @click="copy">
<font-awesome-icon icon="copy" /> Copy JSON
<font-awesome-icon icon="copy" /> {{ locale.modal.gameState.copy }}
</div>
<div class="button demon" @click="load" v-if="!session.isSpectator">
<font-awesome-icon icon="cog" /> Load State
<font-awesome-icon icon="cog" /> {{ locale.modal.gameState.load }}
</div>
</div>
</Modal>
@ -49,7 +49,7 @@ export default {
}))
});
},
...mapState(["modals", "players", "edition", "roles", "session"])
...mapState(["modals", "players", "edition", "roles", "session", "locale"])
},
data() {
return {

View file

@ -11,13 +11,13 @@
title="Show Character Reference"
/>
<h3>
Night Order
{{ locale.modal.nightOrder.title }}
<font-awesome-icon icon="cloud-moon" />
{{ edition.name || "Custom Script" }}
{{ edition.name || locale.modal.nightOrder.custom }}
</h3>
<div class="night">
<ul class="first">
<li class="headline">First Night</li>
<li class="headline">{{ locale.modal.nightOrder.firstNight }}</li>
<li
v-for="role in rolesFirstNight"
:key="role.name"
@ -56,7 +56,7 @@
</li>
</ul>
<ul class="other">
<li class="headline">Other Nights</li>
<li class="headline">{{ locale.modal.nightOrder.otherNights }}</li>
<li
v-for="role in rolesOtherNight"
:key="role.name"
@ -109,29 +109,24 @@ export default {
computed: {
rolesFirstNight: function() {
const rolesFirstNight = [];
// add minion / demon infos to night order sheet
// Ajouter minion / demon infos à l'ordre nocturne
if (this.players.length > 6) {
rolesFirstNight.push(
{
id: "evil",
name: "Minion info",
name: this.locale.modal.nightOrder.minionInfo,
firstNight: 5,
team: "minion",
players: this.players.filter(p => p.role.team === "minion"),
firstNightReminder:
"• If more than one Minion, they all make eye contact with each other. " +
"• Show the “This is the Demon” card. Point to the Demon."
firstNightReminder: this.locale.modal.nightOrder.minionInfoDescription
},
{
id: "evil",
name: "Demon info & bluffs",
name: this.locale.modal.nightOrder.demonInfo,
firstNight: 8,
team: "demon",
players: this.players.filter(p => p.role.team === "demon"),
firstNightReminder:
"• Show the “These are your minions” card. Point to each Minion. " +
"• Show the “These characters are not in play” card. Show 3 character tokens of good " +
"characters not in play."
firstNightReminder: this.locale.modal.nightOrder.demonInfoDescription
}
);
}
@ -165,7 +160,7 @@ export default {
rolesOtherNight.sort((a, b) => a.otherNight - b.otherNight);
return rolesOtherNight;
},
...mapState(["roles", "modals", "edition", "grimoire"]),
...mapState(["roles", "modals", "edition", "grimoire", "locale"]),
...mapState("players", ["players", "fabled"])
},
methods: {

View file

@ -11,7 +11,7 @@
title="Show Night Order"
/>
<h3>
Character Reference
{{ locale.modal.reference.title }}
<font-awesome-icon icon="address-card" />
{{ edition.name || "Custom Script" }}
</h3>
@ -21,7 +21,7 @@
:class="['team', team]"
>
<aside>
<h4>{{ team }}</h4>
<h4>{{ locale.modal.reference.teamNames[team] }}</h4>
</aside>
<ul>
<li v-for="role in teamRoles" :class="[team]" :key="role.id">
@ -53,7 +53,7 @@
<div class="team jinxed" v-if="jinxed.length">
<aside>
<h4>Jinxed</h4>
<h4>{{ locale.modal.reference.jinxed }}</h4>
</aside>
<ul>
<li v-for="(jinx, index) in jinxed" :key="index">
@ -140,7 +140,7 @@ export default {
});
return players;
},
...mapState(["roles", "modals", "edition", "grimoire", "jinxes"]),
...mapState(["roles", "modals", "edition", "grimoire", "jinxes", "locale"]),
...mapState("players", ["players"])
},
methods: {

View file

@ -3,7 +3,7 @@
v-if="modals.reminder && availableReminders.length && players[playerIndex]"
@close="toggleModal('reminder')"
>
<h3>Choose a reminder token:</h3>
<h3>Apposer une marque:</h3>
<ul class="reminders">
<li
v-for="reminder in availableReminders"
@ -82,12 +82,12 @@ export default {
}
});
reminders.push({ role: "good", name: "Good" });
reminders.push({ role: "evil", name: "Evil" });
reminders.push({ role: "custom", name: "Custom note" });
reminders.push({ role: "good", name: this.locale.modal.reminder.good });
reminders.push({ role: "evil", name: this.locale.modal.reminder.evil });
reminders.push({ role: "custom", name: this.locale.modal.reminder.custom });
return reminders;
},
...mapState(["modals", "grimoire"]),
...mapState(["modals", "grimoire", "locale"]),
...mapState("players", ["players"])
},
methods: {
@ -95,7 +95,7 @@ export default {
const player = this.$store.state.players.players[this.playerIndex];
let value;
if (reminder.role === "custom") {
const name = prompt("Add a custom reminder note");
const name = prompt(this.locale.prompt.customNote);
if (!name) return;
value = [...player.reminders, { role: "custom", name }];
} else {

View file

@ -1,11 +1,11 @@
<template>
<Modal v-if="modals.role && availableRoles.length" @close="close">
<h3>
Choose a new character for
{{ locale.modal.role.title }}
{{
playerIndex >= 0 && players.length
? players[playerIndex].name
: "bluffing"
: locale.modal.role.bluff
}}
</h3>
<ul class="tokens" v-if="tab === 'editionRoles' || !otherTravelers.size">
@ -36,13 +36,13 @@
class="button"
:class="{ townsfolk: tab === 'editionRoles' }"
@click="tab = 'editionRoles'"
>Edition Roles</span
>{{ locale.modal.role.editionRoles }}</span
>
<span
class="button"
:class="{ townsfolk: tab === 'otherTravelers' }"
@click="tab = 'otherTravelers'"
>Other Travelers</span
>{{ locale.modal.role.otherTravelers }}</span
>
</div>
</Modal>
@ -73,7 +73,7 @@ export default {
availableRoles.push({});
return availableRoles;
},
...mapState(["modals", "roles", "session"]),
...mapState(["modals", "roles", "session", "locale"]),
...mapState("players", ["players"]),
...mapState(["otherTravelers"])
},

View file

@ -4,7 +4,13 @@
v-if="modals.roles && nonTravelers >= 5"
@close="toggleModal('roles')"
>
<h3>Select the characters for {{ nonTravelers }} players:</h3>
<h3>
{{
locale.modal.roles.titleStart +
nonTravelers +
locale.modal.roles.titleEnd
}}:
</h3>
<ul class="tokens" v-for="(teamRoles, team) in roleSelection" :key="team">
<li class="count" :class="[team]">
{{ teamRoles.reduce((a, { selected }) => a + selected, 0) }} /
@ -30,15 +36,12 @@
</ul>
<div class="warning" v-if="hasSelectedSetupRoles">
<font-awesome-icon icon="exclamation-triangle" />
<span>
Warning: there are characters selected that modify the game setup! The
randomizer does not account for these characters.
</span>
<span>{{ locale.modal.roles.warning }}</span>
</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
{{ locale.modal.roles.allowMultiple }}
</label>
<div class="button-group">
<div
@ -49,11 +52,15 @@
}"
>
<font-awesome-icon icon="people-arrows" />
Assign {{ selectedRoles }} characters randomly
{{
locale.modal.roles.assignStart +
selectedRoles +
locale.modal.roles.assignEnd
}}
</div>
<div class="button" @click="selectRandomRoles">
<font-awesome-icon icon="random" />
Shuffle characters
{{ locale.modal.roles.shuffle }}
</div>
</div>
</Modal>
@ -90,7 +97,7 @@ export default {
roles.some(role => role.selected && role.setup)
);
},
...mapState(["roles", "modals"]),
...mapState(["roles", "modals", "locale"]),
...mapState("players", ["players"]),
...mapGetters({ nonTravelers: "players/nonTravelers" })
},

View file

@ -12,7 +12,7 @@
v-if="session.isSpectator"
/>
<h3>Vote history</h3>
<h3>{{ locale.modal.voteHistory.title }}</h3>
<template v-if="!session.isSpectator">
<div class="options">
@ -23,26 +23,26 @@
session.isVoteHistoryAllowed ? 'check-square' : 'square'
]"
/>
Accessible to players
{{ locale.modal.voteHistory.accessibility }}
</div>
<div class="option" @click="clearVoteHistory">
<font-awesome-icon icon="trash-alt" />
Clear for everyone
{{ locale.modal.voteHistory.clear }}
</div>
</div>
</template>
<table>
<thead>
<tr>
<td>Time</td>
<td>Nominator</td>
<td>Nominee</td>
<td>Type</td>
<td>Votes</td>
<td>Majority</td>
<td>{{ locale.modal.voteHistory.time }}</td>
<td>{{ locale.modal.voteHistory.nominator }}</td>
<td>{{ locale.modal.voteHistory.nominee }}</td>
<td>{{ locale.modal.voteHistory.type }}</td>
<td>{{ locale.modal.voteHistory.votes }}</td>
<td>{{ locale.modal.voteHistory.majority }}</td>
<td>
<font-awesome-icon icon="user-friends" />
Voters
{{ locale.modal.voteHistory.voters }}
</td>
</tr>
</thead>
@ -95,7 +95,7 @@ export default {
Modal
},
computed: {
...mapState(["session", "modals"])
...mapState(["session", "modals", "locale"])
},
methods: {
clearVoteHistory() {

View file

@ -4,10 +4,14 @@ import persistence from "./persistence";
import socket from "./socket";
import players from "./modules/players";
import session from "./modules/session";
import editionJSON from "../editions.json";
import rolesJSON from "../roles.json";
import fabledJSON from "../fabled.json";
import jinxesJSON from "../hatred.json";
import {
locale,
rolesJSON,
jinxesJSON,
fabledJSON,
editionJSON
} from "./modules/locale";
Vue.use(Vuex);
@ -120,10 +124,12 @@ export default new Vuex.Store({
voteHistory: false
},
edition: editionJSONbyId.get("tb"),
editions: editionJSON,
roles: getRolesByEdition(),
otherTravelers: getTravelersNotInEdition(),
fabled,
jinxes
jinxes,
locale
},
getters: {
/**

206
src/store/locale/en/ui.json Normal file
View file

@ -0,0 +1,206 @@
{
"menu":{
"grimoire":{
"title": "Grimoire",
"hide": "Hide",
"show": "Show",
"nightSwitch": "Switch to Night",
"daySwitch": "Switch to Day",
"order": "Night order",
"zoom": "Zoom",
"background": "Background image",
"customImages": "Show Custom Images",
"animations": "Disable Animations",
"mute": "Mute Sounds"
},
"session":{
"title":{
"player": "Playing",
"host": "Hosting",
"create": "Live Session"
},
"player": "Join (Player)",
"players": "players",
"host": "host",
"storyteller": "Host (Storyteller)",
"delay": "Delay",
"link": "Copy player link",
"sendRoles": "Send Characters",
"voteHistory": "Vote history",
"leave": "Leave Session"
},
"players":{
"title": "Players",
"add": "Add",
"randomize": "Randomize",
"removeAll": "Remove all"
},
"characters":{
"title": "Characters",
"selectEdition": "Select Edition",
"assign": "Choose & Assign",
"addFabled": "Add Fabled",
"removeAll": "Remove all"
},
"help":{
"title": "Help",
"reference": "Reference Sheet",
"nightOrder": "Night Order Sheet",
"gameState": "Game State JSON",
"discord": "Join Discord",
"source": "Source code"
}
},
"prompt":{
"background": "Enter custom background URL",
"createSession": "Enter a channel number / name for your session",
"sendRoles": "Do you want to distribute assigned characters to all SEATED players?",
"imageOptIn": "Are you sure you want to allow custom images? A malicious script file author might track your IP address this way.",
"joinSession": "Enter the channel number / name of the session you want to join",
"leaveSession": "Are you sure you want to leave the active live game?",
"addPlayer": "Player name",
"randomizeSeatings": "Are you sure you want to randomize seatings?",
"clearPlayers": "Are you sure you want to remove all players?",
"clearRoles": "Are you sure you want to remove all player roles?",
"customUrl": "Enter URL to a custom-script?json file",
"customError": "Error loading custom script",
"customNote": "Add a custom reminder node"
},
"vote":{
"nominated": "nominated",
"votes": "votes",
"inFavor": "in favor",
"majorityIs": "majority is",
"timePerPlayer": "Time per player:",
"countdown": "Countdown",
"restart": "Restart",
"start": "Start",
"pause": "Pause",
"resume": "Resume",
"reset": "Reset",
"close": "Close",
"setMarked": "Mark for execution",
"removeMarked": "Clear mark",
"secondsBetweenVotes": "seconds between votes",
"handDown": "Hand DOWN",
"handUp": "Hand UP",
"seatToVote": "Pleas claim a seat to vote",
"doVote": "Go"
},
"townsquare":{
"others": "Other characters",
"bluffs": "Demon bluffs",
"fabled": "Fabled"
},
"towninfo":{
"addPlayers":"Please add more players!"
},
"player":{
"handUp": "Hand UP",
"handDown": "Hand DOWN",
"cancel": "Cancel",
"swap": "Swap seats with this player",
"move": "Move player to this seat",
"nominate": "Nominate this player",
"ghostVote": "Ghost vote",
"changePronouns": "Change Pronouns",
"changeName": "Rename",
"movePlayer": "Move player",
"swapPlayers": "Swap seats",
"removePlayer": "Remove",
"emptySeat": "Empty seat",
"nomination": "Nomination",
"claimSeat": "Claim seat",
"vacateSeat": "Vacate seat",
"occupiedSeat": "Seat occupied"
},
"intro":{
"header": "Welcome to the (unofficial) Virtual Town Square and Grimoire for Blood on the Clocktower! Please add more players through the",
"menu": "Menu",
"body": "on the top right or by pressing [A]. You can also join a game session by pressing [J].",
"footerStart": "This project is free and open source and can be found on",
"footerEnd": ". It is not affiliated with The Pandemonium Institute. \"Blood on the Clocktower\" is a trademark of Steven Medway and The Pandemonium Institute."
},
"modal":{
"edition":{
"title": "Select an edition:",
"custom": {
"button": "Custom Script / Characters",
"title": "Load custom script / characters",
"introStart": "To play with a custom script, you need to select the characters you want to play with in the official",
"scriptTool": "Script Tool",
"introEnd": "and then upload the generated \"custom-list.json\" either directly here or provide a URL to such a hosted JSON file.",
"instructionsStart": "To play with custom characters, please read",
"documentation": "the documentation",
"instructionsEnd": "on how to write a custom character definition file.",
"warning": "Only load custom JSON files from sources that you trust!",
"popularScripts": "Some popular custom scripts:",
"upload": "Upload JSON",
"url": "Enter URL",
"clipboard": "Use JSON from Clipboard",
"back": "Back"
}
},
"fabled": {
"title": "Choose a fabled character to add to the game "
},
"gameState": {
"title": "Current Game State",
"copy": "Copy JSON",
"load": "Load State"
},
"nightOrder": {
"title": "Night Order",
"custom": "Custom Script",
"firstNight": "First Night",
"otherNights": "Other Nights",
"minionInfo": "Minion info",
"minionInfoDescription": "• If more than one Minion, they all make eye contact with each other. • Show the “This is the Demon” card. Point to the Demon.",
"demonInfo": "Demon info & bluffs",
"demonInfoDescription": "• Show the “These are your minions” card. Point to each Minion. • Show the “These characters are not in play” card. Show 3 character tokens of good characters not in play."
},
"reference": {
"title": "Character Reference",
"jinxed": "Jinxed",
"teamNames": {
"townsfolk": "townfolk",
"outsider": "outsider",
"minion": "minion",
"demon": "demon"
}
},
"reminder": {
"title": "Choose a reminder token:",
"good": "Good",
"evil": "Evil",
"custom": "Custom Note"
},
"role": {
"title": "Choose a new character for",
"bluff": "bluffing",
"editionRoles": "Edition Roles",
"otherTravelers": "Other Travelers"
},
"roles": {
"titleStart": "Select the characters for",
"titleEnd": "players:",
"warning":"Warning: there are characters selected that modify the game setup! The randomizer does not account for these characters.",
"allowMultiple": "Allow duplicate characters",
"assignStart": "Assign ",
"assignEnd": "characters randomly",
"shuffle": "Shuffle characters"
},
"voteHistory":{
"title": "Vote history",
"accessibility": "Accessible to players",
"clear": "Clear for everyone",
"time": "Time",
"nominator": "Nominator",
"nominee": "Nominee",
"type": "Type",
"votes": "Votes",
"majority": "Majority",
"voters": "Voters"
}
}
}

View file

@ -0,0 +1,38 @@
[
{
"id": "tb",
"name": "Trouble Brewing",
"author": "The Pandemonium Institute",
"description": "Clouds roll in over Ravenswood Bluff, engulfing this sleepy town and its superstitious inhabitants in foreboding shadow. Freshly-washed clothes dance eerily on lines strung between cottages. Chimneys cough plumes of smoke into the air. Exotic scents waft through cracks in windows and under doors, as hidden cauldrons lay bubbling. An unusually warm Autumn breeze wraps around vine-covered walls and whispers ominously to those brave enough to walk the cobbled streets.\n\nAnxious mothers call their children home from play, as thunder begins to clap on the horizon. If you listen more closely, however, noises stranger still can be heard echoing from the neighbouring forest. Under the watchful eye of a looming monastery, silhouetted figures skip from doorway to doorway. Those who can read the signs know there is... Trouble Brewing.",
"level": "Beginner",
"roles": [],
"isOfficial": true
},
{
"id": "bmr",
"name": "Bad Moon Rising",
"author": "The Pandemonium Institute",
"description": "The sun is swallowed by a jagged horizon as another winter's day surrenders to the night. Flecks of orange and red decay into deeper browns, the forest transforming in silent anticipation of the coming snow.\n\nRavenous wolves howl from the bowels of a rocky crevasse beyond the town borders, sending birds scattering from their cozy rooks. Travelers hurry into the inn, seeking shelter from the gathering chill. They warm themselves with hot tea, sweet strains of music and hearty ale, unaware that strange and nefarious eyes stalk them from the ruins of this once great city.\n\nTonight, even the livestock know there is a... Bad Moon Rising.",
"level": "Intermediate",
"roles": [],
"isOfficial": true
},
{
"id": "snv",
"name": "Sects & Violets",
"author": "The Pandemonium Institute",
"description": "Vibrant spring gives way to a warm and inviting summer. Flowers of every description blossom as far as the eye can see, tenderly nurtured in public gardens and window boxes overlooking the lavish promenade. Birds sing, artists paint and philosophers ponder life's greatest mysteries inside a bustling tavern as a circus pitches its endearingly ragged tent on the edge of town.\n\nAs the townsfolk bask in frivolity and mischief, indulging themselves in fine entertainment and even finer wine, dark and clandestine forces are assembling. Witches and cults lurk in majestic ruins on the fringes of the community, hosting secret meetings in underground caves and malevolently plotting the downfall of Ravenswood Bluff and its revelers.\n\nThe time is ripe for... Sects & Violets.",
"level": "Intermediate",
"roles": [],
"isOfficial": true
},
{
"id": "luf",
"name": "Laissez Un Faire",
"author": "The Pandemonium Institute",
"description": "",
"level": "Veteran",
"roles": ["balloonist", "savant", "amnesiac", "fisherman", "artist", "cannibal", "mutant", "lunatic", "widow", "goblin", "leviathan"],
"isOfficial": true
}
]

View file

@ -0,0 +1,146 @@
[
{
"id": "doomsayer",
"name": "Prédicateur",
"team": "fabled",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": [],
"setup": false,
"ability": "Si 4 joueurs ou plus sont en vie, chaque joueur vivant peut, une fois par partie, décider qu'un joueur de son propre alignement meurre."
},
{
"id": "angel",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": ["Protégé", "Catastrophe"],
"setup": false,
"name": "Ange",
"team": "fabled",
"ability": "Quelque chose de mauvais peut arriver à la personne que le Narrateur juge la plus responsable de la mort d'un nouveau joueur."
},
{
"id": "buddhist",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": [],
"setup": false,
"name": "Bouddhiste",
"team": "fabled",
"ability": "Chaque jour, les joueurs vétérans ne peuvent pas parler pendant les 2 premières minutes."
},
{
"id": "hellslibrarian",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": ["Catastrophe"],
"setup": false,
"name": "Libraire infernal",
"team": "fabled",
"ability": "Si un joueur ne cède pas la parole quand le Narrateur appelle au silence, quelque chose de mauvais peut lui arriver."
},
{
"id": "revolutionary",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": ["Utilisé"],
"setup": false,
"name": "Révolutionnaire",
"team": "fabled",
"ability": "2 joueurs voisins désignés avant la partie sont obligatoirement du même alignement. Une fois par partie, l'un d'eux peut apparaitre avec un rôle de l'alignement opposé."
},
{
"id": "fiddler",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": [],
"setup": false,
"name": "Violoniste",
"team": "fabled",
"ability": "Une fois par partie, le Démon choisit un joueur Bon en secret: tous les joueurs votent pour décider l'équipe duquel de ces 2 joueurs a gagné."
},
{
"id": "toymaker",
"firstNightReminder": "",
"otherNight": 1,
"otherNightReminder": "Si le Démon pourrait terminer la partie cette nuit, mais qu'il a toujours son marqueur 'nuit sans attaque', il n'agit pas cette nuit (ne le réveillez pas)",
"reminders": ["Nuit sans attaque"],
"setup": false,
"name": "Fabricant de Jouet",
"team": "fabled",
"ability": "Le Démon peut choisir de ne pas attaquer et doit le faire obligatoirement au moins une fois une fois au cours de la partie. Les joueurs Mauvais ont accès aux informations de début de partie normales."
},
{
"id": "fibbin",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": ["Utilisé"],
"setup": false,
"name": "Mensonge",
"team": "fabled",
"ability": "Une fois par partie, un joueur Bon peut recevoir une information fausse."
},
{
"id": "duchess",
"firstNightReminder": "",
"otherNight": 1,
"otherNightReminder": "Reveillez chaque visiteur dans l'ordre un par un. Indiquez à chacun d'entre eux combien de Visiteurs sont mauvais. Excepté celui qui reçoit les fausses informations qui recevra à la place n'importe quel autre nombre.",
"reminders": ["Visiteur 1", "Visiteur 2", "Visiteur 3", "Fausse Info"],
"setup": false,
"name": "Duchesse",
"team": "fabled",
"ability": "Chaque jour, jusqu'à 3 joueurs peuvent choisir de rendre visite au Narrateur. Chaque nuit*, chaque visiteur apprend combien de joueurs étaient Mauvais parmis eux. L'un d'eux apprend une fausse information."
},
{
"id": "sentinel",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": [],
"setup": true,
"name": "Sentinelle",
"team": "fabled",
"ability": "Il peut y avoir un étranger de plus ou de moins."
},
{
"id": "spiritofivory",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": ["Pas de méchant suplémentaire"],
"setup": false,
"name": "Esprit d'Ivoire",
"team": "fabled",
"ability": "Il ne peut pas y avoir plus d'un joueur mauvais supplémentaire."
},
{
"id": "djinn",
"firstNight": 0,
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": [],
"setup": false,
"name": "Djinn",
"team": "fabled",
"ability": "Le Narrateur met en place une règle spéciale et l'explique aux joueurs avant le début de la partie."
},
{
"id": "stormcatcher",
"firstNight": 1,
"firstNightReminder": "Marquez un joueur comme \"Sûr\". Réveillez chaque joueur Mauvais et indiquez lui qui est ce joueur.",
"otherNightReminder": "",
"reminders": ["Sûr"],
"setup": false,
"name": "Chasse tempête",
"team": "fabled",
"ability": "Désignez un rôle de gentil. S'il est en jeu, il ne peut mourrir que par exécution, mais les joueurs mauvais savent qui a ce rôle."
},
{
"id": "deusexfiasco",
"firstNightReminder": "",
"otherNightReminder": "",
"reminders": ["Whoops"],
"setup": false,
"name": "Deus ex Fiasco",
"team": "fabled",
"ability": "Une fois par partie, le Narrateur fait une \"Erreur\", il la corrige et l'admet publiquement."
}
]

View file

@ -0,0 +1,363 @@
[
{
"id": "Chambermaid",
"hatred": [
{
"id": "Mathematician",
"reason": "La femme de chambre apprend si le Mathématicien va se réveiller, même si elle se réveille avant lui. "
}
]
},
{
"id": "Butler",
"hatred": [
{
"id": "Cannibal",
"reason": "Si le Cannibale gagne le pouvoir du Majordome, il l'apprend. "
}
]
},
{
"id": "Lunatic",
"hatred": [
{
"id": "Mathematician",
"reason": "Le Mathématicien apprend si le Lunatique attaque des joueurs différents du véritable Démon. "
}
]
},
{
"id": "Pit-Hag",
"hatred": [
{
"id": "Heretic",
"reason": "Le chaudronnier ne peut pas créer un Hérétique. "
},
{
"id": "Damsel",
"reason": "Si le Chaudronnier crée une Demoiselle, c'est le Narrateur qui choisi quel joueur le devient. "
},
{
"id": "Politician",
"reason": "Un Chaudronnier ne peut pas créer un Politicien Méchant. "
}
]
},
{
"id": "Cerenovus",
"hatred": [
{
"id": "Goblin",
"reason": "Le Cerenovus peut choisir de rendre un joueur fou d'être le Goblin. "
}
]
},
{
"id": "Leviathan",
"hatred": [
{
"id": "Soldier",
"reason": "Si une accusation par le Léviathan aboutit à l'exécution du Soldat, le Soldat ne meurt pas. "
},
{
"id": "Monk",
"reason": "Si une accusation par le Léviathan aboutit à l'éxécution d'un joueur protégé par le Moine, ce joueur ne meurt pas. "
},
{
"id": "Innkeeper",
"reason": "Si une accusation par le Léviathan aboutit à l'éxécution d'un joueur protégé par l'Aubergiste, ce joueur ne meurt pas. "
},
{
"id": "Ravenkeeper",
"reason": "Si le Léviathan est en jeu et que le Corbeau meurt par exécution, Il se réveille cette nuit pour utiliser son pouvoir. "
},
{
"id": "Sage",
"reason": "Si le Léviathan est en jeu et que le Sage meurt par execution, Il se réveille cette nuit pour utiliser son pouvoir. "
},
{
"id": "Farmer",
"reason": "Si le Léviathan est en jeu et que le Fermier meurt par éxécution, Un joueur gentil devient un Fermier cette nuit. "
},
{
"id": "Mayor",
"reason": "Si le Léviathan est en jeu et qu'il n'y a pas d'exécution le cinquième jour, Les méchants remportent la partie. "
}
]
},
{
"id": "Al-Hadikhia",
"hatred": [
{
"id": "Scarlet Woman",
"reason": "S'il y a deux Al-Hadikhias en vie, le Gourgandin devenu Al-Hadikhia redevient Gourgandin. "
},
{
"id": "Mastermind",
"reason": "Un seul personnage maudit peut être en jeu à la fois. Les joueurs Mauvais commencent la partie en sachant de quel joueur et quel rôle il s'agit. "
}
]
},
{
"id": "Lil' Monsta",
"hatred": [
{
"id": "Poppy Grower",
"reason": "Si le Cultivateur de Pavots est en jeu, Les Serviteurs ne se réveillent pas ensemble. Ils sont réveillés un par un jusqu'à ce que l'un d'entre eux décide d'être babysitter. "
},
{
"id": "Magician",
"reason": "Un seul personnage maudit peut être en jeu. "
},
{
"id": "Scarlet Woman",
"reason": "S'il y a 5 joueurs ou plus en vie et que le babysitter du Bébé Monstre se fait exécuter, le Gourgandin récupére le Bébé Monstre cette nuit. "
}
]
},
{
"id": "Lycanthrope",
"hatred": [
{
"id": "Gambler",
"reason": "Si le Lycanthrope est en vie et que le Joueur se suicide la nuit, aucun autre joueur ne peut mourrir cette nuit. "
}
]
},
{
"id": "Legion",
"hatred": [
{
"id": "Engineer",
"reason": "Legion et l'Ingénieur ne peuvent pas être tous les deux en jeu au début de la partie. Si l'Ingénieur crée une Légion, la moité des joueurs (Y compris les Mauvais joueurs) deviennent de Mauvaises Légions. "
},
{
"id": "Preacher",
"reason": "Un seul personnage Maudit peut être en jeu à la fois. "
}
]
},
{
"id": "Fang Gu",
"hatred": [
{
"id": "Scarlet Woman",
"reason": "Si le Fang Gu désigne un étranger et meurt, Le Gourgandin ne devient pas le Fang Gu . "
}
]
},
{
"id": "Spy",
"hatred": [
{
"id": "Magician",
"reason": "Quand l'Espion regarde le grimoire, les jetons du Démon et du Magicien sont retirés. "
},
{
"id": "Alchemist",
"reason": "L'alchimiste ne peut pas avoir la capacité de l'Espion. "
},
{
"id": "Poppy Grower",
"reason": "Si le Cultivateur de Pavot est en jeu, l'Espion ne peut pas voir le Grimoire avant la mort du Cultivateur de Pavots. "
},
{
"id": "Damsel",
"reason": "Un seul personnage maudit peut être en jeu. "
},
{
"id": "Heretic",
"reason": "Un seul personnage maudit peut être en jeu. "
}
]
},
{
"id": "Widow",
"hatred": [
{
"id": "Magician",
"reason": "Quand la Veuve regarde le Grimoire, Les jetons du Démon et du Magicien sont retirés. "
},
{
"id": "Poppy Grower",
"reason": "Si le Cultivateur de Pavot est en jeu, La Veuve ne voit pas le Grimoire avant la mort du Cultivateur de Pavot. "
},
{
"id": "Alchemist",
"reason": "L'alchimiste ne peut pas avoir la capacité de la Veuve. "
},
{
"id": "Damsel",
"reason": "Un seul personnage maudit peut être en jeu. "
},
{
"id": "Heretic",
"reason": "Un seul personnage maudit peut être en jeu. "
}
]
},
{
"id": "Godfather",
"hatred": [
{
"id": "Heretic",
"reason": "Un seul personnage maudit peut être en jeu. "
}
]
},
{
"id": "Marionette",
"hatred": [
{
"id": "Lil' Monsta",
"reason": "La Marionette est voisine d'un Serviteur, pas du Démon. La Marionette n'est pas réveillée pour décider qui babysit le Bébé Monstre. "
},
{
"id": "Poppy Grower",
"reason": "Quand le Cultivateur de Pavots meurt, le Démon apprend qui est la Marionette, mais la Marionette n'apprend rien. "
},
{
"id": "Snitch",
"reason": "La Marionette n'apprend pas 3 rôles qui ne sont pas en jeu. Le Démon en apprend 3 suplémentaires à la place. "
},
{
"id": "Balloonist",
"reason": "Si la Marionette pense être le Montgolfier, [+1 Etranger]. "
},
{
"id": "Damsel",
"reason": "La Marionette n'apprend pas qu'une Demoiselle est en jeu. "
},
{
"id": "Huntsman",
"reason": "Si la Marionette croit être un Chasseur, Une Demoiselle est ajoutée au jeu. "
}
]
},
{
"id": "Riot",
"hatred": [
{
"id": "Engineer",
"reason": "Emeute et Ingénieur ne peuvent pas être tous les deux en jeu au début de la partie. \nSi l'ingénieur crée une Emeute, tous les joueurs Mauvais deviennent des Emeutes. "
},
{
"id": "Golem",
"reason": "Si le Golem accuse une Emeute, ce joueur ne meurt pas. "
},
{
"id": "Snitch",
"reason": "Si la Balance est en jeu, chaque joueur qui a le rôle d'Emeute reçoit 3 bluffs supplémentaires. "
},
{
"id": "Saint",
"reason": "Si un joueur Gentil accuse et exécute le Saint, l'équipe du Saint perd. "
},
{
"id": "Butler",
"reason": "Le Majordome ne peut pas Accuser son Maître. "
},
{
"id": "Pit-Hag",
"reason": "Si le Chaudronnier crée une Emeute, tous les joueurs Mauvais deviennent des Emeutes. \nSi le Chaudronnier crée une Emeute après le jour 3, la partie continue pour un jour de plus. "
},
{
"id": "Mayor",
"reason": "Si le 3ème jour commence avec seulement 3 joueurs en vie, les joueurs peuvent collectivement décider de ne pas accuser. S'il le font (et que le Maire est en vie) l'équipe du Maire gagne la partie. "
},
{
"id": "Monk",
"reason": "Si un joueur d'Emeute accuse un joueur protégé par le Moine, ce joueur ne meurt pas. "
},
{
"id": "Farmer",
"reason": "Si un joueur d'Emeute accuse et tue un Fermier, le Fermier utilise son Pouvoir cette nuit. "
},
{
"id": "Innkeeper",
"reason": "Si un joueur d'Emeute accuse et tue un joueur protégé par l'Aubergiste, ce joueur ne meurt pas. "
},
{
"id": "Sage",
"reason": "Si un joueur d'Emeute accuse et tue le Sage, le Sage utilise son pouvoir cette nuit. "
},
{
"id": "Ravenkeeper",
"reason": "Si un Joueur d'Emeute accuse et tue le Corbeau, le Corbeau utilise son pouvoir cette nuit. "
},
{
"id": "Soldier",
"reason": "Si un joueur d'Emeute accuse le Soldat, le Soldat ne meurt pas. "
},
{
"id": "Grandmother",
"reason": "Si un joueur d'Emeute accuse et tue la Grand-mère, le Petit-Fils meurt aussi. "
},
{
"id": "King",
"reason": "Si une joueur d'Emeute accuse et tue le Roi et que l'Enfant de Choeur est en vie, l'Enfant de Choeur utilise son pouvoir cette nuit. "
},
{
"id": "Exorcist",
"reason": "Un seul personnage maudit peut être en jeu. "
},
{
"id": "Minstrel",
"reason": "Un seul personnage maudit peut être en jeu. "
},
{
"id": "Flowergirl",
"reason": "Un seul personnage maudit peut être en jeu. "
},
{
"id": "Undertaker",
"reason": "Les joueurs qui meurent par accusation sont considérés comme exécutés pour le Fossoyeur. "
},
{
"id": "Cannibal",
"reason": "Les joueurs qui meurent par accusation sont considérés comme exécutés pour le Cannibale. "
},
{
"id": "Pacifist",
"reason": "Les joueurs qui meurent par accusation sont considérés comme exécutés pour le Pacifiste. "
},
{
"id": "Devil's Advocate",
"reason": "Les joueurs qui meurent par accusation sont considérés comme exécutés pour l'Avocat du Diable. "
},
{
"id": "Investigator",
"reason": "Emeute est considéré comme un Serviteur pour l'Enquéteur. "
},
{
"id": "Clockmaker",
"reason": "Emeute est considéré comme un Serviteur pour l'Horloger. "
},
{
"id": "Town Crier",
"reason": "Emeute est considéré comme un Serviteur pour la Criée. "
},
{
"id": "Damsel",
"reason": "Emeute est considéré comme un Serviteur pour la Demoiselle. "
},
{
"id": "Preacher",
"reason": "Emeute est considéré comme un Serviteur pour le précheur. "
}
]
},
{
"id": "Lleech",
"hatred": [
{
"id": "Mastermind",
"reason": "Si le Cerveau est en vie et que la Sangsue meurt par execution, la Sangsue survie mais perd son pouvoir. "
},
{
"id": "Slayer",
"reason": "Si le Tueur tire sur l'hôte de la Sangsue, l'hôte meurt. "
}
]
}
]

File diff suppressed because it is too large Load diff

206
src/store/locale/fr/ui.json Normal file
View file

@ -0,0 +1,206 @@
{
"menu":{
"grimoire":{
"title": "Grimoire",
"hide": "Cacher",
"show": "Montrer",
"nightSwitch": "Passer à la nuit",
"daySwitch": "Passer au jour",
"order": "Ordre Nocturne",
"zoom": "Zoom",
"background": "Image de fond",
"customImages": "Images Importées",
"animations": "Effets réduits",
"mute": "Silencieux"
},
"session":{
"title":{
"player": "Joueur",
"host": "Hôte",
"create": "Session Live"
},
"player": "Joueur",
"players": "Joueurs",
"host": "Hôte",
"storyteller": "Hôte (Narrateur)",
"delay": "Délais",
"link": "Copier Lien Joueurs",
"sendRoles": "Envoyer les rôles",
"voteHistory": "Historique Votes",
"leave": "Quitter Session"
},
"players":{
"title": "Joueurs",
"add": "Ajouter",
"randomize": "Mélanger Sièges",
"removeAll": "Retirer Joueurs"
},
"characters":{
"title": "Personnages",
"selectEdition": "Choisir Scénario",
"assign": "Attribuer Rôles",
"addFabled": "Ajouter Fabuleux",
"removeAll": "Effacer Rôles"
},
"help":{
"title": "Aide",
"reference": "Référence rôles",
"nightOrder": "Ordre Nocturne",
"gameState": "Etat JSON du jeu",
"discord": "Rejoindre Discord",
"source": "Code Source"
}
},
"prompt":{
"background": "Entrez l'URL de l'image de fond",
"createSession": "Entrez un nom ou numéro de session",
"sendRoles": "Voulez-vous envoyer les rôles à tous les joueurs ASSIS ?",
"imageOptIn": "Etes-vous sûr de vouloir autoriser les images externes ? Un auteur de script mal intentionné pourrait s'en servir pour traquer votre adresse IP.",
"joinSession": "Entrez le nom ou numéro de la session que vous souhaitez rejoindre",
"leaveSession": "Etes-vous sur de vouloir quitter la partie en cours ?",
"addPlayer": "Nom du joueur",
"randomizeSeatings": "Êtes-vous sur de vouloir mélanger les sièges ?",
"clearPlayers": "Etes-vous sur de vouloir supprimer tous les joueurs ?",
"clearRoles": "Etes-vous sur de vouloir effacer tous les rôles ?",
"customUrl": "Entrer l'URL du fichier JSON de personnalisation de script",
"customError": "Erreur lors du chargement du script",
"customNote": "Ajouter une note personnalisée"
},
"vote":{
"nominated": "accuse",
"votes": "votes",
"inFavor": "pour",
"majorityIs": "majorité à",
"timePerPlayer": "Temps par Joueur :",
"countdown": "Compte à rebours",
"restart": "Relancer",
"start": "Démarrer",
"pause": "Pause",
"resume": "Continuer",
"reset": "Annuler",
"close": "Fermer",
"setMarked": "Marquer comme condamné",
"removeMarked": "Effacer la marque",
"secondsBetweenVotes": "secondes par vote",
"handDown": "Baisser la Main",
"handUp": "Lever la Main",
"seatToVote": "Asseyez-vous pour pouvoir voter",
"doVote": "Votez"
},
"townsquare":{
"others": "Autres Rôles",
"bluffs": "Bluffs de Démon",
"fabled": "Fabuleux"
},
"towninfo":{
"addPlayers":"Appuyez sur [A] pour ajouter plus de joueurs !"
},
"player":{
"handUp": "Main levée",
"handDown": "Main baissée",
"cancel": "Annuler",
"swap": "Echanger de siège avec ce joueur",
"move": "Déplacer le joueur vers ce siège",
"nominate": "Accuse ce joueur",
"ghostVote": "Vote Fantôme",
"changePronouns": "Changer de Pronom",
"changeName": "Renommer",
"movePlayer": "Déplacer le joueur",
"swapPlayers": "Echanger les sièges",
"removePlayer": "Retirer le joueur",
"emptySeat": "Vider le siège",
"nomination": "Accusation",
"claimSeat": "S'asseoir ici",
"vacateSeat": "Libérer le Siège",
"occupiedSeat": "Siège Occupé"
},
"intro":{
"header": "Bienvenue sur le Centre-ville Virtuel (non-officiel) pour Blood on the Clocktower! Veuillez ajouter des Joueurs via le",
"menu": "Menu",
"body": "en haut à droite ou en appuyant sur [A] pour commencer. Vous pouvez aussi rejoindre une ssession en appuyant sur [J].",
"footerStart": "Ce programme est libre et ses sources peuvent être trouvées sur",
"footerEnd": ". Ce site n'est pas affilié à The Pandemonium Institute. \"Blood on the Clocktower\" est une marque déposée de Steven Medway & The Pandemonium Institute."
},
"modal":{
"edition":{
"title": "Choisir un Scénario :",
"custom": {
"button": "Scénario Perso / Personnages",
"title": "Charger un Scénario Perso",
"introStart": "Pour jouer avec un script personnalisé, vous pouvez sélectionner les personnages de votre choix grace à l'",
"scriptTool": "outil officiel d'édition de scripts",
"introEnd": "puis téléverser le fichier json généré directement ici ou depuis une URL hébergée.",
"instructionsStart": "Pour plus d'informations, vous pouvez vous réferrer à",
"documentation": "la documentation",
"instructionsEnd": "pour apprendre comment rédiger des fiches de personnages personnalisés.",
"warning": "Ne chargez des fichier JSON qu'à partir de sources de confiance !",
"popularScripts": "Quelques scénarios populaires :",
"upload": "Téléversersement JSON",
"url": "Entrer une URL",
"clipboard": "Presse-papier",
"back": "Retour"
}
},
"fabled": {
"title": "Choisissez un personnage fabuleux à ajouter à la partie"
},
"gameState": {
"title": "Etat Actuel de la Partie",
"copy": "Copier JSON",
"load": "Charger l'état"
},
"nightOrder": {
"title": "Ordre Nocturne",
"custom": "Scénario Perso",
"firstNight": "Première Nuit",
"otherNights": "Autres Nuits",
"minionInfo": "Informations Serviteurs",
"minionInfoDescription": "• S'il y a plusieurs Serviteurs, ils apprennent qui sont les autres Serviteurs. • Indiquez aux Serviteurs qui est le Démon.",
"demonInfo": "Info & Bluffs Démon",
"demonInfoDescription": "• Indiquez au Démon qui sont ses serviteurs.• Indiquez les rôles de 3 personnages Bons qui ne sont pas en jeu."
},
"reference": {
"title": "Réference de rôles",
"jinxed": "Jinx",
"teamNames": {
"townsfolk": "villageois",
"outsider": "étranger",
"minion": "serviteur",
"demon": "démon"
}
},
"reminder": {
"title": "Apposer une note:",
"good": "Bon",
"evil": "Mauvais",
"custom": "Note"
},
"role": {
"title": "Choisissez un personnage pour",
"bluff": "bluffer",
"editionRoles": "Rôles de ce Scénario",
"otherTravelers": "Autres Voyageurs"
},
"roles": {
"titleStart": "Selectionner les personnages pour",
"titleEnd": "joueurs:",
"warning":"Attention: certains des personnages sélectionnés changent la distribution de début de partie ! La distribution aléatoire n'effectue pas elle même ces changements, pensez à modifier les attributions en conséquence avant d'envoyer les rôles aux joueurs.",
"allowMultiple": "Permettre les doublons de personnages",
"assignStart": "Attribuer aléatoirement ces ",
"assignEnd": "rôles",
"shuffle": "Tirer les personnages au sort"
},
"voteHistory":{
"title": "Historique de votes",
"accessibility": "Accessible aux Joueurs",
"clear": "Effacer pour tous",
"time": "Temps",
"nominator": "Accusateur",
"nominee": "Accusé",
"type": "Type",
"votes": "Voix",
"majority": "Majorité",
"voters": "Votants"
}
}
}

View file

@ -0,0 +1,30 @@
const supportedLanguages = ["en", "fr"];
const MASTER_LANGUAGE = "en";
const userLanguages = window.navigator.languages;
let usedLanguage = null;
for (let lang of userLanguages) {
if (supportedLanguages.includes(lang)) {
console.log(`setting to ${lang} locale`);
usedLanguage = lang; // use first fully supported locale found in the user's browser's settings
break;
}
}
if (usedLanguage === null) {
for (let lang of userLanguages) {
if (supportedLanguages.includes(lang.substring(0, 2))) {
console.log(`setting to ${lang.substring(0, 2)} language`);
usedLanguage = lang.substring(0, 2); // use first supported language found in the user's browser's settings
break;
}
}
}
if (!usedLanguage) {
usedLanguage = MASTER_LANGUAGE; // set to master language if no language is supported by both the user and the application
}
export const locale = require(`../locale/${usedLanguage}/ui.json`);
export const rolesJSON = require(`../locale/${usedLanguage}/roles.json`);
export const jinxesJSON = require(`../locale/${usedLanguage}/hatred.json`);
export const fabledJSON = require(`../locale/${usedLanguage}/fabled.json`);
export const editionJSON = require(`../locale/${usedLanguage}/editions.json`);