commit 05

This commit is contained in:
Moritz Ruth 2024-12-27 18:10:40 +01:00
parent 55aa013e1d
commit 58e60e4da4
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
18 changed files with 286 additions and 207 deletions

View file

@ -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
View file

@ -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: {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -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>

View file

@ -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()

View file

@ -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>

View file

@ -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">

View file

@ -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
}
}
}

View file

@ -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")

View file

@ -1,11 +0,0 @@
<template>
<
</template>
<style module lang="scss">
</style>
<script setup lang="ts">
</script>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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()

View file

@ -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)
}),
})

View file

@ -4,3 +4,4 @@ export type GameEvent =
| { type: "room-changed"; roomId: string }
| { type: "interaction-queued"; item: InteractionQueueItem }
| { type: "interaction-votes-changed"; id: string; votes: number }
| { type: "object-visibility-changed"; id: string; isVisible: boolean }

View file

@ -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))
@ -10,3 +11,7 @@ export function getInteractionQueueItemId(interaction: Interaction) {
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))
}

View file

@ -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: {