WIP: Fix and improve interaction scenes
All checks were successful
Build / build (push) Successful in 1m14s

This commit is contained in:
Moritz Ruth 2025-04-14 17:22:56 +02:00
parent 63516d0267
commit dd5e018477
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
12 changed files with 132 additions and 66 deletions

View file

@ -100,7 +100,7 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
finishInteractionExecution(interactionId: string, onlyIfOngoing: boolean = true) {
if (onlyIfOngoing && (this.ongoingInteractionExecution === null || interactionId !== getSuggestedInteractionId(this.ongoingInteractionExecution))) return
this.ongoingInteractionExecution = null
this.emit({ type: "interaction-execution-finished" })
this.emit({ type: "interaction-execution-finished", interactionId })
this.removeInteractionFromQueue(interactionId)
const interaction = this.definition.interactionsById.get(interactionId)
@ -112,6 +112,10 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
this.setObjectVisibility(interaction.objectId, false)
}
interaction.outputObjectIds.forEach(objectId => {
this.revealOrUpgradeObject(objectId, interaction.objectId === objectId && interaction.consume)
})
break
case "combine":
@ -119,26 +123,8 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
if (consume) this.setObjectVisibility(id, false)
})
interaction.outputObjectIds.forEach(id => {
const object = this.definition.objectsById.get(id)!
this.setObjectVisibility(id, true)
if (object.completion !== undefined) {
let step = (this.objectCompletionStepById.get(id) ?? -1)
if (interaction.inputObjects.get(id)?.consume === true) {
step = 0
} else {
step = Math.min(step + 1, object.completion.steps)
}
this.objectCompletionStepById.set(id, step)
this.emit({ type: "object-completion-step-changed", objectId: id, step })
if (step === object.completion.steps) {
this.objectVisibilityById.set(id, false)
this.objectVisibilityById.set(object.completion.replaceWith, true)
}
}
interaction.outputObjectIds.forEach(objectId => {
this.revealOrUpgradeObject(objectId, interaction.inputObjects.get(objectId)?.consume === true)
})
break
@ -166,4 +152,26 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
this.objectVisibilityById.set(objectId, isVisible)
this.emit({ type: "object-visibility-changed", objectId: objectId, isVisible })
}
revealOrUpgradeObject(objectId: string, resetCompletionSteps: boolean = false) {
const object = this.definition.objectsById.get(objectId)!
this.setObjectVisibility(objectId, true)
if (object.completion !== undefined) {
let step = (this.objectCompletionStepById.get(objectId) ?? -1)
if (resetCompletionSteps) {
step = 0
} else {
step = Math.min(step + 1, object.completion.steps)
}
this.objectCompletionStepById.set(objectId, step)
this.emit({ type: "object-completion-step-changed", objectId, step })
if (step === object.completion.steps) {
this.objectVisibilityById.set(objectId, false)
this.objectVisibilityById.set(object.completion.replaceWith, true)
}
}
}
}

View file

