From 93e54b21641ec53db43ac7ab1c0da97efad261a5 Mon Sep 17 00:00:00 2001 From: Dennis Postma <dennis@directonline.io> Date: Sat, 6 Jul 2024 21:15:22 +0200 Subject: [PATCH] Added search functionality for tiles and objects, finished object management and added its logic to the zone editor --- .../assetManager/partials/ObjectDetails.vue | 39 +++++++++++++++++-- .../assetManager/partials/ObjectList.vue | 22 +++++++++-- .../assetManager/partials/TileList.vue | 22 +++++++++-- .../utilities/zoneEditor/Objects.vue | 6 +-- .../utilities/zoneEditor/ZoneEditor.vue | 30 +++----------- src/stores/zoneEditor.ts | 12 +++--- 6 files changed, 88 insertions(+), 43 deletions(-) diff --git a/src/components/utilities/assetManager/partials/ObjectDetails.vue b/src/components/utilities/assetManager/partials/ObjectDetails.vue index 7e6c243..17e1a25 100644 --- a/src/components/utilities/assetManager/partials/ObjectDetails.vue +++ b/src/components/utilities/assetManager/partials/ObjectDetails.vue @@ -5,7 +5,7 @@ <div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div> </div> <div class="modal-form asset-manager m-2.5 p-2.5 block"> - <form class="flex gap-2.5 flex-wrap" @submit.prevent> + <form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject"> <div class="w-full flex flex-col mb-5"> <label class="mb-1.5 font-titles" for="name">Name</label> <input v-model="objectName" class="input-cyan" type="text" name="name" placeholder="Wall #1" /> @@ -18,8 +18,8 @@ <label class="mb-1.5 font-titles" for="origin-y">Origin Y</label> <input v-model="objectOriginY" class="input-cyan" type="number" name="origin-y" placeholder="Origin Y" /> </div> - <button class="btn-cyan px-[15px] py-1.5 min-w-[100px]" type="button" @click="removeObject">Save</button> - <button class="btn-bordeaux px-[15px] py-1.5 min-w-[100px]" type="button" @click="removeObject">Remove</button> + <button class="btn-cyan px-[15px] py-1.5 min-w-[100px]" type="submit">Save</button> + <button class="btn-bordeaux px-[15px] py-1.5 min-w-[100px]" type="button" @click.prevent="removeObject">Remove</button> </form> </div> </div> @@ -27,7 +27,7 @@ <script setup lang="ts"> import type { Object } from '@/types' -import { computed, onBeforeUnmount, onMounted, ref } from 'vue' +import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useAssetManagerStore } from '@/stores/assetManager' import { useSocketStore } from '@/stores/socket' import config from '@/config' @@ -50,6 +50,13 @@ if (selectedObject.value) { objectOriginY.value = selectedObject.value.origin_y } +watch(selectedObject, (object: Object | null) => { + if (!object) return + objectName.value = object.name + objectOriginX.value = object.origin_x + objectOriginY.value = object.origin_y +}) + function removeObject() { socket.connection.emit('gm:object:remove', { object: selectedObject.value }, (response: boolean) => { if (!response) { @@ -67,6 +74,30 @@ function refreshObjectList() { }) } +function saveObject() { + if (!selectedObject.value) { + console.error('No object selected') + return + } + + socket.connection.emit( + 'gm:object:update', + { + id: selectedObject.value.id, + name: objectName.value, + origin_x: objectOriginX.value, + origin_y: objectOriginY.value + }, + (response: boolean) => { + if (!response) { + console.error('Failed to save object') + return + } + refreshObjectList() + } + ) +} + onMounted(() => { if (!selectedObject.value) return }) diff --git a/src/components/utilities/assetManager/partials/ObjectList.vue b/src/components/utilities/assetManager/partials/ObjectList.vue index 38b295d..f38a36e 100644 --- a/src/components/utilities/assetManager/partials/ObjectList.vue +++ b/src/components/utilities/assetManager/partials/ObjectList.vue @@ -4,10 +4,10 @@ <input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" /> Upload object(s) </label> - <input class="input-cyan search-field w-full" placeholder="Search..." /> + <input v-model="searchQuery" class="input-cyan search-field w-full" placeholder="Search..." @input="handleSearch" /> <div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div> </div> - <a class="asset relative p-2.5 cursor-pointer" :class="{ active: assetManagerStore.selectedObject?.id === object.id }" v-for="(object, index) in assetManagerStore.objectList" :key="index" @click="assetManagerStore.setSelectedObject(object as Object)"> + <a class="asset relative p-2.5 cursor-pointer" :class="{ active: assetManagerStore.selectedObject?.id === object.id }" v-for="(object, index) in filteredObjects" :key="index" @click="assetManagerStore.setSelectedObject(object as Object)"> <div class="asset-details flex items-center gap-2.5"> <div class="h-[28px] w-[75px] max-w-[75px] flex justify-center"> <img class="h-[28px]" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" /> @@ -21,7 +21,7 @@ <script setup lang="ts"> import config from '@/config' import { useSocketStore } from '@/stores/socket' -import { onMounted, ref } from 'vue' +import { onMounted, ref, computed } from 'vue' import { useAssetManagerStore } from '@/stores/assetManager' import { useAssetStore } from '@/stores/assets' import type { Object } from '@/types' @@ -31,6 +31,8 @@ const objectUploadField = ref(null) const assetManagerStore = useAssetManagerStore() const assetStore = useAssetStore() +const searchQuery = ref('') + const handleFileUpload = (e: Event) => { const files = (e.target as HTMLInputElement).files if (!files) return @@ -48,6 +50,20 @@ const handleFileUpload = (e: Event) => { }) } +const handleSearch = () => { + // The filtering is handled by the computed property, so we don't need to do anything here + // This function is kept in case you want to add debounce or other functionality later +} + +const filteredObjects = computed(() => { + if (!searchQuery.value) { + return assetManagerStore.objectList + } + return assetManagerStore.objectList.filter(object => + object.name.toLowerCase().includes(searchQuery.value.toLowerCase()) + ) +}) + onMounted(() => { socket.connection.emit('gm:object:list', {}, (response: Object[]) => { if (config.development) console.log(response) diff --git a/src/components/utilities/assetManager/partials/TileList.vue b/src/components/utilities/assetManager/partials/TileList.vue index c29aaf6..85f7fd7 100644 --- a/src/components/utilities/assetManager/partials/TileList.vue +++ b/src/components/utilities/assetManager/partials/TileList.vue @@ -4,10 +4,10 @@ <input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" /> Upload tile(s) </label> - <input class="input-cyan search-field w-full" placeholder="Search..." /> + <input v-model="searchQuery" class="input-cyan search-field w-full" placeholder="Search..." @input="handleSearch" /> <div class="absolute left-0 bottom-0 w-full height-[1px] bg-cyan-200"></div> </div> - <a class="asset relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" :class="{ active: assetManagerStore.selectedTile === tile }" v-for="(tile, index) in assetManagerStore.tileList" :key="index" @click="assetManagerStore.setSelectedTile(tile)"> + <a class="asset relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" :class="{ active: assetManagerStore.selectedTile === tile }" v-for="(tile, index) in filteredTiles" :key="index" @click="assetManagerStore.setSelectedTile(tile)"> <div class="asset-details flex items-center gap-2.5"> <!-- TODO make all img have same width so text aligns nicely --> <img class="h-[28px]" :src="`${config.server_endpoint}/assets/tiles/${tile}.png`" alt="Tile" /> @@ -20,7 +20,7 @@ <script setup lang="ts"> import config from '@/config' import { useSocketStore } from '@/stores/socket' -import { onMounted, ref } from 'vue' +import { onMounted, ref, computed } from 'vue' import { useAssetManagerStore } from '@/stores/assetManager' import { useAssetStore } from '@/stores/assets' @@ -29,6 +29,8 @@ const tileUploadField = ref(null) const assetManagerStore = useAssetManagerStore() const assetStore = useAssetStore() +const searchQuery = ref('') + const handleFileUpload = (e: Event) => { const files = (e.target as HTMLInputElement).files if (!files) return @@ -46,6 +48,20 @@ const handleFileUpload = (e: Event) => { }) } +const handleSearch = () => { + // The filtering is handled by the computed property, so we don't need to do anything here + // This function is kept in case you want to add debounce or other functionality later +} + +const filteredTiles = computed(() => { + if (!searchQuery.value) { + return assetManagerStore.tileList + } + return assetManagerStore.tileList.filter(tile => + tile.toLowerCase().includes(searchQuery.value.toLowerCase()) + ) +}) + onMounted(() => { socket.connection.emit('gm:tile:list', {}, (response: string[]) => { if (config.development) console.log(response) diff --git a/src/components/utilities/zoneEditor/Objects.vue b/src/components/utilities/zoneEditor/Objects.vue index 2d2d824..d54b971 100644 --- a/src/components/utilities/zoneEditor/Objects.vue +++ b/src/components/utilities/zoneEditor/Objects.vue @@ -17,11 +17,11 @@ <img :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" - @click="zoneEditorStore.setSelectedObject(object.id)" + @click="zoneEditorStore.setSelectedObject(object)" :class="{ 'w-full h-auto cursor-pointer border-2 transition-all duration-300': true, - 'border-2 border-solid border-cyan shadow-lg scale-105': zoneEditorStore.selectedObject === object.id, - 'border-transparent hover:border-gray-300': zoneEditorStore.selectedObject !== object.id + 'border-2 border-solid border-cyan shadow-lg scale-105': zoneEditorStore.selectedObject?.id === object.id, + 'border-transparent hover:border-gray-300': zoneEditorStore.selectedObject?.id !== object.id }" /> </div> diff --git a/src/components/utilities/zoneEditor/ZoneEditor.vue b/src/components/utilities/zoneEditor/ZoneEditor.vue index a92fb9f..8bf31c0 100644 --- a/src/components/utilities/zoneEditor/ZoneEditor.vue +++ b/src/components/utilities/zoneEditor/ZoneEditor.vue @@ -2,10 +2,8 @@ <TilemapLayerC :tilemap="zone" :tileset="exampleTilesArray" :layerIndex="0" :cull-padding-x="10" :cull-padding-y="10" /> <Controls :layer="tiles" /> - <!-- @TODO: inside asset manager we need to be able to set the originX and originY per individial asset --> <Container> - <!-- <Image :texture="'wall1'" :x="pos.position_x" :y="pos.position_y" :originY="1.13" :originX="1" />--> - <Image v-for="object in zoneObjects" :key="object.id" :texture="object.object" :x="object.position_x" :y="object.position_y" /> + <Image v-for="object in zoneObjects" :key="object.object.id" :x="object.position_x" :y="object.position_y" :texture="object.object.id" :originY="object.object.origin_x" :originX="object.object.origin_y" /> </Container> <Toolbar :layer="tiles" @eraser="eraser" @pencil="pencil" @paint="paint" @save="save" /> @@ -19,7 +17,7 @@ import config from '@/config' import Tileset = Phaser.Tilemaps.Tileset import TilemapLayer = Phaser.Tilemaps.TilemapLayer import { Container, TilemapLayer as TilemapLayerC, useScene, Image } from 'phavuer' -import { onBeforeMount, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue' +import { onBeforeMount, onBeforeUnmount, ref, toRaw } from 'vue' import Controls from '@/components/utilities/Controls.vue' import { useSocketStore } from '@/stores/socket' import Toolbar from '@/components/utilities/zoneEditor/Toolbar.vue' @@ -29,6 +27,7 @@ import ZoneSettings from '@/components/utilities/zoneEditor/ZoneSettings.vue' import { placeTile, tileToWorldXY } from '@/services/zone' import { useAssetStore } from '@/stores/assets' import Objects from '@/components/utilities/zoneEditor/Objects.vue' +import type { Object } from '@/types' const scene = useScene() const socket = useSocketStore() @@ -50,7 +49,7 @@ const tilesetImages: Tileset[] = [] type ZoneObject = { id: string - object: string + object: Object position_x: number position_y: number } @@ -69,16 +68,6 @@ tilesetImages.push(zone.addTilesetImage('blank_tile', 'blank_tile', config.tile_ const tiles = zone.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as TilemapLayer const exampleTilesArray = Array.from({ length: zoneEditorStore.width }, () => Array.from({ length: zoneEditorStore.height }, () => 'blank_tile')) -// Watch for changes in the zoneStore and update the layer -// watch( -// () => zoneEditorStore.object, -// () => { -// // @TODO : change to zone for when loading other maps -// zoneEditorStore.object.forEach((row, y) => row.forEach((tile, x) => layer.putTileAt(tile, x, y))) -// }, -// { deep: true } -// ) - // socket.connection.on('gm:zone_editor:zone:load', (data: Zone) => { // tileTilemap = generateTilemap(scene, data.width, data.height); // zoneEditorStore.setName(data.name) @@ -91,25 +80,18 @@ function eraser(tile: Phaser.Tilemaps.Tile) { placeTile(zone, tiles, tile.x, tile.y, 'blank_tile') zoneEditorStore.updateTile(tile.x, tile.y, 'blank_tile') } - - // if (zoneEditorStore.drawMode === 'wall') { - // walls.putTileAt(0, tile.x, tile.y) - // zoneEditorStore.updateWall(tile.x, tile.y, 0) - // } } function pencil(tile: Phaser.Tilemaps.Tile) { if (zoneEditorStore.drawMode === 'tile') { if (!zoneEditorStore.selectedTile) return - console.log('placing tile', tile.x, tile.y, zoneEditorStore.selectedTile) placeTile(zone, tiles, tile.x, tile.y, zoneEditorStore.selectedTile) // zoneEditorStore.setTiles(tile.x, tile.y, zoneEditorStore.selectedTile) } if (zoneEditorStore.drawMode === 'object') { - // @TODO fix position if (zoneEditorStore.selectedObject === null) return - if (config.development) console.log('placing object', tile.x, tile.y, zoneEditorStore.selectedObject) + console.log('zoneObjects.value', zoneObjects.value) zoneObjects.value.push({ id: Math.random().toString(10), object: zoneEditorStore.selectedObject, @@ -120,9 +102,7 @@ function pencil(tile: Phaser.Tilemaps.Tile) { } function paint(tile: Phaser.Tilemaps.Tile) { - console.log('yoo') if (!zoneEditorStore.selectedTile) return - console.log('placing tile', tile.x, tile.y, zoneEditorStore.selectedTile) exampleTilesArray.forEach((row, y) => row.forEach((tile, x) => placeTile(zone, tiles, x, y, zoneEditorStore.selectedTile))) } diff --git a/src/stores/zoneEditor.ts b/src/stores/zoneEditor.ts index 95288a7..6eac1b1 100644 --- a/src/stores/zoneEditor.ts +++ b/src/stores/zoneEditor.ts @@ -1,4 +1,5 @@ import { defineStore } from 'pinia' +import type { Object } from '@/types' export const useZoneEditorStore = defineStore('zoneEditor', { state: () => ({ @@ -7,11 +8,11 @@ export const useZoneEditorStore = defineStore('zoneEditor', { width: 10, height: 10, tiles: [] as string[][], - objects: [] as number[][], + objects: [] as Object[], tool: 'move', drawMode: 'tile', selectedTile: '', - selectedObject: null, + selectedObject: null as Object | null, isSettingsModalShown: false }), actions: { @@ -33,11 +34,11 @@ export const useZoneEditorStore = defineStore('zoneEditor', { updateTile(x: number, y: number, tile: string) { this.tiles[y][x] = tile }, - setObjects(objects: number[][]) { + setObjects(objects: Object[]) { this.objects = objects }, - updateObject(x: number, y: number, object: number) { - this.objects[y][x] = object + updateObject(index: number, object: Object) { + this.objects[index] = object }, setTool(tool: string) { this.tool = tool @@ -59,6 +60,7 @@ export const useZoneEditorStore = defineStore('zoneEditor', { this.width = 10 this.height = 10 this.tiles = [] + this.objects = [] this.tool = 'move' this.drawMode = 'tile' this.selectedTile = ''