|
@ -2,7 +2,7 @@ import EventEmitter from "node:events"
|
|||
import type { PlayerBroadcast } from "../shared/broadcast"
|
||||
import { script } from "../shared/script"
|
||||
import type { SceneDefinition } from "../shared/script/types"
|
||||
import { sceneTypesById } from "./scene-types"
|
||||
import { type SceneEvent, sceneTypesById } from "./scene-types"
|
||||
import type { Tagged } from "type-fest"
|
||||
import type { SessionId } from "./session"
|
||||
|
||||
|
@ -32,7 +32,7 @@ export class Game {
|
|||
getConnectionPlayerBroadcasts(): PlayerBroadcast[] {
|
||||
const events: PlayerBroadcast[] = []
|
||||
events.push({ type: "scene-changed", sceneId: this.currentScene.id })
|
||||
events.push(...this.currentScene.state.getConnectionEvents())
|
||||
events.push(...this.currentScene.state.getConnectionEvents().map((event: SceneEvent) => ({ type: "scene-event", event })))
|
||||
|
||||
return events
|
||||
}
|
||||
|
@ -45,12 +45,14 @@ export class Game {
|
|||
const definition = script.scenesById.get(sceneId)
|
||||
if (definition === undefined) throw new Error(`Unknown scene: ${sceneId}`)
|
||||
const type = sceneTypesById[definition.type]
|
||||
|
||||
this.eventBus.emit("player-broadcast", { type: "scene-changed", sceneId: sceneId })
|
||||
|
||||
this.currentScene = {
|
||||
id: sceneId,
|
||||
definition,
|
||||
state: type.createState(this, definition as any)
|
||||
}
|
||||
this.eventBus.emit("player-broadcast", { type: "scene-changed", sceneId: sceneId })
|
||||
}
|
||||
|
||||
withSceneState<Type extends keyof typeof sceneTypesById, R>(type: Type, block: (state: ReturnType<typeof sceneTypesById[Type]["createState"]>) => R): R | null {
|
||||
|
|
|
@ -19,14 +19,14 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
|
|||
private objectVisibilityById = new Map<string, boolean>()
|
||||
|
||||
constructor(private game: Game, private definition: InteractionSceneDefinition) {
|
||||
definition.objectsById.entries().forEach(([id, o]) => this.objectVisibilityById.set(id, o.reveal))
|
||||
definition.objectsById.entries().forEach(([id, o]) => this.setObjectVisibility(id, o.reveal))
|
||||
}
|
||||
|
||||
getConnectionEvents(): InteractionSceneEvent[] {
|
||||
const events: InteractionSceneEvent[] = []
|
||||
events.push(this.getVotesChangedEvent())
|
||||
|
||||
this.objectVisibilityById.entries().forEach(([id, isVisible]) => events.push({ type: "object-visibility-changed", id, isVisible }))
|
||||
this.objectVisibilityById.entries().forEach(([id, isVisible]) => events.push({ type: "object-visibility-changed", objectId: id, isVisible }))
|
||||
|
||||
if (this.ongoingInteractionExecution !== null) events.push({ type: "interaction-execution-started", interaction: this.ongoingInteractionExecution })
|
||||
|
||||
|
@ -107,7 +107,7 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
|
|||
switch (interaction.type) {
|
||||
case "use":
|
||||
if (interaction.consume) {
|
||||
this.emit({ type: "object-visibility-changed", id: interaction.objectId, isVisible: false })
|
||||
this.emit({ type: "object-visibility-changed", objectId: interaction.objectId, isVisible: false })
|
||||
}
|
||||
|
||||
break
|
||||
|
@ -139,10 +139,9 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
|
|||
|
||||
setObjectVisibility(objectId: string, isVisible: boolean) {
|
||||
const current = this.objectVisibilityById.get(objectId)
|
||||
if (current === undefined) throw new Error(`Unknown object: ${objectId}`)
|
||||
if (current === isVisible) return
|
||||
|
||||
this.objectVisibilityById.set(objectId, isVisible)
|
||||
this.emit({ type: "object-visibility-changed", id: objectId, isVisible })
|
||||
this.emit({ type: "object-visibility-changed", objectId: objectId, isVisible })
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="bg-gray-900 h-[100dvh] overflow-hidden text-white">
|
||||
<div class="bg-gray-900 h-[100dvh] text-white">
|
||||
<div :class="$style.noise"/>
|
||||
<div :class="$style.vignette"/>
|
||||
<div class="absolute inset-0">
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
<component :is="SCREENS.find(s => s.id === screenId)!.component"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,10 @@
|
|||
</nav>
|
||||
<main class="flex-grow">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="tabs.find(t => t.id === activeTabId)!.content" @switch-screen="(id: string) => (activeTabId = id)"/>
|
||||
<component
|
||||
:is="tabs.find(t => t.id === activeTabId)!.content"
|
||||
@switch-screen="(id: string) => (activeTabId = id)"
|
||||
/>
|
||||
</transition>
|
||||
</main>
|
||||
</div>
|
||||
|
@ -45,6 +48,7 @@
|
|||
|
||||
const emit = defineEmits<{
|
||||
"update:activeTabId": [string]
|
||||
[k: `content:${string}`]: [unknown]
|
||||
}>()
|
||||
|
||||
const activeTabId = useVModel(props, "activeTabId", emit)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { defineStore } from "pinia"
|
||||
import { ref, shallowRef } from "vue"
|
||||
import { markRaw, ref, shallowRef } from "vue"
|
||||
import { script } from "../shared/script"
|
||||
import type { PlayerBroadcast } from "../shared/broadcast"
|
||||
import { trpcClient } from "./trpc"
|
||||
|
@ -19,7 +19,7 @@ export const useGame = defineStore("gameState", () => {
|
|||
id: "pre-start",
|
||||
type: sceneTypesById.text,
|
||||
definition: script.scenesById.get("pre-start") as TextSceneDefinition,
|
||||
controller: sceneTypesById.text.createController(script.scenesById.get("pre-start") as TextSceneDefinition)
|
||||
controller: markRaw(sceneTypesById.text.createController(script.scenesById.get("pre-start") as TextSceneDefinition))
|
||||
})
|
||||
|
||||
const isConnected = ref(false)
|
||||
|
@ -30,6 +30,7 @@ export const useGame = defineStore("gameState", () => {
|
|||
}
|
||||
|
||||
function handlePlayerBroadcast(broadcast: PlayerBroadcast) {
|
||||
console.log(broadcast)
|
||||
switch (broadcast.type) {
|
||||
case "scene-changed":
|
||||
const definition = script.scenesById.get(broadcast.sceneId) ?? throwError(`Unknown scene: ${broadcast.sceneId}`)
|
||||
|
@ -38,7 +39,7 @@ export const useGame = defineStore("gameState", () => {
|
|||
id: broadcast.sceneId,
|
||||
type,
|
||||
definition,
|
||||
controller: type.createController(definition as any)
|
||||
controller: markRaw(type.createController(definition as any))
|
||||
}
|
||||
|
||||
break
|
||||
|
|
|
@ -18,11 +18,12 @@
|
|||
</div>
|
||||
<transition-group tag="div" name="list" class="grid gap-3 grid-cols-2 flex-grow auto-rows-min p-4 pt-0 relative">
|
||||
<ObjectCard
|
||||
v-for="object in game.visibleObjectsById.values()"
|
||||
:key="object.id"
|
||||
:object="object"
|
||||
:is-over-dropzone="allFloatingObjectIds.has(object.id)"
|
||||
:marked-for="getMarkedFor(object.id)"
|
||||
v-for="objectId in controller.visibleObjectIds.values()"
|
||||
:key="objectId"
|
||||
:objectId="objectId"
|
||||
:object="definition.objectsById.get(objectId)!"
|
||||
:is-over-dropzone="allFloatingObjectIds.has(objectId)"
|
||||
:marked-for="getMarkedFor(objectId)"
|
||||
@drag-start="onObjectDragStart"
|
||||
@drag-end="onObjectDragEnd"
|
||||
/>
|
||||
|
@ -38,11 +39,16 @@
|
|||
import ObjectCard from "./ObjectCard.vue"
|
||||
import { useGame } from "../../game"
|
||||
import { computed, reactive, ref } from "vue"
|
||||
import { useScrollLock } from "@vueuse/core"
|
||||
import ObjectCardDropZone from "./ObjectCardDropZone.vue"
|
||||
import type { InteractionSceneController } from "./index"
|
||||
import type { InteractionSceneDefinition } from "../../../shared/script/types"
|
||||
|
||||
const props = defineProps<{
|
||||
controller: InteractionSceneController
|
||||
definition: InteractionSceneDefinition
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
|
||||
const dragCounter = ref(0)
|
||||
|
||||
const useFloatingObjectIds = reactive(new Set())
|
||||
|
@ -61,14 +67,14 @@
|
|||
function getMarkedFor(objectId: string) {
|
||||
if (firstCombinationObjectId.value !== null) {
|
||||
if (firstCombinationObjectId.value === objectId) return "combine-first"
|
||||
} else if (game.currentInteraction !== null) {
|
||||
switch (game.currentInteraction.type) {
|
||||
} else if (props.controller.suggestedInteraction.value !== null) {
|
||||
switch (props.controller.suggestedInteraction.value.type) {
|
||||
case "use":
|
||||
if (game.currentInteraction.objectId === objectId) return "use"
|
||||
if (props.controller.suggestedInteraction.value.objectId === objectId) return "use"
|
||||
break
|
||||
|
||||
case "combine":
|
||||
if (game.currentInteraction.objectIds.has(objectId)) return "combine"
|
||||
if (props.controller.suggestedInteraction.value.objectIds.has(objectId)) return "combine"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +94,7 @@
|
|||
}
|
||||
|
||||
function onObjectUseDrop(objectId: string) {
|
||||
game.voteForInteraction({
|
||||
props.controller.setInteractionVote({
|
||||
type: "use",
|
||||
objectId
|
||||
})
|
||||
|
@ -96,7 +102,7 @@
|
|||
|
||||
function onObjectInteractionDrop(objectId: string) {
|
||||
if (firstCombinationObjectId.value === null) {
|
||||
game.revokeCurrentInteractionVote()
|
||||
props.controller.setInteractionVote(null)
|
||||
firstCombinationObjectId.value = objectId
|
||||
} else {
|
||||
if (firstCombinationObjectId.value === objectId) {
|
||||
|
@ -104,7 +110,7 @@
|
|||
return
|
||||
}
|
||||
|
||||
game.voteForInteraction({
|
||||
props.controller.setInteractionVote({
|
||||
type: "combine",
|
||||
objectIds: new Set([firstCombinationObjectId.value, objectId])
|
||||
})
|
|
@ -1,51 +1,49 @@
|
|||
<template>
|
||||
<div class="bg-dark-600 rounded-lg flex overflow-hidden">
|
||||
<div class="flex-grow flex items-center justify-center gap-2 px-3 py-4">
|
||||
<template v-if="item.interaction.type === 'use'">
|
||||
<template v-if="interaction.type === 'use'">
|
||||
<HandPointingIcon class="text-4xl mb-6"/>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<ObjectPicture :object-id="item.interaction.objectId"/>
|
||||
<ObjectPicture :object-id="interaction.objectId"/>
|
||||
<div class="text-sm text-gray-200 text-center">
|
||||
{{ game.allObjectsById.get(item.interaction.objectId)!.label }}
|
||||
{{ definition.objectsById.get(interaction.objectId)!.label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else v-for="(objectId, index) in item.interaction.objectIds" :key="objectId">
|
||||
<template v-else v-for="(objectId, index) in interaction.objectIds" :key="objectId">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<ObjectPicture :object-id="objectId"/>
|
||||
<div class="text-sm text-gray-200 text-center">
|
||||
{{ game.allObjectsById.get(objectId)!.label }}
|
||||
{{ definition.objectsById.get(objectId)!.label }}
|
||||
</div>
|
||||
</div>
|
||||
<PlusIcon v-if="index < item.interaction.objectIds.size - 1" class="text-3xl mb-6"/>
|
||||
<PlusIcon v-if="index < interaction.objectIds.size - 1" class="text-3xl mb-6"/>
|
||||
</template>
|
||||
</div>
|
||||
<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-2xl">{{ votes }}</div>
|
||||
<div class="text-sm text-center">
|
||||
Vote{{item.votes === 1 ? "" : "s" }}
|
||||
Vote{{votes === 1 ? "" : "s" }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="mode === 'audience'"
|
||||
class="align-end py-2 w-full"
|
||||
:class="game.currentInteractionId === item.id ? 'bg-blue-500' : 'bg-gray-700'"
|
||||
:class="isCurrentSuggestion ? 'bg-blue-500' : 'bg-gray-700'"
|
||||
@click="toggleVote()"
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
<button
|
||||
v-if="mode === 'director' && (item.interaction.type !== 'combine' || findMatchingCombinationInteraction(game.currentRoom.combinations, item.interaction.objectIds))"
|
||||
v-if="mode === 'director'"
|
||||
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>
|
||||
|
@ -64,19 +62,29 @@
|
|||
import HandPointingIcon from "virtual:icons/ph/hand-pointing-duotone"
|
||||
import ObjectPicture from "./ObjectPicture.vue"
|
||||
import { useGame } from "../../game"
|
||||
import type { SuggestedInteraction } from "../../../shared/mutations"
|
||||
import type { InteractionSceneController } from "./index"
|
||||
import { getSuggestedInteractionId } from "../../../shared/scene-types/interaction"
|
||||
import type { InteractionSceneDefinition } from "../../../shared/script/types"
|
||||
import { computed } from "vue"
|
||||
|
||||
const props = defineProps<{
|
||||
item: InteractionQueueItem
|
||||
interaction: SuggestedInteraction
|
||||
votes: number
|
||||
controller: InteractionSceneController
|
||||
definition: InteractionSceneDefinition
|
||||
mode: "audience" | "director"
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
|
||||
const isCurrentSuggestion = computed(() => props.controller.suggestedInteractionId.value === getSuggestedInteractionId(props.interaction))
|
||||
|
||||
function toggleVote() {
|
||||
if (game.currentInteractionId === props.item.id) {
|
||||
game.revokeCurrentInteractionVote()
|
||||
if (isCurrentSuggestion.value) {
|
||||
props.controller.setInteractionVote(null)
|
||||
} else {
|
||||
game.voteForInteraction(props.item.interaction)
|
||||
props.controller.setInteractionVote(props.interaction)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -6,9 +6,9 @@
|
|||
transform: dragPosition === null ? undefined : `translate(${dragPosition.x}px, ${dragPosition.y}px) scale(${isOverDropzone ? 0.5 : 1})`,
|
||||
transformOrigin: pointerCoordinates === null ? undefined : `${pointerCoordinates.x}px ${pointerCoordinates.y}px`,
|
||||
}"
|
||||
:data-object-id="object.id"
|
||||
:data-object-id="objectId"
|
||||
>
|
||||
<ObjectPicture :object-id="object.id"/>
|
||||
<ObjectPicture :object-id="objectId"/>
|
||||
<div class="text-sm text-gray-200">
|
||||
{{ object.label }}
|
||||
</div>
|
||||
|
@ -44,6 +44,7 @@
|
|||
import ObjectPicture from "./ObjectPicture.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
objectId: string
|
||||
object: SceneObjectDefinition
|
||||
isOverDropzone: boolean
|
||||
markedFor: null | "use" | "combine" | "combine-first"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<img :src="`/objects/${objectId}.png`" alt="" class="invert filter object-contain max-w-15 pointer-events-none" draggable="false"/>
|
||||
<img :src="`/objects/${objectId}.png`" alt="" class="object-contain max-w-15 pointer-events-none" draggable="false"/>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
|
||||
<TabbedContainer v-model:activeTabId="activeTabId" :tabs="TABS" :content-props="{ controller, definition }"
|
||||
@event:switch-tab="(tabId: any) => (activeTabId = tabId)"/>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
|
@ -9,9 +10,36 @@
|
|||
<script setup lang="ts">
|
||||
import type { InteractionSceneController } from "./index"
|
||||
import type { InteractionSceneDefinition } from "../../../shared/script/types"
|
||||
import TabbedContainer, { type Tab } from "../../components/TabbedContainer.vue"
|
||||
import InteractTab from "./InteractTab.vue"
|
||||
import QueueTab from "./QueueTab.vue"
|
||||
import { h, ref } from "vue"
|
||||
import { reactiveComputed } from "@vueuse/core"
|
||||
|
||||
const props = defineProps<{
|
||||
controller: InteractionSceneController
|
||||
definition: InteractionSceneDefinition
|
||||
}>()
|
||||
|
||||
const activeTabId = ref<"interact" | "queue">("interact")
|
||||
|
||||
const TABS: Tab[] = [
|
||||
{
|
||||
id: "interact",
|
||||
label: "Interagieren",
|
||||
content: () => {
|
||||
return h(InteractTab, reactiveComputed(() => ({ controller: props.controller, definition: props.definition })) as any)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "queue",
|
||||
label: "Abstimmen",
|
||||
content: () => h(QueueTab, reactiveComputed(() => ({
|
||||
controller: props.controller, definition: props.definition, onSwitchTab: tabId => {
|
||||
activeTabId.value = tabId
|
||||
}
|
||||
})) as any)
|
||||
}
|
||||
]
|
||||
|
||||
</script>
|
|
@ -1,22 +1,25 @@
|
|||
<template>
|
||||
<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 v-if="controller.sortedInteractionQueue.value.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')">
|
||||
<button class="flex items-center gap-2 px-4 py-2 rounded-lg bg-green-800 text-lg" @click="emit('switch-tab', 'interact')">
|
||||
<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"
|
||||
v-for="(item, index) in controller.sortedInteractionQueue.value"
|
||||
:key="item.id"
|
||||
:style="{ zIndex: 1000 - index }"
|
||||
:item="item"
|
||||
mode="audience"
|
||||
:interaction="item.interaction"
|
||||
:votes="item.votes"
|
||||
:controller="controller"
|
||||
:definition="definition"
|
||||
/>
|
||||
</transition-group>
|
||||
</transition>
|
||||
|
@ -31,9 +34,16 @@
|
|||
import InteractionQueueItemCard from "./InteractionQueueItemCard.vue"
|
||||
import { useGame } from "../../game"
|
||||
import ArrowRightIcon from "virtual:icons/ph/arrow-right"
|
||||
import type { InteractionSceneController } from "./index"
|
||||
import type { InteractionSceneDefinition } from "../../../shared/script/types"
|
||||
|
||||
const props = defineProps<{
|
||||
controller: InteractionSceneController
|
||||
definition: InteractionSceneDefinition
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
"switch-screen": [string]
|
||||
"switch-tab": [string]
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
|
|
|
@ -2,9 +2,10 @@ import type { SceneController, SceneType } from "../base"
|
|||
import type { InteractionSceneDefinition } from "../../../shared/script/types"
|
||||
import PlayerView from "./PlayerView.vue"
|
||||
import type { InteractionSceneEvent } from "../../../shared/scene-types/interaction"
|
||||
import { getSuggestedInteractionId } from "../../../shared/scene-types/interaction"
|
||||
import DuoView from "./DuoView.vue"
|
||||
import type { SuggestedInteraction } from "../../../shared/mutations"
|
||||
import { computed, reactive, type Reactive, type Ref } from "vue"
|
||||
import { computed, reactive, type Reactive, ref, type Ref } from "vue"
|
||||
import { trpcClient } from "../../trpc"
|
||||
|
||||
export const InteractionSceneType: SceneType<InteractionSceneDefinition, InteractionSceneController> = {
|
||||
|
@ -14,16 +15,44 @@ export const InteractionSceneType: SceneType<InteractionSceneDefinition, Interac
|
|||
createController(definition: InteractionSceneDefinition): InteractionSceneController {
|
||||
const visibleObjectIds = reactive(new Set<string>())
|
||||
const interactionQueue = reactive(new Map<string, InteractionQueueItem>())
|
||||
const suggestedInteraction = ref<SuggestedInteraction | null>(null)
|
||||
const suggestedInteractionId = computed(() => suggestedInteraction.value === null ? null : getSuggestedInteractionId(suggestedInteraction.value))
|
||||
|
||||
return {
|
||||
visibleObjectIds,
|
||||
interactionQueue,
|
||||
sortedInteractionQueue: computed(() => [...interactionQueue.values()].sort((a, b) => a.votes - b.votes)),
|
||||
suggestedInteraction,
|
||||
suggestedInteractionId,
|
||||
handleEvent(event: InteractionSceneEvent) {
|
||||
switch (event.type) {
|
||||
case "votes-changed":
|
||||
Object.entries(event.votesByInteractionId).forEach(([interactionId, votes]) => {
|
||||
if (votes <= 0) {
|
||||
interactionQueue.delete(interactionId)
|
||||
if (interactionId === suggestedInteractionId.value) suggestedInteraction.value = null
|
||||
} else interactionQueue.get(interactionId)!.votes = votes
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case "interaction-queued":
|
||||
const id = getSuggestedInteractionId(event.interaction)
|
||||
interactionQueue.set(id, { id, interaction: event.interaction, votes: 1 })
|
||||
break
|
||||
|
||||
case "object-visibility-changed":
|
||||
if (event.isVisible) visibleObjectIds.delete(event.objectId)
|
||||
else visibleObjectIds.add(event.objectId)
|
||||
break
|
||||
}
|
||||
},
|
||||
async setObjectVisibility(objectId: string, isVisible: boolean) {
|
||||
await trpcClient.crew.interactionScene.setObjectVisibility.mutate({ objectId, isVisible })
|
||||
},
|
||||
async setInteractionVote(interaction: SuggestedInteraction | null) {
|
||||
await trpcClient.player.interactionScene.setInteractionVote.mutate({ interaction })
|
||||
suggestedInteraction.value = interaction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +68,10 @@ export interface InteractionSceneController extends SceneController {
|
|||
visibleObjectIds: Reactive<Set<string>>
|
||||
interactionQueue: Reactive<Map<string, InteractionQueueItem>>
|
||||
sortedInteractionQueue: Readonly<Ref<Array<InteractionQueueItem>>>
|
||||
suggestedInteraction: Ref<SuggestedInteraction | null>
|
||||
suggestedInteractionId: Readonly<Ref<string | null>>
|
||||
|
||||
setObjectVisibility(objectId: string, isVisible: boolean): Promise<void>
|
||||
|
||||
setInteractionVote(interaction: SuggestedInteraction | null): Promise<void>
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<component :is="game.currentScene.type.playerView" :controller="game.currentScene.controller" :definition="game.currentScene.definition"/>
|
||||
<div class="flex flex-col justify-center items-center text-lg inset-0 absolute bg-dark-900 transition" :class="game.isConnected && 'opacity-0 pointer-events-none'">
|
||||
<span class="text-center">Verbindung wird hergestellt…</span>
|
||||
</div>
|
||||
<component :is="game.currentScene.type.playerView" :controller="game.currentScene.controller" :definition="game.currentScene.definition"/>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
|
|
|
@ -13,7 +13,7 @@ const headers = {
|
|||
|
||||
export const trpcClient = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
loggerLink(),
|
||||
//loggerLink(),
|
||||
splitLink({
|
||||
condition: op => op.type === "subscription",
|
||||
true: httpSubscriptionLink({
|
||||
|
|
BIN
public/objects/buehnenbildner.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
public/objects/escobar.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
public/objects/h-milch.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
public/objects/haferflocken.png
Normal file
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 12 KiB |
BIN
public/objects/kaffeebohnen.png
Normal file
After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 16 KiB |
BIN
public/objects/kettensaege.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
public/objects/kuehlschrank.png
Normal file
After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 30 KiB |
BIN
public/objects/muesli-unfertig.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
public/objects/muesli.png
Normal file
After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 12 KiB |
BIN
public/objects/peruecke.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
public/objects/redner.png
Normal file
After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 18 KiB |
BIN
public/objects/thunfisch.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
public/objects/vorhang.png
Normal file
After Width: | Height: | Size: 14 KiB |
|
@ -24,7 +24,7 @@ export function findMatchingCombinationInteraction(combinations: Set<CombineInte
|
|||
export type InteractionSceneEvent =
|
||||
| { type: "interaction-queued"; interaction: SuggestedInteraction }
|
||||
| { type: "votes-changed"; votesByInteractionId: Record<string, number> }
|
||||
| { type: "object-visibility-changed"; id: string; isVisible: boolean }
|
||||
| { type: "object-visibility-changed"; objectId: string; isVisible: boolean }
|
||||
| { type: "interaction-execution-started"; interaction: SuggestedInteraction }
|
||||
| { type: "interaction-execution-finished" }
|
||||
| { type: "interaction-execution-cancelled" }
|