Compare commits

...

40 Commits

Author SHA1 Message Date
89938b7f93 npm run format 2024-11-04 01:06:15 +01:00
f076661d4a npm update 2024-11-03 22:35:22 +01:00
0b018f53a7 Merge remote-tracking branch 'origin/main' into feature/#182-reset-password 2024-11-03 22:30:45 +01:00
32a13f0f3c Added more whitespace under error on register form 2024-11-03 22:12:14 +01:00
f203bf9534 Merge conflict fix 2024-11-03 22:00:29 +01:00
67b6339ffc FInished password reset (hopefully) 2024-11-03 21:56:19 +01:00
6233da0044 Temporarily disable sound 2024-11-03 03:26:07 +01:00
56f455a08e TS declaration improvement, moved files 2024-11-03 01:32:37 +01:00
9d541cc46a Merge remote-tracking branch 'origin/main' into feature/#182-reset-password
# Conflicts:
#	src/components/screens/Characters.vue
2024-11-03 01:27:27 +01:00
adf86d369b #216: Added tile & object picker in map editor 2024-11-03 01:26:13 +01:00
bbcb84ed03 Refactored screens & login forms, fixed pw reset modal, WIP new pw form 2024-11-03 00:47:02 +01:00
6f40c774ea TS shit 2024-11-03 00:34:03 +01:00
e3b70df46f npm update 2024-11-03 00:33:00 +01:00
584262a59b Merge remote-tracking branch 'origin/main' into feature/#182-reset-password
# Conflicts:
#	src/screens/Login.vue
#	src/stores/gameStore.ts
2024-11-02 21:47:57 +01:00
7e4e613405 Moved music to character selection screen, disable loader 2024-11-02 16:02:43 +01:00
e2c60bfd5a #210 : Play music on login screen 2024-11-02 14:05:37 +01:00
e38c402266 Fix button sound playing when clicked outside of buttons 2024-11-02 13:54:16 +01:00
a299d5dc55 #209: Play sound when a button is pressed 2024-11-02 13:51:07 +01:00
43c0f0ab1e Added vite-plugin-compression 2024-11-02 13:34:52 +01:00
ed17e7f16e #217: Solved duplicate texture loading issue, #215: Finished Dexie implementation. 2024-11-02 02:20:34 +01:00
7d723530e6 Renamed pw reset form variable 2024-11-02 01:47:23 +01:00
a3532a5940 Typo 2024-10-31 12:33:04 +01:00
74cbf3f2c8 Animations now load dynamically and are cached 2024-10-31 12:31:30 +01:00
d402744955 Map editor fixes (select & move), character sprites are now dynamically loaded and cached, moved repeated code into a composable, updated types 2024-10-30 15:27:37 +01:00
39b65b3884 npm run format 2024-10-30 10:07:22 +01:00
c62ff2efc1 Refactor assetManager, renamed assetManager to assetStorage, renamed AssetT to AssetDataT, added better error handling in authentication service, continued working on dynamic asset loading for both maps and map editor 2024-10-30 09:36:16 +01:00
08f55c9680 #190 : Cookie domain improvement 2024-10-30 07:17:07 +01:00
1afc50ea6a Moved reset password, adjusted modal open logic (also WIP) 2024-10-29 22:50:11 +01:00
7c259f455c Load all tiles in zone editor 2024-10-28 23:41:26 +01:00
be854a79b8 Added reset password modal form 2024-10-27 21:31:50 +01:00
a71890ab68 Clean 2024-10-27 21:09:19 +01:00
dc2b6b9851 WIP: updated zone manager to match with zone logic (dynamic asset/texture loading) 2024-10-27 21:04:08 +01:00
d091aabeb3 Removed onMounted() 2024-10-27 18:53:18 +01:00
c261937cf5 Typo fix 2024-10-27 18:50:30 +01:00
4aa1309797 Small cleanup 2024-10-27 18:50:13 +01:00
4f8517a50c Proof of concept dynamic tile loading 2 2024-10-27 18:48:19 +01:00
446e049e6e npm update, added email field 2024-10-27 17:48:04 +01:00
7db2ba322c Minor improvements, more work on dynamic asset loading 2024-10-26 02:41:16 +02:00
70fb732051 finally some good fucking code 2024-10-26 00:15:31 +02:00
5128aa83f9 Added code comment 2024-10-25 22:25:49 +02:00
42 changed files with 1333 additions and 766 deletions

View File

@ -1,4 +1,4 @@
VITE_NAME=New Quest VITE_NAME=Sylvan Quest
VITE_DEVELOPMENT=true VITE_DEVELOPMENT=true
VITE_SERVER_ENDPOINT=http://localhost:4000 VITE_SERVER_ENDPOINT=http://localhost:4000
VITE_TILE_SIZE_X=64 VITE_TILE_SIZE_X=64

559
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -50,6 +50,7 @@
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.13",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "^5.4.9", "vite": "^5.4.9",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "^7.5.2", "vite-plugin-vue-devtools": "^7.5.2",
"vitest": "^2.0.3", "vitest": "^2.0.3",
"vue-tsc": "^1.6.5" "vue-tsc": "^1.6.5"

View File

@ -2,7 +2,6 @@
<Notifications /> <Notifications />
<GmTools v-if="gameStore.character?.role === 'gm'" /> <GmTools v-if="gameStore.character?.role === 'gm'" />
<GmPanel v-if="gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
<component :is="currentScreen" /> <component :is="currentScreen" />
</template> </template>
@ -12,22 +11,37 @@ import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import Notifications from '@/components/utilities/Notifications.vue' import Notifications from '@/components/utilities/Notifications.vue'
import GmTools from '@/components/gameMaster/GmTools.vue' import GmTools from '@/components/gameMaster/GmTools.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue' import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Login from '@/screens/Login.vue' import Login from '@/components/screens/Login.vue'
import Characters from '@/screens/Characters.vue' import Characters from '@/components/screens/Characters.vue'
import Game from '@/screens/Game.vue' import Game from '@/components/screens/Game.vue'
// import Loading from '@/screens/Loading.vue' import ZoneEditor from '@/components/screens/ZoneEditor.vue'
import ZoneEditor from '@/screens/ZoneEditor.vue' import { computed, watch } from 'vue'
import { computed } from 'vue'
const gameStore = useGameStore() 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
}) })
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
watch(
() => zoneEditorStore.active,
() => {
gameStore.game.loadedAssets = []
}
)
// #209: Play sound when a button is pressed
addEventListener('click', (event) => {
if (!(event.target instanceof HTMLButtonElement)) {
return
}
const audio = new Audio('/assets/music/click-btn.mp3')
audio.play()
})
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<Tiles @tilemap:create="tileMap = $event" /> <ZoneTiles @tilemap:create="tileMap = $event" />
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" /> <ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<EventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" /> <ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Toolbar @save="save" /> <Toolbar @save="save" />
@ -26,13 +26,12 @@ import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.v
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue' import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue' import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue' import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue' import ZoneTiles from '@/components/gameMaster/zoneEditor/ZoneTiles.vue'
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue' import ZoneObjects from '@/components/gameMaster/zoneEditor/ZoneObjects.vue'
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue' import ZoneEventTiles from '@/components/gameMaster/zoneEditor/ZoneEventTiles.vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null) const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
function save() { function save() {

View File

@ -8,7 +8,7 @@ import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable' import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { uuidv4 } from '@/utilities' import { uuidv4 } from '@/utilities'
import { onBeforeMount, onBeforeUnmount } from 'vue' import { onMounted, onUnmounted } from 'vue'
const scene = useScene() const scene = useScene()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
@ -102,14 +102,14 @@ function eraser(pointer: Phaser.Input.Pointer) {
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id) zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
} }
onBeforeMount(() => { onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil) scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil) scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser) scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser) scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
}) })
onBeforeUnmount(() => { onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil) scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil) scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser) scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)

