feature/#215-dexie-caching-implementation

This commit is contained in:
Dennis Postma 2024-10-21 02:08:27 +02:00
parent 279b9bc7a3
commit df19c1094c
16 changed files with 248 additions and 130 deletions

13
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@vueuse/core": "^10.5.0", "@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0", "@vueuse/integrations": "^10.5.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"dexie": "^4.0.8",
"phaser": "^3.86.0", "phaser": "^3.86.0",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"socket.io-client": "^4.8.0", "socket.io-client": "^4.8.0",
@ -3549,6 +3550,12 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/dexie": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz",
"integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ==",
"license": "Apache-2.0"
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -3884,9 +3891,9 @@
} }
}, },
"node_modules/eslint-plugin-vue": { "node_modules/eslint-plugin-vue": {
"version": "9.29.0", "version": "9.29.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.1.tgz",
"integrity": "sha512-hamyjrBhNH6Li6R1h1VF9KHfshJlKgKEg3ARbGTn72CMNDSMhWbgC7NdkRDEh25AFW+4SDATzyNM+3gWuZii8g==", "integrity": "sha512-MH/MbVae4HV/tM8gKAVWMPJbYgW04CK7SuzYRrlNERpxbO0P3+Zdsa2oAcFBW6xNu7W6lIkGOsFAMCRTYmrlWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -18,6 +18,7 @@
"@vueuse/core": "^10.5.0", "@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0", "@vueuse/integrations": "^10.5.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"dexie": "^4.0.8",
"phaser": "^3.86.0", "phaser": "^3.86.0",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"socket.io-client": "^4.8.0", "socket.io-client": "^4.8.0",

Binary file not shown.

Binary file not shown.

View File

@ -15,6 +15,7 @@ import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Login from '@/screens/Login.vue' import Login from '@/screens/Login.vue'
import Characters from '@/screens/Characters.vue' import Characters from '@/screens/Characters.vue'
import Game from '@/screens/Game.vue' import Game from '@/screens/Game.vue'
import Loading from '@/screens/Loading.vue'
import ZoneEditor from '@/screens/ZoneEditor.vue' import ZoneEditor from '@/screens/ZoneEditor.vue'
import { computed } from 'vue' import { computed } from 'vue'
@ -22,16 +23,11 @@ const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
const currentScreen = computed(() => { const currentScreen = computed(() => {
if (!gameStore.isAssetsLoaded) return Loading
if (!gameStore.connection) return Login if (!gameStore.connection) return Login
if (!gameStore.token) return Login if (!gameStore.token) return Login
if (!gameStore.character) return Characters if (!gameStore.character) return Characters
if (zoneEditorStore.active) return ZoneEditor if (zoneEditorStore.active) return ZoneEditor
return Game return Game
}) })
// Disable right click
addEventListener('contextmenu', (event) => event.preventDefault())
// Disable pinch zoom
addEventListener('gesturestart', (event) => event.preventDefault())
</script> </script>

View File

@ -14,11 +14,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeMount, onUnmounted, ref } from 'vue' import { onUnmounted, ref } from 'vue'
import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { loadAssets } from '@/composables/zoneComposable'
import { type Zone } from '@/types' import { type Zone } from '@/types'
// Components // Components
@ -32,7 +30,6 @@ import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue'
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue' import Objects from '@/components/gameMaster/zoneEditor/Objects.vue'
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue' import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue'
const scene = useScene()
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
@ -62,11 +59,6 @@ function save() {
}) })
} }
onBeforeMount(async () => {
await gameStore.fetchAllZoneAssets()
await loadAssets(scene)
})
onUnmounted(() => { onUnmounted(() => {
zoneEditorStore.reset() zoneEditorStore.reset()
}) })

View File

