Worked on dynamic texture loading

This commit is contained in:
2024-10-25 22:17:02 +02:00
parent 347fc0e1e8
commit 9f50b062b0
16 changed files with 392 additions and 511 deletions

View File

@ -15,7 +15,7 @@ import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Login from '@/screens/Login.vue'
import Characters from '@/screens/Characters.vue'
import Game from '@/screens/Game.vue'
import Loading from '@/screens/Loading.vue'
// import Loading from '@/screens/Loading.vue'
import ZoneEditor from '@/screens/ZoneEditor.vue'
import { computed } from 'vue'
@ -23,7 +23,7 @@ const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const currentScreen = computed(() => {
if (!gameStore.isAssetsLoaded) return Loading
// if (!gameStore.isAssetsLoaded) return Loading
if (!gameStore.connection) return Login
if (!gameStore.token) return Login
if (!gameStore.character) return Characters

View File

@ -8,7 +8,7 @@ import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onBeforeMount, onBeforeUnmount } from 'vue'
import { createTileArray, getTile, placeTile, setAllTiles } from '@/composables/zoneComposable'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue'
const emit = defineEmits(['tilemap:create'])
@ -117,7 +117,7 @@ function paint(pointer: Phaser.Input.Pointer) {
if (!pointer.isDown) return
// Set new tileArray with selected tile
setAllTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id))
setLayerTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id))
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles = createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id)
@ -127,7 +127,7 @@ onBeforeMount(() => {
if (!zoneEditorStore.zone?.tiles) {
return
}
setAllTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
setLayerTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)

View File

@ -1,5 +1,5 @@
<template>
<Modal v-for="notification in gameStore.getNotifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification.id)">
<Modal v-for="notification in gameStore.notifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification.id)">
<template #modalHeader v-if="notification.title">
<h3 class="m-0 font-medium shrink-0 text-white">{{ notification.title }}</h3>
</template>

View File

@ -1,28 +0,0 @@
<template>
<Image v-for="object in zoneStore.zone?.zoneObjects" v-bind="getImageProps(object)" />
</template>
<script setup lang="ts">
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { Image, Text } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore'
import type { ZoneObject } from '@/types'
const zoneStore = useZoneStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
const getImageProps = (object: ZoneObject) => {
return {
depth: calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight),
x: tileToWorldX(props.tilemap as any, object.positionX, object.positionY),
y: tileToWorldY(props.tilemap as any, object.positionX, object.positionY),
flipX: object.isRotated,
texture: object.object.id,
originY: Number(object.object.originX),
originX: Number(object.object.originY)
}
}
</script>

View File