View File

@ -1,40 +1,27 @@
<template> <template>
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" /> <SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" :movingZoneObject="movingZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
<Image v-for="object in zoneEditorStore.zone?.zoneObjects" v-bind="getImageProps(object)" @pointerup="() => (selectedZoneObject = object)" /> <ZoneObject v-for="zoneObject in zoneEditorStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject :selectedZoneObject :movingZoneObject @pointerup="clickZoneObject(zoneObject)" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { uuidv4 } from '@/utilities' import { uuidv4 } from '@/utilities'
import { calculateIsometricDepth, getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable' import { getTile } from '@/composables/zoneComposable'
import { Image, useScene } from 'phavuer' import { useScene } from 'phavuer'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import type { ZoneObject } from '@/types'
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue' import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue' import { onMounted, onUnmounted, ref, watch } from 'vue'
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
import type { ZoneObject as ZoneObjectT } from '@/types'
const scene = useScene() const scene = useScene()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
const selectedZoneObject = ref<ZoneObject | null>(null) const selectedZoneObject = ref<ZoneObjectT | null>(null)
const movingZoneObject = ref<ZoneObject | null>(null) const movingZoneObject = ref<ZoneObjectT | null>(null)
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tilemap: Phaser.Tilemaps.Tilemap
}>() }>()
function getImageProps(zoneObject: ZoneObject) {
return {
alpha: zoneObject.id === movingZoneObject.value?.id ? 0.5 : 1,
depth: calculateIsometricDepth(zoneObject.positionX, zoneObject.positionY, zoneObject.object.frameWidth, zoneObject.object.frameHeight),
tint: selectedZoneObject.value?.id === zoneObject.id ? 0x00ff00 : 0xffffff,
x: tileToWorldX(props.tilemap, zoneObject.positionX, zoneObject.positionY),
y: tileToWorldY(props.tilemap, zoneObject.positionX, zoneObject.positionY),
flipX: zoneObject.isRotated,
texture: zoneObject.object.id,
originY: Number(zoneObject.object.originX),
originX: Number(zoneObject.object.originY)
}
}
function pencil(pointer: Phaser.Input.Pointer) { function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set // Check if zone is set
if (!zoneEditorStore.zone) return if (!zoneEditorStore.zone) return
@ -54,6 +41,9 @@ function pencil(pointer: Phaser.Input.Pointer) {
// Check if shift is not pressed, this means we are moving the camera // Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY) const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
@ -66,7 +56,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
id: uuidv4(), id: uuidv4(),
zoneId: zoneEditorStore.zone.id, zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone, zone: zoneEditorStore.zone,
objectId: zoneEditorStore.selectedObject.id, objectId: zoneEditorStore.selectedObject,
object: zoneEditorStore.selectedObject, object: zoneEditorStore.selectedObject,
depth: 0, depth: 0,
isRotated: false, isRotated: false,
@ -94,6 +84,9 @@ function eraser(pointer: Phaser.Input.Pointer) {
// Check if shift is not pressed, this means we are moving the camera // Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY) const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
@ -106,6 +99,37 @@ function eraser(pointer: Phaser.Input.Pointer) {
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id) zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
} }
function objectPicker(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is object
if (zoneEditorStore.drawMode !== 'object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// If alt is not pressed, return
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (!existingObject) return
// Select the object
zoneEditorStore.setSelectedObject(existingObject)
}
function moveZoneObject(id: string) { function moveZoneObject(id: string) {
// Check if zone is set // Check if zone is set
if (!zoneEditorStore.zone) return if (!zoneEditorStore.zone) return
@ -155,18 +179,29 @@ function deleteZoneObject(id: string) {
selectedZoneObject.value = null selectedZoneObject.value = null
} }
onBeforeMount(() => { function clickZoneObject(zoneObject: ZoneObjectT) {
selectedZoneObject.value = zoneObject
// If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) {
zoneEditorStore.setSelectedObject(zoneObject.object)
}
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil) scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil) scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser) scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser) scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
}) })
onBeforeUnmount(() => { onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil) scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil) scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser) scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser) scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
}) })
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects // watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects

View File

@ -5,21 +5,26 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/config'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onBeforeMount, onBeforeUnmount } from 'vue' import { onMounted, onUnmounted } from 'vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable' import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { useGameStore } from '@/stores/gameStore'
import type { AssetDataT } from '@/types'
const emit = defineEmits(['tilemap:create']) const emit = defineEmits(['tilemap:create'])
const scene = useScene() const scene = useScene()
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
const zoneTilemap = createTilemap() const zoneTilemap = createTilemap()
const tiles = createTileLayer() const tiles = createTileLayer()
/**
* A Tilemap is a container for Tilemap data.
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/
function createTilemap() { function createTilemap() {
const zoneData = new Phaser.Tilemaps.MapData({ const zoneData = new Phaser.Tilemaps.MapData({
width: zoneEditorStore.zone?.width, width: zoneEditorStore.zone?.width,
@ -34,11 +39,19 @@ function createTilemap() {
return tilemap return tilemap
} }
/**
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
*/
function createTileLayer() { function createTileLayer() {
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })) const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => {
return zoneTilemap.addTilesetImage(tile.key, tile.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any
// Add blank tile
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0) layer.setDepth(0)
layer.setCullPadding(2, 2) layer.setCullPadding(2, 2)
@ -70,10 +83,10 @@ function pencil(pointer: Phaser.Input.Pointer) {
if (!tile) return if (!tile) return
// Place tile // Place tile
placeTile(zoneTilemap, tiles, tile.x, tile.y, zoneEditorStore.selectedTile.id) placeTile(zoneTilemap, tiles, tile.x, tile.y, zoneEditorStore.selectedTile)
// Adjust zoneEditorStore.zone.tiles // Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile.id zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile
} }
function eraser(pointer: Phaser.Input.Pointer) { function eraser(pointer: Phaser.Input.Pointer) {
@ -92,6 +105,9 @@ function eraser(pointer: Phaser.Input.Pointer) {
// Check if shift is not pressed, this means we are moving the camera // Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tiles, pointer.worldX, pointer.worldY) const tile = getTile(tiles, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
@ -116,6 +132,12 @@ function paint(pointer: Phaser.Input.Pointer) {
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Set new tileArray with selected tile // Set new tileArray with selected tile
setLayerTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id)) setLayerTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id))
@ -123,7 +145,35 @@ function paint(pointer: Phaser.Input.Pointer) {
zoneEditorStore.zone.tiles = createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id) zoneEditorStore.zone.tiles = createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id)
} }
onBeforeMount(() => { // When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (zoneEditorStore.drawMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
zoneEditorStore.setSelectedTile(zoneEditorStore.zone.tiles[tile.y][tile.x])
}
onMounted(() => {
if (!zoneEditorStore.zone?.tiles) { if (!zoneEditorStore.zone?.tiles) {
return return
} }
@ -132,12 +182,14 @@ onBeforeMount(() => {
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil) scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser) scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint) scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
}) })
onBeforeUnmount(() => { onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil) scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser) scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint) scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
zoneTilemap.destroyLayer('tiles') zoneTilemap.destroyLayer('tiles')
zoneTilemap.removeAllLayers() zoneTilemap.removeAllLayers()

