Upgrade node to 22, bundle with vite

Updated docker compose for --watch usage
This commit is contained in:
Pingumask 2024-12-09 22:17:59 +01:00
parent b054ce9f87
commit 67ff3ddf6a
57 changed files with 1518 additions and 7859 deletions

View file

@ -14,7 +14,7 @@ jobs:
- name: Setup node version - name: Setup node version
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: "18"
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: npm install - run: npm install
- run: npm run lint-ci - run: npm run lint-check

View file

@ -4,6 +4,12 @@
### Version 3.23.0
- Upgraded from node 18 to node 22
- Replaced vue-cli with Vite
- Set up for docker watch
### Version 3.22.0 ### Version 3.22.0
- Official abilities rebalances : - Official abilities rebalances :

View file

@ -1,11 +1,9 @@
FROM node:18 FROM node:22
RUN apt update && apt install -y\ RUN apt update && apt install -y\
git\ git\
&& apt clean && apt clean
WORKDIR /app/townsquare WORKDIR /app/townsquare
COPY package*.json .
RUN npm rebuild
RUN npm clean-install
# npm rebuild avoids having misconfigurations if npm install has been run in the folder from windows before building the docker image
COPY . . COPY . .
CMD ["npm","run","serve"] RUN npm rebuild && npm clean-install
EXPOSE 5173 8079
CMD ["npm","run","dev"]

View file

