Add virtual curtain
All checks were successful
Build / build (push) Successful in 1m20s

This commit is contained in:
Moritz Ruth 2025-04-14 17:37:03 +02:00
parent dd5e018477
commit 3498bbb8ad
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
9 changed files with 45 additions and 4 deletions

View file

@ -21,6 +21,8 @@ export class Game {
private currentScene: CurrentScene
public eventBus = new EventEmitter<Events>({ captureRejections: false })
private isVirtualCurtainOpen = false
constructor() {
this.switchScene("pre-start")
@ -33,6 +35,7 @@ export class Game {
const events: PlayerBroadcast[] = []
events.push({ type: "scene-changed", sceneId: this.currentScene.id })
events.push(...this.currentScene.state.getConnectionEvents().map((event: SceneEvent) => ({ type: "scene-event", event })))
events.push({ type: "virtual-curtain-changed", isOpen: this.isVirtualCurtainOpen })
return events
}
@ -62,6 +65,12 @@ export class Game {
return null
}
}
setVirtualCurtainOpen(isOpen: boolean) {
if (this.isVirtualCurtainOpen === isOpen) return
this.isVirtualCurtainOpen = isOpen
this.eventBus.emit("player-broadcast", { type: "virtual-curtain-changed", isOpen: this.isVirtualCurtainOpen })
}
}
export const game = new Game()

View file

@ -18,6 +18,14 @@ export const crewRouter = t.router({
game.switchScene(input.sceneId)
}),
setVirtualCurtainOpen: crewProcedure
.input(z.object({
isOpen: z.boolean()
}))
.mutation(async ({ input }) => {
game.setVirtualCurtainOpen(input.isOpen)
}),
interactionScene: t.router({
setObjectVisibility: crewProcedure
.input(z.object({

View file

@ -22,6 +22,8 @@ export const useGame = defineStore("gameState", () => {
controller: markRaw(sceneTypesById.text.createController(script.scenesById.get("pre-start") as TextSceneDefinition))
})
const isVirtualCurtainOpen = ref(false)
const isConnected = ref(false)
const isCrew = window.localStorage.getItem("crew-token") !== null
@ -47,6 +49,10 @@ export const useGame = defineStore("gameState", () => {
case "scene-event":
currentScene.value.controller.handleEvent(broadcast.event)
break
case "virtual-curtain-changed":
isVirtualCurtainOpen.value = broadcast.isOpen
break
}
}
@ -67,6 +73,10 @@ export const useGame = defineStore("gameState", () => {
isCrew,
isConnected,
currentScene,
switchScene
isVirtualCurtainOpen,
switchScene,
async setVirtualCurtainOpen(isOpen: boolean) {
await trpcClient.crew.setVirtualCurtainOpen.mutate({ isOpen })
}
}
})

View file

@ -17,13 +17,14 @@
/>
</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
<PlayerObjectCard
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)"
:completion-step="controller.objectCompletionById.get(objectId) ?? 0"
@drag-start="onObjectDragStart"
@drag-end="onObjectDragEnd"
/>
@ -36,7 +37,7 @@
</style>
<script setup lang="ts">
import ObjectCard from "./ObjectCard.vue"
import PlayerObjectCard from "./PlayerObjectCard.vue"
import { useGame } from "../../game"
import { computed, reactive, ref } from "vue"
import ObjectCardDropZone from "./ObjectCardDropZone.vue"

View file

@ -12,6 +12,9 @@
<div class="text-gray-200">
{{ object.label }}
</div>
<div v-if="object.completion !== undefined" class="h-4px rounded-full w-80% bg-gray-700 overflow-hidden">
<div class="h-full bg-green-500 transition-all transition-duration-1s" :style="{ width: `${completionStep / object.completion.steps * 100}%` }"></div>
</div>
</div>
</template>
@ -48,6 +51,7 @@
object: SceneObjectDefinition
isOverDropzone: boolean
markedFor: null | "use" | "combine" | "combine-first"
completionStep: number
}>()
const emit = defineEmits<{

View file

@ -26,6 +26,7 @@ export const InteractionSceneType: SceneType<InteractionSceneDefinition, Interac
return {
visibleObjectIds,
objectCompletionById,
interactionQueue,
sortedInteractionQueue: computed(() => [...interactionQueue.values()].sort((a, b) => a.votes - b.votes)),
sortedInteractionQueueWithDefined: computed(() => {
@ -147,6 +148,7 @@ export interface InteractionQueueItem {
export interface InteractionSceneController extends SceneController {
visibleObjectIds: Reactive<Set<string>>
interactionQueue: Reactive<Map<string, InteractionQueueItem>>
objectCompletionById: Reactive<Map<string, number>>
sortedInteractionQueue: Readonly<Ref<Array<InteractionQueueItem>>>
sortedInteractionQueueWithDefined: Readonly<Ref<Array<InteractionQueueItem>>>
suggestedInteraction: Ref<SuggestedInteraction | null>

View file

@ -18,6 +18,9 @@
</div>
</template>
</div>
<div>
<button class="bg-green-800 px-2 text-sm rounded-full" @click="game.setVirtualCurtainOpen(!game.isVirtualCurtainOpen)">Vorhang ist {{ game.isVirtualCurtainOpen ? "auf" : "zu" }}</button>
</div>
</section>
<component :is="game.currentScene.type.crewView" :controller="game.currentScene.controller" :definition="game.currentScene.definition"/>
</div>

View file

@ -1,5 +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.isVirtualCurtainOpen && 'opacity-0 pointer-events-none'">
<span class="text-center">mega_cooler_vorhang.jpg</span>
</div>
<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>

View file

@ -3,4 +3,5 @@ import type { SceneEvent } from "../backend/scene-types"
export type PlayerBroadcast =
| { type: "keep-alive" }
| { type: "scene-changed"; sceneId: string }
| { type: "scene-event"; event: SceneEvent }
| { type: "scene-event"; event: SceneEvent }
| { type: "virtual-curtain-changed", isOpen: boolean }