commit 05
This commit is contained in:
parent
55aa013e1d
commit
58e60e4da4
18 changed files with 286 additions and 207 deletions
|
@ -21,7 +21,6 @@
|
|||
"typescript": "^5.7.2",
|
||||
"unplugin-icons": "^0.22.0",
|
||||
"vite": "^6.0.3",
|
||||
"vite-plugin-pages": "^0.32.4",
|
||||
"vite-plugin-windicss": "^1.9.4",
|
||||
"windicss": "^3.5.6"
|
||||
},
|
||||
|
@ -46,7 +45,6 @@
|
|||
"pinia": "^2.3.0",
|
||||
"superjson": "^2.2.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
|
|
110
pnpm-lock.yaml
generated
110
pnpm-lock.yaml
generated
|
@ -68,9 +68,6 @@ importers:
|
|||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.13(typescript@5.7.2)
|
||||
vue-router:
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.0(vue@3.5.13(typescript@5.7.2))
|
||||
ws:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0(bufferutil@4.0.8)
|
||||
|
@ -111,9 +108,6 @@ importers:
|
|||
vite:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(@types/node@22.10.2)(jiti@2.4.2)(sass@1.83.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite-plugin-pages:
|
||||
specifier: ^0.32.4
|
||||
version: 0.32.4(@vue/compiler-sfc@3.5.13)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(sass@1.83.0)(tsx@4.19.2)(yaml@2.6.1))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.2)))
|
||||
vite-plugin-windicss:
|
||||
specifier: ^1.9.4
|
||||
version: 1.9.4(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(sass@1.83.0)(tsx@4.19.2)(yaml@2.6.1))
|
||||
|
@ -701,9 +695,6 @@ packages:
|
|||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
'@types/estree@1.0.6':
|
||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||
|
||||
|
@ -725,9 +716,6 @@ packages:
|
|||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/ms@0.7.34':
|
||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
||||
|
||||
'@types/node@22.10.2':
|
||||
resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==}
|
||||
|
||||
|
@ -954,10 +942,6 @@ packages:
|
|||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
destr@2.0.3:
|
||||
resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==}
|
||||
|
||||
|
@ -1014,15 +998,6 @@ packages:
|
|||
escape-html@1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
|
||||
esprima-extract-comments@1.1.0:
|
||||
resolution: {integrity: sha512-sBQUnvJwpeE9QnPrxh7dpI/dp67erYG4WXEAreAMoelPRpMR7NWb4YtwRPn9b+H1uLQKl/qS8WYmyaljTpjIsw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
esprima@4.0.1:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
|
@ -1041,10 +1016,6 @@ packages:
|
|||
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
||||
extract-comments@1.1.0:
|
||||
resolution: {integrity: sha512-dzbZV2AdSSVW/4E7Ti5hZdHWbA+Z80RJsJhr5uiL10oyjl/gy7/o+HI1HwK4/WSZhlq4SNKU3oUzXlM13Qx02Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fast-glob@3.3.2:
|
||||
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
@ -1194,11 +1165,6 @@ packages:
|
|||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
|
||||
json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
kolorist@1.8.0:
|
||||
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
||||
|
||||
|
@ -1323,10 +1289,6 @@ packages:
|
|||
package-manager-detector@0.2.7:
|
||||
resolution: {integrity: sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ==}
|
||||
|
||||
parse-code-context@1.0.0:
|
||||
resolution: {integrity: sha512-OZQaqKaQnR21iqhlnPfVisFjBWjhnMl5J9MgbP8xC+EwoVqbXrq78lp+9Zb3ahmLzrIX5Us/qbvBnaS3hkH6OA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -1565,24 +1527,6 @@ packages:
|
|||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
vite-plugin-pages@0.32.4:
|
||||
resolution: {integrity: sha512-OM8CNb8mAzyYR8ASRC0+2LXVB8ecR/5JHc5RpxbWtF+CmhjhmIELs0iV5y8qvU48soZbk+NsFOYlhoIcjw3+ew==}
|
||||
peerDependencies:
|
||||
'@solidjs/router': '*'
|
||||
'@vue/compiler-sfc': ^2.7.0 || ^3.0.0
|
||||
react-router: '*'
|
||||
vite: ^2.0.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0 || ^6.0.0
|
||||
vue-router: '*'
|
||||
peerDependenciesMeta:
|
||||
'@solidjs/router':
|
||||
optional: true
|
||||
'@vue/compiler-sfc':
|
||||
optional: true
|
||||
react-router:
|
||||
optional: true
|
||||
vue-router:
|
||||
optional: true
|
||||
|
||||
vite-plugin-windicss@1.9.4:
|
||||
resolution: {integrity: sha512-3t1AUVrs2XBXGc2BefRPRvy1CLy8qA/5A1J1Z73Ej1DIx+puXn39MQSWluxZ2FHEz8z9OEIvsoIIPc/s/P3OmQ==}
|
||||
peerDependencies:
|
||||
|
@ -1639,11 +1583,6 @@ packages:
|
|||
'@vue/composition-api':
|
||||
optional: true
|
||||
|
||||
vue-router@4.5.0:
|
||||
resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==}
|
||||
peerDependencies:
|
||||
vue: ^3.2.0
|
||||
|
||||
vue@3.5.13:
|
||||
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
|
||||
peerDependencies:
|
||||
|
@ -2067,10 +2006,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 22.10.2
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 0.7.34
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
|
||||
'@types/express-serve-static-core@5.0.2':
|
||||
|
@ -2097,8 +2032,6 @@ snapshots:
|
|||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/ms@0.7.34': {}
|
||||
|
||||
'@types/node@22.10.2':
|
||||
dependencies:
|
||||
undici-types: 6.20.0
|
||||
|
@ -2334,8 +2267,6 @@ snapshots:
|
|||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
destr@2.0.3: {}
|
||||
|
||||
destroy@1.2.0: {}
|
||||
|
@ -2420,12 +2351,6 @@ snapshots:
|
|||
|
||||
escape-html@1.0.3: {}
|
||||
|
||||
esprima-extract-comments@1.1.0:
|
||||
dependencies:
|
||||
esprima: 4.0.1
|
||||
|
||||
esprima@4.0.1: {}
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
@ -2480,11 +2405,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
extract-comments@1.1.0:
|
||||
dependencies:
|
||||
esprima-extract-comments: 1.1.0
|
||||
parse-code-context: 1.0.0
|
||||
|
||||
fast-glob@3.3.2:
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
|
@ -2628,8 +2548,6 @@ snapshots:
|
|||
|
||||
jiti@2.4.2: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
kolorist@1.8.0: {}
|
||||
|
||||
listhen@1.9.0:
|
||||
|
@ -2736,8 +2654,6 @@ snapshots:
|
|||
|
||||
package-manager-detector@0.2.7: {}
|
||||
|
||||
parse-code-context@1.0.0: {}
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
@ -2991,24 +2907,6 @@ snapshots:
|
|||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vite-plugin-pages@0.32.4(@vue/compiler-sfc@3.5.13)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(sass@1.83.0)(tsx@4.19.2)(yaml@2.6.1))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.2))):
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.0
|
||||
dequal: 2.0.3
|
||||
extract-comments: 1.1.0
|
||||
fast-glob: 3.3.2
|
||||
json5: 2.2.3
|
||||
local-pkg: 0.5.1
|
||||
picocolors: 1.1.1
|
||||
vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.2)(sass@1.83.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
yaml: 2.6.1
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.13
|
||||
vue-router: 4.5.0(vue@3.5.13(typescript@5.7.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-windicss@1.9.4(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(sass@1.83.0)(tsx@4.19.2)(yaml@2.6.1)):
|
||||
dependencies:
|
||||
'@windicss/plugin-utils': 1.9.4
|
||||
|
@ -3036,11 +2934,6 @@ snapshots:
|
|||
dependencies:
|
||||
vue: 3.5.13(typescript@5.7.2)
|
||||
|
||||
vue-router@4.5.0(vue@3.5.13(typescript@5.7.2)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.13(typescript@5.7.2)
|
||||
|
||||
vue@3.5.13(typescript@5.7.2):
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.13
|
||||
|
@ -3063,6 +2956,7 @@ snapshots:
|
|||
optionalDependencies:
|
||||
bufferutil: 4.0.8
|
||||
|
||||
yaml@2.6.1: {}
|
||||
yaml@2.6.1:
|
||||
optional: true
|
||||
|
||||
zod@3.24.1: {}
|
||||
|
|
BIN
public/objects/schüssel.png
Normal file
BIN
public/objects/schüssel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
90
src/App.vue
90
src/App.vue
|
@ -6,21 +6,16 @@
|
|||
<span class="text-center">Verbindung wird hergestellt…</span>
|
||||
</div>
|
||||
<div class="h-full relative flex flex-col">
|
||||
<nav class="mx-auto py-4 flex gap-4">
|
||||
<div class="flex items-center rounded-lg bg-dark-400 overflow-hidden">
|
||||
<nav class="mx-auto p-4 flex gap-4 overflow-x-auto max-w-full">
|
||||
<div class="flex-shrink-0 flex items-center rounded-lg bg-dark-400 overflow-hidden">
|
||||
<button
|
||||
v-for="tab in screens"
|
||||
:key="tab.id"
|
||||
class="px-4 py-2 bg-gray-700"
|
||||
:class="activeTab !== 'interactions' && 'bg-opacity-0'"
|
||||
@click="activeTab = 'interactions'"
|
||||
:class="activeScreenId !== tab.id && 'bg-opacity-0'"
|
||||
@click="activeScreenId = tab.id"
|
||||
>
|
||||
Interagieren
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 bg-gray-700"
|
||||
:class="activeTab !== 'queue' && 'bg-opacity-0'"
|
||||
@click="activeTab = 'queue'"
|
||||
>
|
||||
Abstimmen
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
<button v-if="!isFullscreen" class="px-3 rounded-lg bg-dark-400 flex items-center justify-center" @click="enterFullscreen()">
|
||||
|
@ -28,8 +23,9 @@
|
|||
</button>
|
||||
</nav>
|
||||
<main class="flex-grow">
|
||||
<InteractionsScreen v-if="activeTab === 'interactions'"/>
|
||||
<QueueScreen v-else-if="activeTab === 'queue'"/>
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="screens.find(t => t.id === activeScreenId)!.component" @switch-screen="(id: string) => (activeScreenId = id)"/>
|
||||
</transition>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -78,34 +74,76 @@
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-down-enter-active,
|
||||
.slide-down-leave-active {
|
||||
position: absolute;
|
||||
transition: 200ms ease;
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: 400ms ease;
|
||||
transition-property: opacity, transform;
|
||||
}
|
||||
|
||||
.slide-down-enter-from,
|
||||
.slide-down-leave-to {
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(40%);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { type Component, computed, ref, watchEffect } from "vue"
|
||||
import { trpcClient } from "./trpc"
|
||||
import { useGame } from "./game"
|
||||
import InteractionsScreen from "./screens/InteractionsScreen.vue"
|
||||
import QueueScreen from "./screens/QueueScreen.vue"
|
||||
import CornersOutIcon from "virtual:icons/ph/corners-out-bold"
|
||||
import { useFullscreen } from "@vueuse/core"
|
||||
import { useBrowserLocation, useFullscreen } from "@vueuse/core"
|
||||
import DirectorScreen from "./screens/DirectorScreen.vue"
|
||||
|
||||
const isLoading = ref(true)
|
||||
const activeTab = ref<"interactions" | "queue">("interactions")
|
||||
const game = useGame()
|
||||
const activeScreenId = ref<string>("interactions")
|
||||
|
||||
const game = useGame()
|
||||
const { isFullscreen, enter: enterFullscreen } = useFullscreen()
|
||||
const location = useBrowserLocation()
|
||||
|
||||
const isDirector = computed(() => location.value.hash === "#director")
|
||||
|
||||
watchEffect(() => {
|
||||
if (isDirector.value) activeScreenId.value = "director"
|
||||
else activeScreenId.value = "interactions"
|
||||
})
|
||||
|
||||
interface Screen {
|
||||
id: string
|
||||
label: string
|
||||
component: Component
|
||||
}
|
||||
|
||||
const screens = computed(() => {
|
||||
const result: Screen[] = [
|
||||
{
|
||||
id: "interactions",
|
||||
label: "Interagieren",
|
||||
component: InteractionsScreen
|
||||
},
|
||||
{
|
||||
id: "queue",
|
||||
label: "Abstimmen",
|
||||
component: QueueScreen
|
||||
}
|
||||
]
|
||||
|
||||
if (isDirector.value) result.push({
|
||||
id: "director",
|
||||
label: "Regie",
|
||||
component: DirectorScreen
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
trpcClient.join.subscribe(undefined, {
|
||||
onStarted: () => {
|
||||
|
@ -116,7 +154,7 @@
|
|||
console.error("🔴", error)
|
||||
},
|
||||
onStopped() {
|
||||
location.reload()
|
||||
window.location.reload()
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<HandPointingIcon class="text-4xl mb-6"/>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<ObjectPicture :object-id="item.interaction.objectId"/>
|
||||
<div class="text-sm text-gray-300 text-center">
|
||||
<div class="text-sm text-gray-200 text-center">
|
||||
{{ game.allObjectsById.get(item.interaction.objectId)!.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,27 +13,42 @@
|
|||
<template v-else v-for="(objectId, index) in item.interaction.objectIds" :key="objectId">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<ObjectPicture :object-id="objectId"/>
|
||||
<div class="text-sm text-gray-300 text-center">
|
||||
<div class="text-sm text-gray-200 text-center">
|
||||
{{ game.allObjectsById.get(objectId)!.label }}
|
||||
</div>
|
||||
</div>
|
||||
<PlusIcon v-if="index < item.interaction.objectIds.size - 1" class="text-3xl mb-6"/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-between items-center bg-gray-800 w-17">
|
||||
<div class="flex flex-col justify-center items-center py-3">
|
||||
<div class="flex flex-col justify-between items-center bg-gray-800 w-17">
|
||||
<div class="flex flex-col justify-center items-center pt-3 pb-2">
|
||||
<div class="text-2xl">{{ item.votes }}</div>
|
||||
<div class="text-sm text-center">
|
||||
Vote{{item.votes === 1 ? "" : "s" }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="align-end flex-grow py-2 w-full"
|
||||
v-if="mode === 'audience'"
|
||||
class="align-end py-2 w-full"
|
||||
:class="game.currentInteractionId === item.id ? 'bg-blue-500' : 'bg-gray-700'"
|
||||
@click="toggleVote()"
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
<button
|
||||
v-if="mode === 'director' && (item.interaction.type !== 'combine' || findMatchingCombination(game.currentRoom.combinations, item.interaction.objectIds))"
|
||||
class="align-end py-1 w-full bg-green-800"
|
||||
@click="game.activateInteractionQueueItem(item.id)"
|
||||
>
|
||||
<CheckIcon class="relative top-2px"/>
|
||||
</button>
|
||||
<button
|
||||
v-if="mode === 'director'"
|
||||
class="align-end py-1 w-full bg-red-900"
|
||||
@click="game.removeInteractionQueueItem(item.id)"
|
||||
>
|
||||
<TrashIcon class="relative top-2px"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -44,13 +59,17 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import PlusIcon from "virtual:icons/ph/plus-bold"
|
||||
import TrashIcon from "virtual:icons/ph/trash-bold"
|
||||
import CheckIcon from "virtual:icons/ph/check-bold"
|
||||
import HandPointingIcon from "virtual:icons/ph/hand-pointing-duotone"
|
||||
import type { InteractionQueueItem } from "../shared/script/types"
|
||||
import ObjectPicture from "./ObjectPicture.vue"
|
||||
import { useGame } from "../game"
|
||||
import { findMatchingCombination } from "../shared/util"
|
||||
|
||||
const props = defineProps<{
|
||||
item: InteractionQueueItem
|
||||
mode: "audience" | "director"
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
:data-object-id="object.id"
|
||||
>
|
||||
<ObjectPicture :object-id="object.id"/>
|
||||
<div class="text-sm text-gray-300">
|
||||
<div class="text-sm text-gray-200">
|
||||
{{ object.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<img :src="`/objects/${objectId}.png`" alt="" class="invert filter object-contain max-w-15"/>
|
||||
<img :src="`/objects/${objectId}.png`" alt="" class="invert filter object-contain max-w-15 pointer-events-none" draggable="false"/>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
|
|
39
src/game.ts
39
src/game.ts
|
@ -7,27 +7,37 @@ import type { GameEvent } from "./shared/gameEvents"
|
|||
import { getInteractionQueueItemId } from "./shared/util"
|
||||
|
||||
export const useGame = defineStore("gameState", () => {
|
||||
const currentRoom = computed(() => script.roomsById.get(currentRoomId.value)!)
|
||||
const currentRoomId = ref(script.roomsById.values().next().value!.id)
|
||||
const interactionQueue = reactive(new Map<string, InteractionQueueItem>())
|
||||
|
||||
const currentInteraction = ref<null | Interaction>(null)
|
||||
const currentInteractionId = computed(() =>
|
||||
currentInteraction.value === null ? null : getInteractionQueueItemId(currentInteraction.value))
|
||||
|
||||
const sortedInteractionQueue = computed(() => [...interactionQueue.values()].sort((a, b) => b.votes - a.votes))
|
||||
const interactionQueue = reactive(new Map<string, InteractionQueueItem>())
|
||||
const sortedInteractionQueue = computed(() =>
|
||||
[...interactionQueue.values()].sort((a, b) => b.votes - a.votes))
|
||||
|
||||
const currentRoom = computed(() => script.roomsById.get(currentRoomId.value)!)
|
||||
const visibleObjectIds = reactive(new Set<string>())
|
||||
|
||||
return {
|
||||
currentRoomId,
|
||||
interactionQueue,
|
||||
sortedInteractionQueue,
|
||||
currentRoom,
|
||||
interactionQueue,
|
||||
visibleObjectIds,
|
||||
sortedInteractionQueue,
|
||||
allObjectsById: computed(() => {
|
||||
const map = new Map<string, GameObject>()
|
||||
currentRoom.value.initialObjects.forEach(o => map.set(o.id, o))
|
||||
currentRoom.value.hiddenObjects.forEach(o => map.set(o.id, o))
|
||||
return map
|
||||
}),
|
||||
visibleObjectsById: computed(() => {
|
||||
const map = new Map<string, GameObject>()
|
||||
currentRoom.value.initialObjects.values().filter(o => visibleObjectIds.has(o.id)).forEach(o => map.set(o.id, o))
|
||||
currentRoom.value.hiddenObjects.values().filter(o => visibleObjectIds.has(o.id)).forEach(o => map.set(o.id, o))
|
||||
return map
|
||||
}),
|
||||
currentInteraction,
|
||||
currentInteractionId,
|
||||
voteForInteraction(interaction: Interaction) {
|
||||
|
@ -44,16 +54,24 @@ export const useGame = defineStore("gameState", () => {
|
|||
switchRoom(roomId: string) {
|
||||
trpcClient.director.switchRoom.mutate({ roomId })
|
||||
},
|
||||
activateInteractionQueueItem(id: string) {
|
||||
trpcClient.director.activateInteractionQueueItem.mutate({ id })
|
||||
},
|
||||
removeInteractionQueueItem(id: string) {
|
||||
trpcClient.director.removeInteractionQueueItem.mutate({ id })
|
||||
},
|
||||
setObjectVisibility(id: string, isVisible: boolean) {
|
||||
trpcClient.director.setObjectVisibility.mutate({ id, isVisible })
|
||||
},
|
||||
handleGameEvent(event: GameEvent) {
|
||||
console.log(event)
|
||||
switch (event.type) {
|
||||
case "room-changed":
|
||||
interactionQueue.clear()
|
||||
currentRoomId.value = event.roomId
|
||||
currentInteraction.value = null
|
||||
currentRoomId.value = event.roomId
|
||||
interactionQueue.clear()
|
||||
|
||||
visibleObjectIds.clear()
|
||||
currentRoom.value.initialObjects.forEach(o => visibleObjectIds.add(o.id))
|
||||
break
|
||||
|
||||
case "interaction-queued":
|
||||
|
@ -69,6 +87,11 @@ export const useGame = defineStore("gameState", () => {
|
|||
}
|
||||
|
||||
break
|
||||
|
||||
case "object-visibility-changed":
|
||||
if (event.isVisible) visibleObjectIds.add(event.id)
|
||||
else visibleObjectIds.delete(event.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,9 @@ import "@interactjs/actions"
|
|||
import "@interactjs/auto-start"
|
||||
|
||||
import { createApp } from "vue"
|
||||
import { createRouter, createWebHistory } from "vue-router"
|
||||
import App from "./App.vue"
|
||||
import routes from "virtual:generated-pages"
|
||||
import { createPinia } from "pinia"
|
||||
|
||||
const router = createRouter({
|
||||
routes,
|
||||
history: createWebHistory()
|
||||
})
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.use(createPinia())
|
||||
.mount("#app")
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<template>
|
||||
<
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
70
src/screens/DirectorScreen.vue
Normal file
70
src/screens/DirectorScreen.vue
Normal file
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div class="flex gap-6 p-4">
|
||||
<section class="w-80">
|
||||
<div class="font-bold text-2xl pb-2">Raum</div>
|
||||
<div class="flex flex-col overflow-y-auto bg-dark-900 bg-opacity-50 h-80vh">
|
||||
<div
|
||||
v-for="room in script.roomsById.values()"
|
||||
:key="room.id"
|
||||
class="flex-shrink-0 bg-dark-600 not-last:border-b border-solid border-dark-300 flex items-center"
|
||||
>
|
||||
<div class="px-3 py-2 flex-grow">
|
||||
{{ room.label }}
|
||||
</div>
|
||||
<button v-if="game.currentRoomId === room.id" disabled class="bg-green-900 h-full px-3 text-sm cursor-not-allowed">
|
||||
Aktiv
|
||||
</button>
|
||||
<button v-else class="bg-dark-300 h-full px-3 text-sm" @click="game.switchRoom(room.id)">
|
||||
Aktivieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex-grow">
|
||||
<div class="font-bold text-2xl pb-2">Versteckte Objekte</div>
|
||||
<div class="grid gap-3 grid-cols-2 flex-grow auto-rows-min p-4 pt-0 relative">
|
||||
<button
|
||||
v-for="object in game.allObjectsById.values().filter(o => !game.visibleObjectIds.has(o.id))"
|
||||
:key="object.id"
|
||||
class="flex flex-col items-center gap-2 bg-dark-600 rounded-lg p-3"
|
||||
@click="game.setObjectVisibility(object.id, true)"
|
||||
>
|
||||
<ObjectPicture :object-id="object.id"/>
|
||||
<span class="text-sm text-gray-200 text-center">
|
||||
{{ object.label }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="w-80">
|
||||
<div class="font-bold text-2xl pb-2">Interaktionen</div>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="game.sortedInteractionQueue.length === 0" class="text-xl text-gray-200">
|
||||
Keine Interaktionen vorhanden.
|
||||
</div>
|
||||
<transition-group v-else tag="div" name="list" class="flex flex-col gap-4 relative h-80vh">
|
||||
<InteractionQueueItemCard
|
||||
v-for="(item, index) in game.sortedInteractionQueue"
|
||||
:key="item.id"
|
||||
:style="{ zIndex: 1000 - index }"
|
||||
:item="item"
|
||||
mode="director"
|
||||
/>
|
||||
</transition-group>
|
||||
</transition>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { script } from "../shared/script"
|
||||
import { useGame } from "../game"
|
||||
import ObjectPicture from "../components/ObjectPicture.vue"
|
||||
import InteractionQueueItemCard from "../components/InteractionQueueItemCard.vue"
|
||||
|
||||
const game = useGame()
|
||||
</script>
|
|
@ -16,9 +16,9 @@
|
|||
@object-drop="onObjectInteractionDrop"
|
||||
/>
|
||||
</div>
|
||||
<div ref="objectsContainerElement" class="grid gap-3 grid-cols-2 flex-grow auto-rows-min p-4 pt-0">
|
||||
<transition-group tag="div" name="list" ref="objectsContainerElement" class="grid gap-3 grid-cols-2 flex-grow auto-rows-min p-4 pt-0 relative">
|
||||
<ObjectCard
|
||||
v-for="object in game.currentRoom.initialObjects"
|
||||
v-for="object in game.visibleObjectsById.values()"
|
||||
:key="object.id"
|
||||
:object="object"
|
||||
:is-over-dropzone="allFloatingObjectIds.has(object.id)"
|
||||
|
@ -26,7 +26,7 @@
|
|||
@drag-start="onObjectDragStart"
|
||||
@drag-end="onObjectDragEnd"
|
||||
/>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,37 +1,40 @@
|
|||
<template>
|
||||
<transition-group tag="div" name="list" class="h-full flex flex-col gap-4 p-4 pt-0 relative">
|
||||
<InteractionQueueItemCard
|
||||
v-for="(item, index) in game.sortedInteractionQueue"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
class="relative"
|
||||
:style="{ zIndex: 1000 - index }"
|
||||
/>
|
||||
</transition-group>
|
||||
<div class="h-full p-4 pt-0">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="game.sortedInteractionQueue.length === 0" class="h-full flex flex-col justify-center items-center gap-4 p-6">
|
||||
<div class="text-xl text-center text-gray-200">
|
||||
Noch keine Interaktionen zum Abstimmen vorhanden.
|
||||
</div>
|
||||
<button class="flex items-center gap-2 px-4 py-2 rounded-lg bg-green-800 text-lg" @click="emit('switch-screen', 'interactions')">
|
||||
<ArrowRightIcon/>
|
||||
<span class="relative top-1px">Interagieren</span>
|
||||
</button>
|
||||
</div>
|
||||
<transition-group v-else tag="div" name="list" class="flex flex-col gap-4 relative">
|
||||
<InteractionQueueItemCard
|
||||
v-for="(item, index) in game.sortedInteractionQueue"
|
||||
:key="item.id"
|
||||
:style="{ zIndex: 1000 - index }"
|
||||
:item="item"
|
||||
mode="audience"
|
||||
/>
|
||||
</transition-group>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: 400ms ease;
|
||||
transition-property: opacity, transform;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import InteractionQueueItemCard from "../components/InteractionQueueItemCard.vue"
|
||||
import { useGame } from "../game"
|
||||
import ArrowRightIcon from "virtual:icons/ph/arrow-right"
|
||||
|
||||
const emit = defineEmits<{
|
||||
"switch-screen": [string]
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
</script>
|
|
@ -2,7 +2,7 @@ import EventEmitter from "eventemitter3"
|
|||
import type { GameEvent } from "../shared/gameEvents"
|
||||
import type { Interaction, InteractionQueueItem } from "../shared/script/types"
|
||||
import { script } from "../shared/script"
|
||||
import { getInteractionQueueItemId } from "../shared/util"
|
||||
import { findMatchingCombination, getInteractionQueueItemId } from "../shared/util"
|
||||
|
||||
interface Events {
|
||||
"game-event": [GameEvent]
|
||||
|
@ -59,11 +59,42 @@ export class Game extends EventEmitter<Events> {
|
|||
this.emit("game-event", { type: "room-changed", roomId })
|
||||
}
|
||||
|
||||
activateInteractionQueueItem(id: string) {
|
||||
const item = this.interactionQueue.get(id)
|
||||
if (item === undefined) return
|
||||
this.interactionQueue.delete(id)
|
||||
this.emit("game-event", { type: "interaction-votes-changed", id, votes: 0 })
|
||||
switch (item.interaction.type) {
|
||||
case "use":
|
||||
this.emit("game-event", { type: "object-visibility-changed", id: item.interaction.objectId, isVisible: false })
|
||||
break
|
||||
|
||||
case "combine":
|
||||
const matchingCombination = findMatchingCombination(script.roomsById.get(this.currentRoomId)!.combinations, item.interaction.objectIds)
|
||||
|
||||
if (matchingCombination !== undefined) {
|
||||
matchingCombination.inputs.forEach(input => {
|
||||
if (input.isConsumed) this.emit("game-event", { type: "object-visibility-changed", id: input.objectId, isVisible: false })
|
||||
})
|
||||
|
||||
matchingCombination.outputIds.forEach(outputId => {
|
||||
this.emit("game-event", { type: "object-visibility-changed", id: outputId, isVisible: true })
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
removeInteractionQueueItem(id: string) {
|
||||
if (!this.interactionQueue.has(id)) return
|
||||
this.emit("game-event", { type: "interaction-votes-changed", id: id, votes: 0 })
|
||||
this.emit("game-event", { type: "interaction-votes-changed", id, votes: 0 })
|
||||
this.interactionQueue.delete(id)
|
||||
}
|
||||
|
||||
setObjectVisibility(id: string, isVisible: boolean) {
|
||||
this.emit("game-event", { type: "object-visibility-changed", id, isVisible })
|
||||
}
|
||||
}
|
||||
|
||||
export const game = new Game()
|
|
@ -1,20 +1,38 @@
|
|||
import { t } from "./base"
|
||||
import { z } from "zod"
|
||||
import { game } from "../game"
|
||||
|
||||
export const directorRouter = t.router({
|
||||
switchRoom: t.procedure
|
||||
.input(z.object({
|
||||
roomId: z.string()
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
ctx.game.switchRoom(input.roomId)
|
||||
.mutation(async ({ input }) => {
|
||||
game.switchRoom(input.roomId)
|
||||
}),
|
||||
|
||||
removeInteractionQueueItem: t.procedure
|
||||
.input(z.object({
|
||||
id: z.string()
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
ctx.game.removeInteractionQueueItem(input.id)
|
||||
.mutation(async ({ input }) => {
|
||||
game.removeInteractionQueueItem(input.id)
|
||||
}),
|
||||
|
||||
activateInteractionQueueItem: t.procedure
|
||||
.input(z.object({
|
||||
id: z.string()
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
game.activateInteractionQueueItem(input.id)
|
||||
}),
|
||||
|
||||
setObjectVisibility: t.procedure
|
||||
.input(z.object({
|
||||
id: z.string(),
|
||||
isVisible: z.boolean()
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
game.setObjectVisibility(input.id, input.isVisible)
|
||||
}),
|
||||
})
|
|
@ -3,4 +3,5 @@ import type { InteractionQueueItem } from "./script/types"
|
|||
export type GameEvent =
|
||||
| { type: "room-changed"; roomId: string }
|
||||
| { type: "interaction-queued"; item: InteractionQueueItem }
|
||||
| { type: "interaction-votes-changed"; id: string; votes: number }
|
||||
| { type: "interaction-votes-changed"; id: string; votes: number }
|
||||
| { type: "object-visibility-changed"; id: string; isVisible: boolean }
|
|
@ -1,4 +1,5 @@
|
|||
import type { Interaction } from "./script/types"
|
||||
import type { Combination, Interaction } from "./script/types"
|
||||
import { isEqual } from "lodash-es"
|
||||
|
||||
export const cSet = <T>(...values: T[]) => new Set(values)
|
||||
export const cMap = <K extends string | number | symbol, V>(object: Record<K, V>) => new Map(Object.entries(object))
|
||||
|
@ -9,4 +10,8 @@ export function getInteractionQueueItemId(interaction: Interaction) {
|
|||
else if (interaction.type === "combine") id = `combine-${[...interaction.objectIds].sort().join("_")}`
|
||||
else throw new Error("Unknown interaction type")
|
||||
return id
|
||||
}
|
||||
|
||||
export function findMatchingCombination(combinations: Set<Combination>, objectIds: Set<string>) {
|
||||
return combinations.values().find(c => isEqual(new Set(c.inputs.values().map(i => i.objectId)), objectIds))
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
import { defineConfig } from "vite"
|
||||
import vuePlugin from "@vitejs/plugin-vue"
|
||||
import iconsPlugin from "unplugin-icons/vite"
|
||||
import pagesPlugin from "vite-plugin-pages"
|
||||
import windiPlugin from "vite-plugin-windicss"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vuePlugin(),
|
||||
iconsPlugin(),
|
||||
pagesPlugin(),
|
||||
windiPlugin()
|
||||
],
|
||||
server: {
|
||||
|
|
Loading…
Add table
Reference in a new issue