forked from noxious/client
Rebuilt side panel for object & tile lists
Reorganised file structure
This commit is contained in:
parent
838610d041
commit
ec6f3031b8
4
public/assets/icons/mapEditor/dropdown-chevron.svg
Normal file
4
public/assets/icons/mapEditor/dropdown-chevron.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 9.5L12 14.5L7 9.5" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 325 B |
3
public/assets/icons/mapEditor/search.svg
Normal file
3
public/assets/icons/mapEditor/search.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.3333 17.5L11.0833 12.25C10.6667 12.5833 10.1875 12.8472 9.64583 13.0417C9.10417 13.2361 8.52778 13.3333 7.91667 13.3333C6.40278 13.3333 5.12153 12.809 4.07292 11.7604C3.02431 10.7118 2.5 9.43056 2.5 7.91667C2.5 6.40278 3.02431 5.12153 4.07292 4.07292C5.12153 3.02431 6.40278 2.5 7.91667 2.5C9.43056 2.5 10.7118 3.02431 11.7604 4.07292C12.809 5.12153 13.3333 6.40278 13.3333 7.91667C13.3333 8.52778 13.2361 9.10417 13.0417 9.64583C12.8472 10.1875 12.5833 10.6667 12.25 11.0833L17.5 16.3333L16.3333 17.5ZM7.91667 11.6667C8.95833 11.6667 9.84375 11.3021 10.5729 10.5729C11.3021 9.84375 11.6667 8.95833 11.6667 7.91667C11.6667 6.875 11.3021 5.98958 10.5729 5.26042C9.84375 4.53125 8.95833 4.16667 7.91667 4.16667C6.875 4.16667 5.98958 4.53125 5.26042 5.26042C4.53125 5.98958 4.16667 6.875 4.16667 7.91667C4.16667 8.95833 4.53125 9.84375 5.26042 10.5729C5.98958 11.3021 6.875 11.6667 7.91667 11.6667Z" fill="#808080"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
@ -73,7 +73,7 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
@apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300;
|
@apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300 font-default;
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
@apply outline-none border-cyan rounded bg-gray-900;
|
@apply outline-none border-cyan rounded bg-gray-900;
|
||||||
}
|
}
|
||||||
@ -88,6 +88,12 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
&.input-field {
|
||||||
|
@apply appearance-none bg-[url('/assets/icons/mapEditor/dropdown-chevron.svg')] bg-no-repeat bg-[calc(100%_-_10px)_center] bg-[length:20px] text-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-field-full {
|
.form-field-full {
|
||||||
@apply w-full flex flex-col mb-5;
|
@apply w-full flex flex-col mb-5;
|
||||||
label {
|
label {
|
||||||
|
68
src/components/gameMaster/mapEditor/partials/ListPanel.vue
Normal file
68
src/components/gameMaster/mapEditor/partials/ListPanel.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="isOpen">
|
||||||
|
<div class="flex flex-col gap-2.5 p-2.5">
|
||||||
|
<div class="relative flex">
|
||||||
|
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
||||||
|
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
||||||
|
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<select class="input-field w-full" name="lists" v-model="lists">
|
||||||
|
<option value="tiles">Tiles</option>
|
||||||
|
<option value="objects">Objects</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
|
||||||
|
<TileList v-if="lists === 'tiles'" />
|
||||||
|
<ObjectList v-if="lists === 'objects'" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
|
||||||
|
<span>Tags:</span>
|
||||||
|
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
|
||||||
|
<span class="m-auto">No tags selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import type { Tile } from '@/application/types'
|
||||||
|
import { TileStorage } from '@/storage/storages'
|
||||||
|
import { liveQuery } from 'dexie'
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import TileList from '@/components/gameMaster/mapEditor/partials/lists/TileList.vue'
|
||||||
|
import ObjectList from '@/components/gameMaster/mapEditor/partials/lists/MapObjectList.vue'
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const tileStorage = new TileStorage()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const tiles = ref<Tile[]>([])
|
||||||
|
const lists = ref<'tiles' | 'objects'>('tiles')
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open: () => (isOpen.value = true),
|
||||||
|
close: () => (isOpen.value = false),
|
||||||
|
toggle: () => (isOpen.value = !isOpen.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
let subscription: any = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
tiles.value = result
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Failed to fetch tiles:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,132 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="absolute border-0 border-l-2 border-solid border-gray-500 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" :style="{ width: width + 'px' }" v-if="isOpen">
|
|
||||||
<div class="absolute left-0 top-0 w-1 h-full cursor-ew-resize hover:bg-cyan transition-colors duration-200 z-50" @mousedown.stop="startResize"></div>
|
|
||||||
<div class="relative z-10 p-2.5 border-solid border-0 border-b border-gray-500">
|
|
||||||
<h3 class="text-lg text-white">Map objects</h3>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-hidden grow relative">
|
|
||||||
<div class="absolute w-full h-full top-0 left-0">
|
|
||||||
<div class="relative z-10 h-full">
|
|
||||||
<div class="flex pt-4 pl-4">
|
|
||||||
<div class="w-full flex gap-1.5 flex-row">
|
|
||||||
<div>
|
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
|
||||||
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col h-[calc(100%_-_170px)] p-4 pb-24">
|
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
|
||||||
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
|
||||||
{{ tag }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="h-full overflow-auto">
|
|
||||||
<div class="flex justify-between flex-wrap gap-2.5 items-center">
|
|
||||||
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
|
|
||||||
<img
|
|
||||||
class="border-2 border-solid rounded max-w-full"
|
|
||||||
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
|
|
||||||
alt="Object"
|
|
||||||
@click="mapEditor.setSelectedMapObject(mapObject)"
|
|
||||||
:class="{
|
|
||||||
'cursor-pointer transition-all duration-300': true,
|
|
||||||
'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id,
|
|
||||||
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import config from '@/application/config'
|
|
||||||
import type { MapObject } from '@/application/types'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { MapObjectStorage } from '@/storage/storages'
|
|
||||||
import { liveQuery } from 'dexie'
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
const mapObjectStorage = new MapObjectStorage()
|
|
||||||
const isModalOpen = ref(false)
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const selectedTags = ref<string[]>([])
|
|
||||||
const mapObjectList = ref<MapObject[]>([])
|
|
||||||
const width = ref(320)
|
|
||||||
const minWidth = 320
|
|
||||||
const maxWidth = 800
|
|
||||||
|
|
||||||
const startResize = (event: MouseEvent) => {
|
|
||||||
const startX = event.clientX
|
|
||||||
const startWidth = width.value
|
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
const delta = startX - e.clientX
|
|
||||||
const newWidth = Math.min(Math.max(startWidth + delta, minWidth), maxWidth)
|
|
||||||
width.value = newWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove)
|
|
||||||
document.removeEventListener('mouseup', handleMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => (isOpen.value = true),
|
|
||||||
close: () => (isOpen.value = false),
|
|
||||||
toggle: () => (isOpen.value = !isOpen.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
|
||||||
const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
|
|
||||||
return Array.from(new Set(allTags))
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredMapObjects = computed(() => {
|
|
||||||
return mapObjectList.value.filter((object) => {
|
|
||||||
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
||||||
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
|
|
||||||
return matchesSearch && matchesTags
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggleTag = (tag: string) => {
|
|
||||||
if (selectedTags.value.includes(tag)) {
|
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
|
||||||
} else {
|
|
||||||
selectedTags.value.push(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let subscription: any = null
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
isModalOpen.value = true
|
|
||||||
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
|
|
||||||
next: (result) => {
|
|
||||||
mapObjectList.value = result
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Failed to fetch tiles:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (subscription) {
|
|
||||||
subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,176 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="absolute border-0 border-l-2 border-solid border-gray-500 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" :style="{ width: width + 'px' }" v-if="isOpen">
|
|
||||||
<div class="absolute left-0 top-0 w-1 h-full cursor-ew-resize hover:bg-cyan transition-colors duration-200 z-20" @mousedown.stop="startDragging"></div>
|
|
||||||
<div class="relative z-10 p-2.5 border-solid border-0 border-b border-gray-500">
|
|
||||||
<h3 class="text-lg text-white">Tiles</h3>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-y-auto grow relative">
|
|
||||||
<div class="h-full w-full">
|
|
||||||
<div class="relative z-10 h-full">
|
|
||||||
<div class="grid auto-rows-max gap-2 justify-items-center p-4" style="grid-template-columns: repeat(auto-fill, minmax(80px, 1fr))">
|
|
||||||
<template v-if="!selectedGroup">
|
|
||||||
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
|
||||||
<img
|
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
|
||||||
:alt="group.parent.name"
|
|
||||||
@click="openGroup(group)"
|
|
||||||
@load="() => tileProcessor.processTile(group.parent)"
|
|
||||||
:class="{
|
|
||||||
'border-cyan shadow-lg': isActiveTile(group.parent),
|
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
|
|
||||||
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
|
|
||||||
{{ group.children.length + 1 }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="col-span-full mb-4 flex items-center">
|
|
||||||
<button @click="closeGroup" class="flex items-center text-white hover:text-cyan transition-colors duration-200">
|
|
||||||
<img class="invert w-5 h-5 rotate-90 mr-2" src="/assets/icons/mapEditor/chevron.svg" alt="Back" />
|
|
||||||
Back to groups
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-for="tile in [selectedGroup.parent, ...selectedGroup.children]" :key="tile.id" class="flex flex-col items-center justify-center relative">
|
|
||||||
<img
|
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${tile.id}.png`"
|
|
||||||
:alt="tile.name"
|
|
||||||
@click="selectTile(tile.id)"
|
|
||||||
@load="() => tileProcessor.processTile(tile)"
|
|
||||||
:class="{
|
|
||||||
'border-cyan shadow-lg': isActiveTile(tile),
|
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(tile)
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span class="text-xs mt-1">{{ getTileCategory(tile) }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import config from '@/application/config'
|
|
||||||
import type { Tile } from '@/application/types'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable'
|
|
||||||
import { TileStorage } from '@/storage/storages'
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
const width = ref(320)
|
|
||||||
const isDragging = ref(false)
|
|
||||||
|
|
||||||
const tileStorage = new TileStorage()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const tileProcessor = useTileProcessingComposable()
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const selectedTags = ref<string[]>([])
|
|
||||||
const tileCategories = ref<Map<string, string>>(new Map())
|
|
||||||
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
|
|
||||||
const tiles = ref<Tile[]>([])
|
|
||||||
|
|
||||||
function startDragging(event: MouseEvent) {
|
|
||||||
isDragging.value = true
|
|
||||||
const startX = event.clientX
|
|
||||||
const startWidth = width.value
|
|
||||||
|
|
||||||
function onMouseMove(e: MouseEvent) {
|
|
||||||
if (isDragging.value) {
|
|
||||||
const deltaX = startX - e.clientX
|
|
||||||
width.value = Math.max(320, Math.min(800, startWidth + deltaX))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseUp() {
|
|
||||||
isDragging.value = false
|
|
||||||
document.removeEventListener('mousemove', onMouseMove)
|
|
||||||
document.removeEventListener('mouseup', onMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', onMouseMove)
|
|
||||||
document.addEventListener('mouseup', onMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => (isOpen.value = true),
|
|
||||||
close: () => (isOpen.value = false),
|
|
||||||
toggle: () => (isOpen.value = !isOpen.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
|
||||||
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
|
|
||||||
return Array.from(new Set(allTags))
|
|
||||||
})
|
|
||||||
|
|
||||||
const groupedTiles = computed(() => {
|
|
||||||
const groups: { parent: Tile; children: Tile[] }[] = []
|
|
||||||
const filteredTiles = tiles.value.filter((tile) => {
|
|
||||||
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
||||||
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
|
|
||||||
return matchesSearch && matchesTags
|
|
||||||
})
|
|
||||||
|
|
||||||
filteredTiles.forEach((tile) => {
|
|
||||||
const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile))
|
|
||||||
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
|
||||||
parentGroup.children.push(tile)
|
|
||||||
} else {
|
|
||||||
groups.push({ parent: tile, children: [] })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return groups
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggleTag = (tag: string) => {
|
|
||||||
if (selectedTags.value.includes(tag)) {
|
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
|
||||||
} else {
|
|
||||||
selectedTags.value.push(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTileCategory(tile: Tile): string {
|
|
||||||
return tileCategories.value.get(tile.id) || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function openGroup(group: { parent: Tile; children: Tile[] }) {
|
|
||||||
selectedGroup.value = group
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeGroup() {
|
|
||||||
selectedGroup.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectTile(tile: string) {
|
|
||||||
mapEditor.setSelectedTile(tile)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isActiveTile(tile: Tile): boolean {
|
|
||||||
return mapEditor.selectedTile.value === tile.id
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
tiles.value = await tileStorage.getAll()
|
|
||||||
const initialBatchSize = 20
|
|
||||||
const initialTiles = tiles.value.slice(0, initialBatchSize)
|
|
||||||
initialTiles.forEach((tile) => tileProcessor.processTile(tile))
|
|
||||||
|
|
||||||
// Process remaining tiles in background
|
|
||||||
setTimeout(() => {
|
|
||||||
tiles.value.slice(initialBatchSize).forEach((tile) => tileProcessor.processTile(tile))
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
tileProcessor.cleanup()
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -76,7 +76,7 @@
|
|||||||
<input @change="handleCheck" v-model="checkboxValue" type="checkbox" />
|
<input @change="handleCheck" v-model="checkboxValue" type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
|
<div class="toolbar fixed bottom-0 right-80 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
|
<button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
|
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
|
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
|
||||||
@ -93,7 +93,7 @@ import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
|
|
||||||
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-tile-list', 'open-map-object-list', 'close-lists'])
|
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-lists', 'close-lists'])
|
||||||
|
|
||||||
// track when clicked outside of toolbar items
|
// track when clicked outside of toolbar items
|
||||||
const toolbar = ref(null)
|
const toolbar = ref(null)
|
||||||
@ -113,8 +113,7 @@ defineExpose({ tileListShown, mapObjectListShown })
|
|||||||
function setDrawMode(value: string) {
|
function setDrawMode(value: string) {
|
||||||
if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
|
if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
|
||||||
emit('close-lists')
|
emit('close-lists')
|
||||||
if (value === 'tile') emit('open-tile-list')
|
if (value === 'tile' || value === 'map_object') emit('open-lists')
|
||||||
if (value === 'map_object') emit('open-map-object-list')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mapEditor.setDrawMode(value)
|
mapEditor.setDrawMode(value)
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="flex justify-between flex-wrap gap-2.5 items-center">
|
||||||
|
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
|
||||||
|
<img
|
||||||
|
class="border-2 border-solid rounded max-w-full"
|
||||||
|
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
|
||||||
|
alt="Object"
|
||||||
|
@click="mapEditor.setSelectedMapObject(mapObject)"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer transition-all duration-300': true,
|
||||||
|
'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id,
|
||||||
|
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import type { MapObject } from '@/application/types'
|
||||||
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
|
import { liveQuery } from 'dexie'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const selectedTags = ref<string[]>([])
|
||||||
|
const mapObjectList = ref<MapObject[]>([])
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open: () => (isOpen.value = true),
|
||||||
|
close: () => (isOpen.value = false),
|
||||||
|
toggle: () => (isOpen.value = !isOpen.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const uniqueTags = computed(() => {
|
||||||
|
const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
|
||||||
|
return Array.from(new Set(allTags))
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredMapObjects = computed(() => {
|
||||||
|
return mapObjectList.value.filter((object) => {
|
||||||
|
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
|
||||||
|
return matchesSearch && matchesTags
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleTag = (tag: string) => {
|
||||||
|
if (selectedTags.value.includes(tag)) {
|
||||||
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
|
} else {
|
||||||
|
selectedTags.value.push(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscription: any = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
isModalOpen.value = true
|
||||||
|
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
mapObjectList.value = result
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Failed to fetch tiles:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
232
src/components/gameMaster/mapEditor/partials/lists/TileList.vue
Normal file
232
src/components/gameMaster/mapEditor/partials/lists/TileList.vue
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full" v-if="!selectedGroup">
|
||||||
|
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
||||||
|
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
||||||
|
<img
|
||||||
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
|
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
||||||
|
:alt="group.parent.name"
|
||||||
|
@click="openGroup(group)"
|
||||||
|
@load="() => processTile(group.parent)"
|
||||||
|
:class="{
|
||||||
|
'border-cyan shadow-lg': isActiveTile(group.parent),
|
||||||
|
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
|
||||||
|
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
|
||||||
|
{{ group.children.length + 1 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="h-full overflow-auto">
|
||||||
|
<div class="p-4">
|
||||||
|
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
|
||||||
|
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
|
||||||
|
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<img
|
||||||
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
|
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
|
||||||
|
:alt="selectedGroup.parent.name"
|
||||||
|
@click="selectTile(selectedGroup.parent.id)"
|
||||||
|
:class="{
|
||||||
|
'border-cyan shadow-lg': isActiveTile(selectedGroup.parent),
|
||||||
|
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
|
||||||
|
<img
|
||||||
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
|
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
|
||||||
|
:alt="childTile.name"
|
||||||
|
@click="selectTile(childTile.id)"
|
||||||
|
:class="{
|
||||||
|
'border-cyan shadow-lg': isActiveTile(childTile),
|
||||||
|
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import type { Tile } from '@/application/types'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { TileStorage } from '@/storage/storages'
|
||||||
|
import { liveQuery } from 'dexie'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const tileStorage = new TileStorage()
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const selectedTags = ref<string[]>([])
|
||||||
|
const tileCategories = ref<Map<string, string>>(new Map())
|
||||||
|
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
|
||||||
|
const tiles = ref<Tile[]>([])
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open: () => (isOpen.value = true),
|
||||||
|
close: () => (isOpen.value = false),
|
||||||
|
toggle: () => (isOpen.value = !isOpen.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const uniqueTags = computed(() => {
|
||||||
|
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
|
||||||
|
return Array.from(new Set(allTags))
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupedTiles = computed(() => {
|
||||||
|
const groups: { parent: Tile; children: Tile[] }[] = []
|
||||||
|
const filteredTiles = tiles.value.filter((tile) => {
|
||||||
|
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
|
||||||
|
return matchesSearch && matchesTags
|
||||||
|
})
|
||||||
|
|
||||||
|
filteredTiles.forEach((tile) => {
|
||||||
|
const parentGroup = groups.find((group) => areTilesRelated(group.parent, tile))
|
||||||
|
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
||||||
|
parentGroup.children.push(tile)
|
||||||
|
} else {
|
||||||
|
groups.push({ parent: tile, children: [] })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return groups
|
||||||
|
})
|
||||||
|
|
||||||
|
const tileColorData = ref<Map<string, { r: number; g: number; b: number }>>(new Map())
|
||||||
|
const tileEdgeData = ref<Map<string, number>>(new Map())
|
||||||
|
|
||||||
|
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
|
||||||
|
const colorSimilarityThreshold = 30 // Adjust this value as needed
|
||||||
|
const edgeComplexitySimilarityThreshold = 20 // Adjust this value as needed
|
||||||
|
|
||||||
|
const color1 = tileColorData.value.get(tile1.id)
|
||||||
|
const color2 = tileColorData.value.get(tile2.id)
|
||||||
|
const edge1 = tileEdgeData.value.get(tile1.id)
|
||||||
|
const edge2 = tileEdgeData.value.get(tile2.id)
|
||||||
|
|
||||||
|
if (!color1 || !color2 || edge1 === undefined || edge2 === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorDifference = Math.sqrt(Math.pow(color1.r - color2.r, 2) + Math.pow(color1.g - color2.g, 2) + Math.pow(color1.b - color2.b, 2))
|
||||||
|
|
||||||
|
const edgeComplexityDifference = Math.abs(edge1 - edge2)
|
||||||
|
|
||||||
|
const namePrefix1 = tile1.name.split('_')[0]
|
||||||
|
const namePrefix2 = tile2.name.split('_')[0]
|
||||||
|
|
||||||
|
return colorDifference <= colorSimilarityThreshold && edgeComplexityDifference <= edgeComplexitySimilarityThreshold && namePrefix1 === namePrefix2
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTag = (tag: string) => {
|
||||||
|
if (selectedTags.value.includes(tag)) {
|
||||||
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
|
} else {
|
||||||
|
selectedTags.value.push(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processTile(tile: Tile) {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'Anonymous'
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
canvas.width = img.width
|
||||||
|
canvas.height = img.height
|
||||||
|
ctx!.drawImage(img, 0, 0, img.width, img.height)
|
||||||
|
|
||||||
|
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height)
|
||||||
|
tileColorData.value.set(tile.id, getDominantColor(imageData))
|
||||||
|
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
|
||||||
|
}
|
||||||
|
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDominantColor(imageData: ImageData) {
|
||||||
|
let r = 0,
|
||||||
|
g = 0,
|
||||||
|
b = 0,
|
||||||
|
total = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||||
|
if (imageData.data[i + 3] > 0) {
|
||||||
|
// Only consider non-transparent pixels
|
||||||
|
r += imageData.data[i]
|
||||||
|
g += imageData.data[i + 1]
|
||||||
|
b += imageData.data[i + 2]
|
||||||
|
total++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: Math.round(r / total),
|
||||||
|
g: Math.round(g / total),
|
||||||
|
b: Math.round(b / total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeComplexity(imageData: ImageData) {
|
||||||
|
let edgePixels = 0
|
||||||
|
for (let y = 0; y < imageData.height; y++) {
|
||||||
|
for (let x = 0; x < imageData.width; x++) {
|
||||||
|
const i = (y * imageData.width + x) * 4
|
||||||
|
if (imageData.data[i + 3] > 0 && (x === 0 || y === 0 || x === imageData.width - 1 || y === imageData.height - 1 || imageData.data[i - 1] === 0 || imageData.data[i + 7] === 0)) {
|
||||||
|
edgePixels++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edgePixels
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTileCategory(tile: Tile): string {
|
||||||
|
return tileCategories.value.get(tile.id) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGroup(group: { parent: Tile; children: Tile[] }) {
|
||||||
|
selectedGroup.value = group
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGroup() {
|
||||||
|
selectedGroup.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTile(tile: string) {
|
||||||
|
mapEditor.setSelectedTile(tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActiveTile(tile: Tile): boolean {
|
||||||
|
return mapEditor.selectedTile.value === tile.id
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscription: any = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
tiles.value = result
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Failed to fetch tiles:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
@ -12,14 +12,11 @@
|
|||||||
@open-maps="mapModal?.open"
|
@open-maps="mapModal?.open"
|
||||||
@open-settings="mapSettingsModal?.open"
|
@open-settings="mapSettingsModal?.open"
|
||||||
@close-editor="mapEditor.toggleActive"
|
@close-editor="mapEditor.toggleActive"
|
||||||
@close-lists="tileList?.close"
|
@close-lists="list?.close"
|
||||||
@closeLists="objectList?.close"
|
@open-lists="list?.open"
|
||||||
@open-tile-list="tileList?.open"
|
|
||||||
@open-map-object-list="objectList?.open"
|
|
||||||
/>
|
/>
|
||||||
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
|
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
|
||||||
<TileList ref="tileList" />
|
<ListPanel ref="list" />
|
||||||
<ObjectList ref="objectList" />
|
|
||||||
<MapSettings ref="mapSettingsModal" />
|
<MapSettings ref="mapSettingsModal" />
|
||||||
<TeleportModal ref="teleportModal" />
|
<TeleportModal ref="teleportModal" />
|
||||||
</div>
|
</div>
|
||||||
@ -34,10 +31,9 @@ import 'phaser'
|
|||||||
import type { Map as MapT } from '@/application/types'
|
import type { Map as MapT } from '@/application/types'
|
||||||
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
||||||
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
||||||
import ObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
|
import ListPanel from '@/components/gameMaster/mapEditor/partials/ListPanel.vue'
|
||||||
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
|
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
|
||||||
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
|
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
|
||||||
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
|
|
||||||
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { loadAllTileTextures } from '@/services/mapService'
|
import { loadAllTileTextures } from '@/services/mapService'
|
||||||
@ -51,8 +47,7 @@ const mapEditor = useMapEditorComposable()
|
|||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
const mapModal = useTemplateRef('mapModal')
|
const mapModal = useTemplateRef('mapModal')
|
||||||
const tileList = useTemplateRef('tileList')
|
const list = useTemplateRef('list')
|
||||||
const objectList = useTemplateRef('objectList')
|
|
||||||
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
||||||
|
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user