Change script types and add a few scenes
Some checks failed
Build / build (push) Failing after 39s

This commit is contained in:
Moritz Ruth 2025-04-12 18:44:43 +02:00
parent 68a91aa31a
commit 34fa93ad44
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
18 changed files with 354 additions and 215 deletions

View file

@ -3,7 +3,7 @@ import type { SceneDefinitionBase } from "../../shared/script/types"
import type { SessionId } from "../session"
export interface SceneType<DefinitionT extends SceneDefinitionBase, StateT extends SceneState> {
id: DefinitionT["id"]
id: DefinitionT["type"]
createState(game: Game, definition: DefinitionT): StateT
}

View file

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

View file

@ -157,7 +157,7 @@
onStarted: () => {
isLoading.value = false
},
onData: game.handleGameEvent,
onData: game.handlePlayerBroadcast,
onError: error => {
console.error("🔴", error)
},

View file

@ -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 | Interaction>(null)
const currentInteractionId = computed(() =>
currentInteraction.value === null ? null : getInteractionQueueItemId(currentInteraction.value))
const interactionQueue = reactive(new Map<string, InteractionQueueItem>())
const sortedInteractionQueue = computed(() =>
[...interactionQueue.values()].sort((a, b) => b.votes - a.votes))
const visibleObjectIds = reactive(new Set<string>())
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<string, SceneObjectDefinition>()
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, SceneObjectDefinition>()
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)
}
}
})

View file

@ -0,0 +1,12 @@
import type { SceneDefinitionBase } from "../../shared/script/types"
import type { Component } from "vue"
export interface SceneType<DefinitionT extends SceneDefinitionBase, ControllerT extends SceneController> {
id: DefinitionT["type"]
playerView: Component<{ controller: ControllerT }>
createController(definition: DefinitionT): ControllerT
}
export interface SceneController<EventT = any> {
handleEvent(event: EventT): void
}

View file

@ -0,0 +1,5 @@
import { TextSceneType } from "./text/text"
export const sceneTypesById = {
"text": TextSceneType,
} as const

View file

@ -0,0 +1,15 @@
<template>
</template>
<style module lang="scss">
</style>
<script setup lang="ts">
import type { TextSceneController } from "./text"
const props = defineProps<{
controller: TextSceneController
}>()
</script>

View file

@ -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<TextSceneDefinition, TextSceneController> = {
id: "text",
playerView: PlayerView,
createController(definition: TextSceneDefinition): TextSceneController {
return {
handleEvent(event: TextSceneEvent) {
}
}
}
}
export interface TextSceneController extends SceneController {
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: []
}
]
})

View file

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

View file

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

View file

@ -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<string, SceneDefinition>
@ -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 = <ObjectIds extends string>(d: SceneDefinitionBase & {
export const defineInteractionScene = <ObjectsT extends Record<string, SceneObjectDefinition<keyof ObjectsT & string>>>(d: SceneDefinitionBase & {
type: "interaction"
objects: IdMap<SceneObjectDefinition & { id: ObjectIds }>
interactions: Array<UseInteractionDefinition<ObjectIds> | CombineInteractionDefinition<ObjectIds>>
}) => ({
objects: ObjectsT
interactions: Array<RawUseInteractionDefinition<keyof ObjectsT & string> | RawCombineInteractionDefinition<keyof ObjectsT & string>>
}): InteractionSceneDefinition<keyof ObjectsT & string> => ({
...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<keyof ObjectsT & string> | CombineInteractionDefinition<keyof ObjectsT & string>
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<ObjectIds extends string = string> extends SceneDefinitionBase {
type: "interaction"
objects: IdMap<SceneObjectDefinition & { id: ObjectIds }>
objectsById: Map<ObjectIds, SceneObjectDefinition<ObjectIds>>
interactionsById: Map<string, UseInteractionDefinition<ObjectIds> | CombineInteractionDefinition<ObjectIds>>
}
export interface SceneObjectDefinition {
id: string
export interface SceneObjectDefinition<AvailableObjectIds extends string = string> {
label: string
reveal: boolean
relevant: boolean
completionSteps?: number
completion?: SceneObjectDefinitionCompletionOptions<AvailableObjectIds>
}
export interface SceneObjectDefinitionCompletionOptions<AvailableObjectIds extends string = string> {
steps: number
replaceWith: AvailableObjectIds
}
interface RawUseInteractionDefinition<AvailableObjectIds extends string> {
type: "use"
objectId: AvailableObjectIds
consume: boolean
revealedObjectIds: Array<AvailableObjectIds>
note?: string
}
export interface UseInteractionDefinition<AvailableObjectIds extends string> {
type: "use"
objectId: AvailableObjectIds
consume: boolean
note: string
revealedObjectIds: Set<AvailableObjectIds>
note?: string
}
interface RawCombineInteractionDefinition<AvailableObjectIds extends string> {
type: "combine"
inputObjects: Partial<Record<AvailableObjectIds, CombinationInteractionDefinitionInputOptions>>
outputObjectIds: Array<AvailableObjectIds>
note?: string
}
export interface CombineInteractionDefinition<AvailableObjectIds extends string> {
type: "combine"
inputObjects: Map<AvailableObjectIds, CombinationInteractionDefinitionInputOptions>
outputObjectIds: Set<AvailableObjectIds>
note: string
note?: string
}
export interface CombinationInteractionDefinitionInputOptions {

View file

@ -1,19 +1,7 @@
export type IdMap<T extends { id: string }> = Map<T["id"], T>
export const cSet = <T>(...values: T[]) => new Set(values)
export const cMap = <K extends string, V>(object: Record<K, V>): Map<K, V> => {
export const cMap = <K extends string, V>(object: Partial<Record<K, V>>): Map<K, V> => {
const result = new Map<K, V>();
for (const key in object) {
result.set(key, object[key])
}
return result
}
export const cIdMap = <K extends string, V extends { id: K }>(...values: V[]) => {
const result = new Map<K, V>();
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
}