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

View File

@ -18,6 +18,7 @@
"@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0",
"axios": "^1.7.7",
"dexie": "^4.0.8",
"phaser": "^3.86.0",
"pinia": "^2.1.6",
"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 Characters from '@/screens/Characters.vue'
import Game from '@/screens/Game.vue'
import Loading from '@/screens/Loading.vue'
import ZoneEditor from '@/screens/ZoneEditor.vue'
import { computed } from 'vue'
@ -22,16 +23,11 @@ const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const currentScreen = computed(() => {
if (!gameStore.isAssetsLoaded) return Loading
if (!gameStore.connection) return Login
if (!gameStore.token) return Login
if (!gameStore.character) return Characters
if (zoneEditorStore.active) return ZoneEditor
return Game
})
// Disable right click
addEventListener('contextmenu', (event) => event.preventDefault())
// Disable pinch zoom
addEventListener('gesturestart', (event) => event.preventDefault())
</script>

View File

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

View File

@ -5,19 +5,16 @@
</template>
<script setup lang="ts">
import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
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 Tiles from '@/components/zone/Tiles.vue'
import Objects from '@/components/zone/Objects.vue'
import Characters from '@/components/zone/Characters.vue'
import { loadAssets } from '@/composables/zoneComposable'
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const scene = useScene()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
@ -28,13 +25,6 @@ type zoneLoadData = {
// Event listeners
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
*/
@ -61,16 +51,9 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
zoneStore.updateCharacter(data)
})
onBeforeMount(() => {
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
// Fetch assets for new zone
await gameStore.fetchZoneAssets(response.zone.id)
await loadAssets(scene)
// Set zone and characters
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)
})
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)
})
onBeforeUnmount(() => {

View File

@ -4,6 +4,7 @@ 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)
@ -89,17 +90,35 @@ export const clearAssets = (scene: Phaser.Scene) => {
export const loadAssets = (scene: Phaser.Scene): Promise<void> => {
return new Promise((resolve) => {
const gameStore = useGameStore()
let assetManager = useAssetManager
let addedLoad = false
gameStore.assets.forEach((asset) => {
if (scene.load.textureManager.exists(asset.key)) return
addedLoad = true
if (asset.group === 'sprite_animations') {
scene.load.spritesheet(asset.key, config.server_endpoint + asset.url, { frameWidth: asset.frameWidth ?? 0, frameHeight: asset.frameHeight ?? 0 })
} else {
scene.load.image(asset.key, config.server_endpoint + asset.url)
}
assetManager.getAssetsByGroup('tiles').then((assets) => {
assets.forEach((asset) => {
if (scene.load.textureManager.exists(asset.key)) return
addedLoad = true
scene.textures.addBase64(asset.key, asset.data)
})
})
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) {

View File

@ -114,13 +114,7 @@ gameStore.connection?.on('character:list', (data: any) => {
})
onMounted(async () => {
/**
* 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
// wait 0.75 sec
setTimeout(() => {
gameStore.connection?.emit('character:list')
isLoading.value = false

View File

@ -19,11 +19,12 @@
</div>
</template>
<script setup lang="ts">
<script setup lang="ts" async>
import config from '@/config'
import 'phaser'
import { ref, 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,6 +37,7 @@ import Effects from '@/components/Effects.vue'
import { loadAssets } from '@/composables/zoneComposable'
import Minimap from '@/components/gui/Minimap.vue'
const assetManager = useAssetManager
const gameStore = useGameStore()
const isLoaded = ref(false)
@ -120,16 +122,16 @@ const createScene = async (scene: Phaser.Scene) => {
* Create sprite animations
* This is done here because phaser forces us to
*/
gameStore.assets.forEach((asset) => {
if (asset.group !== 'sprite_animations') return
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(() => {

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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
import { loadAssets } from '@/composables/zoneComposable'
const gameStore = useGameStore()
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('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png')
/**
* Load the assets into the Phaser scene
*/
await loadAssets(scene)
}
const createScene = async (scene: Phaser.Scene) => {

View File

@ -1,9 +1,8 @@
import axios from 'axios'
import config from '@/config'
import { useGameStore } from '@/stores/gameStore'
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 {
const response = await axios.post(`${config.server_endpoint}/register`, { username, password })
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 {
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
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

@ -7,9 +7,8 @@ import { useCookies } from '@vueuse/integrations/useCookies'
export const useGameStore = defineStore('game', {
state: () => {
return {
loginMessage: null as string | null,
notifications: [] as Notification[],
assets: [] as Asset[],
isAssetsLoaded: false,
token: '' as string | null,
connection: null as Socket | null,
user: null as User | null,
@ -47,54 +46,6 @@ export const useGameStore = defineStore('game', {
removeNotification(id: string) {
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) {
this.token = token
},
@ -152,15 +103,15 @@ export const useGameStore = defineStore('game', {
})
},
disconnectSocket() {
if (this.connection) this.connection.disconnect()
this.connection?.disconnect()
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.assets = []
// this.isAssetsLoaded = false
this.connection = null
this.token = null
this.user = null

View File

@ -4,10 +4,11 @@ export type Notification = {
message?: string
}
export type Asset = {
export type AssetT = {
key: string
url: string
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date
frameCount?: number
frameWidth?: 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();