@ -1,12 +1,13 @@
<template>
<div class="bg-dark-600 rounded-lg flex flex-shrink-0 overflow-hidden">
<div class="flex-grow flex items-center justify-center gap-2 px-3 py-4">
<div class="bg-dark-600 rounded-lg flex-shrink-0 flex flex-col">
<div class="flex">
<div class="flex-grow flex items-center justify-center gap-2 px-3 py-4 text-xl">
<template v-if="interaction.type === 'use'">
<HandPointingIcon class="text-4xl mb-6"/>
<HandPointingIcon class="mb-6"/>
<div class="flex flex-col items-center gap-2">
<ObjectPicture :object-id="interaction.objectId"/>
<div class="text-sm text-gray-200 text-center">
{{ definition.objectsById.get(interaction.objectId)!.label }}
{{ sceneDefinition.objectsById.get(interaction.objectId)!.label }}
</div>
</div>
</template>
@ -14,20 +15,48 @@
<div class="flex flex-col items-center gap-2">
<ObjectPicture :object-id="objectId"/>
<div class="text-sm text-gray-200 text-center">
{{ definition.objectsById.get(objectId)!.label }}
{{ sceneDefinition.objectsById.get(objectId)!.label }}
</div>
</div>
<PlusIcon v-if="index < interaction.objectIds.size - 1" class="text-3xl mb-6"/>
<PlusIcon v-if="index < interaction.objectIds.size - 1" class="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="flex flex-col justify-center items-center pt-3 pb-2 w-17 bg-gray-800">
<div class="text-2xl">{{ votes }}</div>
<div class="text-sm text-center">
Vote{{votes === 1 ? "" : "s" }}
</div>
</div>
</div>
<div class="flex">
<button
class="w-50% pt-1"
:class="isOngoing ? 'bg-amber-600' : 'bg-blue-700'"
@click="isOngoing ? controller.cancelInteractionExecution(interactionId, true) : controller.startInteractionExecution(interaction)"
>
<StopIcon v-if="isOngoing"/>
<PlayIcon v-else/>
</button>
<button v-if="definition !== null" class="w-50% pt-1 bg-green-700" @click="(event: MouseEvent) => controller.finishInteractionExecution(interactionId, !event.shiftKey)">
<CheckIcon/>
</button>
</div>
<div v-if="definition !== null" class="p-2 border-t border-t-solid border-gray-700 text-sm flex flex-col gap-2">
<template v-if="(definition.type === 'use' || definition.type === 'combine')">
<p v-if="outcomeObjectLabels.consumed.length > 0" class="text-gray-300 m-0">
Versteckt {{ outcomeObjectLabels.consumed.join(", ") }}.
</p>
<p v-if="outcomeObjectLabels.revealed.length > 0" class="text-gray-300 m-0">
Offenbart {{ outcomeObjectLabels.revealed.join(", ") }}.
</p>
<p v-if="outcomeObjectLabels.upgraded.length > 0" class="text-gray-300 m-0">
Entwickelt {{ outcomeObjectLabels.upgraded.join(", ") }}.
</p>
</template>
<p v-if="definition.note" class="m-0">
{{ definition.note }}
</p>
</div>
</div>
</template>
@ -37,11 +66,11 @@
<script setup lang="ts">
import PlusIcon from "virtual:icons/ph/plus-bold"
import TrashIcon from "virtual:icons/ph/trash-bold"
import CheckIcon from "virtual:icons/ph/check-bold"
import PlayIcon from "virtual:icons/ph/play-duotone"
import StopIcon from "virtual:icons/ph/stop-duotone"
import CheckIcon from "virtual:icons/ph/check"
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"
@ -51,7 +80,32 @@
const props = defineProps<{
interaction: SuggestedInteraction
controller: InteractionSceneController
definition: InteractionSceneDefinition
sceneDefinition: InteractionSceneDefinition
votes: number
}>()
const interactionId = computed(() => getSuggestedInteractionId(props.interaction))
const isOngoing = computed(() => props.controller.ongoingInteractionExecutionId.value === interactionId.value)
const definition = computed(() => props.sceneDefinition.interactionsById.get(interactionId.value) ?? null)
const outcomeObjectLabels = computed(() => {
const result = { revealed: [] as string[], upgraded: [] as string[], consumed: [] as string[] }
if (definition.value === null) return result
if (definition.value.type === "use") {
if (definition.value.consume) result.consumed.push(props.sceneDefinition.objectsById.get(definition.value.objectId)!.label)
} else {
for (const [objectId, { consume }] of definition.value.inputObjects.entries()) {
if (consume) result.consumed.push(props.sceneDefinition.objectsById.get(objectId)!.label)
}
}
for (const objectId of definition.value.outputObjectIds.values()) {
const object = props.sceneDefinition.objectsById.get(objectId)!
if (object.completion === undefined) result.revealed.push(object.label)
else result.upgraded.push(object.label)
}
return result
})
</script>

View file

@ -4,7 +4,7 @@
{{ object.label }}
</span>
<div class="flex gap-3 w-full">
<ObjectPicture class="w-10" :object-id="objectId"/>
<ObjectPicture class="w-10 text-2xl" :object-id="objectId"/>
<div class="flex flex-col gap-1">
<button
class="flex items-center gap-1 rounded-full border border-solid border-green-600 px-2 py-1 transition-colors text-xs"

View file

@ -25,7 +25,7 @@
:style="{ zIndex: 1000 - index }"
:interaction="item.interaction"
:controller="controller"
:definition="definition"
:sceneDefinition="definition"
:votes="item.votes"
/>
</transition-group>

View file

@ -1,8 +1,8 @@
<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">
<div class="flex-grow flex items-center justify-center gap-2 px-3 py-4 text-4xl">
<template v-if="interaction.type === 'use'">
<HandPointingIcon class="text-4xl mb-6"/>
<HandPointingIcon class="mb-6"/>
<div class="flex flex-col items-center gap-2">
<ObjectPicture :object-id="interaction.objectId"/>
<div class="text-sm text-gray-200 text-center">
@ -17,7 +17,7 @@
{{ definition.objectsById.get(objectId)!.label }}
</div>
</div>
<PlusIcon v-if="index < interaction.objectIds.size - 1" class="text-3xl mb-6"/>
<PlusIcon v-if="index < interaction.objectIds.size - 1" class="mb-6"/>
</template>
</div>
<div class="flex flex-col justify-between items-center bg-gray-800 w-17">

View file

@ -1,6 +1,6 @@
<template>
<div
class="flex flex-col items-center gap-2 bg-dark-600 rounded-lg p-3 border border-solid"
class="flex flex-col items-center gap-3 bg-dark-600 rounded-lg p-3 border border-solid"
:class="[$style.root, borderClass]"
:style="{
transform: dragPosition === null ? undefined : `translate(${dragPosition.x}px, ${dragPosition.y}px) scale(${isOverDropzone ? 0.5 : 1})`,
@ -8,8 +8,8 @@
}"
:data-object-id="objectId"
>
<ObjectPicture :object-id="objectId"/>
<div class="text-sm text-gray-200">
<ObjectPicture class="text-4xl" :object-id="objectId"/>
<div class="text-gray-200">
{{ object.label }}
</div>
</div>

