diff --git a/backend/game.ts b/backend/game.ts index c3333db..db4b8a5 100644 --- a/backend/game.ts +++ b/backend/game.ts @@ -23,6 +23,10 @@ export class Game { constructor() { this.switchScene("pre-start") + + setInterval(() => { + this.eventBus.emit("player-broadcast", { type: "keep-alive" }) + }, 10000) } getConnectionPlayerBroadcasts(): PlayerBroadcast[] { diff --git a/backend/main.ts b/backend/main.ts index 4b6d8c5..942f0b2 100644 --- a/backend/main.ts +++ b/backend/main.ts @@ -19,6 +19,7 @@ const { server } = await listen(expressApp, { isProd: !isDev, autoClose: false } const stop = () => { console.log("Received stop signal") + server.closeAllConnections() server.close() } diff --git a/backend/scene-types/interaction.ts b/backend/scene-types/interaction.ts index 704de74..7a07495 100644 --- a/backend/scene-types/interaction.ts +++ b/backend/scene-types/interaction.ts @@ -19,7 +19,7 @@ export class InteractionSceneState implements SceneState private objectVisibilityById = new Map() constructor(private game: Game, private definition: InteractionSceneDefinition) { - Object.values(definition.objects).forEach(o => this.objectVisibilityById.set(o.id, o.reveal)) + definition.objectsById.entries().forEach(([id, o]) => this.objectVisibilityById.set(id, o.reveal)) } getConnectionEvents(): InteractionSceneEvent[] { diff --git a/backend/trpc/base.ts b/backend/trpc/base.ts index 39b22d1..0af8d14 100644 --- a/backend/trpc/base.ts +++ b/backend/trpc/base.ts @@ -10,7 +10,7 @@ export interface Context { } export async function createContext(req: Request, res: Response | undefined): Promise { - const sessionId = req.headers["auio-session"] + const sessionId = req.headers["auio-session-id"] if (sessionId === null || typeof sessionId !== "string" || sessionId.length !== 64) throw new Error(`Missing or invalid session ID: ${sessionId}`) return { diff --git a/backend/trpc/crew.ts b/backend/trpc/crew.ts index 4533a2f..d852310 100644 --- a/backend/trpc/crew.ts +++ b/backend/trpc/crew.ts @@ -10,12 +10,12 @@ const crewProcedure = t.procedure.use(({ ctx, next }) => { }) export const crewRouter = t.router({ - switchRoom: crewProcedure + switchScene: crewProcedure .input(z.object({ - roomId: z.string() + sceneId: z.string() })) .mutation(async ({ input }) => { - game.switchScene(input.roomId) + game.switchScene(input.sceneId) }), interactionScene: t.router({ diff --git a/backend/trpc/index.ts b/backend/trpc/index.ts index 4122b06..ca7bae3 100644 --- a/backend/trpc/index.ts +++ b/backend/trpc/index.ts @@ -7,7 +7,7 @@ import type { PlayerBroadcast } from "../../shared/broadcast" export const appRouter = t.router({ player: playerRouter, - director: crewRouter, + crew: crewRouter, join: t.procedure .subscription(async function*({ signal, ctx }) { @@ -21,8 +21,10 @@ export const appRouter = t.router({ yield broadcast } - for await (const broadcast of iterable) { - yield broadcast as unknown as PlayerBroadcast + for await (const broadcasts of iterable) { + for (const broadcast of broadcasts) { + yield broadcast as unknown as PlayerBroadcast + } } }), }) diff --git a/frontend/App.vue b/frontend/App.vue index df5685b..dd4c2e3 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -2,31 +2,8 @@
-
- -
- - - -
-
-
- Verbindung wird hergestellt… +
+
@@ -101,68 +78,42 @@ diff --git a/frontend/components/TabbedContainer.vue b/frontend/components/TabbedContainer.vue new file mode 100644 index 0000000..ebe2d02 --- /dev/null +++ b/frontend/components/TabbedContainer.vue @@ -0,0 +1,51 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/game.ts b/frontend/game.ts index e8645ca..734cdfc 100644 --- a/frontend/game.ts +++ b/frontend/game.ts @@ -1,16 +1,71 @@ import { defineStore } from "pinia" -import { computed, ref } from "vue" +import { ref, shallowRef } from "vue" import { script } from "../shared/script" import type { PlayerBroadcast } from "../shared/broadcast" +import { trpcClient } from "./trpc" +import type { SceneDefinition, TextSceneDefinition } from "../shared/script/types" +import { sceneTypesById } from "./scene-types" +import { throwError } from "../shared/util" + +export interface CurrentScene { + id: string + type: typeof sceneTypesById[Type] + definition: SceneDefinition & { type: Type } + controller: ReturnType +} export const useGame = defineStore("gameState", () => { - const currentSceneId = ref(script.scenesById.values().next().value!.id) - const currentScene = computed(() => script.scenesById.get(currentSceneId.value)!) + const currentScene = shallowRef>({ + 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) + }) - return { - currentScene, - handlePlayerBroadcast(broadcast: PlayerBroadcast) { - console.log(broadcast) + const isConnected = ref(false) + const isCrew = window.localStorage.getItem("crew-token") !== null + + async function switchScene(id: string) { + await trpcClient.crew.switchScene.mutate({ sceneId: id }) + } + + function handlePlayerBroadcast(broadcast: PlayerBroadcast) { + switch (broadcast.type) { + case "scene-changed": + const definition = script.scenesById.get(broadcast.sceneId) ?? throwError(`Unknown scene: ${broadcast.sceneId}`) + const type = sceneTypesById[definition.type] + currentScene.value = { + id: broadcast.sceneId, + type, + definition, + controller: type.createController(definition as any) + } + + break + + case "scene-event": + currentScene.value.controller.handleEvent(broadcast.event) + break } } + + trpcClient.join.subscribe(undefined, { + onStarted: () => { + isConnected.value = true + }, + onData: handlePlayerBroadcast, + onError(error) { + console.error("🔴", error) + }, + onStopped() { + window.location.reload() + }, + }) + + return { + isCrew, + isConnected, + currentScene, + switchScene + } }) \ No newline at end of file diff --git a/frontend/scene-types/base.ts b/frontend/scene-types/base.ts index 9046f07..78037ba 100644 --- a/frontend/scene-types/base.ts +++ b/frontend/scene-types/base.ts @@ -3,7 +3,8 @@ import type { Component } from "vue" export interface SceneType { id: DefinitionT["type"] - playerView: Component<{ controller: ControllerT }> + playerView: Component<{ controller: ControllerT, definition: DefinitionT }>, + duoView: Component<{ controller: ControllerT, definition: DefinitionT }> createController(definition: DefinitionT): ControllerT } diff --git a/frontend/scene-types/choice/DuoView.vue b/frontend/scene-types/choice/DuoView.vue new file mode 100644 index 0000000..1f9284e --- /dev/null +++ b/frontend/scene-types/choice/DuoView.vue @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/frontend/scene-types/choice/PlayerView.vue b/frontend/scene-types/choice/PlayerView.vue new file mode 100644 index 0000000..1f9284e --- /dev/null +++ b/frontend/scene-types/choice/PlayerView.vue @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/frontend/scene-types/choice/index.ts b/frontend/scene-types/choice/index.ts new file mode 100644 index 0000000..683cfec --- /dev/null +++ b/frontend/scene-types/choice/index.ts @@ -0,0 +1,22 @@ +import type { SceneController, SceneType } from "../base" +import type { ChoiceSceneDefinition } from "../../../shared/script/types" +import PlayerView from "./PlayerView.vue" +import type { ChoiceSceneEvent } from "../../../shared/scene-types/choice" +import DuoView from "./DuoView.vue" + +export const ChoiceSceneType: SceneType = { + id: "choice", + playerView: PlayerView, + duoView: DuoView, + createController(definition: ChoiceSceneDefinition): ChoiceSceneController { + return { + handleEvent(event: ChoiceSceneEvent) { + + } + } + } +} + +export interface ChoiceSceneController extends SceneController { + +} \ No newline at end of file diff --git a/frontend/scene-types/index.ts b/frontend/scene-types/index.ts index 79b1407..2751e06 100644 --- a/frontend/scene-types/index.ts +++ b/frontend/scene-types/index.ts @@ -1,5 +1,9 @@ -import { TextSceneType } from "./text/text" +import { TextSceneType } from "./text" +import { ChoiceSceneType } from "./choice" +import { InteractionSceneType } from "./interaction" export const sceneTypesById = { "text": TextSceneType, + "choice": ChoiceSceneType, + "interaction": InteractionSceneType, } as const \ No newline at end of file diff --git a/frontend/scene-types/interaction/DuoView.vue b/frontend/scene-types/interaction/DuoView.vue new file mode 100644 index 0000000..e17e467 --- /dev/null +++ b/frontend/scene-types/interaction/DuoView.vue @@ -0,0 +1,54 @@ + + + + + \ No newline at end of file diff --git a/frontend/components/InteractionQueueItemCard.vue b/frontend/scene-types/interaction/InteractionQueueItemCard.vue similarity index 94% rename from frontend/components/InteractionQueueItemCard.vue rename to frontend/scene-types/interaction/InteractionQueueItemCard.vue index d9542e6..ebc2998 100644 --- a/frontend/components/InteractionQueueItemCard.vue +++ b/frontend/scene-types/interaction/InteractionQueueItemCard.vue @@ -62,10 +62,8 @@ 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 { findMatchingCombinationInteraction } from "../../shared/util" + import { useGame } from "../../game" const props = defineProps<{ item: InteractionQueueItem diff --git a/frontend/components/ObjectCard.vue b/frontend/scene-types/interaction/ObjectCard.vue similarity index 97% rename from frontend/components/ObjectCard.vue rename to frontend/scene-types/interaction/ObjectCard.vue index 4186f2c..a8be21b 100644 --- a/frontend/components/ObjectCard.vue +++ b/frontend/scene-types/interaction/ObjectCard.vue @@ -37,7 +37,7 @@ \ No newline at end of file diff --git a/frontend/screens/QueueScreen.vue b/frontend/scene-types/interaction/QueueTab.vue similarity index 91% rename from frontend/screens/QueueScreen.vue rename to frontend/scene-types/interaction/QueueTab.vue index 36c24d0..daf29ad 100644 --- a/frontend/screens/QueueScreen.vue +++ b/frontend/scene-types/interaction/QueueTab.vue @@ -28,8 +28,8 @@ \ No newline at end of file diff --git a/frontend/scene-types/text/PlayerView.vue b/frontend/scene-types/text/PlayerView.vue index edea242..9e257de 100644 --- a/frontend/scene-types/text/PlayerView.vue +++ b/frontend/scene-types/text/PlayerView.vue @@ -1,5 +1,9 @@ \ No newline at end of file diff --git a/frontend/scene-types/text/text.ts b/frontend/scene-types/text/index.ts similarity index 91% rename from frontend/scene-types/text/text.ts rename to frontend/scene-types/text/index.ts index 146afd6..2deac3d 100644 --- a/frontend/scene-types/text/text.ts +++ b/frontend/scene-types/text/index.ts @@ -2,10 +2,12 @@ import type { SceneController, SceneType } from "../base" import type { TextSceneDefinition } from "../../../shared/script/types" import type { TextSceneEvent } from "../../../shared/scene-types/text" import PlayerView from "./PlayerView.vue" +import DuoView from "./DuoView.vue" export const TextSceneType: SceneType = { id: "text", playerView: PlayerView, + duoView: DuoView, createController(definition: TextSceneDefinition): TextSceneController { return { handleEvent(event: TextSceneEvent) { diff --git a/frontend/screens/CrewLoginScreen.vue b/frontend/screens/CrewLoginScreen.vue new file mode 100644 index 0000000..2bc9b0b --- /dev/null +++ b/frontend/screens/CrewLoginScreen.vue @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/frontend/screens/DirectorScreen.vue b/frontend/screens/DirectorScreen.vue deleted file mode 100644 index c1fd3db..0000000 --- a/frontend/screens/DirectorScreen.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/screens/DuoScreen.vue b/frontend/screens/DuoScreen.vue new file mode 100644 index 0000000..9b493f1 --- /dev/null +++ b/frontend/screens/DuoScreen.vue @@ -0,0 +1,35 @@ + + + + + \ No newline at end of file diff --git a/frontend/screens/PlayerScreen.vue b/frontend/screens/PlayerScreen.vue new file mode 100644 index 0000000..4dd604e --- /dev/null +++ b/frontend/screens/PlayerScreen.vue @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/frontend/trpc.ts b/frontend/trpc.ts index 81db07f..2204950 100644 --- a/frontend/trpc.ts +++ b/frontend/trpc.ts @@ -1,14 +1,34 @@ import { createTRPCClient, httpLink, httpSubscriptionLink, loggerLink, splitLink } from "@trpc/client" import superjson from "superjson" import type { AppRouter } from "../backend" +import { EventSourcePolyfill } from "event-source-polyfill" +import { nanoid } from "nanoid" + +const crewToken = window.localStorage.getItem("crew-token") +const sessionId = nanoid(64) +const headers = { + "auio-crew-token": crewToken ?? "", + "auio-session-id": sessionId, +} export const trpcClient = createTRPCClient({ links: [ loggerLink(), splitLink({ condition: op => op.type === "subscription", - true: httpSubscriptionLink({ url: "/trpc", transformer: superjson }), - false: httpLink({ url: "/trpc", transformer: superjson }), + true: httpSubscriptionLink({ + url: "/trpc", + transformer: superjson, + EventSource: EventSourcePolyfill, + eventSourceOptions: { + headers + } + }), + false: httpLink({ + url: "/trpc", + transformer: superjson, + headers + }), }) ] }) diff --git a/package.json b/package.json index a4c244a..29704f2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "devDependencies": { "@iconify-json/ph": "^1.2.2", + "@types/event-source-polyfill": "^1.0.5", "@types/express": "^5.0.1", "@types/lodash-es": "^4.17.12", "@types/node": "^22.14.0", @@ -36,6 +37,7 @@ "bufferutil": "^4.0.9", "core-js": "^3.41.0", "date-fns": "^4.1.0", + "event-source-polyfill": "^1.0.31", "eventemitter3": "^5.0.1", "express": "^5.1.0", "listhen": "^1.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33e2bca..b5f029d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + event-source-polyfill: + specifier: ^1.0.31 + version: 1.0.31 eventemitter3: specifier: ^5.0.1 version: 5.0.1 @@ -90,6 +93,9 @@ importers: '@iconify-json/ph': specifier: ^1.2.2 version: 1.2.2 + '@types/event-source-polyfill': + specifier: ^1.0.5 + version: 1.0.5 '@types/express': specifier: ^5.0.1 version: 5.0.1 @@ -585,6 +591,9 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/event-source-polyfill@1.0.5': + resolution: {integrity: sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==} + '@types/express-serve-static-core@5.0.6': resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} @@ -1001,6 +1010,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-source-polyfill@1.0.31: + resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -1998,6 +2010,8 @@ snapshots: '@types/estree@1.0.7': {} + '@types/event-source-polyfill@1.0.5': {} + '@types/express-serve-static-core@5.0.6': dependencies: '@types/node': 22.14.0 @@ -2486,6 +2500,8 @@ snapshots: etag@1.8.1: {} + event-source-polyfill@1.0.31: {} + eventemitter3@5.0.1: {} execa@8.0.1: diff --git a/shared/broadcast.ts b/shared/broadcast.ts index cd82278..b9944f4 100644 --- a/shared/broadcast.ts +++ b/shared/broadcast.ts @@ -1,5 +1,6 @@ import type { SceneEvent } from "../backend/scene-types" export type PlayerBroadcast = + | { type: "keep-alive" } | { type: "scene-changed"; sceneId: string } | { type: "scene-event"; event: SceneEvent } \ No newline at end of file diff --git a/shared/util.ts b/shared/util.ts index e48aa0d..1d82b6f 100644 --- a/shared/util.ts +++ b/shared/util.ts @@ -4,4 +4,8 @@ export const cMap = (object: Partial>): Map(); Object.entries(object).forEach(([k, v]) => result.set(k as K, v as V)) return result +} + +export const throwError = (message: string): never => { + throw new Error(message) } \ No newline at end of file