@ -5,19 +5,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeMount, onBeforeUnmount, ref } from 'vue' import { onBeforeUnmount, ref } from 'vue'
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types' import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
import Tiles from '@/components/zone/Tiles.vue' import Tiles from '@/components/zone/Tiles.vue'
import Objects from '@/components/zone/Objects.vue' import Objects from '@/components/zone/Objects.vue'
import Characters from '@/components/zone/Characters.vue' import Characters from '@/components/zone/Characters.vue'
import { loadAssets } from '@/composables/zoneComposable'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneStore = useZoneStore() const zoneStore = useZoneStore()
const scene = useScene()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null) const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
@ -28,13 +25,6 @@ type zoneLoadData = {
// Event listeners // Event listeners
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => { gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
/**
* This is the cause of the bug
*/
// Fetch assets for new zone
await gameStore.fetchZoneAssets(data.zone.id)
await loadAssets(scene)
/** /**
* @TODO : Update character via global event server-side, remove this and listen for it somewhere not here * @TODO : Update character via global event server-side, remove this and listen for it somewhere not here
*/ */
@ -61,16 +51,9 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
zoneStore.updateCharacter(data) zoneStore.updateCharacter(data)
}) })
onBeforeMount(() => { gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => { zoneStore.setZone(response.zone)
// Fetch assets for new zone zoneStore.setCharacters(response.characters)
await gameStore.fetchZoneAssets(response.zone.id)
await loadAssets(scene)
// Set zone and characters
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)
})
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -4,6 +4,7 @@ import TilemapLayer = Phaser.Tilemaps.TilemapLayer
import Tileset = Phaser.Tilemaps.Tileset import Tileset = Phaser.Tilemaps.Tileset
import Tile = Phaser.Tilemaps.Tile import Tile = Phaser.Tilemaps.Tile
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useAssetManager } from '@/utilities/assets'
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined { export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
const tile = layer.getTileAtWorldXY(x, y) const tile = layer.getTileAtWorldXY(x, y)
@ -89,17 +90,35 @@ export const clearAssets = (scene: Phaser.Scene) => {
export const loadAssets = (scene: Phaser.Scene): Promise<void> => { export const loadAssets = (scene: Phaser.Scene): Promise<void> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const gameStore = useGameStore() let assetManager = useAssetManager
let addedLoad = false let addedLoad = false
gameStore.assets.forEach((asset) => { assetManager.getAssetsByGroup('tiles').then((assets) => {
if (scene.load.textureManager.exists(asset.key)) return assets.forEach((asset) => {
addedLoad = true if (scene.load.textureManager.exists(asset.key)) return
if (asset.group === 'sprite_animations') { addedLoad = true
scene.load.spritesheet(asset.key, config.server_endpoint + asset.url, { frameWidth: asset.frameWidth ?? 0, frameHeight: asset.frameHeight ?? 0 }) scene.textures.addBase64(asset.key, asset.data)
} else { })
scene.load.image(asset.key, config.server_endpoint + asset.url) })
}
assetManager.getAssetsByGroup('objects').then((assets) => {
assets.forEach((asset) => {
if (scene.load.textureManager.exists(asset.key)) return
addedLoad = true
scene.textures.addBase64(asset.key, asset.data)
})
})
assetManager.getAssetsByGroup('sprites').then((assets) => {
assets.forEach((asset) => {
if (scene.load.textureManager.exists(asset.key)) return
let img = new Image();
img.onload = () => {
scene.textures.addSpriteSheet(asset.key, img, { frameWidth: asset.frameWidth ?? 0, frameHeight: asset.frameHeight ?? 0 })
};
img.src = asset.data;
addedLoad = true
})
}) })
if (addedLoad) { if (addedLoad) {

View File

@ -114,13 +114,7 @@ gameStore.connection?.on('character:list', (data: any) => {
}) })
onMounted(async () => { onMounted(async () => {
/** // wait 0.75 sec
* Fetch sprite assets from the server
* This is done here because phaser needs it in createScene in Game.vue.
*/
await gameStore.fetchSpriteAssets()
// wait 0.5 sec
setTimeout(() => { setTimeout(() => {
gameStore.connection?.emit('character:list') gameStore.connection?.emit('character:list')
isLoading.value = false isLoading.value = false

View File

@ -19,11 +19,12 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts" async>
import config from '@/config' import config from '@/config'
import 'phaser' import 'phaser'
import { ref, onBeforeUnmount } from 'vue' import { ref, onBeforeUnmount } from 'vue'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'
import { useAssetManager } from '@/utilities/assets'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import Menu from '@/components/gui/Menu.vue' import Menu from '@/components/gui/Menu.vue'
import ExpBar from '@/components/gui/ExpBar.vue' import ExpBar from '@/components/gui/ExpBar.vue'
@ -36,6 +37,7 @@ import Effects from '@/components/Effects.vue'
import { loadAssets } from '@/composables/zoneComposable' import { loadAssets } from '@/composables/zoneComposable'
import Minimap from '@/components/gui/Minimap.vue' import Minimap from '@/components/gui/Minimap.vue'
const assetManager = useAssetManager
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoaded = ref(false) const isLoaded = ref(false)
@ -120,16 +122,16 @@ const createScene = async (scene: Phaser.Scene) => {
* Create sprite animations * Create sprite animations
* This is done here because phaser forces us to * This is done here because phaser forces us to
*/ */
gameStore.assets.forEach((asset) => { // assetManager.getAssetsByGroup('sprite_animations').then((assets) => {
if (asset.group !== 'sprite_animations') return // assets.forEach((asset) => {
// scene.anims.create({
scene.anims.create({ // key: asset.key,
key: asset.key, // frameRate: 7,
frameRate: 7, // frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }), // repeat: -1
repeat: -1 // })
}) // })
}) // })
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {

78
src/screens/Loading.vue Normal file
View File

@ -0,0 +1,78 @@
<template>
<div class="flex flex-col justify-center items-center h-dvh relative">
<div v-if="!isLoaded" class="w-20 h-20 rounded-full border-4 border-solid border-gray-300 border-t-transparent animate-spin"></div>
<button v-else @click="continueBtnClick" class="w-20 h-20 rounded-full bg-gray-500 flex items-center justify-center hover:bg-gray-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<div v-if="!isLoaded" class="text-center mt-6">
<h1 class="text-2xl font-bold">Loading...</h1>
<p class="text-gray-400">Please wait while we load the assets.</p>
</div>
</div>
</template>
<script setup lang="ts" async>
import { onMounted, ref } from 'vue'
import config from '@/config'
import type { AssetT as ServerAsset } from '@/types'
import { useAssetManager } from '@/utilities/assets'
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore();
const assetManager = useAssetManager;
const isLoaded = ref(false)
async function getAssets() {
return fetch(config.server_endpoint + '/assets/')
.then((response) => {
if (!response.ok) throw new Error('Failed to fetch assets')
console.log(response)
return response.json()
})
.catch((error) => {
console.error('Error fetching assets:', error)
return false
})
}
async function loadAssetsIntoAssetManager(assets: ServerAsset[]): Promise<void> {
for (const asset of assets) {
// Check if the asset is already loaded
const existingAsset = await assetManager.getAsset(asset.key);
// Check if the asset needs to be updated
if (!existingAsset || new Date(asset.updatedAt) > new Date(existingAsset.updatedAt)) {
// Check if the asset is already loaded, if so, delete it
if (existingAsset) {
await assetManager.deleteAsset(asset.key);
}
// Add the asset to the asset manager
await assetManager.addAsset(
asset.key,
asset.url,
asset.group,
asset.updatedAt,
asset.frameCount,
asset.frameWidth,
asset.frameHeight
);
}
}
}
function continueBtnClick() {
gameStore.isAssetsLoaded = true
}
onMounted(async () => {
const assets = await getAssets()
if (assets) {
await loadAssetsIntoAssetManager(assets)
isLoaded.value = true
}
})
</script>

View File

@ -16,7 +16,6 @@ import { Game, Scene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue' import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
import { loadAssets } from '@/composables/zoneComposable'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
@ -93,11 +92,6 @@ const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('TELEPORT', '/assets/zone/tp_tile.png') scene.load.image('TELEPORT', '/assets/zone/tp_tile.png')
scene.load.image('blank_tile', '/assets/zone/blank_tile.png') scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png') scene.load.image('waypoint', '/assets/waypoint.png')
/**
* Load the assets into the Phaser scene
*/
await loadAssets(scene)
} }
const createScene = async (scene: Phaser.Scene) => { const createScene = async (scene: Phaser.Scene) => {

View File

@ -1,9 +1,8 @@
import axios from 'axios' import axios from 'axios'
import config from '@/config' import config from '@/config'
import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
export async function register(username: string, password: string, gameStore = useGameStore()) { export async function register(username: string, password: string) {
try { try {
const response = await axios.post(`${config.server_endpoint}/register`, { username, password }) const response = await axios.post(`${config.server_endpoint}/register`, { username, password })
useCookies().set('token', response.data.token as string) useCookies().set('token', response.data.token as string)
@ -13,13 +12,13 @@ export async function register(username: string, password: string, gameStore = u
} }
} }
export async function login(username: string, password: string, gameStore = useGameStore()) { export async function login(username: string, password: string) {
try { try {
const response = await axios.post(`${config.server_endpoint}/login`, { username, password }) const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
useCookies().set('token', response.data.token as string, { useCookies().set('token', response.data.token as string, {
// for whole domain // for whole domain
// @TODO : #190 // @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 } return { success: true, token: response.data.token }
} catch (error: any) { } catch (error: any) {

View File

@ -7,9 +7,8 @@ import { useCookies } from '@vueuse/integrations/useCookies'
export const useGameStore = defineStore('game', { export const useGameStore = defineStore('game', {
state: () => { state: () => {
return { return {
loginMessage: null as string | null,
notifications: [] as Notification[], notifications: [] as Notification[],
assets: [] as Asset[], isAssetsLoaded: false,
token: '' as string | null, token: '' as string | null,
connection: null as Socket | null, connection: null as Socket | null,
user: null as User | null, user: null as User | null,
@ -47,54 +46,6 @@ export const useGameStore = defineStore('game', {
removeNotification(id: string) { removeNotification(id: string) {
this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id) this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id)
}, },
setAssets(assets: Asset[]) {
this.assets = assets
},
addAsset(asset: Asset) {
this.assets.push(asset)
},
addAssets(assets: Asset[]) {
this.assets = this.assets.concat(assets)
},
async fetchSpriteAssets() {
return fetch(config.server_endpoint + '/assets/sprites')
.then((response) => response.json())
.then((assets) => {
// Only add the sprites that are not already in the store
this.addAssets(assets.filter((asset: Asset) => !this.getAssetByKey(asset.key)))
return true
})
.catch((error) => {
console.error('Error fetching assets:', error)
return false
})
},
async fetchZoneAssets(zoneId: number) {
return fetch(config.server_endpoint + '/assets/zone/' + zoneId)
.then((response) => response.json())
.then((assets) => {
// Only add the zones that are not already in the store
this.addAssets(assets.filter((asset: Asset) => !this.getAssetByKey(asset.key)))
return true
})
.catch((error) => {
console.error('Error fetching assets:', error)
return false
})
},
async fetchAllZoneAssets() {
return fetch(config.server_endpoint + '/assets/zone')
.then((response) => response.json())
.then((assets) => {
// Only add the zones that are not already in the store
this.addAssets(assets.filter((asset: Asset) => !this.getAssetByKey(asset.key)))
return true
})
.catch((error) => {
console.error('Error fetching assets:', error)
return false
})
},
setToken(token: string) { setToken(token: string) {
this.token = token this.token = token
}, },
@ -152,15 +103,15 @@ export const useGameStore = defineStore('game', {
}) })
}, },
disconnectSocket() { disconnectSocket() {
if (this.connection) this.connection.disconnect() this.connection?.disconnect()
useCookies().remove('token', { useCookies().remove('token', {
// for whole domain // for whole domain
// @TODO : #190 // @TODO : #190
domain: window.location.hostname.split('.').slice(-2).join('.') // domain: window.location.hostname.split('.').slice(-2).join('.')
}) })
this.assets = [] // this.isAssetsLoaded = false
this.connection = null this.connection = null
this.token = null this.token = null
this.user = null this.user = null

View File

@ -4,10 +4,11 @@ export type Notification = {
message?: string message?: string
} }
export type Asset = { export type AssetT = {
key: string key: string
url: string url: string
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other' group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date
frameCount?: number frameCount?: number
frameWidth?: number frameWidth?: number
frameHeight?: number frameHeight?: number

101
src/utilities/assets.ts Normal file
View File

@ -0,0 +1,101 @@
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>;
constructor() {
super('AssetManager');
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> {
try {
const response = await fetch(config.server_endpoint + url);
const blob = await response.blob();
await this.assets.put({ key, data: blob, group, updatedAt, frameCount, frameWidth, frameHeight });
} catch (error) {
console.error(`Failed to add asset ${key}:`, error);
}
}
async getAsset(key: string): Promise<AssetWithUrl | null> {
try {
const asset = await this.assets.get(key);
if (asset) {
return {
...asset,
data: URL.createObjectURL(asset.data)
};
}
} catch (error) {
console.error(`Failed to retrieve asset ${key}:`, error);
}
return null;
}
async getAssetsByGroup(group: string): Promise<AssetWithUrl[]> {
try {
const assets = await this.assets.where('group').equals(group).toArray();
return assets.map(asset => ({
...asset,
data: URL.createObjectURL(asset.data)
}));
} catch (error) {
console.error(`Failed to retrieve assets for group ${group}:`, error);
return [];
}
}
async clearAssets(): Promise<void> {
try {
await this.assets.clear();
} catch (error) {
console.error('Failed to clear assets:', error);
}
}
async deleteAsset(key: string): Promise<void> {
try {
await this.assets.delete(key);
} catch (error) {
console.error(`Failed to delete asset ${key}:`, error);
}
}
}
export const useAssetManager = new AssetManager();