View file

@ -1,9 +1,11 @@
<template>
<img :src="`/objects/${objectId}.png`" alt="" class="object-contain max-w-15 pointer-events-none" draggable="false"/>
<img :src="`/objects/${objectId}.png`" alt="" class="object-contain pointer-events-none" draggable="false" :class="$style.root"/>
</template>
<style module lang="scss">
.root {
max-width: calc(2.5em - 30px)
}
</style>
<script setup lang="ts">

View file

@ -22,6 +22,7 @@ export const InteractionSceneType: SceneType<InteractionSceneDefinition, Interac
const suggestedInteractionId = computed(() => suggestedInteraction.value === null ? null : getSuggestedInteractionId(suggestedInteraction.value))
const ongoingInteractionExecution = ref<SuggestedInteraction | null>(null)
const ongoingInteractionExecutionId = computed(() => ongoingInteractionExecution.value === null ? null : getSuggestedInteractionId(ongoingInteractionExecution.value))
return {
visibleObjectIds,
@ -62,6 +63,7 @@ export const InteractionSceneType: SceneType<InteractionSceneDefinition, Interac
suggestedInteraction,
suggestedInteractionId,
ongoingInteractionExecution,
ongoingInteractionExecutionId,
handleEvent(event: InteractionSceneEvent) {
switch (event.type) {
case "votes-changed":
@ -110,10 +112,9 @@ export const InteractionSceneType: SceneType<InteractionSceneDefinition, Interac
break
case "interaction-execution-finished":
const interactionId = getSuggestedInteractionId(this.ongoingInteractionExecution.value!)
ongoingInteractionExecution.value = null
interactionQueue.delete(interactionId)
if (suggestedInteractionId.value === interactionId) suggestedInteraction.value = null
interactionQueue.delete(event.interactionId)
if (suggestedInteractionId.value === event.interactionId) suggestedInteraction.value = null
break
}
},
@ -151,6 +152,7 @@ export interface InteractionSceneController extends SceneController {
suggestedInteraction: Ref<SuggestedInteraction | null>
suggestedInteractionId: Readonly<Ref<string | null>>
ongoingInteractionExecution: Ref<SuggestedInteraction | null>
ongoingInteractionExecutionId: Readonly<Ref<string | null>>
setObjectVisibility(objectId: string, isVisible: boolean): Promise<void>

View file

@ -27,5 +27,5 @@ export type InteractionSceneEvent =
| { type: "object-visibility-changed"; objectId: string; isVisible: boolean }
| { type: "object-completion-step-changed"; objectId: string; step: number }
| { type: "interaction-execution-started"; interaction: SuggestedInteraction }
| { type: "interaction-execution-finished" }
| { type: "interaction-execution-finished"; interactionId: string }
| { type: "interaction-execution-cancelled" }

View file

@ -31,7 +31,7 @@ export const sceneTutorialKettensaege: SceneDefinition = defineInteractionScene(
type: "use",
objectId: "kettensaege",
consume: false,
revealedObjectIds: []
outputObjectIds: []
}
]
})

View file

@ -57,7 +57,7 @@ export const sceneTutorialMuesli: SceneDefinition = defineInteractionScene({
type: "use",
objectId: "kuehlschrank",
consume: false,
revealedObjectIds: ["milch", "thunfisch", "haferflocken", "kaffeebohnen"],
outputObjectIds: ["milch", "thunfisch", "haferflocken", "kaffeebohnen"],
},
{
type: "combine",
@ -105,7 +105,7 @@ export const sceneTutorialMuesli: SceneDefinition = defineInteractionScene({
type: "use",
objectId: "milch",
consume: false,
revealedObjectIds: [],
outputObjectIds: [],
note: "Leider ist die Milch schon abgelaufen. → Duo: »Hätten wir nur H-Milch besorgt.«"
},
{

View file

@ -28,7 +28,7 @@ export const defineInteractionScene = <ObjectsT extends Record<string, SceneObje
type: "use",
objectId: raw.objectId,
consume: raw.consume,
revealedObjectIds: cSet(...raw.revealedObjectIds),
outputObjectIds: cSet(...raw.outputObjectIds),
note: raw.note
}
break
@ -66,7 +66,7 @@ interface RawUseInteractionDefinition<AvailableObjectIds extends string> {
type: "use"
objectId: AvailableObjectIds
consume: boolean
revealedObjectIds: Array<AvailableObjectIds>
outputObjectIds: Array<AvailableObjectIds>
note?: string
}
@ -74,7 +74,7 @@ export interface UseInteractionDefinition<AvailableObjectIds extends string> {
type: "use"
objectId: AvailableObjectIds
consume: boolean
revealedObjectIds: Set<AvailableObjectIds>
outputObjectIds: Set<AvailableObjectIds>
note?: string
}