diff --git a/backend/scene-types/base.ts b/backend/scene-types/base.ts index f29c9ba..80251d7 100644 --- a/backend/scene-types/base.ts +++ b/backend/scene-types/base.ts @@ -3,7 +3,7 @@ import type { SceneDefinitionBase } from "../../shared/script/types" import type { SessionId } from "../session" export interface SceneType { - id: DefinitionT["id"] + id: DefinitionT["type"] createState(game: Game, definition: DefinitionT): StateT } diff --git a/backend/trpc/index.ts b/backend/trpc/index.ts index 1f05eaf..4122b06 100644 --- a/backend/trpc/index.ts +++ b/backend/trpc/index.ts @@ -3,6 +3,7 @@ import { playerRouter } from "./player" import { crewRouter } from "./crew" import { game } from "../game" import { on } from "node:events" +import type { PlayerBroadcast } from "../../shared/broadcast" export const appRouter = t.router({ player: playerRouter, @@ -21,7 +22,7 @@ export const appRouter = t.router({ } for await (const broadcast of iterable) { - yield broadcast + yield broadcast as unknown as PlayerBroadcast } }), }) diff --git a/frontend/App.vue b/frontend/App.vue index 572e35c..df5685b 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -157,7 +157,7 @@ onStarted: () => { isLoading.value = false }, - onData: game.handleGameEvent, + onData: game.handlePlayerBroadcast, onError: error => { console.error("🔴", error) }, diff --git a/frontend/game.ts b/frontend/game.ts index 720e13e..e8645ca 100644 --- a/frontend/game.ts +++ b/frontend/game.ts @@ -1,99 +1,16 @@ import { defineStore } from "pinia" -import { computed, reactive, ref } from "vue" +import { computed, ref } from "vue" import { script } from "../shared/script" -import type { SceneObjectDefinition, Interaction, InteractionQueueItem } from "../shared/script/types" -import { trpcClient } from "./trpc" import type { PlayerBroadcast } from "../shared/broadcast" -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 currentInteraction = ref(null) - const currentInteractionId = computed(() => - currentInteraction.value === null ? null : getInteractionQueueItemId(currentInteraction.value)) - - const interactionQueue = reactive(new Map()) - const sortedInteractionQueue = computed(() => - [...interactionQueue.values()].sort((a, b) => b.votes - a.votes)) - - const visibleObjectIds = reactive(new Set()) + const currentSceneId = ref(script.scenesById.values().next().value!.id) + const currentScene = computed(() => script.scenesById.get(currentSceneId.value)!) return { - currentRoomId, - currentRoom, - interactionQueue, - visibleObjectIds, - sortedInteractionQueue, - allObjectsById: computed(() => { - const map = new Map() - 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() - 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) { - if (currentInteractionId.value === getInteractionQueueItemId(interaction)) return - if (currentInteractionId.value !== null) trpcClient.player.removeInteractionVote.mutate({ queueItemId: currentInteractionId.value }) - currentInteraction.value = interaction - trpcClient.player.voteForInteraction.mutate({ interaction }) - }, - revokeCurrentInteractionVote() { - if (currentInteractionId.value === null) return - trpcClient.player.removeInteractionVote.mutate({ queueItemId: currentInteractionId.value }) - currentInteraction.value = null - }, - 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: PlayerBroadcast) { - console.log(event) - switch (event.type) { - case "room-changed": - currentInteraction.value = null - currentRoomId.value = event.roomId - interactionQueue.clear() - - visibleObjectIds.clear() - currentRoom.value.initialObjects.forEach(o => visibleObjectIds.add(o.id)) - break - - case "interaction-queued": - interactionQueue.set(event.item.id, event.item) - break - - case "interaction-votes-changed": - if (event.votes <= 0) { - interactionQueue.delete(event.id) - if (currentInteractionId.value === event.id) currentInteraction.value = null - } else { - interactionQueue.get(event.id)!.votes = event.votes - } - - break - - case "object-visibility-changed": - if (event.isVisible) visibleObjectIds.add(event.id) - else visibleObjectIds.delete(event.id) - break - } + currentScene, + handlePlayerBroadcast(broadcast: PlayerBroadcast) { + console.log(broadcast) } } }) \ No newline at end of file diff --git a/frontend/scene-types/base.ts b/frontend/scene-types/base.ts new file mode 100644 index 0000000..9046f07 --- /dev/null +++ b/frontend/scene-types/base.ts @@ -0,0 +1,12 @@ +import type { SceneDefinitionBase } from "../../shared/script/types" +import type { Component } from "vue" + +export interface SceneType { + id: DefinitionT["type"] + playerView: Component<{ controller: ControllerT }> + createController(definition: DefinitionT): ControllerT +} + +export interface SceneController { + handleEvent(event: EventT): void +} \ No newline at end of file diff --git a/frontend/scene-types/index.ts b/frontend/scene-types/index.ts new file mode 100644 index 0000000..79b1407 --- /dev/null +++ b/frontend/scene-types/index.ts @@ -0,0 +1,5 @@ +import { TextSceneType } from "./text/text" + +export const sceneTypesById = { + "text": TextSceneType, +} as const \ No newline at end of file diff --git a/frontend/scene-types/text/PlayerView.vue b/frontend/scene-types/text/PlayerView.vue new file mode 100644 index 0000000..edea242 --- /dev/null +++ b/frontend/scene-types/text/PlayerView.vue @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/frontend/scene-types/text/text.ts b/frontend/scene-types/text/text.ts new file mode 100644 index 0000000..146afd6 --- /dev/null +++ b/frontend/scene-types/text/text.ts @@ -0,0 +1,20 @@ +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" + +export const TextSceneType: SceneType = { + id: "text", + playerView: PlayerView, + createController(definition: TextSceneDefinition): TextSceneController { + return { + handleEvent(event: TextSceneEvent) { + + } + } + } +} + +export interface TextSceneController extends SceneController { + +} \ No newline at end of file diff --git a/shared/script/index.ts b/shared/script/index.ts index 7bf1078..3d97e47 100644 --- a/shared/script/index.ts +++ b/shared/script/index.ts @@ -1,12 +1,18 @@ import type { Script } from "./types" -import { roomHauseingang } from "./rooms/hauseingang" -import { roomKueche } from "./rooms/küche" +import { scenePreStart } from "./scenes/pre-start" +import { sceneTutorialKettensaege } from "./scenes/tutorial-kettensaege" +import { sceneTutorialMuesli } from "./scenes/tutorial-muesli" +import { sceneTutorialVorhang } from "./scenes/tutorial-vorhang" +import { sceneChoiceTest } from "./scenes/choice-test" const script: Script = { scenesById: new Map() } -script.scenesById.set(roomHauseingang.id, roomHauseingang) -script.scenesById.set(roomKueche.id, roomKueche) +script.scenesById.set(scenePreStart.id, scenePreStart) +script.scenesById.set(sceneTutorialKettensaege.id, sceneTutorialKettensaege) +script.scenesById.set(sceneTutorialMuesli.id, sceneTutorialMuesli) +script.scenesById.set(sceneTutorialVorhang.id, sceneTutorialVorhang) +script.scenesById.set(sceneChoiceTest.id, sceneChoiceTest) export { script } \ No newline at end of file diff --git a/shared/script/rooms/hauseingang.ts b/shared/script/rooms/hauseingang.ts deleted file mode 100644 index 186c095..0000000 --- a/shared/script/rooms/hauseingang.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { SceneDefinition } from "../types" -import { cSet } from "../../util" - -export const roomHauseingang: SceneDefinition = { - id: "hauseingang", - label: "Hauseingang", - initialObjects: cSet( - { - id: "schlüssel", - label: "Schlüssel", - }, - { - id: "haustür", - label: "Haustür" - } - ), - hiddenObjects: cSet( - { - id: "offene-haustür", - label: "Offene Haustür" - } - ), - combinations: cSet( - { - id: "open-door", - inputs: cSet( - { - objectId: "schlüssel", - isConsumed: false - }, - { - objectId: "haustür", - isConsumed: true - } - ), - outputIds: cSet( - "offene-haustür" - ) - } - ) -} \ No newline at end of file diff --git a/shared/script/rooms/küche.ts b/shared/script/rooms/küche.ts deleted file mode 100644 index c5ff92f..0000000 --- a/shared/script/rooms/küche.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { SceneDefinition } from "../types" -import { cSet } from "../../util" - -export const roomKueche: SceneDefinition = { - id: "küche", - label: "Küche", - initialObjects: cSet( - { - id: "schüssel", - label: "Schüssel" - }, - { - id: "kühlschrank", - label: "Kühlschrank" - }, - { - id: "kakaopulver", - label: "Kakaopulver" - } - ), - hiddenObjects: cSet( - { - id: "milch", - label: "Milch" - }, - { - id: "kakao", - label: "Kakao" - } - ), - combinations: cSet( - { - id: "kakao", - inputs: cSet( - { - objectId: "milch", - isConsumed: true - }, - { - objectId: "kakaopulver", - isConsumed: false - } - ), - outputIds: cSet( - "kakao" - ) - } - ) -} \ No newline at end of file diff --git a/shared/script/scenes/choice-test.ts b/shared/script/scenes/choice-test.ts new file mode 100644 index 0000000..fcc3bc4 --- /dev/null +++ b/shared/script/scenes/choice-test.ts @@ -0,0 +1,19 @@ +import { Temporal } from "temporal-polyfill" +import type { SceneDefinition } from "../types" + +export const sceneChoiceTest: SceneDefinition = { + id: "choice-test", + type: "choice", + label: "Auswahl-Test", + plannedDuration: Temporal.Duration.from({ seconds: 30 }), + options: [ + { + id: "a", + label: "Option A" + }, + { + id: "b", + label: "Option B" + } + ] +} \ No newline at end of file diff --git a/shared/script/scenes/pre-start.ts b/shared/script/scenes/pre-start.ts new file mode 100644 index 0000000..db5869b --- /dev/null +++ b/shared/script/scenes/pre-start.ts @@ -0,0 +1,9 @@ +import type { SceneDefinition } from "../types" + +export const scenePreStart: SceneDefinition = { + id: "pre-start", + type: "text", + label: "Vor Beginn", + plannedDuration: null, + text: "Die Vorstellung hat noch nicht begonnen." +} \ No newline at end of file diff --git a/shared/script/scenes/tutorial-kettensaege.ts b/shared/script/scenes/tutorial-kettensaege.ts new file mode 100644 index 0000000..2d7a19c --- /dev/null +++ b/shared/script/scenes/tutorial-kettensaege.ts @@ -0,0 +1,37 @@ +import { Temporal } from "temporal-polyfill" +import { type SceneDefinition } from "../types" +import { defineInteractionScene } from "../types" + +export const sceneTutorialKettensaege: SceneDefinition = defineInteractionScene({ + id: "tutorial-kettensaege", + type: "interaction", + label: "Tutorial: Kettensäge", + plannedDuration: Temporal.Duration.from({ minutes: 8 }), + objects: { + kettensaege: { + label: "Kettensäge", + reveal: true, + }, + redner: { + label: "Redner", + reveal: true, + } + }, + interactions: [ + { + type: "combine", + inputObjects: { + "kettensaege": { consume: false }, + "redner": { consume: true }, + }, + outputObjectIds: [], + note: "Bei den ersten zwei Versuchen unterbricht der Redner Faba, bevor er ihn verjagen kann. Beim dritten Mal flüchtet der Redner dann." + }, + { + type: "use", + objectId: "kettensaege", + consume: false, + revealedObjectIds: [] + } + ] +}) \ No newline at end of file diff --git a/shared/script/scenes/tutorial-muesli.ts b/shared/script/scenes/tutorial-muesli.ts new file mode 100644 index 0000000..a6b6ce6 --- /dev/null +++ b/shared/script/scenes/tutorial-muesli.ts @@ -0,0 +1,129 @@ +import { Temporal } from "temporal-polyfill" +import { defineInteractionScene, type SceneDefinition } from "../types" + +export const sceneTutorialMuesli: SceneDefinition = defineInteractionScene({ + id: "tutorial-muesli", + type: "interaction", + label: "Tutorial: Müsli", + plannedDuration: Temporal.Duration.from({ minutes: 8 }), + objects: { + "escobar": { + label: "Escobar", + reveal: true, + }, + "kuehlschrank": { + label: "Kühlschrank", + reveal: true, + }, + "peruecke": { + label: "Perücke", + reveal: true, + }, + "thunfisch": { + label: "Thunfisch", + reveal: false, + }, + "haferflocken": { + label: "Haferflocken", + reveal: false, + }, + "milch": { + label: "Milch", + reveal: false, + }, + "h-milch": { + label: "H-Milch", + reveal: false, + }, + "kaffeebohnen": { + label: "Kaffeebohnen", + reveal: false, + }, + "muesli-unfertig": { + label: "Muesli (unfertig)", + reveal: true, + completion: { + replaceWith: "muesli", + steps: 3 + } + }, + "muesli": { + label: "Müsli", + reveal: false + } + }, + interactions: [ + { + type: "use", + objectId: "kuehlschrank", + consume: false, + revealedObjectIds: ["milch", "thunfisch", "haferflocken", "kaffeebohnen"], + }, + { + type: "combine", + inputObjects: { + "escobar": { consume: false }, + "kuehlschrank": { consume: false }, + }, + outputObjectIds: [], + note: "»Was sagst du Escobar, dir ist es hier zu warm?«" + }, + { + type: "combine", + inputObjects: { + "kaffeebohnen": { consume: true }, + "muesli-unfertig": { consume: false } + }, + outputObjectIds: ["muesli-unfertig"] + }, + { + type: "combine", + inputObjects: { + "thunfisch": { consume: true }, + "muesli-unfertig": { consume: false } + }, + outputObjectIds: ["muesli-unfertig"] + }, + { + type: "combine", + inputObjects: { + "haferflocken": { consume: true }, + "muesli-unfertig": { consume: false } + }, + outputObjectIds: ["muesli-unfertig"] + }, + { + type: "combine", + inputObjects: { + "milch": { consume: false }, + "muesli-unfertig": { consume: false } + }, + outputObjectIds: [], + note: "Leider ist die Milch schon abgelaufen. → Duo: »Hätten wir nur H-Milch besorgt.«" + }, + { + type: "use", + objectId: "milch", + consume: false, + revealedObjectIds: [], + note: "Leider ist die Milch schon abgelaufen. → Duo: »Hätten wir nur H-Milch besorgt.«" + }, + { + type: "combine", + inputObjects: { + "milch": { consume: true }, + "peruecke": { consume: true } + }, + outputObjectIds: ["h-milch"], + note: "Ein Haar der Perücke in der Milch → H-Milch" + }, + { + type: "combine", + inputObjects: { + "h-milch": { consume: true }, + "muesli-unfertig": { consume: false } + }, + outputObjectIds: ["muesli-unfertig"] + } + ] +}) \ No newline at end of file diff --git a/shared/script/scenes/tutorial-vorhang.ts b/shared/script/scenes/tutorial-vorhang.ts new file mode 100644 index 0000000..ac7fa35 --- /dev/null +++ b/shared/script/scenes/tutorial-vorhang.ts @@ -0,0 +1,29 @@ +import { Temporal } from "temporal-polyfill" +import { defineInteractionScene, type SceneDefinition } from "../types" + +export const sceneTutorialVorhang: SceneDefinition = defineInteractionScene({ + id: "tutorial-vorhang", + type: "interaction", + label: "Tutorial: Vorhang", + plannedDuration: Temporal.Duration.from({ seconds: 30 }), + objects: { + "buehnenbildner": { + label: "Bühnenbildner", + reveal: true + }, + "vorhang": { + label: "Vorhang", + reveal: true + } + }, + interactions: [ + { + type: "combine", + inputObjects: { + "buehnenbildner": { consume: true }, + "vorhang": { consume: true }, + }, + outputObjectIds: [], + }, + ] +}) \ No newline at end of file diff --git a/shared/script/types.ts b/shared/script/types.ts index 1d5785a..0b98c41 100644 --- a/shared/script/types.ts +++ b/shared/script/types.ts @@ -1,7 +1,6 @@ import { Temporal } from "temporal-polyfill" -import type { Exact } from "type-fest" import { getSuggestedInteractionFromDefinition, getSuggestedInteractionId } from "../scene-types/interaction" -import type { IdMap } from "../util" +import { cMap, cSet } from "../util" export interface Script { scenesById: Map @@ -9,45 +8,88 @@ export interface Script { export interface SceneDefinitionBase { id: string + type: string label: string - plannedDuration: Temporal.Duration + plannedDuration: Temporal.Duration | null } -export const defineInteractionScene = (d: SceneDefinitionBase & { +export const defineInteractionScene = >>(d: SceneDefinitionBase & { type: "interaction" - objects: IdMap - interactions: Array | CombineInteractionDefinition> -}) => ({ + objects: ObjectsT + interactions: Array | RawCombineInteractionDefinition> +}): InteractionSceneDefinition => ({ ...d, - interactionsById: new Map(d.interactions.map(i => [getSuggestedInteractionId(getSuggestedInteractionFromDefinition(i)), i])) + objectsById: cMap(d.objects), + interactionsById: new Map(d.interactions.values().map(raw => { + let i: UseInteractionDefinition | CombineInteractionDefinition + switch (raw.type) { + case "use": + i = { + type: "use", + objectId: raw.objectId, + consume: raw.consume, + revealedObjectIds: cSet(...raw.revealedObjectIds), + note: raw.note + } + break + + case "combine": + i = { + type: "combine", + inputObjects: cMap(raw.inputObjects), + outputObjectIds: cSet(...raw.outputObjectIds), + note: raw.note + } + } + return [getSuggestedInteractionId(getSuggestedInteractionFromDefinition(i)), i] + })) }) export interface InteractionSceneDefinition extends SceneDefinitionBase { type: "interaction" - objects: IdMap + objectsById: Map> interactionsById: Map | CombineInteractionDefinition> } -export interface SceneObjectDefinition { - id: string +export interface SceneObjectDefinition { label: string reveal: boolean - relevant: boolean - completionSteps?: number + completion?: SceneObjectDefinitionCompletionOptions +} + +export interface SceneObjectDefinitionCompletionOptions { + steps: number + replaceWith: AvailableObjectIds +} + +interface RawUseInteractionDefinition { + type: "use" + objectId: AvailableObjectIds + consume: boolean + revealedObjectIds: Array + note?: string } export interface UseInteractionDefinition { type: "use" objectId: AvailableObjectIds consume: boolean - note: string + revealedObjectIds: Set + note?: string +} + +interface RawCombineInteractionDefinition { + type: "combine" + inputObjects: Partial> + outputObjectIds: Array + note?: string } export interface CombineInteractionDefinition { type: "combine" inputObjects: Map outputObjectIds: Set - note: string + note?: string } export interface CombinationInteractionDefinitionInputOptions { diff --git a/shared/util.ts b/shared/util.ts index f783396..e48aa0d 100644 --- a/shared/util.ts +++ b/shared/util.ts @@ -1,19 +1,7 @@ -export type IdMap = Map - export const cSet = (...values: T[]) => new Set(values) -export const cMap = (object: Record): Map => { +export const cMap = (object: Partial>): Map => { const result = new Map(); - for (const key in object) { - result.set(key, object[key]) - } - return result -} - -export const cIdMap = (...values: V[]) => { - const result = new Map(); - for (const value of values) { - result.set(value.id, value) - } + Object.entries(object).forEach(([k, v]) => result.set(k as K, v as V)) return result } \ No newline at end of file