commit 03
This commit is contained in:
parent
6b9b020c5d
commit
9a71f52528
7 changed files with 158 additions and 13 deletions
27
src/App.vue
27
src/App.vue
|
@ -2,12 +2,31 @@
|
||||||
<div class="bg-gray-900 h-100vh w-100vw overflow-hidden text-white">
|
<div class="bg-gray-900 h-100vh w-100vw overflow-hidden text-white">
|
||||||
<div :class="$style.noise"/>
|
<div :class="$style.noise"/>
|
||||||
<div :class="$style.vignette"/>
|
<div :class="$style.vignette"/>
|
||||||
<div class="relative h-full">
|
|
||||||
<div v-if="isLoading" class="flex flex-col justify-center items-center text-lg inset-0 absolute">
|
<div v-if="isLoading" class="flex flex-col justify-center items-center text-lg inset-0 absolute">
|
||||||
<span class="text-center">Verbindung wird hergestellt…</span>
|
<span class="text-center">Verbindung wird hergestellt…</span>
|
||||||
</div>
|
</div>
|
||||||
<main>
|
<div class="h-full relative flex flex-col">
|
||||||
<InteractionsScreen/>
|
<nav class="mx-auto py-4">
|
||||||
|
<div class="flex items-center rounded-lg bg-dark-400 overflow-hidden">
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-gray-700"
|
||||||
|
:class="activeTab !== 'interactions' && 'bg-opacity-0'"
|
||||||
|
@click="activeTab = 'interactions'"
|
||||||
|
>
|
||||||
|
Interagieren
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-gray-700"
|
||||||
|
:class="activeTab !== 'queue' && 'bg-opacity-0'"
|
||||||
|
@click="activeTab = 'queue'"
|
||||||
|
>
|
||||||
|
Abstimmen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main class="flex-grow">
|
||||||
|
<InteractionsScreen v-if="activeTab === 'interactions'"/>
|
||||||
|
<QueueScreen v-else-if="activeTab === 'queue'"/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,8 +93,10 @@
|
||||||
import { trpcClient } from "./trpc"
|
import { trpcClient } from "./trpc"
|
||||||
import { useGame } from "./game"
|
import { useGame } from "./game"
|
||||||
import InteractionsScreen from "./screens/InteractionsScreen.vue"
|
import InteractionsScreen from "./screens/InteractionsScreen.vue"
|
||||||
|
import QueueScreen from "./screens/QueueScreen.vue"
|
||||||
|
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
const activeTab = ref<"interactions" | "queue">("interactions")
|
||||||
const game = useGame()
|
const game = useGame()
|
||||||
|
|
||||||
trpcClient.join.subscribe(undefined, {
|
trpcClient.join.subscribe(undefined, {
|
||||||
|
|
65
src/components/InteractionQueueItemCard.vue
Normal file
65
src/components/InteractionQueueItemCard.vue
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<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">
|
||||||
|
<template v-if="item.interaction.type === 'use'">
|
||||||
|
<HandPointingIcon class="text-4xl mb-6"/>
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<ObjectPicture :object-id="item.interaction.objectId"/>
|
||||||
|
<div class="text-sm text-gray-300 text-center">
|
||||||
|
{{ game.allObjectsById.get(item.interaction.objectId)!.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else v-for="(objectId, index) in item.interaction.objectIds" :key="objectId">
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<ObjectPicture :object-id="objectId"/>
|
||||||
|
<div class="text-sm text-gray-300 text-center">
|
||||||
|
{{ game.allObjectsById.get(objectId)!.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PlusIcon v-if="index < item.interaction.objectIds.size - 1" class="text-3xl mb-6"/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 justify-between items-center bg-gray-800 w-17">
|
||||||
|
<div class="flex flex-col justify-center items-center py-3">
|
||||||
|
<div class="text-2xl">{{ item.votes }}</div>
|
||||||
|
<div class="text-sm text-center">
|
||||||
|
Vote{{item.votes === 1 ? "" : "s" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="align-end flex-grow py-2 w-full"
|
||||||
|
:class="game.currentInteractionId === item.id ? 'bg-blue-500' : 'bg-gray-700'"
|
||||||
|
@click="toggleVote()"
|
||||||
|
>
|
||||||
|
+1
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import PlusIcon from "virtual:icons/ph/plus-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"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: InteractionQueueItem
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const game = useGame()
|
||||||
|
|
||||||
|
function toggleVote() {
|
||||||
|
if (game.currentInteractionId === props.item.id) {
|
||||||
|
game.revokeCurrentInteractionVote()
|
||||||
|
} else {
|
||||||
|
game.voteForInteraction(props.item.interaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -8,7 +8,7 @@
|
||||||
}"
|
}"
|
||||||
:data-object-id="object.id"
|
:data-object-id="object.id"
|
||||||
>
|
>
|
||||||
<img :src="`/objects/${props.object.id}.png`" :alt="object.label" class="invert filter object-contain max-w-15"/>
|
<ObjectPicture :object-id="object.id"/>
|
||||||
<div class="text-sm text-gray-300">
|
<div class="text-sm text-gray-300">
|
||||||
{{ object.label }}
|
{{ object.label }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,6 +41,7 @@
|
||||||
import interact from "@interactjs/interact"
|
import interact from "@interactjs/interact"
|
||||||
import { useCurrentElement } from "@vueuse/core"
|
import { useCurrentElement } from "@vueuse/core"
|
||||||
import { computed, onMounted, onUnmounted, ref, useCssModule } from "vue"
|
import { computed, onMounted, onUnmounted, ref, useCssModule } from "vue"
|
||||||
|
import ObjectPicture from "./ObjectPicture.vue"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
object: GameObject
|
object: GameObject
|
||||||
|
|
13
src/components/ObjectPicture.vue
Normal file
13
src/components/ObjectPicture.vue
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
<img :src="`/objects/${objectId}.png`" alt="" class="invert filter object-contain max-w-15"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
objectId: string
|
||||||
|
}>()
|
||||||
|
</script>
|
14
src/game.ts
14
src/game.ts
|
@ -1,7 +1,7 @@
|
||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
import { computed, reactive, ref } from "vue"
|
import { computed, reactive, ref } from "vue"
|
||||||
import { script } from "./shared/script"
|
import { script } from "./shared/script"
|
||||||
import type { Interaction, InteractionQueueItem } from "./shared/script/types"
|
import type { GameObject, Interaction, InteractionQueueItem } from "./shared/script/types"
|
||||||
import { trpcClient } from "./trpc"
|
import { trpcClient } from "./trpc"
|
||||||
import type { GameEvent } from "./shared/gameEvents"
|
import type { GameEvent } from "./shared/gameEvents"
|
||||||
import { getInteractionQueueItemId } from "./shared/util"
|
import { getInteractionQueueItemId } from "./shared/util"
|
||||||
|
@ -13,10 +13,20 @@ export const useGame = defineStore("gameState", () => {
|
||||||
const currentInteractionId = computed(() =>
|
const currentInteractionId = computed(() =>
|
||||||
currentInteraction.value === null ? null : getInteractionQueueItemId(currentInteraction.value))
|
currentInteraction.value === null ? null : getInteractionQueueItemId(currentInteraction.value))
|
||||||
|
|
||||||
|
const sortedInteractionQueue = computed(() => [...interactionQueue.values()].sort((a, b) => b.votes - a.votes))
|
||||||
|
|
||||||
|
const currentRoom = computed(() => script.roomsById.get(currentRoomId.value)!)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentRoomId,
|
currentRoomId,
|
||||||
interactionQueue,
|
interactionQueue,
|
||||||
currentRoom: computed(() => script.roomsById.get(currentRoomId.value)!),
|
sortedInteractionQueue,
|
||||||
|
currentRoom,
|
||||||
|
allObjectsById: computed(() => {
|
||||||
|
const map = new Map<string, GameObject>()
|
||||||
|
currentRoom.value.initialObjects.union(currentRoom.value.hiddenObjects).forEach(o => map.set(o.id, o))
|
||||||
|
return map
|
||||||
|
}),
|
||||||
currentInteraction,
|
currentInteraction,
|
||||||
currentInteractionId,
|
currentInteractionId,
|
||||||
voteForInteraction(interaction: Interaction) {
|
voteForInteraction(interaction: Interaction) {
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-100vh flex flex-col relative">
|
<div class="h-full flex flex-col relative">
|
||||||
<div class="flex-shrink-0 flex order-1">
|
<div class="flex-shrink-0 flex order-1">
|
||||||
<ObjectCardDropZone
|
<ObjectCardDropZone
|
||||||
label="Use"
|
label="Benutzen"
|
||||||
class="bg-green-800"
|
class="bg-green-800"
|
||||||
:has-floating="useFloatingObjectIds.size > 0"
|
:has-floating="useFloatingObjectIds.size > 0"
|
||||||
@object-change="(v, i) => createSetInclusionSetter(useFloatingObjectIds)(v, i)"
|
@object-change="(v, i) => createSetInclusionSetter(useFloatingObjectIds)(v, i)"
|
||||||
@object-drop="onObjectUseDrop"
|
@object-drop="onObjectUseDrop"
|
||||||
/>
|
/>
|
||||||
<ObjectCardDropZone
|
<ObjectCardDropZone
|
||||||
label="Combine"
|
label="Kombinieren"
|
||||||
class="bg-blue-900"
|
class="bg-blue-900"
|
||||||
:has-floating="combineFloatingObjectIds.size > 0"
|
:has-floating="combineFloatingObjectIds.size > 0"
|
||||||
@object-change="(v, i) => createSetInclusionSetter(combineFloatingObjectIds)(v, i)"
|
@object-change="(v, i) => createSetInclusionSetter(combineFloatingObjectIds)(v, i)"
|
||||||
@object-drop="onObjectInteractionDrop"
|
@object-drop="onObjectInteractionDrop"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div ref="objectsContainerElement" class="grid gap-3 grid-cols-2 flex-grow auto-rows-min p-4">
|
<div ref="objectsContainerElement" class="grid gap-3 grid-cols-2 flex-grow auto-rows-min p-4 pt-0">
|
||||||
<ObjectCard
|
<ObjectCard
|
||||||
v-for="object in game.currentRoom.initialObjects"
|
v-for="object in game.currentRoom.initialObjects"
|
||||||
:key="object.id"
|
:key="object.id"
|
||||||
|
|
35
src/screens/QueueScreen.vue
Normal file
35
src/screens/QueueScreen.vue
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<transition-group tag="div" name="list" class="h-full flex flex-col gap-4 p-4 pt-0 relative">
|
||||||
|
<InteractionQueueItemCard
|
||||||
|
v-for="item in game.sortedInteractionQueue"
|
||||||
|
:key="item.id"
|
||||||
|
:item="item"
|
||||||
|
/>
|
||||||
|
</transition-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.list-move,
|
||||||
|
.list-enter-active,
|
||||||
|
.list-leave-active {
|
||||||
|
transition: 400ms ease;
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-enter-from,
|
||||||
|
.list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import InteractionQueueItemCard from "../components/InteractionQueueItemCard.vue"
|
||||||
|
import { useGame } from "../game"
|
||||||
|
|
||||||
|
const game = useGame()
|
||||||
|
</script>
|
Loading…
Add table
Reference in a new issue