Work on sprite stuff

This commit is contained in:
Dennis Postma 2024-07-20 15:18:06 +02:00
parent 5c8d4947b1
commit 33fd32348f
7 changed files with 266 additions and 28 deletions

View File

@ -62,7 +62,7 @@ function onPointerClick(pointer: Phaser.Input.Pointer) {
gameStore.connection?.emit('character:move', { position_x: pointer_tile.x, position_y: pointer_tile.y }) gameStore.connection?.emit('character:move', { position_x: pointer_tile.x, position_y: pointer_tile.y })
//Directions for player sprites + animations //Directions for player sprite + animations
if (px < 0 && py > 0) { if (px < 0 && py > 0) {
console.log('down left') console.log('down left')
} else if (px < 0 && py < 0) { } else if (px < 0 && py < 0) {

View File

@ -11,13 +11,29 @@
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div> <div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer"> <a class="relative p-2.5 hover:cursor-pointer">
<span>Loot</span> <span>Items</span>
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div> <div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer"> <a class="relative p-2.5 hover:cursor-pointer">
<span>NPC's</span> <span>NPC's</span>
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div> <div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Characters</span>
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Mounts</span>
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Pets</span>
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprite' }" @click="() => (selectedCategory = 'sprite')">
<span>Sprites</span>
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</a>
</div> </div>
<div class="absolute w-[1px] bg-cyan-200 h-full top-0 left-[15%]"></div> <div class="absolute w-[1px] bg-cyan-200 h-full top-0 left-[15%]"></div>
@ -25,6 +41,7 @@
<div class="overflow-auto h-full w-[35%] flex flex-col relative" ref="elementToScroll" @scroll="onScroll"> <div class="overflow-auto h-full w-[35%] flex flex-col relative" ref="elementToScroll" @scroll="onScroll">
<TileList v-if="selectedCategory === 'tiles'" /> <TileList v-if="selectedCategory === 'tiles'" />
<ObjectList v-if="selectedCategory === 'objects'" /> <ObjectList v-if="selectedCategory === 'objects'" />
<ObjectList v-if="selectedCategory === 'objects'" />
</div> </div>
<button class="left-[calc(50%_-_60px)] absolute bottom-2.5 min-w-[unset] w-[50px] h-[50px] rounded-lg bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop"> <button class="left-[calc(50%_-_60px)] absolute bottom-2.5 min-w-[unset] w-[50px] h-[50px] rounded-lg bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop">
@ -34,28 +51,23 @@
<!-- Asset details --> <!-- Asset details -->
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto"> <div class="flex w-1/2 after:hidden flex-col relative overflow-auto">
<TileDetails :tile="selectedTile" v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" /> <TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
<ObjectDetails :object="selectedTile" v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" /> <ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useGameStore } from '@/stores/game'
import TileList from '@/components/utilities/assetManager/partials/TileList.vue' import TileList from '@/components/utilities/assetManager/partials/TileList.vue'
import TileDetails from '@/components/utilities/assetManager/partials/TileDetails.vue' import TileDetails from '@/components/utilities/assetManager/partials/TileDetails.vue'
import ObjectList from '@/components/utilities/assetManager/partials/ObjectList.vue' import ObjectList from '@/components/utilities/assetManager/partials/ObjectList.vue'
import ObjectDetails from '@/components/utilities/assetManager/partials/ObjectDetails.vue' import ObjectDetails from '@/components/utilities/assetManager/partials/ObjectDetails.vue'
import { useAssetManagerStore } from '@/stores/assetManager' import { useAssetManagerStore } from '@/stores/assetManager'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
const selectedCategory = ref('tiles') const selectedCategory = ref('tiles')
const selectedTile = ref('')
const selectedObject = ref('')
const hasScrolled = ref(false) const hasScrolled = ref(false)
const elementToScroll = ref() const elementToScroll = ref()

View File

@ -0,0 +1,118 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-between h-[300px]">
<div class="filler"></div>
<img class="max-h-[280px]" :src="`${config.server_endpoint}/assets/sprites/${selectedSprite?.id}.png`" :alt="'Sprite ' + selectedSprite?.id" />
<button class="btn-bordeaux px-[15px] py-1.5 min-w-[100px]" type="button" @click.prevent="removeSprite">Remove</button>
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</div>
<div class="m-2.5 p-2.5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
<div class="w-full flex flex-col mb-5">
<label class="mb-1.5 font-titles" for="name">Name</label>
<input v-model="spriteName" class="input-cyan" type="text" name="name" placeholder="Wall #1" />
</div>
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
<label class="mb-1.5 font-titles" for="origin-x">Origin X</label>
<input v-model="spriteOriginX" class="input-cyan" type="number" step="any" name="origin-x" placeholder="Origin X" />
</div>
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
<label class="mb-1.5 font-titles" for="origin-y">Origin Y</label>
<input v-model="spriteOriginY" class="input-cyan" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="w-full flex flex-col mb-5">
<label class="mb-1.5 font-titles" for="origin-x">Tags</label>
<ChipsInput v-model="spriteTags" @update:modelValue="spriteTags = $event" />
</div>
<button class="btn-cyan px-[15px] py-1.5 min-w-[100px]" type="submit">Save</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { Sprite } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManager'
import { useGameStore } from '@/stores/game'
import config from '@/config'
import ChipsInput from '@/components/forms/ChipsInput.vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const selectedSprite = computed(() => assetManagerStore.selectedSprite)
const spriteName = ref('')
const spriteTags = ref([] as string[])
const spriteOriginX = ref(0)
const spriteOriginY = ref(0)
if (!selectedSprite.value) {
console.error('No sprite selected')
}
if (selectedSprite.value) {
spriteName.value = selectedSprite.value.name
spriteOriginX.value = selectedSprite.value.origin_x
spriteOriginY.value = selectedSprite.value.origin_y
}
function removeSprite() {
gameStore.connection?.emit('gm:sprite:remove', { sprite: selectedSprite.value?.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove sprite')
return
}
refreshSpriteList()
})
}
function refreshSpriteList() {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
assetManagerStore.setSelectedSprite(null)
})
}
function saveSprite() {
if (!selectedSprite.value) {
console.error('No sprite selected')
return
}
console.log(spriteTags.value)
gameStore.connection?.emit(
'gm:sprite:update',
{
id: selectedSprite.value.id,
name: spriteName.value,
tags: spriteTags.value,
origin_x: spriteOriginX.value,
origin_y: spriteOriginY.value
},
(response: boolean) => {
if (!response) {
console.error('Failed to save sprite')
return
}
refreshSpriteList()
}
)
}
watch(selectedSprite, (sprite: Sprite | null) => {
if (!sprite) return
spriteName.value = sprite.name
spriteOriginX.value = sprite.origin_x
spriteOriginY.value = sprite.origin_y
})
onMounted(() => {
if (!selectedSprite.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedSprite(null)
})
</script>

View File

@ -0,0 +1,70 @@
<template>
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap">
<label for="upload-asset" class="bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 py-1.5 px-[15px] inline-flex hover:bg-cyan hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="spriteUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
Upload sprite(s)
</label>
<input v-model="searchQuery" class="input-cyan w-full" placeholder="Search..." @input="handleSearch" />
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</div>
<a class="relative p-2.5 cursor-pointer" :class="{ 'bg-cyan/80': assetManagerStore.selectedSprite?.id === sprite.id }" v-for="(sprite, index) in filteredSprites" :key="index" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
<div class="flex items-center gap-2.5">
<div class="h-[28px] w-[75px] max-w-[75px] flex justify-center">
<img class="h-[28px]" :src="`${config.server_endpoint}/assets/sprites/${sprite.id}.png`" alt="Sprite" />
</div>
<span>{{ sprite.name }}</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</a>
</template>
<script setup lang="ts">
import config from '@/config'
import { useGameStore } from '@/stores/game'
import { onMounted, ref, computed } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManager'
import { useAssetStore } from '@/stores/assets'
import type { Sprite } from '@/types'
const gameStore = useGameStore()
const spriteUploadField = ref(null)
const assetManagerStore = useAssetManagerStore()
const assetStore = useAssetStore()
const searchQuery = ref('')
const handleFileUpload = (e: Event) => {
const files = (e.target as HTMLInputElement).files
if (!files) return
gameStore.connection?.emit('gm:sprite:upload', files, (response: boolean) => {
if (!response) {
if (config.development) console.error('Failed to upload sprite')
return
}
assetStore.fetchAssets()
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})
}
const handleSearch = () => {
// The filtering is handled by the computed property, so we don't need to do anything here
// This function is kept in case you want to add debounce or other functionality later
}
const filteredSprites = computed(() => {
if (!searchQuery.value) {
return assetManagerStore.spriteList
}
return assetManagerStore.spriteList.filter((sprite) => sprite.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
onMounted(() => {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})
</script>

View File

@ -7,14 +7,20 @@
<input v-model="searchQuery" class="input-cyan w-full" placeholder="Search..." @input="handleSearch" /> <input v-model="searchQuery" class="input-cyan w-full" placeholder="Search..." @input="handleSearch" />
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div> <div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</div> </div>
<div v-bind="containerProps"> <div v-bind="containerProps" style="height: 400px; overflow-y: auto;">
<div v-bind="wrapperProps"> <div v-bind="wrapperProps">
<a class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedTile?.id === tile.data.id }" v-for="(tile, index) in list" :key="index" @click="assetManagerStore.setSelectedTile(tile.data as Tile)"> <a
v-for="{ data: tile } in list"
:key="tile.id"
class="relative p-2.5 cursor-pointer block"
:class="{ 'bg-cyan/80': assetManagerStore.selectedTile?.id === tile.id }"
@click="assetManagerStore.setSelectedTile(tile)"
>
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<div class="h-[28px] w-[75px] max-w-[75px] flex justify-center"> <div class="h-[28px] w-[75px] max-w-[75px] flex justify-center">
<img class="h-[28px]" :src="`${config.server_endpoint}/assets/tiles/${tile.data.id}.png`" alt="Tile" /> <img class="h-[28px]" :src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`" alt="Tile" />
</div> </div>
<span>{{ tile.data.name }}</span> <span>{{ tile.name }}</span>
</div> </div>
<div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div> <div class="absolute left-0 bottom-0 w-full h-[1px] bg-cyan-200"></div>
</a> </a>
@ -56,28 +62,31 @@ const handleFileUpload = (e: Event) => {
} }
const handleSearch = () => { const handleSearch = () => {
// The filtering is handled by the computed property, so we don't need to do anything here // Trigger a re-render of the virtual list
// This function is kept in case you want to add debounce or other functionality later virtualList.value?.scrollTo(0)
} }
const filteredTiles = computed(() => { const filteredTiles = computed(() => {
if (!searchQuery.value) { if (!searchQuery.value) {
return assetManagerStore.tileList return assetManagerStore.tileList
} }
return assetManagerStore.tileList.filter((tile) => tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())) return assetManagerStore.tileList.filter((tile) =>
tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}) })
const { list, containerProps, wrapperProps } = useVirtualList( const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
filteredTiles, filteredTiles,
{ {
// Keep `itemHeight` in sync with the item's row.
itemHeight: 28, itemHeight: 28,
}, },
) )
const virtualList = ref({ scrollTo })
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
}) })
}) })
</script> </script>

View File

@ -1,6 +1,6 @@
import { ref } from 'vue' import { ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { Tile, Object } from '@/types' import type { Tile, Object, Sprite } from '@/types'
export const useAssetManagerStore = defineStore('assetManager', () => { export const useAssetManagerStore = defineStore('assetManager', () => {
const tileList = ref<Tile[]>([]) const tileList = ref<Tile[]>([])
@ -9,30 +9,45 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
const objectList = ref<Object[]>([]) const objectList = ref<Object[]>([])
const selectedObject = ref<Object | null>(null) const selectedObject = ref<Object | null>(null)
const spriteList = ref<Sprite[]>([])
const selectedSprite = ref<Sprite | null>(null)
function setTileList(tiles: Tile[]) { function setTileList(tiles: Tile[]) {
tileList.value = tiles tileList.value = tiles
} }
function setObjectList(objects: Object[]) {
objectList.value = objects
}
function setSelectedTile(tile: Tile | null) { function setSelectedTile(tile: Tile | null) {
selectedTile.value = tile selectedTile.value = tile
} }
function setObjectList(objects: Object[]) {
objectList.value = objects
}
function setSelectedObject(object: Object | null) { function setSelectedObject(object: Object | null) {
selectedObject.value = object selectedObject.value = object
} }
function setSpriteList(sprites: Sprite[]) {
spriteList.value = sprites
}
function setSelectedSprite(sprite: Sprite | null) {
selectedSprite.value = sprite
}
return { return {
tileList, tileList,
objectList,
setTileList,
setObjectList,
selectedTile, selectedTile,
objectList,
selectedObject, selectedObject,
spriteList,
selectedSprite,
setTileList,
setSelectedTile, setSelectedTile,
setSelectedObject setObjectList,
setSelectedObject,
setSpriteList,
setSelectedSprite
} }
}) })

View File

@ -10,6 +10,20 @@ export type Asset = {
type: 'base64' | 'link' type: 'base64' | 'link'
} }
export type Sprite = {
id: string;
name: string;
origin_x: number;
origin_y: number;
frameSpeed: number;
isAnimated: boolean;
isLooping: boolean;
isPlayableCharacter: boolean;
isEnemy: boolean;
createdAt: Date;
updatedAt: Date;
}
export type Tile = { export type Tile = {
id: string id: string
name: string name: string