Tiny improvements
This commit is contained in:
@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<div class="flex gap-4 h-[calc(100%_-_32px)] w-[calc(100%_-_32px)] relative m-4">
|
||||
<div class="w-2/12 flex flex-col relative overflow-auto rounded-md default-border bg-gray p-2.5">
|
||||
<!-- Asset Categories -->
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'map_objects' }" @click="() => (selectedCategory = 'map_objects')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'map_objects' }">Map objects</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'items' }" @click="() => (selectedCategory = 'items')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'items' }">Items</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||
<span class="group-hover:text-white">NPC's</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||
<span class="group-hover:text-white">Mounts</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||
<span class="group-hover:text-white">Pets</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||
<span class="group-hover:text-white">Emoticons</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Assets list -->
|
||||
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
||||
<TileList v-if="selectedCategory === 'tiles'" />
|
||||
<MapObjectList v-if="selectedCategory === 'map_objects'" />
|
||||
<SpriteList v-if="selectedCategory === 'sprites'" />
|
||||
<ItemList v-if="selectedCategory === 'items'" />
|
||||
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
||||
<CharacterHairList v-if="selectedCategory === 'characterHair'" />
|
||||
</div>
|
||||
|
||||
<!-- Asset details -->
|
||||
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
|
||||
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
||||
<MapObjectDetails v-if="selectedCategory === 'map_objects' && assetManagerStore.selectedMapObject" />
|
||||
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
||||
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
|
||||
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
||||
<CharacterHairDetails v-if="selectedCategory === 'characterHair' && assetManagerStore.selectedCharacterHair" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CharacterHairDetails from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairDetails.vue'
|
||||
import CharacterHairList from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairList.vue'
|
||||
import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.vue'
|
||||
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
|
||||
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
|
||||
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
|
||||
import MapObjectDetails from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectDetails.vue'
|
||||
import MapObjectList from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectList.vue'
|
||||
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
|
||||
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
|
||||
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
|
||||
import TileList from '@/components/gameMaster/assetManager/partials/tile/TileList.vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
const selectedCategory = ref('tiles')
|
||||
</script>
|
@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="p-2.5 block rounded-md default-border bg-gray">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterHair">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input v-model="characterName" class="input-field" type="text" name="name" placeholder="Character Type Name" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="gender">Gender</label>
|
||||
<select v-model="characterGender" class="input-field" name="gender">
|
||||
<option v-for="gender in genderOptions" :key="gender" :value="gender">{{ gender }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="isSelectable">Is selectable</label>
|
||||
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="spriteId">Sprite</label>
|
||||
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||
<option disabled selected value="">Select sprite</option>
|
||||
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
||||
|
||||
const characterName = ref('')
|
||||
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||
const characterIsSelectable = ref<boolean>(false)
|
||||
const characterSpriteId = ref<string | null | undefined>(null)
|
||||
|
||||
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||
|
||||
if (!selectedCharacterHair.value) {
|
||||
console.error('No character hair selected')
|
||||
}
|
||||
|
||||
if (selectedCharacterHair.value) {
|
||||
characterName.value = selectedCharacterHair.value.name
|
||||
characterGender.value = selectedCharacterHair.value.gender
|
||||
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
||||
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
|
||||
}
|
||||
|
||||
function removeCharacterHair() {
|
||||
if (!selectedCharacterHair.value) return
|
||||
|
||||
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove character hair')
|
||||
return
|
||||
}
|
||||
refreshCharacterHairList()
|
||||
})
|
||||
}
|
||||
|
||||
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||
assetManagerStore.setCharacterHairList(response)
|
||||
|
||||
if (unsetSelectedCharacterHair) {
|
||||
assetManagerStore.setSelectedCharacterHair(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function saveCharacterHair() {
|
||||
const characterHairData = {
|
||||
id: selectedCharacterHair.value!.id,
|
||||
name: characterName.value,
|
||||
gender: characterGender.value,
|
||||
isSelectable: characterIsSelectable.value,
|
||||
spriteId: characterSpriteId.value
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save character type')
|
||||
return
|
||||
}
|
||||
refreshCharacterHairList(false)
|
||||
})
|
||||
}
|
||||
|
||||
watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
||||
if (!characterHair) return
|
||||
characterName.value = characterHair.name
|
||||
characterGender.value = characterHair.gender
|
||||
characterIsSelectable.value = characterHair.isSelectable
|
||||
characterSpriteId.value = characterHair.sprite?.id
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedCharacterHair.value) return
|
||||
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
assetManagerStore.setSelectedCharacterHair(null)
|
||||
})
|
||||
</script>
|
@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||
<button class="p-0 h-5" id="create-character" @click="createNewCharacterHair">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<a
|
||||
v-for="{ data: characterHair } in list"
|
||||
:key="characterHair.id"
|
||||
class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group"
|
||||
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterHair?.id === characterHair.id }"
|
||||
@click="assetManagerStore.setSelectedCharacterHair(characterHair as CharacterHair)"
|
||||
>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterHair?.id === characterHair.id }">{{ characterHair.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CharacterHair } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
const hasScrolled = ref(false)
|
||||
const elementToScroll = ref()
|
||||
|
||||
const handleSearch = () => {
|
||||
// Trigger a re-render of the virtual list
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
const createNewCharacterHair = () => {
|
||||
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to create new character type')
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||
assetManagerStore.setCharacterHairList(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const filteredCharacterHairs = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return assetManagerStore.characterHairList
|
||||
}
|
||||
return assetManagerStore.characterHairList.filter((character) => character.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
})
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredCharacterHairs, {
|
||||
itemHeight: 48
|
||||
})
|
||||
|
||||
const virtualList = ref({ scrollTo })
|
||||
|
||||
const onScroll = () => {
|
||||
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
|
||||
|
||||
if (scrollTop > 80) {
|
||||
hasScrolled.value = true
|
||||
} else if (scrollTop <= 80) {
|
||||
hasScrolled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toTop() {
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||
console.log(response)
|
||||
assetManagerStore.setCharacterHairList(response)
|
||||
})
|
||||
})
|
||||
</script>
|
@ -1,135 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="p-2.5 block rounded-md default-border bg-gray">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input v-model="characterName" class="input-field" type="text" name="name" placeholder="Character Type Name" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="gender">Gender</label>
|
||||
<select v-model="characterGender" class="input-field" name="gender">
|
||||
<option v-for="gender in genderOptions" :key="gender" :value="gender">{{ gender }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="race">Race</label>
|
||||
<select v-model="characterRace" class="input-field" name="race">
|
||||
<option v-for="race in raceOptions" :key="race" :value="race">{{ race }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="isSelectable">Is selectable</label>
|
||||
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="spriteId">Sprite</label>
|
||||
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||
<option disabled selected value="">Select sprite</option>
|
||||
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterType">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
|
||||
|
||||
const characterName = ref('')
|
||||
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||
const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN)
|
||||
const characterIsSelectable = ref<boolean>(false)
|
||||
const characterSpriteId = ref<string | null | undefined>(null)
|
||||
|
||||
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||
const raceOptions: CharacterRace[] = ['HUMAN' as CharacterRace.HUMAN, 'ELF' as CharacterRace.ELF, 'DWARF' as CharacterRace.DWARF, 'ORC' as CharacterRace.ORC, 'GOBLIN' as CharacterRace.GOBLIN]
|
||||
|
||||
if (!selectedCharacterType.value) {
|
||||
console.error('No character type selected')
|
||||
}
|
||||
|
||||
if (selectedCharacterType.value) {
|
||||
characterName.value = selectedCharacterType.value.name
|
||||
characterGender.value = selectedCharacterType.value.gender
|
||||
characterRace.value = selectedCharacterType.value.race
|
||||
characterIsSelectable.value = selectedCharacterType.value.isSelectable
|
||||
characterSpriteId.value = selectedCharacterType.value.sprite?.id
|
||||
}
|
||||
|
||||
function removeCharacterType() {
|
||||
if (!selectedCharacterType.value) return
|
||||
|
||||
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove character type')
|
||||
return
|
||||
}
|
||||
refreshCharacterTypeList()
|
||||
})
|
||||
}
|
||||
|
||||
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||
assetManagerStore.setCharacterTypeList(response)
|
||||
|
||||
if (unsetSelectedCharacterType) {
|
||||
assetManagerStore.setSelectedCharacterType(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function saveCharacterType() {
|
||||
const characterTypeData = {
|
||||
id: selectedCharacterType.value!.id,
|
||||
name: characterName.value,
|
||||
gender: characterGender.value,
|
||||
race: characterRace.value,
|
||||
isSelectable: characterIsSelectable.value,
|
||||
spriteId: characterSpriteId.value
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save character type')
|
||||
return
|
||||
}
|
||||
refreshCharacterTypeList(false)
|
||||
})
|
||||
}
|
||||
|
||||
watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
||||
if (!characterType) return
|
||||
characterName.value = characterType.name
|
||||
characterGender.value = characterType.gender
|
||||
characterRace.value = characterType.race
|
||||
characterIsSelectable.value = characterType.isSelectable
|
||||
characterSpriteId.value = characterType.sprite?.id
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedCharacterType.value) return
|
||||
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
assetManagerStore.setSelectedCharacterType(null)
|
||||
})
|
||||
</script>
|
@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||
<button class="p-0 h-5" id="create-character" @click="createNewCharacterType">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<a
|
||||
v-for="{ data: characterType } in list"
|
||||
:key="characterType.id"
|
||||
class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group"
|
||||
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterType?.id === characterType.id }"
|
||||
@click="assetManagerStore.setSelectedCharacterType(characterType as CharacterType)"
|
||||
>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterType?.id === characterType.id }">{{ characterType.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CharacterType } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
const hasScrolled = ref(false)
|
||||
const elementToScroll = ref()
|
||||
|
||||
const handleSearch = () => {
|
||||
// Trigger a re-render of the virtual list
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
const createNewCharacterType = () => {
|
||||
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to create new character type')
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||
assetManagerStore.setCharacterTypeList(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const filteredCharacterTypes = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return assetManagerStore.characterTypeList
|
||||
}
|
||||
return assetManagerStore.characterTypeList.filter((character) => character.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
})
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredCharacterTypes, {
|
||||
itemHeight: 48
|
||||
})
|
||||
|
||||
const virtualList = ref({ scrollTo })
|
||||
|
||||
const onScroll = () => {
|
||||
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
|
||||
|
||||
if (scrollTop > 80) {
|
||||
hasScrolled.value = true
|
||||
} else if (scrollTop <= 80) {
|
||||
hasScrolled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toTop() {
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||
console.log(response)
|
||||
assetManagerStore.setCharacterTypeList(response)
|
||||
})
|
||||
})
|
||||
</script>
|
@ -1,143 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="p-2.5 block rounded-md default-border bg-gray">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveItem">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input v-model="itemName" class="input-field" type="text" name="name" placeholder="Item Name" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="description">Description</label>
|
||||
<input v-model="itemDescription" class="input-field" type="text" name="description" placeholder="Item Description" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="itemType">Type</label>
|
||||
<select v-model="itemType" class="input-field" name="itemType">
|
||||
<option v-for="type in itemTypeOptions" :key="type" :value="type">{{ type }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="rarity">Rarity</label>
|
||||
<select v-model="itemRarity" class="input-field" name="rarity">
|
||||
<option v-for="rarity in rarityOptions" :key="rarity" :value="rarity">{{ rarity }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="stackable">Stackable</label>
|
||||
<select v-model="itemStackable" class="input-field" name="stackable">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="spriteId">Sprite</label>
|
||||
<select v-model="itemSpriteId" class="input-field" name="spriteId">
|
||||
<option disabled selected value="">Select sprite</option>
|
||||
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeItem">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedItem = computed(() => assetManagerStore.selectedItem)
|
||||
|
||||
const itemName = ref('')
|
||||
const itemDescription = ref('')
|
||||
const itemType = ref<ItemType>('WEAPON' as ItemType)
|
||||
const itemRarity = ref<ItemRarity>('COMMON' as ItemRarity)
|
||||
const itemStackable = ref<boolean>(false)
|
||||
const itemSpriteId = ref<string | null | undefined>(null)
|
||||
|
||||
const itemTypeOptions: ItemType[] = ['WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE']
|
||||
const rarityOptions: ItemRarity[] = ['COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY']
|
||||
|
||||
if (!selectedItem.value) {
|
||||
console.error('No item selected')
|
||||
}
|
||||
|
||||
if (selectedItem.value) {
|
||||
itemName.value = selectedItem.value.name
|
||||
itemDescription.value = selectedItem.value.description || ''
|
||||
itemType.value = selectedItem.value.itemType
|
||||
itemRarity.value = selectedItem.value.rarity
|
||||
itemStackable.value = selectedItem.value.stackable
|
||||
itemSpriteId.value = selectedItem.value.spriteId
|
||||
}
|
||||
|
||||
function removeItem() {
|
||||
if (!selectedItem.value) return
|
||||
|
||||
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove item')
|
||||
return
|
||||
}
|
||||
refreshItemList()
|
||||
})
|
||||
}
|
||||
|
||||
function refreshItemList(unsetSelectedItem = true) {
|
||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||
assetManagerStore.setItemList(response)
|
||||
|
||||
if (unsetSelectedItem) {
|
||||
assetManagerStore.setSelectedItem(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function saveItem() {
|
||||
const itemData = {
|
||||
id: selectedItem.value!.id,
|
||||
name: itemName.value,
|
||||
description: itemDescription.value,
|
||||
itemType: itemType.value,
|
||||
rarity: itemRarity.value,
|
||||
stackable: itemStackable.value,
|
||||
spriteId: itemSpriteId.value
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save item')
|
||||
return
|
||||
}
|
||||
refreshItemList(false)
|
||||
})
|
||||
}
|
||||
|
||||
watch(selectedItem, (item: Item | null) => {
|
||||
if (!item) return
|
||||
itemName.value = item.name
|
||||
itemDescription.value = item.description || ''
|
||||
itemType.value = item.itemType
|
||||
itemRarity.value = item.rarity
|
||||
itemStackable.value = item.stackable
|
||||
itemSpriteId.value = item.spriteId
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedItem.value) return
|
||||
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
assetManagerStore.setSelectedItem(null)
|
||||
})
|
||||
</script>
|
@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||
<label for="create-item" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||
<button class="p-0 h-5" id="create-item" @click="createNewItem">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
|
||||
{{ item.name }}
|
||||
<small class="text-gray-400">({{ item.itemType }})</small>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Item } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
const hasScrolled = ref(false)
|
||||
const elementToScroll = ref()
|
||||
|
||||
const handleSearch = () => {
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
const createNewItem = () => {
|
||||
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to create new item')
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||
assetManagerStore.setItemList(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return assetManagerStore.itemList
|
||||
}
|
||||
return assetManagerStore.itemList.filter((item) => item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || item.itemType.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
})
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredItems, {
|
||||
itemHeight: 48
|
||||
})
|
||||
|
||||
const virtualList = ref({ scrollTo })
|
||||
|
||||
const onScroll = () => {
|
||||
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
|
||||
|
||||
if (scrollTop > 80) {
|
||||
hasScrolled.value = true
|
||||
} else if (scrollTop <= 80) {
|
||||
hasScrolled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toTop() {
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||
assetManagerStore.setItemList(response)
|
||||
})
|
||||
})
|
||||
</script>
|
@ -1,163 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" />
|
||||
</div>
|
||||
<div class="mt-5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input v-model="mapObjectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input v-model="mapObjectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input v-model="mapObjectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="tags">Tags</label>
|
||||
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="is-animated">Is animated</label>
|
||||
<select v-model="mapObjectIsAnimated" class="input-field" name="is-animated">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="frame-speed">Frame rate</label>
|
||||
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="frame-width">Frame width</label>
|
||||
<input v-model="mapObjectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="frame-height">Frame height</label>
|
||||
<input v-model="mapObjectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import type { MapObject } from '@/application/types'
|
||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
const mapEditorStore = useMapEditorStore()
|
||||
|
||||
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
||||
|
||||
const mapObjectName = ref('')
|
||||
const mapObjectTags = ref<string[]>([])
|
||||
const mapObjectOriginX = ref(0)
|
||||
const mapObjectOriginY = ref(0)
|
||||
const mapObjectIsAnimated = ref(false)
|
||||
const mapObjectFrameRate = ref(0)
|
||||
const mapObjectFrameWidth = ref(0)
|
||||
const mapObjectFrameHeight = ref(0)
|
||||
|
||||
if (!selectedMapObject.value) {
|
||||
console.error('No map mapObject selected')
|
||||
}
|
||||
|
||||
if (selectedMapObject.value) {
|
||||
mapObjectName.value = selectedMapObject.value.name
|
||||
mapObjectTags.value = selectedMapObject.value.tags
|
||||
mapObjectOriginX.value = selectedMapObject.value.originX
|
||||
mapObjectOriginY.value = selectedMapObject.value.originY
|
||||
mapObjectIsAnimated.value = selectedMapObject.value.isAnimated
|
||||
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
||||
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
|
||||
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
||||
}
|
||||
|
||||
function removeObject() {
|
||||
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove mapObject')
|
||||
return
|
||||
}
|
||||
refreshObjectList()
|
||||
})
|
||||
}
|
||||
|
||||
function refreshObjectList(unsetSelectedMapObject = true) {
|
||||
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||
assetManagerStore.setMapObjectList(response)
|
||||
|
||||
if (unsetSelectedMapObject) {
|
||||
assetManagerStore.setSelectedMapObject(null)
|
||||
}
|
||||
|
||||
if (mapEditorStore.active) {
|
||||
mapEditorStore.setMapObjectList(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function saveObject() {
|
||||
if (!selectedMapObject.value) {
|
||||
console.error('No mapObject selected')
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.connection?.emit(
|
||||
'gm:mapObject:update',
|
||||
{
|
||||
id: selectedMapObject.value.id,
|
||||
name: mapObjectName.value,
|
||||
tags: mapObjectTags.value,
|
||||
originX: mapObjectOriginX.value,
|
||||
originY: mapObjectOriginY.value,
|
||||
isAnimated: mapObjectIsAnimated.value,
|
||||
frameRate: mapObjectFrameRate.value,
|
||||
frameWidth: mapObjectFrameWidth.value,
|
||||
frameHeight: mapObjectFrameHeight.value
|
||||
},
|
||||
(response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save mapObject')
|
||||
return
|
||||
}
|
||||
refreshObjectList(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
watch(selectedMapObject, (mapObject: MapObject | null) => {
|
||||
if (!mapObject) return
|
||||
mapObjectName.value = mapObject.name
|
||||
mapObjectTags.value = mapObject.tags
|
||||
mapObjectOriginX.value = mapObject.originX
|
||||
mapObjectOriginY.value = mapObject.originY
|
||||
mapObjectIsAnimated.value = mapObject.isAnimated
|
||||
mapObjectFrameRate.value = mapObject.frameRate
|
||||
mapObjectFrameWidth.value = mapObject.frameWidth
|
||||
mapObjectFrameHeight.value = mapObject.frameHeight
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedMapObject.value) return
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
assetManagerStore.setSelectedMapObject(null)
|
||||
})
|
||||
</script>
|
@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<a v-for="{ data: mapObject } in list" :key="mapObject.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedMapObject?.id === mapObject.id }" @click="assetManagerStore.setSelectedMapObject(mapObject as MapObject)">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="h-7 w-16 max-w-16 flex justify-center">
|
||||
<img class="h-7" :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" alt="Object" />
|
||||
</div>
|
||||
<span :class="{ 'text-white': assetManagerStore.selectedMapObject?.id === mapObject.id }">{{ mapObject.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import type { MapObject } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const objectUploadField = ref(null)
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
const hasScrolled = ref(false)
|
||||
const elementToScroll = ref()
|
||||
|
||||
const handleFileUpload = (e: Event) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => {
|
||||
if (!response) {
|
||||
if (config.development) console.error('Failed to upload object')
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||
assetManagerStore.setMapObjectList(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// Trigger a re-render of the virtual list
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
const filteredObjects = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return assetManagerStore.mapObjectList
|
||||
}
|
||||
return assetManagerStore.mapObjectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
})
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, {
|
||||
itemHeight: 48
|
||||
})
|
||||
|
||||
const virtualList = ref({ scrollTo })
|
||||
|
||||
const onScroll = () => {
|
||||
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
|
||||
|
||||
if (scrollTop > 80) {
|
||||
hasScrolled.value = true
|
||||
} else if (scrollTop <= 80) {
|
||||
hasScrolled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toTop() {
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||
assetManagerStore.setMapObjectList(response)
|
||||
})
|
||||
})
|
||||
</script>
|
@ -1,204 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative flex flex-col">
|
||||
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
|
||||
<div class="w-full flex flex-col">
|
||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
||||
</div>
|
||||
|
||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||
<button class="btn bg-indigo-500 hover:bg-indigo-600 rounded text-white px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
|
||||
<Accordion v-for="action in spriteActions" :key="action.id">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
{{ action.action }}
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
||||
<div class="form-field-full">
|
||||
<label for="action">Action</label>
|
||||
<input v-model="action.action" class="input-field" type="text" name="action" placeholder="Action" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input v-model.number="action.originX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="is-animated">Is animated</label>
|
||||
<select v-model="action.isAnimated" class="input-field" name="is-animated">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-half" v-if="action.isAnimated">
|
||||
<label for="is-looping">Is looping</label>
|
||||
<select v-model="action.isLooping" class="input-field" name="is-looping">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full" v-if="action.isAnimated">
|
||||
<label for="frame-speed">Frame rate</label>
|
||||
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<SpriteActionsInput v-model="action.sprites" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Sprite, SpriteAction } from '@/application/types'
|
||||
import { uuidv4 } from '@/application/utilities'
|
||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
||||
import Accordion from '@/components/utilities/Accordion.vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
||||
|
||||
const spriteName = ref('')
|
||||
const spriteActions = ref<SpriteAction[]>([])
|
||||
|
||||
if (!selectedSprite.value) {
|
||||
console.error('No sprite selected')
|
||||
}
|
||||
|
||||
if (selectedSprite.value) {
|
||||
spriteName.value = selectedSprite.value.name
|
||||
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
||||
}
|
||||
|
||||
function deleteSprite() {
|
||||
gameStore.connection?.emit('gm:sprite:delete', { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to delete sprite')
|
||||
return
|
||||
}
|
||||
refreshSpriteList()
|
||||
})
|
||||
}
|
||||
|
||||
function copySprite() {
|
||||
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to copy sprite')
|
||||
return
|
||||
}
|
||||
refreshSpriteList(false)
|
||||
})
|
||||
}
|
||||
|
||||
function refreshSpriteList(unsetSelectedSprite = true) {
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
|
||||
if (unsetSelectedSprite) {
|
||||
assetManagerStore.setSelectedSprite(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function saveSprite() {
|
||||
if (!selectedSprite.value) {
|
||||
console.error('No sprite selected')
|
||||
return
|
||||
}
|
||||
|
||||
const updatedSprite = {
|
||||
id: selectedSprite.value.id,
|
||||
name: spriteName.value,
|
||||
spriteActions:
|
||||
spriteActions.value?.map((action) => {
|
||||
return {
|
||||
action: action.action,
|
||||
sprites: action.sprites,
|
||||
originX: action.originX,
|
||||
originY: action.originY,
|
||||
isAnimated: action.isAnimated,
|
||||
isLooping: action.isLooping,
|
||||
frameRate: action.frameRate,
|
||||
frameWidth: action.frameWidth,
|
||||
frameHeight: action.frameHeight
|
||||
}
|
||||
}) ?? []
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:sprite:update', updatedSprite, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save sprite')
|
||||
return
|
||||
}
|
||||
refreshSpriteList(false)
|
||||
})
|
||||
}
|
||||
|
||||
function addNewImage() {
|
||||
if (!selectedSprite.value) return
|
||||
|
||||
const newImage: SpriteAction = {
|
||||
id: uuidv4(),
|
||||
spriteId: selectedSprite.value.id,
|
||||
sprite: selectedSprite.value,
|
||||
action: 'new_action',
|
||||
sprites: [],
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
isAnimated: false,
|
||||
isLooping: false,
|
||||
frameRate: 0,
|
||||
frameWidth: 0,
|
||||
frameHeight: 0
|
||||
}
|
||||
|
||||
if (!spriteActions.value) {
|
||||
spriteActions.value = []
|
||||
}
|
||||
|
||||
spriteActions.value = sortSpriteActions([...spriteActions.value, newImage])
|
||||
}
|
||||
|
||||
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
||||
if (!actions) return []
|
||||
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||
}
|
||||
|
||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||
if (!sprite) return
|
||||
spriteName.value = sprite.name
|
||||
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedSprite.value) return
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
assetManagerStore.setSelectedSprite(null)
|
||||
})
|
||||
</script>
|
@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import type { Sprite } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
const hasScrolled = ref(false)
|
||||
const elementToScroll = ref()
|
||||
|
||||
function newButtonClickHandler() {
|
||||
gameStore.connection?.emit('gm:sprite:create', {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
if (config.development) console.error('Failed to create new sprite')
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// Trigger a re-render of the virtual list
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
const filteredSprites = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return assetManagerStore.spriteList
|
||||
}
|
||||
return assetManagerStore.spriteList.filter((sprite) => sprite.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
})
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredSprites, {
|
||||
itemHeight: 48
|
||||
})
|
||||
|
||||
const virtualList = ref({ scrollTo })
|
||||
|
||||
const onScroll = () => {
|
||||
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
|
||||
|
||||
if (scrollTop > 80) {
|
||||
hasScrolled.value = true
|
||||
} else if (scrollTop <= 80) {
|
||||
hasScrolled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toTop() {
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
</script>
|
@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
||||
<img :src="image" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
|
||||
<div class="absolute top-1 left-1 flex-row space-y-1">
|
||||
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" ref="fileInput" @change="onFileChange" multiple accept="image/png" class="hidden" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: string[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
}>()
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const draggedIndex = ref<number | null>(null)
|
||||
|
||||
const triggerFileInput = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const onFileChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.files) {
|
||||
handleFiles(target.files)
|
||||
}
|
||||
}
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
if (event.dataTransfer?.files) {
|
||||
handleFiles(event.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFiles = (files: FileList) => {
|
||||
Array.from(files).forEach((file) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (typeof e.target?.result === 'string') {
|
||||
updateImages([...props.modelValue, e.target.result])
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateImages = (newImages: string[]) => {
|
||||
emit('update:modelValue', newImages)
|
||||
}
|
||||
|
||||
const deleteImage = (index: number) => {
|
||||
const newImages = [...props.modelValue]
|
||||
newImages.splice(index, 1)
|
||||
updateImages(newImages)
|
||||
}
|
||||
|
||||
const dragStart = (event: DragEvent, index: number) => {
|
||||
draggedIndex.value = index
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
const drop = (event: DragEvent, dropIndex: number) => {
|
||||
event.preventDefault()
|
||||
if (draggedIndex.value !== null && draggedIndex.value !== dropIndex) {
|
||||
const newImages = [...props.modelValue]
|
||||
const [reorderedItem] = newImages.splice(draggedIndex.value, 1)
|
||||
newImages.splice(dropIndex, 0, reorderedItem)
|
||||
updateImages(newImages)
|
||||
}
|
||||
draggedIndex.value = null
|
||||
}
|
||||
</script>
|
@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||
<img class="max-h-72" :src="`${config.server_endpoint}/textures/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
||||
</div>
|
||||
<div class="mt-5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input v-model="tileName" class="input-field" type="text" name="name" placeholder="Tile #1" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="origin-x">Tags</label>
|
||||
<ChipsInput v-model="tileTags" @update:modelValue="tileTags = $event" />
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import type { Tile } from '@/application/types'
|
||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
const mapEditorStore = useMapEditorStore()
|
||||
|
||||
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
||||
|
||||
const tileName = ref('')
|
||||
const tileTags = ref<string[]>([])
|
||||
|
||||
if (!selectedTile.value) {
|
||||
console.error('No tile selected')
|
||||
}
|
||||
|
||||
if (selectedTile.value) {
|
||||
tileName.value = selectedTile.value.name
|
||||
tileTags.value = selectedTile.value.tags
|
||||
}
|
||||
|
||||
watch(selectedTile, (tile: Tile | null) => {
|
||||
if (!tile) return
|
||||
tileName.value = tile.name
|
||||
tileTags.value = tile.tags
|
||||
})
|
||||
|
||||
function deleteTile() {
|
||||
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to delete tile')
|
||||
return
|
||||
}
|
||||
refreshTileList()
|
||||
})
|
||||
}
|
||||
|
||||
function refreshTileList(unsetSelectedTile = true) {
|
||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||
assetManagerStore.setTileList(response)
|
||||
|
||||
if (unsetSelectedTile) {
|
||||
assetManagerStore.setSelectedTile(null)
|
||||
}
|
||||
|
||||
if (mapEditorStore.active) {
|
||||
mapEditorStore.setTileList(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function saveTile() {
|
||||
if (!selectedTile.value) {
|
||||
console.error('No tile selected')
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.connection?.emit(
|
||||
'gm:tile:update',
|
||||
{
|
||||
id: selectedTile.value.id,
|
||||
name: tileName.value,
|
||||
tags: tileTags.value
|
||||
},
|
||||
(response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save tile')
|
||||
return
|
||||
}
|
||||
refreshTileList(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedTile.value) return
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
assetManagerStore.setSelectedTile(null)
|
||||
})
|
||||
</script>
|
@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="h-7 w-16 max-w-16 flex justify-center">
|
||||
<img class="h-7" :src="`${config.server_endpoint}/textures/tiles/${tile.id}.png`" alt="Tile" />
|
||||
</div>
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import type { Tile } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const tileUploadField = ref(null)
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
const hasScrolled = ref(false)
|
||||
const elementToScroll = ref()
|
||||
|
||||
const handleFileUpload = (e: Event) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
gameStore.connection?.emit('gm:tile:upload', files, (response: boolean) => {
|
||||
if (!response) {
|
||||
if (config.development) console.error('Failed to upload tile')
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||
assetManagerStore.setTileList(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// Trigger a re-render of the virtual list
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
const filteredTiles = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return assetManagerStore.tileList
|
||||
}
|
||||
return assetManagerStore.tileList.filter((tile) => tile.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
})
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredTiles, {
|
||||
itemHeight: 48
|
||||
})
|
||||
|
||||
const virtualList = ref({ scrollTo })
|
||||
|
||||
const onScroll = () => {
|
||||
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
|
||||
|
||||
if (scrollTop > 80) {
|
||||
hasScrolled.value = true
|
||||
} else if (scrollTop <= 80) {
|
||||
hasScrolled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toTop() {
|
||||
virtualList.value?.scrollTo(0)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||
assetManagerStore.setTileList(response)
|
||||
})
|
||||
})
|
||||
</script>
|
Reference in New Issue
Block a user