@ -2,10 +2,16 @@ module.exports = {
root: true, root: true,
env: { env: {
node: true, node: true,
es2022: true,
browser: true,
}, },
extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"], extends: [
"plugin:vue/essential",
"eslint:recommended",
"@vue/prettier"
],
parserOptions: { parserOptions: {
ecmaVersion: 2020, ecmaVersion: 2022,
}, },
rules: { rules: {
"no-console": process.env.NODE_ENV === "production" ? 1 : 0, "no-console": process.env.NODE_ENV === "production" ? 1 : 0,

View file

@ -8,22 +8,22 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<title>Blood on the Clocktower Town Square</title> <title>Blood on the Clocktower Town Square</title>
<link rel="apple-touch-icon" sizes="57x57" href="static/apple-icon-57x57.png"> <link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/apple-icon-60x60.png"> <link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="static/apple-icon-72x72.png"> <link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="static/apple-icon-76x76.png"> <link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="static/apple-icon-114x114.png"> <link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="static/apple-icon-120x120.png"> <link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="static/apple-icon-144x144.png"> <link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="static/apple-icon-152x152.png"> <link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="static/apple-icon-180x180.png"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="static/android-icon-192x192.png"> <link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="static/favicon-96x96.png"> <link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="static/manifest.json"> <link rel="manifest" href="/manifest.json">
<meta name="msapplication-TileColor" content="#000000"> <meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="/static/ms-icon-144x144.png"> <meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
<meta name="theme-color" content="#ff0000"> <meta name="theme-color" content="#ff0000">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto+Condensed&display=swap" rel="stylesheet">
<meta name="description" content="A free, virtual Blood on the Clocktower Town Square and Grimoire to help you run (online) games both as a storyteller and player. Batteries included!"> <meta name="description" content="A free, virtual Blood on the Clocktower Town Square and Grimoire to help you run (online) games both as a storyteller and player. Batteries included!">
@ -41,5 +41,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./src/main.js"></script>
</body> </body>
</html> </html>

8551
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,36 @@
{ {
"name": "townsquare", "name": "townsquare",
"version": "3.22.0", "version": "3.23.0",
"description": "Blood on the Clocktower Town Square", "description": "Blood on the Clocktower Town Square",
"author": "Pingumaskt", "author": "Pingumaskt",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite --host 0.0.0.0",
"build": "vue-cli-service build ./src/main.js", "build": "vite build",
"lint": "vue-cli-service lint", "serve": "vite preview",
"lint-ci": "vue-cli-service lint --no-fix --max-warnings=0" "lint": "eslint src --fix",
"lint-check": "eslint src --no-fix --max-warnings=0"
}, },
"type": "module",
"main": "App.vue", "main": "App.vue",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/fontawesome-svg-core": "^6.7.1",
"@fortawesome/free-brands-svg-icons": "^5.15.1", "@fortawesome/free-brands-svg-icons": "^6.7.1",
"@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^6.7.1",
"@fortawesome/vue-fontawesome": "^0.1.10", "@fortawesome/vue-fontawesome": "^2.0.10",
"@vue/cli-service": "^5.0.8", "@vitejs/plugin-vue2": "^2.3.3",
"prom-client": "^13.0.0", "vite": "^6.0.3",
"vue": "^2.7.16", "vue": "^2.7.16",
"vue-template-compiler": "^2.7.15", "vue-template-compiler": "^2.7.15",
"vuex": "^3.6.2", "vuex": "^3.6.2",
"ws": "^7.4.6" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-eslint": "^5.0.8", "@vue/eslint-config-prettier": "^10.1.0",
"@vue/eslint-config-prettier": "^8.0.0", "eslint": "^9.16.0",
"eslint": "^8.53.0", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-vue": "^9.32.0",
"eslint-plugin-vue": "^9.18.1",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"sass": "^1.82.0", "sass": "^1.82.0"
"sass-loader": "^16.0.4"
}, },
"keywords": [ "keywords": [
"botc", "botc",
@ -44,6 +44,6 @@
"url": "https://github.com//bra1n/townsquare.git" "url": "https://github.com//bra1n/townsquare.git"
}, },
"engines": { "engines": {
"node": "^18" "node": "^22"
} }
} }

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View file

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View file

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View file

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View file

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -43,20 +43,20 @@
<script> <script>
import { mapState } from "vuex"; import { mapState } from "vuex";
import app from "../package.json"; import app from "../package.json";
import TownSquare from "./components/TownSquare"; import TownSquare from "./components/TownSquare.vue";
import TownInfo from "./components/TownInfo"; import TownInfo from "./components/TownInfo.vue";
import Menu from "./components/Menu"; import Menu from "./components/Menu.vue";
import RolesModal from "./components/modals/RolesModal"; import RolesModal from "./components/modals/RolesModal.vue";
import EditionModal from "./components/modals/EditionModal"; import EditionModal from "./components/modals/EditionModal.vue";
import Intro from "./components/Intro"; import Intro from "./components/Intro.vue";
import ReferenceModal from "./components/modals/ReferenceModal"; import ReferenceModal from "./components/modals/ReferenceModal.vue";
import Vote from "./components/Vote"; import Vote from "./components/Vote.vue";
import Gradients from "./components/Gradients"; import Gradients from "./components/Gradients.vue";
import NightOrderModal from "./components/modals/NightOrderModal"; import NightOrderModal from "./components/modals/NightOrderModal.vue";
import FabledModal from "@/components/modals/FabledModal"; import FabledModal from "./components/modals/FabledModal.vue";
import VoteHistoryModal from "@/components/modals/VoteHistoryModal"; import VoteHistoryModal from "./components/modals/VoteHistoryModal.vue";
import GameStateModal from "@/components/modals/GameStateModal"; import GameStateModal from "./components/modals/GameStateModal.vue";
import SpecialVoteModal from "@/components/modals/SpecialVoteModal"; import SpecialVoteModal from "./components/modals/SpecialVoteModal.vue";
export default { export default {
components: { components: {

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="intro"> <div class="intro">
<img src="static/apple-icon.png" alt="" class="logo" /> <img src="../../public/apple-icon.png" alt="" class="logo" />
<div> <div>
{{ locale.intro.header }} {{ locale.intro.header }}
<span class="button" @click="toggleMenu"> <span class="button" @click="toggleMenu">

View file

@ -242,11 +242,7 @@
backgroundImage: `url(${ backgroundImage: `url(${
reminder.image && grimoire.isImageOptIn reminder.image && grimoire.isImageOptIn
? reminder.image ? reminder.image
: require( : rolePath(reminder.role)
'../assets/icons/' +
(reminder.imageAlt || reminder.role) +
'.png',
)
})`, })`,
}" }"
></span> ></span>
@ -261,7 +257,7 @@
</template> </template>
<script> <script>
import Token from "./Token"; import Token from "./Token.vue";
import { mapGetters, mapState } from "vuex"; import { mapGetters, mapState } from "vuex";
export default { export default {
@ -356,6 +352,12 @@ export default {
reminders.splice(this.player.reminders.indexOf(reminder), 1); reminders.splice(this.player.reminders.indexOf(reminder), 1);
this.updatePlayer("reminders", reminders, true); this.updatePlayer("reminders", reminders, true);
}, },
rolePath(role) {
return new URL(
`../assets/icons/${role.imageAlt || role.id}.png`,
import.meta.url,
).href;
},
updatePlayer(property, value, closeMenu = false) { updatePlayer(property, value, closeMenu = false) {
if ( if (
this.session.isSpectator && this.session.isSpectator &&

View file

@ -5,9 +5,7 @@
v-if="role.id" v-if="role.id"
:style="{ :style="{
backgroundImage: `url(${ backgroundImage: `url(${
role.image && grimoire.isImageOptIn role.image && grimoire.isImageOptIn ? role.image : rolePath
? role.image
: require('../assets/icons/' + (role.imageAlt || role.id) + '.png')
})`, })`,
}" }"
></span> ></span>
@ -64,6 +62,12 @@ export default {
(this.role.remindersGlobal || []).length (this.role.remindersGlobal || []).length
); );
}, },
rolePath() {
return new URL(
`../assets/icons/${this.role.imageAlt || this.role.id}.png`,
import.meta.url,
).href;
},
...mapState(["grimoire"]), ...mapState(["grimoire"]),
}, },
data() { data() {

View file

@ -5,9 +5,7 @@
:class="['edition-' + edition.id]" :class="['edition-' + edition.id]"
:style="{ :style="{
backgroundImage: `url(${ backgroundImage: `url(${
edition.logo && grimoire.isImageOptIn edition.logo && grimoire.isImageOptIn ? edition.logo : logoPath
? edition.logo
: require('../assets/logos/' + edition.id + '.png')
})`, })`,
}" }"
></li> ></li>
@ -102,15 +100,16 @@
</template> </template>
<script> <script>
import gameJSON from "./../game"; import gameJSON from "../game.json";
import { mapState } from "vuex"; import { mapState } from "vuex";
import Countdown from "./Countdown"; import Countdown from "./Countdown.vue";
export default { export default {
components: { components: {
Countdown, Countdown,
}, },
computed: { computed: {
logoPath: () => `../assets/logos/${this?.edition?.id ?? "custom"}.png`,
teams: function () { teams: function () {
const { players } = this.$store.state.players; const { players } = this.$store.state.players;
const nonTravelers = this.$store.getters["players/nonTravelers"]; const nonTravelers = this.$store.getters["players/nonTravelers"];

View file

@ -192,10 +192,10 @@
<script> <script>
import { mapGetters, mapState } from "vuex"; import { mapGetters, mapState } from "vuex";
import Player from "./Player"; import Player from "./Player.vue";
import Token from "./Token"; import Token from "./Token.vue";
import ReminderModal from "./modals/ReminderModal"; import ReminderModal from "./modals/ReminderModal.vue";
import RoleModal from "./modals/RoleModal"; import RoleModal from "./modals/RoleModal.vue";
export default { export default {
components: { components: {

View file

@ -169,7 +169,7 @@
<script> <script>
import { mapGetters, mapState } from "vuex"; import { mapGetters, mapState } from "vuex";
import Countdown from "./Countdown"; import Countdown from "./Countdown.vue";
export default { export default {
components: { components: {

View file

@ -42,9 +42,7 @@
class="edition" class="edition"
:class="['edition-' + edition.id]" :class="['edition-' + edition.id]"
:style="{ :style="{
backgroundImage: `url(${require( backgroundImage: `url(${editionPath(edition)})`,
'../../assets/logos/' + edition.id + '.png',
)})`,
}" }"
:key="edition.id" :key="edition.id"
@click="runEdition(edition)" @click="runEdition(edition)"
@ -158,9 +156,9 @@
<script> <script>
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
import Modal from "./Modal"; import Modal from "./Modal.vue";
import { rolesJSON } from "../../store/modules/locale"; import { rolesJSON } from "../../store/modules/locale";
import Token from "../Token"; import Token from "../Token.vue";
export default { export default {
components: { components: {
@ -186,8 +184,7 @@ export default {
methods: { methods: {
initPool() { initPool() {
console.log("init pool"); console.log("init pool");
console.table(this.roles); this.draftPool = rolesJSON.default;
this.draftPool = rolesJSON;
this.resetBuilt(); this.resetBuilt();
for (let [role] of this.roles) { for (let [role] of this.roles) {
this.toggleRole(role); this.toggleRole(role);
@ -327,6 +324,10 @@ export default {
// The editions contain no Fabled // The editions contain no Fabled
this.$store.commit("players/setFabled", { fabled: [] }); this.$store.commit("players/setFabled", { fabled: [] });
}, },
editionPath(edition) {
return new URL(`../../assets/logos/${edition.id}.png`, import.meta.url)
.href;
},
...mapMutations(["toggleModal", "setEdition"]), ...mapMutations(["toggleModal", "setEdition"]),
}, },
}; };

View file

@ -11,8 +11,8 @@
<script> <script>
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
import Modal from "./Modal"; import Modal from "./Modal.vue";
import Token from "../Token"; import Token from "../Token.vue";
export default { export default {
components: { Token, Modal }, components: { Token, Modal },

View file

@ -23,7 +23,7 @@
</template> </template>
<script> <script>
import Modal from "./Modal"; import Modal from "./Modal.vue";
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
export default { export default {

View file

@ -56,11 +56,7 @@
backgroundImage: `url(${ backgroundImage: `url(${
role.image && grimoire.isImageOptIn role.image && grimoire.isImageOptIn
? role.image ? role.image
: require( : rolePath(role)
'../../assets/icons/' +
(role.imageAlt || role.id) +
'.png',
)
})`, })`,
}" }"
></span> ></span>
@ -83,11 +79,7 @@
backgroundImage: `url(${ backgroundImage: `url(${
role.image && grimoire.isImageOptIn role.image && grimoire.isImageOptIn
? role.image ? role.image
: require( : rolePath(role)
'../../assets/icons/' +
(role.imageAlt || role.id) +
'.png',
)
})`, })`,
}" }"
></span> ></span>
@ -127,7 +119,7 @@
</template> </template>
<script> <script>
import Modal from "./Modal"; import Modal from "./Modal.vue";
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
export default { export default {
@ -274,6 +266,12 @@ export default {
...mapState("players", ["players", "fabled"]), ...mapState("players", ["players", "fabled"]),
}, },
methods: { methods: {
rolePath(role) {
return new URL(
`../../assets/icons/${role.imageAlt || role.id}.png`,
import.meta.url,
).href;
},
...mapMutations(["toggleModal"]), ...mapMutations(["toggleModal"]),
}, },
}; };

View file

@ -32,11 +32,7 @@
backgroundImage: `url(${ backgroundImage: `url(${
role.image && grimoire.isImageOptIn role.image && grimoire.isImageOptIn
? role.image ? role.image
: require( : rolePath(role)
'../../assets/icons/' +
(role.imageAlt || role.id) +
'.png',
)
})`, })`,
}" }"
></span> ></span>
@ -62,17 +58,13 @@
<span <span
class="icon" class="icon"
:style="{ :style="{
backgroundImage: `url(${require( backgroundImage: rolePath(jinx.first),
'../../assets/icons/' + jinx.first.id + '.png',
)})`,
}" }"
></span> ></span>
<span <span
class="icon" class="icon"
:style="{ :style="{
backgroundImage: `url(${require( backgroundImage: rolePath(jinx.second),
'../../assets/icons/' + jinx.second.id + '.png',
)})`,
}" }"
></span> ></span>
<div class="role"> <div class="role">
@ -91,7 +83,7 @@
</template> </template>
<script> <script>
import Modal from "./Modal"; import Modal from "./Modal.vue";
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
export default { export default {
@ -147,6 +139,12 @@ export default {
...mapState("players", ["players"]), ...mapState("players", ["players"]),
}, },
methods: { methods: {
rolePath(role) {
return new URL(
`../../assets/icons/${role.imageAlt || role.id}.png`,
import.meta.url,
).href;
},
...mapMutations(["toggleModal"]), ...mapMutations(["toggleModal"]),
}, },
}; };

View file

@ -18,11 +18,7 @@
backgroundImage: `url(${ backgroundImage: `url(${
reminder.image && grimoire.isImageOptIn reminder.image && grimoire.isImageOptIn
? reminder.image ? reminder.image
: require( : rolePath(reminder.role)
'../../assets/icons/' +
(reminder.imageAlt || reminder.role) +
'.png',
)
})`, })`,
}" }"
></span> ></span>
@ -33,7 +29,7 @@
</template> </template>
<script> <script>
import Modal from "./Modal"; import Modal from "./Modal.vue";
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
/** /**
@ -137,6 +133,12 @@ export default {
}); });
this.$store.commit("toggleModal", "reminder"); this.$store.commit("toggleModal", "reminder");
}, },
rolePath(role) {
return new URL(
`../assets/icons/${role.imageAlt || role.id}.png`,
import.meta.url,
).href;
},
...mapMutations(["toggleModal"]), ...mapMutations(["toggleModal"]),
}, },
}; };

View file

@ -50,8 +50,8 @@
<script> <script>
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
import Modal from "./Modal"; import Modal from "./Modal.vue";
import Token from "../Token"; import Token from "../Token.vue";
export default { export default {
components: { Token, Modal }, components: { Token, Modal },

View file

@ -77,9 +77,9 @@
</template> </template>
<script> <script>
import Modal from "./Modal"; import Modal from "./Modal.vue";
import gameJSON from "./../../game"; import gameJSON from "../../game.json";
import Token from "./../Token"; import Token from "../Token.vue";
import { mapGetters, mapMutations, mapState } from "vuex"; import { mapGetters, mapMutations, mapState } from "vuex";
const randomElement = (arr) => arr[Math.floor(Math.random() * arr.length)]; const randomElement = (arr) => arr[Math.floor(Math.random() * arr.length)];

View file

@ -26,7 +26,7 @@
<script> <script>
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
import Modal from "./Modal"; import Modal from "./Modal.vue";
export default { export default {
components: { components: {

View file

@ -88,7 +88,7 @@
</template> </template>
<script> <script>
import Modal from "./Modal"; import Modal from "./Modal.vue";
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
export default { export default {

View file

@ -1,5 +1,5 @@
import Vue from "vue"; import Vue from "vue";
import App from "./App"; import App from "./App.vue";
import store from "./store"; import store from "./store";
import { library } from "@fortawesome/fontawesome-svg-core"; import { library } from "@fortawesome/fontawesome-svg-core";
import { fas } from "@fortawesome/free-solid-svg-icons"; import { fas } from "@fortawesome/free-solid-svg-icons";

View file

@ -1,19 +1,34 @@
import Vue from "vue"; import Vue from "vue";
import Vuex from "vuex"; import Vuex from "vuex";
import persistence from "./persistence"; import persistence from "./persistence.js";
import socket from "./socket"; import socket from "./socket.js";
import players from "./modules/players"; import players from "./modules/players.js";
import session from "./modules/session"; import session from "./modules/session.js";
import editionJSON from "../editions.json"; import editionJSON from "../editions.json";
import { locale, rolesJSON, jinxesJSON, fabledJSON } from "./modules/locale";
Vue.use(Vuex); Vue.use(Vuex);
// helper functions const set = (key) => (state, value) => {
state.grimoire[key] = value;
};
const toggle = (key) => (state) => {
state.grimoire[key] = !state.grimoire[key];
};
const loadLocale = async () => {
const { locale, rolesJSON, jinxesJSON, fabledJSON } = await import(
"./modules/locale"
);
return { locale, rolesJSON, jinxesJSON, fabledJSON };
};
const initializeStore = async () => {
const { locale, rolesJSON, jinxesJSON, fabledJSON } = await loadLocale();
const getRolesByEdition = (edition = editionJSON.official[0]) => { const getRolesByEdition = (edition = editionJSON.official[0]) => {
return new Map( return new Map(
rolesJSON rolesJSON.default
.filter((r) => r.edition === edition.id || edition.roles.includes(r.id)) .filter((r) => r.edition === edition.id || edition.roles.includes(r.id))
.sort((a, b) => b.team.localeCompare(a.team)) .sort((a, b) => b.team.localeCompare(a.team))
.map((role) => [role.id, role]), .map((role) => [role.id, role]),
@ -22,7 +37,7 @@ const getRolesByEdition = (edition = editionJSON.official[0]) => {
const getTravelersNotInEdition = (edition = editionJSON.official[0]) => { const getTravelersNotInEdition = (edition = editionJSON.official[0]) => {
return new Map( return new Map(
rolesJSON rolesJSON.default
.filter( .filter(
(r) => (r) =>
r.team === "traveler" && r.team === "traveler" &&
@ -33,45 +48,25 @@ const getTravelersNotInEdition = (edition = editionJSON.official[0]) => {
); );
}; };
const set =
(key) =>
({ grimoire }, val) => {
grimoire[key] = val;
};
const toggle =
(key) =>
({ grimoire }, val) => {
if (val === true || val === false) {
grimoire[key] = val;
} else {
grimoire[key] = !grimoire[key];
}
};
const clean = (id) => id.toLocaleLowerCase().replace(/[^a-z0-9]/g, ""); const clean = (id) => id.toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
// global data maps
const editionJSONbyId = new Map( const editionJSONbyId = new Map(
editionJSON.official.map((edition) => [edition.id, edition]), editionJSON.official.map((edition) => [edition.id, edition]),
); );
const rolesJSONbyId = new Map(rolesJSON.map((role) => [role.id, role])); const rolesJSONbyId = new Map(
const fabled = new Map(fabledJSON.map((role) => [role.id, role])); rolesJSON.default.map((role) => [role.id, role]),
);
const fabled = new Map(fabledJSON.default.map((role) => [role.id, role]));
// jinxes // jinxes
let jinxes = {}; let jinxes = {};
try { try {
// Note: can't fetch live list due to lack of CORS headers
// fetch("https://bloodontheclocktower.com/script/data/hatred.json")
// .then(res => res.json())
// .then(jinxesJSON => {
jinxes = new Map( jinxes = new Map(
jinxesJSON.map(({ id, hatred }) => [ jinxesJSON.default.map(({ id, hatred }) => [
clean(id), clean(id),
new Map(hatred.map(({ id, reason }) => [clean(id), reason])), new Map(hatred.map(({ id, reason }) => [clean(id), reason])),
]), ]),
); );
// });
} catch (e) { } catch (e) {
console.error("couldn't load jinxes", e); console.error("couldn't load jinxes", e);
} }
@ -94,7 +89,7 @@ const customRole = {
isCustom: true, isCustom: true,
}; };
export default new Vuex.Store({ return new Vuex.Store({
modules: { modules: {
players, players,
session, session,
@ -139,12 +134,6 @@ export default new Vuex.Store({
locale, locale,
}, },
getters: { getters: {
/**
* Return all custom roles, with default values and non-essential data stripped.
* Role object keys will be replaced with a numerical index to conserve bandwidth.
* @param roles
* @returns {[]}
*/
customRolesStripped: ({ roles }) => { customRolesStripped: ({ roles }) => {
const customRoles = []; const customRoles = [];
const customKeys = Object.keys(customRole); const customKeys = Object.keys(customRole);
@ -199,14 +188,8 @@ export default new Vuex.Store({
modals[modal] = false; modals[modal] = false;
} }
}, },
/**
* Store custom roles
* @param state
* @param roles Array of role IDs or full role definitions
*/
setCustomRoles(state, roles) { setCustomRoles(state, roles) {
const processedRoles = roles const processedRoles = roles
// replace numerical role object keys with matching key names
.map((role) => { .map((role) => {
if (role[0]) { if (role[0]) {
const customKeys = Object.keys(customRole); const customKeys = Object.keys(customRole);
@ -221,22 +204,18 @@ export default new Vuex.Store({
return role; return role;
} }
}) })
// clean up role.id
.map((role) => { .map((role) => {
role.id = clean(role.id); role.id = clean(role.id);
return role; return role;
}) })
// map existing roles to base definition or pre-populate custom roles to ensure all properties
.map( .map(
(role) => (role) =>
rolesJSONbyId.get(role.id) || rolesJSONbyId.get(role.id) ||
state.roles.get(role.id) || state.roles.get(role.id) || { ...customRole, ...role },
Object.assign({}, customRole, role),
) )
// default empty icons and placeholders, clean up firstNight / otherNight
.map((role) => { .map((role) => {
if (rolesJSONbyId.get(role.id)) return role; if (rolesJSONbyId.get(role.id)) return role;
role.imageAlt = // map team to generic icon role.imageAlt =
{ {
townsfolk: "townsfolk", townsfolk: "townsfolk",
outsider: "outsider", outsider: "outsider",
@ -249,26 +228,24 @@ export default new Vuex.Store({
role.otherNight = Math.abs(role.otherNight); role.otherNight = Math.abs(role.otherNight);
return role; return role;
}) })
// filter out roles that don't match an existing role and also don't have name/ability/team
.filter((role) => role.name && role.ability && role.team) .filter((role) => role.name && role.ability && role.team)
// sort by team
.sort((a, b) => b.team.localeCompare(a.team)); .sort((a, b) => b.team.localeCompare(a.team));
// convert to Map without Fabled
state.roles = new Map( state.roles = new Map(
processedRoles processedRoles
.filter((role) => role.team !== "fabled") .filter((role) => role.team !== "fabled")
.map((role) => [role.id, role]), .map((role) => [role.id, role]),
); );
// update Fabled to include custom Fabled from this script
state.fabled = new Map([ state.fabled = new Map([
...processedRoles ...processedRoles
.filter((r) => r.team === "fabled") .filter((r) => r.team === "fabled")
.map((r) => [r.id, r]), .map((r) => [r.id, r]),
...fabledJSON.map((role) => [role.id, role]), ...fabledJSON.default.map((role) => [role.id, role]),
]); ]);
// update extraTravelers map to only show travelers not in this script
state.otherTravelers = new Map( state.otherTravelers = new Map(
rolesJSON rolesJSON.default
.filter( .filter(
(r) => r.team === "traveler" && !roles.some((i) => i.id === r.id), (r) => r.team === "traveler" && !roles.some((i) => i.id === r.id),
) )
@ -288,3 +265,8 @@ export default new Vuex.Store({
}, },
plugins: [persistence, socket], plugins: [persistence, socket],
}); });
};
// Create the store and export it
const store = await initializeStore();
export default store;

View file

@ -23,7 +23,7 @@ if (!usedLanguage) {
usedLanguage = MASTER_LANGUAGE; // set to master language if no language is supported by both the user and the application 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 locale = await import(`../locale/${usedLanguage}/ui.json`);
export const rolesJSON = require(`../locale/${usedLanguage}/roles.json`); export const rolesJSON = await import(`../locale/${usedLanguage}/roles.json`);
export const jinxesJSON = require(`../locale/${usedLanguage}/hatred.json`); export const jinxesJSON = await import(`../locale/${usedLanguage}/hatred.json`);
export const fabledJSON = require(`../locale/${usedLanguage}/fabled.json`); export const fabledJSON = await import(`../locale/${usedLanguage}/fabled.json`);

View file

@ -1,4 +1,4 @@
module.exports = (store) => { export default (store) => {
const updatePagetitle = (isPublic) => const updatePagetitle = (isPublic) =>
(document.title = `Blood on the Clocktower ${ (document.title = `Blood on the Clocktower ${
isPublic ? "Town Square" : "Grimoire" isPublic ? "Town Square" : "Grimoire"

17
vite.config.js Normal file
View file

@ -0,0 +1,17 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue2";
export default defineConfig({
plugins: [vue()],
base: "/",
build: {
rollupOptions: {
output: {
entryFileNames: "[name].js",
chunkFileNames: "[name].[hash].js",
assetFileNames: "[name].[hash].[extname]",
},
},
target: "es2022",
},
});

View file

@ -1,12 +0,0 @@
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
configureWebpack: {
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css",
}),
],
},
publicPath: "/",
};