View File

@ -42,23 +42,18 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/config'
import { ref, onMounted, computed, watch } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import type { Object } from '@/types' import type { Object, ZoneObject } from '@/types'
const gameStore = useGameStore() const gameStore = useGameStore()
const isModalOpen = ref(false) const isModalOpen = ref(false)
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
const searchQuery = ref('') const searchQuery = ref('')
// const objectDepth = ref(0)
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
// watch(objectDepth, (depth) => {
// zoneEditorStore.setObjectDepth(depth)
// })
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || []) const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))
@ -81,8 +76,6 @@ const toggleTag = (tag: string) => {
} }
onMounted(async () => { onMounted(async () => {
zoneEditorStore.setObjectDepth(0)
isModalOpen.value = true isModalOpen.value = true
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => { gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
zoneEditorStore.setObjectList(response) zoneEditorStore.setObjectList(response)

View File

@ -52,7 +52,7 @@
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300" class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`" :src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
:alt="selectedGroup.parent.name" :alt="selectedGroup.parent.name"
@click="selectTile(selectedGroup.parent)" @click="selectTile(selectedGroup.parent.id)"
:class="{ :class="{
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent), 'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent) 'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
@ -65,7 +65,7 @@
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300" class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`" :src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
:alt="childTile.name" :alt="childTile.name"
@click="selectTile(childTile)" @click="selectTile(childTile.id)"
:class="{ :class="{
'border-cyan shadow-lg scale-105': isActiveTile(childTile), 'border-cyan shadow-lg scale-105': isActiveTile(childTile),
'border-transparent hover:border-gray-300': !isActiveTile(childTile) 'border-transparent hover:border-gray-300': !isActiveTile(childTile)
@ -218,7 +218,7 @@ function closeGroup() {
selectedGroup.value = null selectedGroup.value = null
} }
function selectTile(tile: Tile) { function selectTile(tile: string) {
zoneEditorStore.setSelectedTile(tile) zoneEditorStore.setSelectedTile(tile)
} }

View File

@ -0,0 +1,45 @@
<template>
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Image, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { loadTexture } from '@/composables/gameComposable'
import type { AssetDataT, ZoneObject } from '@/types'
import { useGameStore } from '@/stores/gameStore'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
zoneObject: ZoneObject
selectedZoneObject: ZoneObject | null
movingZoneObject: ZoneObject | null
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
alpha: props.movingZoneObject?.id === props.zoneObject.id ? 0.5 : 1,
tint: props.selectedZoneObject?.id === props.zoneObject.id ? 0x00ff00 : 0xffffff,
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)
}))
loadTexture(scene, {
key: props.zoneObject.object.id,
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
group: 'objects',
updatedAt: props.zoneObject.object.updatedAt,
frameWidth: props.zoneObject.object.frameWidth,
frameHeight: props.zoneObject.object.frameHeight
} as AssetDataT).catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -168,8 +168,8 @@ function stopDrag() {
} }
function adjustPosition() { function adjustPosition() {
x.value = Math.max(0, Math.min(x.value, window.innerWidth - width.value)) x.value = Math.min(x.value, window.innerWidth - width.value)
y.value = Math.max(0, Math.min(y.value, window.innerHeight - height.value)) y.value = Math.min(y.value, window.innerHeight - height.value)
} }
function initializePosition() { function initializePosition() {
@ -236,6 +236,7 @@ onMounted(() => {
}) })
onUnmounted(() => { onUnmounted(() => {
removeEventListener('keydown', keyPress)
removeEventListener('mousemove', drag) removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag) removeEventListener('mouseup', stopDrag)
}) })

View File

@ -6,8 +6,8 @@
<div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading"> <div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading">
<!-- CHARACTER LIST --> <!-- CHARACTER LIST -->
<div v-for="character in characters" :key="character.id" class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character" :class="{ active: selected_character == character.id }"> <div v-for="character in characters" :key="character.id" class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character" :class="{ active: selected_character == character.id }">
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" /> <img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" /> <img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="UI box inner" />
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" /> <input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
<label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label> <label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
@ -103,10 +103,10 @@ import Modal from '@/components/utilities/Modal.vue'
import { type Character as CharacterT } from '@/types' import { type Character as CharacterT } from '@/types'
import ConfirmationModal from '@/components/utilities/ConfirmationModal.vue' import ConfirmationModal from '@/components/utilities/ConfirmationModal.vue'
const isLoading = ref(true)
const characters = ref([])
const gameStore = useGameStore() const gameStore = useGameStore()
const deletingCharacter = ref(null) const isLoading = ref(true)
const characters = ref([] as CharacterT[])
const deletingCharacter = ref(null as CharacterT | null)
// Fetch characters // Fetch characters
gameStore.connection?.on('character:list', (data: any) => { gameStore.connection?.on('character:list', (data: any) => {

View File

@ -32,10 +32,8 @@ import CharacterProfile from '@/components/gui/CharacterProfile.vue'
import Effects from '@/components/Effects.vue' import Effects from '@/components/Effects.vue'
import Minimap from '@/components/gui/Minimap.vue' import Minimap from '@/components/gui/Minimap.vue'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin' import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { useAssetManager } from '@/utilities/assetManager'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManager = useAssetManager
const gameConfig = { const gameConfig = {
name: config.name, name: config.name,
@ -78,41 +76,7 @@ function preloadScene(scene: Phaser.Scene) {
*/ */
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')
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)
})
})
successCallback()
})
} }
function 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
})
})
})
}
</script> </script>

View File

@ -0,0 +1,25 @@
<template>
<div class="flex flex-col justify-center items-center h-dvh relative">
<button @click="continueBtnClick" class="w-32 h-12 rounded-full bg-gray-500 flex items-center justify-between px-4 hover:bg-gray-600 transition-colors">
<span class="text-white text-lg flex-1 text-center">Play</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 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>
</template>
<script setup lang="ts" async>
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
function continueBtnClick() {
// Play music
const audio = new Audio('/assets/music/login.mp3')
audio.play()
// Set isLoaded to true
gameStore.game.isLoaded = true
}
</script>

View File

@ -0,0 +1,51 @@
<template>
<!-- @TODO this must be shown over the login screen -->
<div class="relative max-lg:h-dvh flex flex-row-reverse">
<ResetPassword :isModalOpen="isPasswordResetFormShown" @close="() => (isPasswordResetFormShown = false)" />
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" alt="Sylvan Quest logo" />
<div class="relative">
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" alt="UI box inner" />
<!-- Login Form -->
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />
<!-- Register Form -->
<RegisterForm v-if="currentForm === 'register' && !doesUrlHaveToken" @switchToLogin="currentForm = 'login'" />
<!-- New Password Form -->
<NewPasswordForm v-if="doesUrlHaveToken" @switchToLogin="currentForm = 'login'" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies'
import LoginForm from '@/components/screens/partials/login/LoginForm.vue'
import RegisterForm from '@/components/screens/partials/login/RegisterForm.vue'
import NewPasswordForm from '@/components/screens/partials/login/NewPasswordForm.vue'
import ResetPassword from '@/components/utilities/ResetPassword.vue'
const isPasswordResetFormShown = ref(false)
const doesUrlHaveToken = window.location.hash.includes('#')
const gameStore = useGameStore()
const currentForm = ref('login')
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
</script>