@ -1,16 +1,16 @@
<template>
<Tiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" />
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeUnmount, ref } from 'vue'
import { onBeforeUnmount, ref, onBeforeMount } from 'vue'
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
import Tiles from '@/components/zone/Tiles.vue'
import Objects from '@/components/zone/Objects.vue'
import ZoneTiles from '@/components/zone/ZoneTiles.vue'
import ZoneObjects from '@/components/zone/ZoneObjects.vue'
import Characters from '@/components/zone/Characters.vue'
const gameStore = useGameStore()
@ -51,9 +51,12 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
zoneStore.updateCharacter(data)
})
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)
onBeforeMount(async () => {
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
// Set zone and characters
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)
})
})
onBeforeUnmount(() => {

View File

@ -0,0 +1,14 @@
<template>
<ZoneObject v-for="zoneObject in zoneStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject />
</template>
<script setup lang="ts">
import { useZoneStore } from '@/stores/zoneStore'
import ZoneObject from '@/components/zone/partials/ZoneObject.vue'
const zoneStore = useZoneStore()
defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
</script>

View File

@ -7,14 +7,13 @@ import config from '@/config'
import { useScene } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeMount, onBeforeUnmount } from 'vue'
import { placeTile, setAllTiles } from '@/composables/zoneComposable'
import { setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue'
const emit = defineEmits(['tilemap:create'])
const zoneStore = useZoneStore()
const scene = useScene()
const zoneTilemap = createTilemap()
const tiles = createTileLayer()
@ -54,7 +53,7 @@ onBeforeMount(() => {
if (!zoneStore.zone?.tiles) {
return
}
setAllTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
setLayerTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
})
onBeforeUnmount(() => {

View File

@ -0,0 +1,63 @@
<template>
<Image v-if="isTextureLoaded" v-bind="imageProps" />
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { Image, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { useAssetManager } from '@/utilities/assetManager'
import type { ZoneObject } from '@/types'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
zoneObject: ZoneObject
}>()
const scene = useScene()
const assetManager = useAssetManager
const isTextureLoaded = ref(false)
const imageProps = computed(() => ({
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
flipX: props.zoneObject.isRotated,
texture: props.zoneObject.object.id,
originY: Number(props.zoneObject.object.originX),
originX: Number(props.zoneObject.object.originY)
}))
const loadTexture = async () => {
const textureId = props.zoneObject.object.id
// Check if the texture is already loaded in Phaser
if (scene.textures.exists(textureId)) {
isTextureLoaded.value = true
return
}
let assetData = await assetManager.getAsset(textureId)
if (!assetData) {
await assetManager.downloadAsset(textureId, `/assets/objects/${textureId}.png`, 'objects', props.zoneObject.object.updatedAt)
assetData = await assetManager.getAsset(textureId)
}
if (assetData) {
return new Promise<void>((resolve) => {
scene.textures.addBase64(textureId, assetData.data)
scene.textures.once(`addtexture-${textureId}`, () => {
isTextureLoaded.value = true
resolve()
})
})
}
}
onMounted(() => {
loadTexture().catch((error) => {
console.error('Error loading texture:', error)
})
})
</script>

View File

View File

@ -3,8 +3,6 @@ import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
import Tileset = Phaser.Tilemaps.Tileset
import Tile = Phaser.Tilemaps.Tile
import { useGameStore } from '@/stores/gameStore'
import { useAssetManager } from '@/utilities/assets'
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
const tile = layer.getTileAtWorldXY(x, y)
@ -52,11 +50,7 @@ export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: numb
layer.putTileAt(tileImg.firstgid, x, y)
}
export function deleteTile(layer: TilemapLayer, x: number, y: number) {
layer.removeTileAt(x, y)
}
export function setAllTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
tiles.forEach((row: string[], y: number) => {
row.forEach((tile: string, x: number) => {
placeTile(zone, layer, x, y, tile)
@ -73,17 +67,5 @@ export const calculateIsometricDepth = (x: number, y: number, width: number = 0,
if (isCharacter) {
return baseDepth // @TODO: Fix collision, this is a hack
}
// For objects, use their back bottom corner
return baseDepth + (width + height) / (2 * config.tile_size.x)
}
export const sortByIsometricDepth = <T extends { positionX: number; positionY: number }>(items: T[]) => {
return [...items].sort((a, b) => {
return calculateIsometricDepth(a.positionX, a.positionY, 0, 0) - calculateIsometricDepth(b.positionX, b.positionY, 0, 0)
})
}
export const clearAssets = (scene: Phaser.Scene) => {
scene.load.destroy()
}

View File

@ -2,18 +2,16 @@
<div class="flex justify-center items-center h-dvh relative">
<Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene">
<div v-if="isLoaded">
<Menu />
<Hud />
<Hotkeys />
<Minimap />
<Zone />
<Chat />
<ExpBar />
<Menu />
<Hud />
<Hotkeys />
<Minimap />
<Zone />
<Chat />
<ExpBar />
<Inventory />
<Effects />
</div>
<Inventory />
<Effects />
</Scene>
</Game>
</div>
@ -22,9 +20,8 @@
<script setup lang="ts">
import config from '@/config'
import 'phaser'
import { ref, onBeforeUnmount } from 'vue'
import { onBeforeUnmount } from 'vue'
import { Game, Scene } from 'phavuer'
import { useAssetManager } from '@/utilities/assets'
import { useGameStore } from '@/stores/gameStore'
import Menu from '@/components/gui/Menu.vue'
import ExpBar from '@/components/gui/ExpBar.vue'
@ -36,10 +33,10 @@ import Inventory from '@/components/gui/UserPanel.vue'
import Effects from '@/components/Effects.vue'
import Minimap from '@/components/gui/Minimap.vue'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { useAssetManager } from '@/utilities/assetManager'
const assetManager = useAssetManager
const gameStore = useGameStore()
const isLoaded = ref(false)
const assetManager = useAssetManager
const gameConfig = {
name: config.name,
@ -77,61 +74,24 @@ const createGame = (game: Phaser.Game) => {
}
function preloadScene(scene: Phaser.Scene) {
isLoaded.value = false
/**
* Create loading bar
*/
const width = scene.cameras.main.width
const height = scene.cameras.main.height
const progressBox = scene.add.graphics()
const progressBar = scene.add.graphics()
progressBox.fillStyle(0x222222, 0.8)
progressBox.fillRect(width / 2 - 180, height / 2, 320, 50)
const loadingText = scene.make.text({
x: width / 2,
y: height / 2 - 50,
text: 'Loading...',
style: {
font: '20px monospace',
fill: '#ffffff'
}
})
loadingText.setOrigin(0.5, 0.5)
scene.load.on(Phaser.Loader.Events.PROGRESS, function (value: any) {
progressBar.clear()
progressBar.fillStyle(0x368f8b, 1)
progressBar.fillRect(width / 2 - 180 + 10, height / 2 + 10, 300 * value, 30)
})
scene.load.on(Phaser.Loader.Events.COMPLETE, function () {
progressBar.destroy()
progressBox.destroy()
loadingText.destroy()
isLoaded.value = true
})
/**
* Load the base assets into the Phaser scene
*/
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png')
scene.load.rexAwait(async (successCallback: any, failureCallback: any) => {
/**
* Load the assets into the Phaser scene
*/
scene.load.rexAwait(async function (successCallback) {
await assetManager.getAssetsByGroup('tiles').then((assets) => {
assets.forEach((asset) => {
if (scene.load.textureManager.exists(asset.key)) return
scene.textures.addBase64(asset.key, asset.data)
})
})
// Load objects
await assetManager.getAssetsByGroup('objects').then((assets) => {
assets.forEach((asset) => {
if (scene.load.textureManager.exists(asset.key)) return
scene.textures.addBase64(asset.key, asset.data)
})
})
@ -140,24 +100,22 @@ function preloadScene(scene: Phaser.Scene) {
})
}
const createScene = (scene: Phaser.Scene) => {
function createScene(scene: Phaser.Scene) {
/**
* Create sprite animations
* This is done here because phaser forces us to
*/
// assetManager.getAssetsByGroup('sprite_animations').then((assets) => {
// assets.forEach((asset) => {
// scene.anims.create({
// key: asset.key,
// frameRate: 7,
// frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
// repeat: -1
// })
// })
// })
assetManager.getAssetsByGroup('sprite_animations').then((assets) => {
assets.forEach((asset) => {
scene.anims.create({
key: asset.key,
frameRate: 7,
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
repeat: -1
})
})
})
}
onBeforeUnmount(() => {
isLoaded.value = false
})
onBeforeUnmount(() => {})
</script>

View File

@ -17,9 +17,14 @@
import { onMounted, ref } from 'vue'
import config from '@/config'
import type { AssetT as ServerAsset } from '@/types'
import { useAssetManager } from '@/utilities/assets'
import { useAssetManager } from '@/utilities/assetManager'
import { useGameStore } from '@/stores/gameStore'
/**
* This component downloads all assets from the server and
* stores them in the asset manager.
*/
const gameStore = useGameStore()
const assetManager = useAssetManager
const isLoaded = ref(false)
@ -50,7 +55,7 @@ async function loadAssetsIntoAssetManager(assets: ServerAsset[]): Promise<void>
}
// Add the asset to the asset manager
await assetManager.addAsset(asset.key, asset.url, asset.group, asset.updatedAt, asset.frameCount, asset.frameWidth, asset.frameHeight)
await assetManager.downloadAsset(asset.key, asset.url, asset.group, asset.updatedAt, asset.frameCount, asset.frameWidth, asset.frameHeight)
}
}
}

View File

@ -18,7 +18,7 @@ export async function login(username: string, password: string) {
useCookies().set('token', response.data.token as string, {
// for whole domain
// @TODO : #190
domain: window.location.hostname.split('.').slice(-2).join('.')
// domain: window.location.hostname.split('.').slice(-2).join('.')
})
return { success: true, token: response.data.token }
} catch (error: any) {

View File

@ -9,6 +9,7 @@ export const useGameStore = defineStore('game', {
return {
notifications: [] as Notification[],
isAssetsLoaded: false,
loadedAssets: [] as string[],
token: '' as string | null,
connection: null as Socket | null,
user: null as User | null,
@ -30,12 +31,6 @@ export const useGameStore = defineStore('game', {
}
}
},
getters: {
getNotifications: (state: any) => state.notifications,
getAssetByKey: (state) => {
return (key: string) => state.assets.find((asset) => asset.key === key)
}
},
actions: {
addNotification(notification: Notification) {
if (!notification.id) {
@ -108,10 +103,10 @@ export const useGameStore = defineStore('game', {
useCookies().remove('token', {
// for whole domain
// @TODO : #190
domain: window.location.hostname.split('.').slice(-2).join('.')
// domain: window.location.hostname.split('.').slice(-2).join('.')
})
// this.isAssetsLoaded = false
this.isAssetsLoaded = false
this.connection = null
this.token = null
this.user = null

View File

@ -1,41 +1,28 @@
import config from '@/config'
import Dexie from 'dexie'
interface Asset {
key: string
data: Blob
group: string
updatedAt: Date
frameCount?: number
frameWidth?: number
frameHeight?: number
}
interface AssetWithUrl extends Omit<Asset, 'data'> {
data: string
}
class AssetManager extends Dexie {
assets!: Dexie.Table<Asset, string>
assets!: Dexie.Table<
{
key: string
data: Blob
group: string
updatedAt: Date
frameCount?: number
frameWidth?: number
frameHeight?: number
},
string
>
constructor() {
super('AssetManager')
super('Assets')
this.version(1).stores({
assets: 'key, group'
})
}
async getAssetCount(): Promise<number> {
try {
const count = await this.assets.count()
return count
} catch (error) {
console.error('Failed to get asset count:', error)
return 0
}
}
async addAsset(key: string, url: string, group: string, updatedAt: Date, frameCount?: number, frameWidth?: number, frameHeight?: number): Promise<void> {
async downloadAsset(key: string, url: string, group: string, updatedAt: Date, frameCount?: number, frameWidth?: number, frameHeight?: number) {
try {
const response = await fetch(config.server_endpoint + url)
const blob = await response.blob()
@ -45,7 +32,7 @@ class AssetManager extends Dexie {
}
}
async getAsset(key: string): Promise<AssetWithUrl | null> {
async getAsset(key: string) {
try {
const asset = await this.assets.get(key)
if (asset) {
@ -60,7 +47,7 @@ class AssetManager extends Dexie {
return null
}
async getAssetsByGroup(group: string): Promise<AssetWithUrl[]> {
async getAssetsByGroup(group: string) {
try {
const assets = await this.assets.where('group').equals(group).toArray()
return assets.map((asset) => ({
@ -73,15 +60,7 @@ class AssetManager extends Dexie {
}
}
async clearAssets(): Promise<void> {
try {
await this.assets.clear()
} catch (error) {
console.error('Failed to clear assets:', error)
}
}
async deleteAsset(key: string): Promise<void> {
async deleteAsset(key: string) {
try {
await this.assets.delete(key)
} catch (error) {