View File

@ -2,7 +2,7 @@
<div class="flex justify-center items-center h-dvh relative"> <div class="flex justify-center items-center h-dvh relative">
<Game :config="gameConfig" @create="createGame"> <Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene"> <Scene name="main" @preload="preloadScene" @create="createScene">
<ZoneEditor v-if="isLoaded" :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" /> <ZoneEditor :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" />
</Scene> </Scene>
</Game> </Game>
</div> </div>
@ -11,22 +11,32 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/config'
import 'phaser' import 'phaser'
import { ref, onBeforeUnmount } from 'vue'
import { Game, Scene } from 'phavuer' 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 AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { loadTexture } from '@/composables/gameComposable'
import type { AssetDataT } from '@/types'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
const isLoaded = ref(false)
const gameConfig = { const gameConfig = {
name: config.name, name: config.name,
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5 resolution: 5,
plugins: {
global: [
{
key: 'rexAwaitLoader',
plugin: AwaitLoaderPlugin,
start: true
}
]
}
} }
const createGame = (game: Phaser.Game) => { const createGame = (game: Phaser.Game) => {
@ -48,43 +58,6 @@ const createGame = (game: Phaser.Game) => {
} }
const preloadScene = async (scene: Phaser.Scene) => { const preloadScene = async (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 * Load the base assets into the Phaser scene
*/ */
@ -92,26 +65,21 @@ 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')
}
const createScene = async (scene: Phaser.Scene) => {
/** /**
* Create sprite animations * Because Phaser can't load tiles after the scene with map in it is created,
* This is done here because phaser forces us to * we need to load and cache all the tiles first.
* Then load them into the scene.
*/ */
gameStore.assets.forEach((asset) => { scene.load.rexAwait(async function (successCallback: any) {
if (asset.group !== 'sprite_animations') return const tiles: AssetDataT[] = await fetch(config.server_endpoint + '/assets/list_tiles').then((response) => response.json())
for await (const tile of tiles) {
await loadTexture(scene, tile)
}
scene.anims.create({ successCallback()
key: asset.key,
frameRate: 7,
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
repeat: -1
})
}) })
} }
onBeforeUnmount(() => { const createScene = async (scene: Phaser.Scene) => {}
isLoaded.value = false
})
</script> </script>

View File

@ -0,0 +1,69 @@
<template>
<form @submit.prevent="loginFunc" class="relative px-6 py-11">
<div class="flex flex-col gap-5 p-2 mb-8 relative">
<div class="w-full grid gap-3 relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
<div class="relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
</div>
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
</div>
<button @click.stop="() => emit('openResetPasswordModal')" type="button" class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
<!-- Divider shape -->
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
<div class="w-0.5 h-full bg-gray-300"></div>
<div class="w-36 h-full bg-gray-300"></div>
<div class="w-0.5 h-full bg-gray-300"></div>
</div>
</div>
<div class="pt-8">
<p class="m-0 text-center">Don't have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="() => emit('switchToRegister')">Sign up</button></p>
</div>
</form>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { login } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies'
const emit = defineEmits(['openResetPasswordModal', 'switchToRegister'])
const gameStore = useGameStore()
const username = ref('')
const password = ref('')
const loginError = ref('')
const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function loginFunc() {
// check if username and password are valid
if (username.value === '' || password.value === '') {
loginError.value = 'Please enter a valid username and password'
return
}
// send login event to server
const response = await login(username.value, password.value)
if (response.success === undefined) {
loginError.value = response.error
return
}
gameStore.setToken(response.token)
gameStore.initConnection()
return true // Indicate success
}
</script>

View File

@ -0,0 +1,68 @@
<template>
<form @submit.prevent="newPasswordFunc" class="relative px-6 py-11">
<div class="flex flex-col gap-5 p-2 mb-8 relative">
<div class="w-full grid gap-3 relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
<span v-if="newPasswordError" class="text-red-200 text-xs absolute top-full mt-1">{{ newPasswordError }}</span>
</div>
<button class="btn-cyan xs:w-full" type="submit">Change password</button>
<!-- Divider shape -->
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
<div class="w-0.5 h-full bg-gray-300"></div>
<div class="w-36 h-full bg-gray-300"></div>
<div class="w-0.5 h-full bg-gray-300"></div>
</div>
</div>
<div class="pt-8">
<p class="m-0 text-center"><button class="text-cyan-300 text-base p-0" @click.prevent="cancelNewPassword">Back to login</button></p>
</div>
</form>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { newPassword } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies'
const emit = defineEmits(['switchToLogin'])
const gameStore = useGameStore()
const password = ref('')
const newPasswordError = ref('')
const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function newPasswordFunc() {
// check if username and password are valid
if (password.value === '') {
newPasswordError.value = 'Please enter a password'
return
}
const urlToken = window.location.hash.split('#')[1]
// send new password event to server along with the token
const response = await newPassword(urlToken, password.value)
if (response.success === undefined) {
newPasswordError.value = response.error
return
}
window.location.href = '/'
}
function cancelNewPassword() {
window.location.href = '/'
}
</script>

View File

@ -0,0 +1,97 @@
<template>
<form @submit.prevent="registerFunc" class="relative px-6 py-11">
<div class="flex flex-col gap-5 p-2 mb-8 relative">
<div class="w-full grid gap-3 relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="username-register" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
<input class="input-field xs:min-w-[350px] min-w-64" id="email-register" v-model="email" type="email" name="email" placeholder="Email" required />
<div class="relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
</div>
<span v-if="loginError" class="text-red-200 text-xs -mt-2">{{ loginError }}</span>
</div>
<button class="btn-cyan xs:w-full" type="submit">Register now</button>
<!-- Divider shape -->
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
<div class="w-0.5 h-full bg-gray-300"></div>
<div class="w-36 h-full bg-gray-300"></div>
<div class="w-0.5 h-full bg-gray-300"></div>
</div>
</div>
<div class="pt-8">
<p class="m-0 text-center">Already have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="() => emit('switchToLogin')">Log in</button></p>
</div>
</form>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { login, register } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies'
const emit = defineEmits(['switchToLogin'])
const gameStore = useGameStore()
const username = ref('')
const password = ref('')
const email = ref('')
const loginError = ref('')
const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function loginFunc() {
// check if username and password are valid
if (username.value === '' || password.value === '') {
loginError.value = 'Please enter a valid username and password'
return
}
// send login event to server
const response = await login(username.value, password.value)
if (response.success === undefined) {
loginError.value = response.error
return
}
gameStore.setToken(response.token)
gameStore.initConnection()
return true // Indicate success
}
async function registerFunc() {
// check if username and password are valid
if (username.value === '' || email.value === '' || password.value === '') {
loginError.value = 'Please enter a valid username, email, and password'
return
}
if (email.value === '') {
loginError.value = 'Please enter an email'
return
}
// send register event to server
const response = await register(username.value, email.value, password.value)
if (response.success === undefined) {
loginError.value = response.error
return
}
const loginSuccess = await loginFunc()
if (!loginSuccess) {
loginError.value = 'Login after registration failed. Please try logging in manually.'
return
}
}
</script>

View File

@ -18,12 +18,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import config from '@/config' import config from '@/config'
import { type ExtendedCharacter } from '@/types' import { type ExtendedCharacter, type Sprite as SpriteT } from '@/types'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { watch, computed, ref, onMounted, onUnmounted } from 'vue' import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer' import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable' import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { loadSpriteTextures } from '@/composables/gameComposable'
enum Direction { enum Direction {
POSITIVE, POSITIVE,
@ -181,6 +182,15 @@ watch(
watch(() => props.character.isMoving, updateSprite) watch(() => props.character.isMoving, updateSprite)
watch(() => props.character.rotation, updateSprite) watch(() => props.character.rotation, updateSprite)
loadSpriteTextures(scene, props.character.characterType?.sprite as SpriteT)
.then(() => {
charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value)
})
.catch((error) => {
console.error('Error loading texture:', error)
})
onMounted(() => { onMounted(() => {
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`) charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false) charChatContainer.value!.setVisible(false)
@ -194,10 +204,6 @@ onMounted(() => {
scene.cameras.main.stopFollow() scene.cameras.main.stopFollow()
} }
// Set sprite
charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value)
updatePosition(props.character.positionX, props.character.positionY, props.character.rotation) updatePosition(props.character.positionX, props.character.positionY, props.character.rotation)
}) })

View File

@ -2,7 +2,7 @@
<Teleport to="body"> <Teleport to="body">
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle"> <div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative"> <div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
<div class="rounded-t-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div> <div class="rounded-t absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div>
<div class="relative z-10"> <div class="relative z-10">
<slot name="modalHeader" /> <slot name="modalHeader" />
</div> </div>
@ -16,7 +16,7 @@
</div> </div>
</div> </div>
<div class="overflow-hidden grow relative"> <div class="overflow-hidden grow relative">
<div class="rounded-b-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div> <div class="rounded-b absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div>
<div class="relative z-10 h-full"> <div class="relative z-10 h-full">
<slot name="modalBody" /> <slot name="modalBody" />
</div> </div>
@ -146,8 +146,8 @@ function stopDrag() {
function adjustPosition() { function adjustPosition() {
if (isFullScreen.value) return if (isFullScreen.value) return
x.value = Math.max(0, Math.min(x.value, window.innerWidth - width.value)) x.value = Math.min(x.value, window.innerWidth - width.value)
y.value = Math.max(0, Math.min(y.value, window.innerHeight - height.value)) y.value = Math.min(y.value, window.innerHeight - height.value)
} }
function handleResize() { function handleResize() {
@ -223,23 +223,23 @@ watch(
) )
onMounted(() => { onMounted(() => {
window.addEventListener('mousemove', drag) addEventListener('mousemove', drag)
window.addEventListener('mouseup', stopDrag) addEventListener('mouseup', stopDrag)
window.addEventListener('mousemove', resizeModal) addEventListener('mousemove', resizeModal)
window.addEventListener('mouseup', stopResize) addEventListener('mouseup', stopResize)
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) { if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
window.addEventListener('resize', handleResize) addEventListener('resize', handleResize)
} }
initializePosition() initializePosition()
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('mousemove', drag) removeEventListener('mousemove', drag)
window.removeEventListener('mouseup', stopDrag) removeEventListener('mouseup', stopDrag)
window.removeEventListener('mousemove', resizeModal) removeEventListener('mousemove', resizeModal)
window.removeEventListener('mouseup', stopResize) removeEventListener('mouseup', stopResize)
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) { if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
window.removeEventListener('resize', handleResize) removeEventListener('resize', handleResize)
} }
}) })
</script> </script>

View File

@ -12,7 +12,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { onBeforeMount, onBeforeUnmount, watch } from 'vue' import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
@ -34,7 +34,7 @@ function setupNotificationListener(connection: any) {
}) })
} }
onBeforeMount(() => { onMounted(() => {
const connection = gameStore.connection const connection = gameStore.connection
if (connection) { if (connection) {
setupNotificationListener(connection) setupNotificationListener(connection)
@ -49,7 +49,7 @@ onBeforeMount(() => {
} }
}) })
onBeforeUnmount(() => { onUnmounted(() => {
const connection = gameStore.connection const connection = gameStore.connection
if (connection) { if (connection) {
connection.off('notification') connection.off('notification')

View File

@ -0,0 +1,55 @@
<template>
<Modal @modal:close="() => emit('close')" :modal-width="400" :modal-height="300" :is-resizable="false">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Reset Password</h3>
</template>
<template #modalBody>
<div class="h-[calc(100%_-_32px)] p-4">
<form class="h-full flex flex-col justify-between" @submit.prevent="resetPasswordFunc">
<div class="flex flex-col relative">
<p>Fill in your email to receive a password reset request.</p>
<input type="email" name="email" class="input-field" v-model="email" placeholder="E-mail" />
<span v-if="resetPasswordError" class="text-red-200 text-xs absolute top-full mt-1">{{ resetPasswordError }}</span>
</div>
<div class="grid grid-flow-col justify-stretch gap-4">
<button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click.stop="() => emit('close')">Cancel</button>
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" type="submit">Send mail</button>
</div>
</form>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { resetPassword } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue'
const emit = defineEmits(['close'])
const gameStore = useGameStore()
const email = ref('')
const resetPasswordError = ref('')
const isPasswordResetOpen = ref(false)
async function resetPasswordFunc() {
// check if email is valid
if (email.value === '') {
resetPasswordError.value = 'Please enter an email'
return
}
// send reset password event to server
const response = await resetPassword(email.value)
if (response.success === undefined) {
resetPasswordError.value = response.error
return
}
emit('close')
}
</script>

View File

@ -5,14 +5,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onUnmounted } from 'vue'
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 { onBeforeUnmount, ref, onBeforeMount } from 'vue' import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
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 ZoneTiles from '@/components/zone/ZoneTiles.vue' import ZoneTiles from '@/components/zone/ZoneTiles.vue'
import ZoneObjects from '@/components/zone/ZoneObjects.vue' import ZoneObjects from '@/components/zone/ZoneObjects.vue'
import Characters from '@/components/zone/Characters.vue' import Characters from '@/components/zone/Characters.vue'
const scene = useScene()
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneStore = useZoneStore() const zoneStore = useZoneStore()
@ -33,6 +36,7 @@ gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) =
zoneId: data.zone.id zoneId: data.zone.id
}) })
await loadZoneTilesIntoScene(data.zone, scene)
zoneStore.setZone(data.zone) zoneStore.setZone(data.zone)
zoneStore.setCharacters(data.characters) zoneStore.setCharacters(data.characters)
}) })
@ -51,15 +55,14 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
zoneStore.updateCharacter(data) zoneStore.updateCharacter(data)
}) })
onBeforeMount(async () => { // Emit zone:character:join event to server and wait for response, then set zone and characters
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => { gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
// Set zone and characters await loadZoneTilesIntoScene(response.zone, scene)
zoneStore.setZone(response.zone) zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters) zoneStore.setCharacters(response.characters)
})
}) })
onBeforeUnmount(() => { onUnmounted(() => {
zoneStore.reset() zoneStore.reset()
gameStore.connection!.off('zone:character:teleport') gameStore.connection!.off('zone:character:teleport')
gameStore.connection!.off('zone:character:join') gameStore.connection!.off('zone:character:join')

View File

@ -6,17 +6,23 @@
import config from '@/config' import config from '@/config'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeMount, onBeforeUnmount } from 'vue' import { onBeforeUnmount } from 'vue'
import { setLayerTiles } from '@/composables/zoneComposable' import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { unduplicateArray } from '@/utilities'
const emit = defineEmits(['tilemap:create']) const emit = defineEmits(['tilemap:create'])
const zoneStore = useZoneStore()
const scene = useScene() const scene = useScene()
const zoneStore = useZoneStore()
const zoneTilemap = createTilemap() const zoneTilemap = createTilemap()
const tiles = createTileLayer() const tiles = createTileLayer()
/**
* A Tilemap is a container for Tilemap data.
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/
function createTilemap() { function createTilemap() {
const zoneData = new Phaser.Tilemaps.MapData({ const zoneData = new Phaser.Tilemaps.MapData({
width: zoneStore.zone?.width, width: zoneStore.zone?.width,
@ -31,11 +37,13 @@ function createTilemap() {
return tilemap return tilemap
} }
/**
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
*/
function createTileLayer() { function createTileLayer() {
const tilesFromZone = zoneStore.zone?.tiles || [] const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
const uniqueTiles = new Set(tilesFromZone.flat().filter(Boolean))
const tilesetImages = Array.from(uniqueTiles).map((tile, index) => { const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }) return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any }) as any
@ -49,12 +57,7 @@ function createTileLayer() {
return layer return layer
} }
onBeforeMount(() => { setLayerTiles(zoneTilemap, tiles, zoneStore.zone?.tiles)
if (!zoneStore.zone?.tiles) {
return
}
setLayerTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
zoneTilemap.destroyLayer('tiles') zoneTilemap.destroyLayer('tiles')

View File

@ -1,22 +1,22 @@
<template> <template>
<Image v-if="isTextureLoaded" v-bind="imageProps" /> <Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue' import { computed, onMounted } from 'vue'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable' import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { useAssetManager } from '@/utilities/assetManager' import { loadTexture } from '@/composables/gameComposable'
import type { ZoneObject } from '@/types' import type { AssetDataT, ZoneObject } from '@/types'
import { useGameStore } from '@/stores/gameStore'
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tilemap: Phaser.Tilemaps.Tilemap
zoneObject: ZoneObject zoneObject: ZoneObject
}>() }>()
const gameStore = useGameStore()
const scene = useScene() const scene = useScene()
const assetManager = useAssetManager
const isTextureLoaded = ref(false)
const imageProps = computed(() => ({ const imageProps = computed(() => ({
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight), depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
@ -28,36 +28,14 @@ const imageProps = computed(() => ({
originX: Number(props.zoneObject.object.originY) originX: Number(props.zoneObject.object.originY)
})) }))
const loadTexture = async () => { loadTexture(scene, {
const textureId = props.zoneObject.object.id key: props.zoneObject.object.id,
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
// Check if the texture is already loaded in Phaser group: 'objects',
if (scene.textures.exists(textureId)) { updatedAt: props.zoneObject.object.updatedAt,
isTextureLoaded.value = true frameWidth: props.zoneObject.object.frameWidth,
return frameHeight: props.zoneObject.object.frameHeight
} } as AssetDataT).catch((error) => {
console.error('Error loading texture:', error)
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> </script>

View File

@ -0,0 +1,34 @@
export function createSceneLoader(scene: Phaser.Scene) {
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',
// @ts-ignore
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()
return true
})
}

View File

@ -0,0 +1,86 @@
import type { AssetDataT, Sprite } from '@/types'
import { useGameStore } from '@/stores/gameStore'
import { AssetStorage } from '@/storage/assetStorage'
import config from '@/config'
const textureLoadingPromises = new Map<string, Promise<boolean>>()
export async function loadTexture(scene: Phaser.Scene, assetData: AssetDataT): Promise<boolean> {
const gameStore = useGameStore()
const assetStorage = new AssetStorage()
// Check if the texture is already loaded in Phaser
if (gameStore.game.loadedAssets.find((asset) => asset.key === assetData.key)) {
return Promise.resolve(true)
}
// If there's already a loading promise for this texture, return it
if (textureLoadingPromises.has(assetData.key)) {
return await textureLoadingPromises.get(assetData.key)!
}
// Create new loading promise
const loadingPromise = (async () => {
// Check if the asset is already cached
let asset = await assetStorage.get(assetData.key)
// If asset is not found, download it
if (!asset) {
await assetStorage.download(assetData)
asset = await assetStorage.get(assetData.key)
}
// If asset is found, add it to the scene
if (asset) {
return new Promise<boolean>((resolve) => {
// Remove existing texture if it exists
if (scene.textures.exists(asset.key)) {
scene.textures.remove(asset.key)
}
scene.textures.addBase64(asset.key, asset.data)
scene.textures.once(`addtexture-${asset.key}`, () => {
gameStore.game.loadedAssets.push(assetData)
textureLoadingPromises.delete(assetData.key) // Clean up the promise
resolve(true)
})
})
}
textureLoadingPromises.delete(assetData.key) // Clean up the promise
return Promise.resolve(false)
})()
// Store the loading promise
textureLoadingPromises.set(assetData.key, loadingPromise)
return loadingPromise
}
export async function loadSpriteTextures(scene: Phaser.Scene, sprite: Sprite) {
const sprite_actions = await fetch(config.server_endpoint + '/assets/list_sprite_actions/' + sprite?.id).then((response) => response.json())
for await (const sprite_action of sprite_actions) {
await loadTexture(scene, {
key: sprite_action.key,
data: sprite_action.data,
group: sprite_action.isAnimated ? 'sprite_animations' : 'sprites',
updatedAt: sprite_action.updatedAt,
frameCount: sprite_action.frameCount,
frameWidth: sprite_action.frameWidth,
frameHeight: sprite_action.frameHeight
} as AssetDataT)
// If the sprite is not animated, skip
if (!sprite_action.isAnimated) continue
// Add the animation to the scene
const anim = scene.textures.get(sprite_action.key)
scene.textures.addSpriteSheet(sprite_action.key, anim, { frameWidth: sprite_action.frameWidth ?? 0, frameHeight: sprite_action.frameHeight ?? 0 })
scene.anims.create({
key: sprite_action.key,
frameRate: 7,
frames: scene.anims.generateFrameNumbers(sprite_action.key, { start: 0, end: sprite_action.frameCount! - 1 }),
repeat: -1
})
}
return Promise.resolve(true)
}

View File

@ -31,7 +31,7 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
const { worldX, worldY } = pointer const { worldX, worldY } = pointer
updateWaypoint(worldX, worldY) updateWaypoint(worldX, worldY)
if (gameStore.isPlayerDraggingCamera) { if (gameStore.game.isPlayerDraggingCamera) {
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y) const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
if (distance > dragThreshold) { if (distance > dragThreshold) {

View File

@ -26,7 +26,7 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
} }
function dragZone(pointer: Phaser.Input.Pointer) { function dragZone(pointer: Phaser.Input.Pointer) {
if (gameStore.isPlayerDraggingCamera) { if (gameStore.game.isPlayerDraggingCamera) {
const { x, y, prevPosition } = pointer const { x, y, prevPosition } = pointer
const { scrollX, scrollY, zoom } = camera const { scrollX, scrollY, zoom } = camera
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom) camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)

View File

@ -3,6 +3,8 @@ import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer 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 type { AssetDataT, Zone as ZoneT } from '@/types'
import { loadTexture } from '@/composables/gameComposable'
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)
@ -45,12 +47,15 @@ export function tileToWorldY(layer: TilemapLayer | Tilemap, pos_x: number, pos_y
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) { export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
let tileImg = zone.getTileset(tileName) as Tileset let tileImg = zone.getTileset(tileName) as Tileset
if (!tileImg) { if (!tileImg) {
console.log('tile not found:', tileName)
tileImg = zone.getTileset('blank_tile') as Tileset tileImg = zone.getTileset('blank_tile') as Tileset
} }
layer.putTileAt(tileImg.firstgid, x, y) layer.putTileAt(tileImg.firstgid, x, y)
} }
export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) { export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
if (!tiles) return
tiles.forEach((row: string[], y: number) => { tiles.forEach((row: string[], y: number) => {
row.forEach((tile: string, x: number) => { row.forEach((tile: string, x: number) => {
placeTile(zone, layer, x, y, tile) placeTile(zone, layer, x, y, tile)
@ -69,3 +74,23 @@ export const calculateIsometricDepth = (x: number, y: number, width: number = 0,
} }
return baseDepth + (width + height) / (2 * config.tile_size.x) return baseDepth + (width + height) / (2 * config.tile_size.x)
} }
export function FlattenZoneArray(tiles: string[][]) {
const normalArray = []
for (const row of tiles) {
normalArray.push(...row)
}
return normalArray
}
export async function loadZoneTilesIntoScene(zone: ZoneT, scene: Phaser.Scene) {
// Fetch the list of tiles from the server
const tileArray: AssetDataT[] = await fetch(config.server_endpoint + '/assets/list_tiles/' + zone.id).then((response) => response.json())
// Load each tile into the scene
for (const tile of tileArray) {
await loadTexture(scene, tile)
}
}

View File

@ -1,74 +0,0 @@
<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/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)
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.downloadAsset(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

@ -1,130 +0,0 @@
<template>
<div class="relative max-lg:h-dvh flex flex-row-reverse">
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" />
<div class="relative">
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" />
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
<!-- Login Form -->
<form v-show="switchForm === 'login'" @submit.prevent="loginFunc" class="relative px-6 py-11">
<div class="flex flex-col gap-5 p-2 mb-8 relative">
<div class="w-full grid gap-3 relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
<div class="relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
</div>
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
</div>
<button class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
<!-- Divider shape -->
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
<div class="w-0.5 h-full bg-gray-300"></div>
<div class="w-36 h-full bg-gray-300"></div>
<div class="w-0.5 h-full bg-gray-300"></div>
</div>
</div>
<div class="pt-8">
<p class="m-0 text-center">Don't have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'register'">Sign up</button></p>
</div>
</form>
<!-- Register Form -->
<form v-show="switchForm === 'register'" @submit.prevent="registerFunc" class="relative px-6 py-11">
<div class="flex flex-col gap-5 p-2 mb-8 relative">
<div class="w-full grid gap-3 relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="username-register" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
<div class="relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
</div>
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
</div>
<button class="btn-cyan xs:w-full" type="submit">Register now</button>
<!-- Divider shape -->
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
<div class="w-0.5 h-full bg-gray-300"></div>
<div class="w-36 h-full bg-gray-300"></div>
<div class="w-0.5 h-full bg-gray-300"></div>
</div>
</div>
<div class="pt-8">
<p class="m-0 text-center">Already have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'login'">Log in</button></p>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { login, register } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies'
const gameStore = useGameStore()
const username = ref('')
const password = ref('')
const switchForm = ref('login')
const loginError = ref('')
const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function loginFunc() {
// check if username and password are valid
if (username.value === '' || password.value === '') {
loginError.value = 'Please enter a valid username and password'
return
}
// send login event to server
const response = await login(username.value, password.value)
if (response.success === undefined) {
loginError.value = response.error
return
}
gameStore.setToken(response.token)
gameStore.initConnection()
return true // Indicate success
}
async function registerFunc() {
// check if username and password are valid
if (username.value === '' || password.value === '') {
loginError.value = 'Please enter a valid username and password'
return
}
// send register event to server
const response = await register(username.value, password.value)
if (response.success === undefined) {
loginError.value = response.error
return
}
const loginSuccess = await loginFunc()
if (!loginSuccess) {
loginError.value = 'Login after registration failed. Please try logging in manually.'
return
}
}
</script>

View File

@ -1,13 +1,17 @@
import axios from 'axios' import axios from 'axios'
import config from '@/config' import config from '@/config'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import { getDomain } from '@/utilities'
export async function register(username: string, password: string) { export async function register(username: string, email: 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, email, password })
useCookies().set('token', response.data.token as string) useCookies().set('token', response.data.token as string)
return { success: true, token: response.data.token } return { success: true, token: response.data.token }
} catch (error: any) { } catch (error: any) {
if (typeof error.response.data === 'undefined') {
return { error: 'Could not connect to server' }
}
return { error: error.response.data.message } return { error: error.response.data.message }
} }
} }
@ -16,12 +20,34 @@ 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 domain: getDomain()
// @TODO : #190
// 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) {
return { error: error.response.data.message } return { error: error.response.data.message }
} }
} }
export async function resetPassword(email: string) {
try {
const response = await axios.post(`${config.server_endpoint}/reset-password`, { email })
return { success: true, token: response.data.token }
} catch (error: any) {
if (typeof error.response.data === 'undefined') {
return { error: 'Could not connect to server' }
}
return { error: error.response.data.message }
}
}
export async function newPassword(urlToken: string, password: string) {
try {
const response = await axios.post(`${config.server_endpoint}/new-password`, { urlToken, password })
return { success: true, token: response.data.token }
} catch (error: any) {
if (typeof error.response.data === 'undefined') {
return { error: 'Could not connect to server' }
}
return { error: error.response.data.message }
}
}

View File

@ -0,0 +1,69 @@
import config from '@/config'
import Dexie from 'dexie'
import type { AssetDataT } from '@/types'
export class AssetStorage {
private db: Dexie
constructor() {
this.db = new Dexie('assets')
this.db.version(1).stores({
assets: 'key, group'
})
}
async download(asset: AssetDataT) {
try {
// Check if the asset already exists, then check if updatedAt is newer
const _asset = await this.db.table('assets').get(asset.key)
if (_asset && _asset.updatedAt > asset.updatedAt) {
return
}
// Download the asset
const response = await fetch(config.server_endpoint + asset.data)
const blob = await response.blob()
// Store the asset in the database
await this.db.table('assets').put({ key: asset.key, data: blob, group: asset.group, updatedAt: asset.updatedAt, frameCount: asset.frameCount, frameWidth: asset.frameWidth, frameHeight: asset.frameHeight })
} catch (error) {
console.error(`Failed to add asset ${asset.key}:`, error)
}
}
async get(key: string) {
try {
const asset = await this.db.table('assets').get(key)
if (asset) {
return {
...asset,
data: URL.createObjectURL(asset.data) // Convert blob to data URL
}
}
} catch (error) {
console.error(`Failed to retrieve asset ${key}:`, error)
}
return null
}
async getByGroup(group: string) {
try {
const assets = await this.db.table('assets').where('group').equals(group).toArray()
return assets.map((asset) => ({
...asset,
data: URL.createObjectURL(asset.data) // Convert blob to data URL
}))
} catch (error) {
console.error(`Failed to retrieve assets for group ${group}:`, error)
return []
}
}
async delete(key: string) {
try {
await this.db.table('assets').delete(key)
} catch (error) {
console.error(`Failed to delete asset ${key}:`, error)
}
}
}

View File

@ -1,27 +1,29 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { io, Socket } from 'socket.io-client' import { io, Socket } from 'socket.io-client'
import type { Asset, Character, Notification, User, WorldSettings } from '@/types' import type { AssetDataT, Character, Notification, User, WorldSettings } from '@/types'
import config from '@/config' import config from '@/config'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import { getDomain } from '@/utilities'
export const useGameStore = defineStore('game', { export const useGameStore = defineStore('game', {
state: () => { state: () => {
return { return {
notifications: [] as Notification[], notifications: [] as Notification[],
isAssetsLoaded: false,
loadedAssets: [] as string[],
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,
character: null as Character | null, character: null as Character | null,
isPlayerDraggingCamera: false,
world: { world: {
date: new Date(), date: new Date(),
isRainEnabled: false, isRainEnabled: false,
isFogEnabled: false, isFogEnabled: false,
fogDensity: 0.5 fogDensity: 0.5
} as WorldSettings, } as WorldSettings,
gameSettings: { game: {
isLoading: false,
isLoaded: false, // isLoaded is currently being used to determine if the player has interacted with the game
loadedAssets: [] as AssetDataT[],
isPlayerDraggingCamera: false,
isCameraFollowingCharacter: false isCameraFollowingCharacter: false
}, },
uiSettings: { uiSettings: {
@ -31,6 +33,17 @@ export const useGameStore = defineStore('game', {
} }
} }
}, },
getters: {
getLoadedAssets: (state) => {
return state.game.loadedAssets
},
getLoadedAsset: (state) => {
return (key: string) => state.game.loadedAssets.find((asset) => asset.key === key)
},
getLoadedAssetsByGroup: (state) => {
return (group: string) => state.game.loadedAssets.filter((asset) => asset.group === group)
}
},
actions: { actions: {
addNotification(notification: Notification) { addNotification(notification: Notification) {
if (!notification.id) { if (!notification.id) {
@ -53,17 +66,8 @@ export const useGameStore = defineStore('game', {
toggleGmPanel() { toggleGmPanel() {
this.uiSettings.isGmPanelOpen = !this.uiSettings.isGmPanelOpen this.uiSettings.isGmPanelOpen = !this.uiSettings.isGmPanelOpen
}, },
togglePlayerDraggingCamera() {
this.isPlayerDraggingCamera = !this.isPlayerDraggingCamera
},
setPlayerDraggingCamera(moving: boolean) { setPlayerDraggingCamera(moving: boolean) {
this.isPlayerDraggingCamera = moving this.game.isPlayerDraggingCamera = moving
},
toggleCameraFollowingCharacter() {
this.gameSettings.isCameraFollowingCharacter = !this.gameSettings.isCameraFollowingCharacter
},
setCameraFollowingCharacter(following: boolean) {
this.gameSettings.isCameraFollowingCharacter = following
}, },
toggleChat() { toggleChat() {
this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen
@ -101,21 +105,22 @@ export const useGameStore = defineStore('game', {
this.connection?.disconnect() this.connection?.disconnect()
useCookies().remove('token', { useCookies().remove('token', {
// for whole domain domain: getDomain()
// @TODO : #190
// domain: window.location.hostname.split('.').slice(-2).join('.')
}) })
this.isAssetsLoaded = false
this.connection = null this.connection = null
this.token = null this.token = null
this.user = null this.user = null
this.character = null this.character = null
this.uiSettings.isGmPanelOpen = false
this.isPlayerDraggingCamera = false this.game.isLoaded = false
this.gameSettings.isCameraFollowingCharacter = false this.game.loadedAssets = []
this.game.isPlayerDraggingCamera = false
this.game.isCameraFollowingCharacter = false
this.uiSettings.isChatOpen = false this.uiSettings.isChatOpen = false
this.uiSettings.isCharacterProfileOpen = false this.uiSettings.isCharacterProfileOpen = false
this.uiSettings.isGmPanelOpen = false
this.world.date = new Date() this.world.date = new Date()
this.world.isRainEnabled = false this.world.isRainEnabled = false

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import type { Zone, Object, Tile, ZoneEffect } from '@/types' import type { Zone, Object, Tile, ZoneEffect, ZoneObject } from '@/types'
export type TeleportSettings = { export type TeleportSettings = {
toZoneId: number toZoneId: number
@ -20,9 +20,8 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
zoneList: [] as Zone[], zoneList: [] as Zone[],
tileList: [] as Tile[], tileList: [] as Tile[],
objectList: [] as Object[], objectList: [] as Object[],
selectedTile: null as Tile | null, selectedTile: '',
selectedObject: null as Object | null, selectedObject: null as Object | null,
objectDepth: 0,
isTileListModalShown: false, isTileListModalShown: false,
isObjectListModalShown: false, isObjectListModalShown: false,
isZoneListModalShown: false, isZoneListModalShown: false,
@ -88,15 +87,12 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
setObjectList(objects: Object[]) { setObjectList(objects: Object[]) {
this.objectList = objects this.objectList = objects
}, },
setSelectedTile(tile: Tile) { setSelectedTile(tile: string) {
this.selectedTile = tile this.selectedTile = tile
}, },
setSelectedObject(object: any) { setSelectedObject(object: Object) {
this.selectedObject = object this.selectedObject = object
}, },
setObjectDepth(depth: number) {
this.objectDepth = depth
},
toggleSettingsModal() { toggleSettingsModal() {
this.isSettingsModalShown = !this.isSettingsModalShown this.isSettingsModalShown = !this.isSettingsModalShown
}, },
@ -117,9 +113,8 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
this.objectList = [] this.objectList = []
this.tool = 'move' this.tool = 'move'
this.drawMode = 'tile' this.drawMode = 'tile'
this.selectedTile = null this.selectedTile = ''
this.selectedObject = null this.selectedObject = null
this.objectDepth = 0
this.isSettingsModalShown = false this.isSettingsModalShown = false
this.isZoneListModalShown = false this.isZoneListModalShown = false
this.isCreateZoneModalShown = false this.isCreateZoneModalShown = false

View File

@ -4,11 +4,12 @@ export type Notification = {
message?: string message?: string
} }
export type AssetT = { export type AssetDataT = {
key: string key: string
url: string data: 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 updatedAt: Date
isAnimated?: boolean
frameCount?: number frameCount?: number
frameWidth?: number frameWidth?: number
frameHeight?: number frameHeight?: number

View File

@ -1,3 +1,25 @@
export function uuidv4() { export function uuidv4() {
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16)) return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
} }
export function unduplicateArray(array: any[]) {
return [...new Set(array.flat())]
}
export function getDomain() {
// Check if not localhost
if (window.location.hostname !== 'localhost') {
return window.location.hostname
}
// Check if not IP address
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
return window.location.hostname
}
if (window.location.hostname.split('.').length < 3) {
return window.location.hostname
}
return window.location.hostname.split('.').slice(-2).join('.')
}

View File

@ -1,72 +0,0 @@
import config from '@/config'
import Dexie from 'dexie'
class AssetManager extends Dexie {
assets!: Dexie.Table<
{
key: string
data: Blob
group: string
updatedAt: Date
frameCount?: number
frameWidth?: number
frameHeight?: number
},
string
>
constructor() {
super('Assets')
this.version(1).stores({
assets: 'key, group'
})
}
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()
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) {
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) {
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 deleteAsset(key: string) {
try {
await this.assets.delete(key)
} catch (error) {
console.error(`Failed to delete asset ${key}:`, error)
}
}
}
export const useAssetManager = new AssetManager()

View File

@ -2,12 +2,14 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import VueDevTools from 'vite-plugin-vue-devtools' import VueDevTools from 'vite-plugin-vue-devtools'
import viteCompression from 'vite-plugin-compression';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
VueDevTools(), VueDevTools(),
viteCompression()
], ],
resolve: { resolve: {
alias: { alias: {