Compare commits

..

105 Commits

Author SHA1 Message Date
0f46e3b6d2 #323 - Added alt to all images, also removed unused templates
Templates for old user panel hadnt been removed yet
2025-01-31 19:35:29 +01:00
6ca82733eb #324 - Fix Character profile image 2025-01-31 19:04:42 +01:00
eb61f45535 npm update 2025-01-31 18:49:03 +01:00
a181fc7fe3 npm update 2025-01-31 17:06:16 +01:00
507d4226ac Bug fix for character profile, greatly improved javascript 2025-01-31 03:23:23 +01:00
5dd9d1e7af Added temp. offset logic for easier sprite management 2025-01-31 02:16:19 +01:00
15f9e9861e Zoom 2025-01-31 01:55:52 +01:00
7fd334d414 Applied styling fix 2025-01-31 01:19:49 +01:00
c7d4b5f2c3 Updates TS hints 2025-01-31 01:18:49 +01:00
5747166822 Don't toggle accordion on button press 2025-01-31 01:17:44 +01:00
c010373e5b Updated loading indicator to match the other one we have 2025-01-30 18:35:51 +01:00
57ad9d4889 Don't update after closing sprite action img offset modal 2025-01-30 18:32:33 +01:00
f268ac9e5b Removed comment, updated types for sprite actions, minor modal component improvement, added components for better sprite management 2025-01-30 18:29:55 +01:00
8befce7ffb Update packages 2025-01-29 23:28:04 +01:00
014c08b17a Set depth to 9999 to always show above character sprite 2025-01-28 17:54:12 +01:00
bdbda6456c CharHair work 2025-01-28 16:32:16 +01:00
85537840ab Improved code 2025-01-28 05:14:37 +01:00
2b7082ac92 Bug fix caused by formatting 2025-01-28 05:01:52 +01:00
abc58bfa38 npm run format, removed container that character was in 2025-01-28 04:57:21 +01:00
027325f2bf Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-01-28 01:59:29 +01:00
517e92b07b Fully restored tile picker function 2025-01-27 14:49:27 -06:00
6bede8c44e Map editor ui fixes, switching back to game from map editor, draw/tap checkbox, and partial restoration of tile picker function 2025-01-27 14:33:29 -06:00
9e652868ca Prepping to work on Placed map objects 2025-01-27 00:18:46 -06:00
35f0dcca64 Hierarchical pointer handling logic 2025-01-26 21:21:24 -06:00
9618e07bc6 refactoring pointer events and input handling improvements 2025-01-26 20:50:34 -06:00
791830fd6f Refactoring of modalShown booleans 2025-01-26 18:56:13 -06:00
37acf1782b Removed isAnimated and isLooping fields 2025-01-27 01:51:53 +01:00
14aa696197 Changes to mapeditorcomposable, fix pencil and fill tool for tiles
Locked in, made mapeditor my bi-
2025-01-26 23:28:15 +01:00
cfac1d508b Minor change 2025-01-25 16:49:45 +01:00
82cfe5902f Bye 2025-01-25 16:49:25 +01:00
284ca6f64e Finish improving effects component 2025-01-25 15:44:49 +01:00
967cb1893d Re-applied changes 2025-01-25 15:38:20 +01:00
18db005bc1 Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-01-25 14:48:24 +01:00
c8473fc206 Removed console.log 2025-01-25 14:48:16 +01:00
b5e84c133a Merge remote-tracking branch 'origin/main' into feature/map-refactor
# Conflicts:
#	src/components/game/map/Effects.vue
2025-01-25 14:47:00 +01:00
3f75e4acd8 Merge remote-tracking branch 'origin/main' into feature/#313 2025-01-25 14:46:15 +01:00
765d0986bf Merge remote-tracking branch 'origin/feature/#313' 2025-01-25 14:45:48 +01:00
95c3a1af61 Merge remote-tracking branch 'origin/main' into feature/#313 2025-01-25 14:45:38 +01:00
45a9d8cfdb Formatted code 2025-01-25 14:45:09 +01:00
e0a48a089a Sorted imports 2025-01-25 13:49:52 +01:00
69f9944dc7 Use Dexie instead of Pinia store values in tileList inside mapEditor 2025-01-25 13:49:03 +01:00
9cdfcbcc56 npm run format 2025-01-25 13:24:26 +01:00
a614ee6241 Fixed value could be undefined ts error 2025-01-24 19:43:31 -06:00
7a51323682 Load map data inside a composable instead of Pinia store 2025-01-25 02:38:40 +01:00
807bc2066e Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-01-25 02:38:13 +01:00
fab0b08425 Load map data inside a composable instead of Pinia store 2025-01-25 02:38:02 +01:00
b074270c75 TS fix 2025-01-25 00:06:25 +01:00
30845b80e9 Minor formatting improvement 2025-01-25 00:02:22 +01:00
bde0f74f19 Re-added required package 2025-01-24 22:58:46 +01:00
bc685c63ef Cleaned code; overwrite cache if newer results are found 2025-01-23 20:40:41 +01:00
7a922261e3 Removed mapList from Pinia store, we fetch them from mapStorage 2025-01-23 20:04:47 +01:00
3936676f2c fog transparency fix 2025-01-23 13:04:43 -06:00
9744083dea Renamed downloadAndStore to downloadCache 2025-01-23 19:46:19 +01:00
176f31d84a Light calculations with minute percision 2025-01-23 02:38:30 -06:00
faad00b2a5 Simplified effects code and implemented smooth interpolation between day an night light values 2025-01-23 01:56:20 -06:00
a61e05592d Removed weather effects booleans, now to disable weather effects, setting the value to 0 is the way 2025-01-22 18:00:39 -06:00
5202251ac7 Tailwind improvement 2025-01-23 00:50:35 +01:00
5e2781b265 Removed placedMapObject depth field, cleaned package.json, creating & deleting maps now works with mapStorage 2025-01-23 00:47:34 +01:00
ebd6d96e54 Started streamlining map editor with regular map logic 2025-01-22 20:34:56 +01:00
41005735f9 Added version to dexie base class 2025-01-22 20:34:16 +01:00
78f1c6e6a0 Added font to loading text 2025-01-22 00:36:00 +01:00
2d48f83802 Load tiles before showing map editor UI 2025-01-22 00:31:14 +01:00
7dccb73698 TS improvement 2025-01-21 23:38:42 +01:00
7171112881 Moved effects.vue into map folder 2025-01-21 23:33:56 +01:00
9a601b7e2e Wait for map load before loading effects
Both command and mapSettings effects work again
2025-01-21 19:59:40 +01:00
5c68b02fff Removed redundant const 2025-01-19 00:06:36 +01:00
b86d9dd4ce Half working effects
Command triggered effects still brokey
2025-01-19 00:03:08 +01:00
93baa10acf Improvements 2025-01-14 02:39:41 +01:00
419cf319be TS fix 2025-01-14 02:17:35 +01:00
1cd7f28402 Removed debugging code 2025-01-13 15:02:53 +01:00
0657dbcb1b Tiny improvements 2025-01-13 14:51:28 +01:00
5cf7423a5c PlacedMapObjects.vue refactor for map 2025-01-13 11:40:41 +01:00
4d88917526 Loading placed map objects works again 2025-01-13 11:02:04 +01:00
8f07cf5093 Map editor tiles are now fully loaded from cache 2025-01-13 10:52:40 +01:00
367d536c52 more shit 2025-01-12 22:11:33 +01:00
3f8c911e9d Clean up 2025-01-12 20:54:59 +01:00
689e443b3d . 2025-01-12 03:24:31 +01:00
4fead371d7 sprite shit 2025-01-11 21:48:08 +01:00
b9bcfc719f Merge remote-tracking branch 'origin/feature/cache' 2025-01-10 23:23:00 +01:00
9de7af961e Improved map tile initialising 2025-01-10 23:22:32 +01:00
4067ec2585 . 2025-01-10 21:03:02 +01:00
fb18841c91 npm run format 2025-01-09 16:02:17 +01:00
7b1dcf7ce3 Added comments 2025-01-09 16:00:36 +01:00
7546116878 Better variable namings 2025-01-09 15:58:02 +01:00
03fef60621 Load textures using cache data instead of data sent from server 2025-01-08 21:13:04 +01:00
574777da80 stuffs 2025-01-07 22:20:46 +01:00
2b84bfcad2 #305 - Made all list types full height 2025-01-07 21:00:26 +01:00
f829cfb883 #309 - Removed mobile hidden styling from inner ui box 2025-01-07 20:20:52 +01:00
c2db9b5469 POC working new caching method - moved controllers folder, renamed assets to textures, fixed HTTP bug, formatted code 2025-01-07 03:59:08 +01:00
6e30a8530a POC working new caching method - moved controllers folder, renamed assets to textures, fixed HTTP bug, formatted code 2025-01-07 03:59:02 +01:00
41f82897a8 Better naming 2025-01-06 00:11:19 +01:00
37b97b0aac Fixes 2025-01-05 22:06:05 +01:00
c1d9cc3a11 Disabled vue dev tools, replaced ref with shallowRef, better naming 2025-01-05 21:00:16 +01:00
b54b825422 Map event tile improvements 2025-01-05 06:22:28 +01:00
0142850983 Map editor WIP 2025-01-05 04:01:50 +01:00
2d09715dc4 Map editor improvements 2025-01-05 01:43:39 +01:00
ef807982a5 Map editor improvements 2025-01-05 00:07:55 +01:00
ae0841889b Fixed emits 2025-01-04 23:17:54 +01:00
bdd2f93175 Added gap between list elements
(Cyan boxes touched when 1 item was selected and other was hovered over)
2025-01-04 22:41:20 +01:00
10f6dc3802 TS improvements 2025-01-04 19:17:10 +01:00
700bd57e67 Almost finalised refactoring 2025-01-03 14:35:08 +01:00
145143cdc5 4k on chat bubble text 2025-01-02 20:37:37 +01:00
201853a3ec Activated 4k on username text rendering 2025-01-02 20:33:54 +01:00
40c87f0ee3 Renamed zone > map 2025-01-02 17:31:31 +01:00
736ddddc54 Fix chips input component 2025-01-02 01:38:42 +01:00
118 changed files with 3492 additions and 6040 deletions

View File

@ -1,5 +1,5 @@
VITE_NAME=Noxious VITE_NAME=Noxious
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_WIDTH=64
VITE_TILE_SIZE_Y=32 VITE_TILE_SIZE_HEIGHT=32

View File

@ -1,13 +0,0 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier/skip-formatting'],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'vue/multi-word-component-names': 'off'
}
}

View File

@ -1,7 +1,6 @@
{ {
"recommendations": [ "recommendations": [
"Vue.volar", "Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode" "esbenp.prettier-vscode"
] ]
} }

3570
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,6 @@
"test:unit": "vitest", "test:unit": "vitest",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build --force", "type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
@ -28,18 +27,13 @@
}, },
"devDependencies": { "devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.0", "@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@rushstack/eslint-patch": "^1.10.3",
"@tsconfig/node20": "^20.1.4", "@tsconfig/node20": "^20.1.4",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0",
"jsdom": "^24.1.1", "jsdom": "^24.1.1",
"npm-run-all2": "^6.2.3", "npm-run-all2": "^6.2.3",
"phaser3-rex-plugins": "^1.80.8", "phaser3-rex-plugins": "^1.80.8",
@ -51,7 +45,6 @@
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "^5.4.9", "vite": "^5.4.9",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"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

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 847 B

View File

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 745 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 109 B

After

Width:  |  Height:  |  Size: 109 B

View File

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

View File

Before

Width:  |  Height:  |  Size: 708 B

After

Width:  |  Height:  |  Size: 708 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 454 KiB

View File

@ -1,7 +1,8 @@
<template> <template>
<Debug />
<Notifications /> <Notifications />
<BackgroundImageLoader /> <BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" @open-map-editor="mapEditor.toggleActive" />
<component :is="currentScreen" /> <component :is="currentScreen" />
</template> </template>
@ -9,34 +10,39 @@
import GmPanel from '@/components/gameMaster/GmPanel.vue' import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Characters from '@/components/screens/Characters.vue' import Characters from '@/components/screens/Characters.vue'
import Game from '@/components/screens/Game.vue' import Game from '@/components/screens/Game.vue'
import Loading from '@/components/screens/Loading.vue'
import Login from '@/components/screens/Login.vue' import Login from '@/components/screens/Login.vue'
import ZoneEditor from '@/components/screens/ZoneEditor.vue' import MapEditor from '@/components/screens/MapEditor.vue'
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue' import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import Debug from '@/components/utilities/Debug.vue'
import Notifications from '@/components/utilities/Notifications.vue' import Notifications from '@/components/utilities/Notifications.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, watch } from 'vue' import { computed, ref, useTemplateRef, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const mapEditor = useMapEditorComposable()
const currentScreen = computed(() => { const currentScreen = computed(() => {
if (!gameStore.game.isLoaded) 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 (mapEditor.active.value) return MapEditor
return Game return Game
}) })
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets // Watch mapEditor.active and empty gameStore.game.loadedAssets
watch( watch(
() => zoneEditorStore.active, () => mapEditor.active.value,
() => { () => {
gameStore.game.loadedAssets = [] gameStore.game.loadedTextures = []
} }
) )
// #209: Play sound when a button is pressed // #209: Play sound when a button is pressed
// @TODO: Not all button-like elements will actually be a button, so we need to find a better way to do this
addEventListener('click', (event) => { addEventListener('click', (event) => {
if (!(event.target instanceof HTMLButtonElement)) { if (!(event.target instanceof HTMLButtonElement)) {
return return
@ -50,7 +56,9 @@ addEventListener('keydown', (event) => {
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
// Check if no input is active or focus is on an input // Check if no input is active or focus is on an input
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
return
}
if (event.key === 'G') { if (event.key === 'G') {
gameStore.toggleGmPanel() gameStore.toggleGmPanel()

View File

@ -1,81 +0,0 @@
import config from '@/application/config'
import type { AssetDataT } from '@/application/types'
import Dexie from 'dexie'
export class Assets {
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,
originX: asset.originX,
originY: asset.originY,
isAnimated: asset.isAnimated,
frameRate: asset.frameRate,
frameWidth: asset.frameWidth,
frameHeight: asset.frameHeight,
frameCount: asset.frameCount
})
} 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

@ -3,7 +3,7 @@ export default {
development: import.meta.env.VITE_DEVELOPMENT === 'true', development: import.meta.env.VITE_DEVELOPMENT === 'true',
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT, server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
tile_size: { tile_size: {
x: Number(import.meta.env.VITE_TILE_SIZE_X), width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
y: Number(import.meta.env.VITE_TILE_SIZE_Y) height: Number(import.meta.env.VITE_TILE_SIZE_HEIGHT)
} }
} }

View File

@ -12,14 +12,13 @@ export type HttpResponse<T> = {
data?: T data?: T
} }
export type AssetDataT = { export type TextureData = {
key: string key: string
data: string data: string // URL or Base64 encoded blob
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other' group: 'tiles' | 'map_objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date updatedAt: Date
originX?: number originX?: number
originY?: number originY?: number
isAnimated?: boolean
frameRate?: number frameRate?: number
frameWidth?: number frameWidth?: number
frameHeight?: number frameHeight?: number
@ -34,19 +33,17 @@ export type Tile = {
updatedAt: Date updatedAt: Date
} }
export type Object = { export type MapObject = {
id: UUID id: UUID
name: string name: string
tags: any | null tags: any | null
originX: number originX: number
originY: number originY: number
isAnimated: boolean
frameRate: number frameRate: number
frameWidth: number frameWidth: number
frameHeight: number frameHeight: number
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
ZoneObject: ZoneObject[]
} }
export type Item = { export type Item = {
@ -56,7 +53,6 @@ export type Item = {
itemType: ItemType itemType: ItemType
stackable: boolean stackable: boolean
rarity: ItemRarity rarity: ItemRarity
spriteId: UUID | null
sprite?: Sprite sprite?: Sprite
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
@ -65,65 +61,59 @@ export type Item = {
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE' export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY' export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
export type Zone = { export type Map = {
id: UUID id: UUID
name: string name: string
width: number width: number
height: number height: number
tiles: any | null tiles: string[][]
pvp: boolean pvp: boolean
zoneEffects: ZoneEffect[] mapEffects: MapEffect[]
zoneEventTiles: ZoneEventTile[] mapEventTiles: MapEventTile[]
zoneObjects: ZoneObject[] placedMapObjects: PlacedMapObject[]
characters: Character[] characters: Character[]
chats: Chat[] chats: Chat[]
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
} }
export type ZoneEffect = { export type MapEffect = {
id: UUID id: UUID
zoneId: UUID map: Map
zone: Zone
effect: string effect: string
strength: number strength: number
} }
export type ZoneObject = { export type PlacedMapObject = {
id: UUID id: UUID
zoneId: UUID map: Map
zone: Zone mapObject: MapObject
objectId: UUID
object: Object
depth: number depth: number
isRotated: boolean isRotated: boolean
positionX: number positionX: number
positionY: number positionY: number
} }
export enum ZoneEventTileType { export enum MapEventTileType {
BLOCK = 'BLOCK', BLOCK = 'BLOCK',
TELEPORT = 'TELEPORT', TELEPORT = 'TELEPORT',
NPC = 'NPC', NPC = 'NPC',
ITEM = 'ITEM' ITEM = 'ITEM'
} }
export type ZoneEventTile = { export type MapEventTile = {
id: UUID id: UUID
zoneId: UUID mapId: UUID
zone: Zone type: MapEventTileType
type: ZoneEventTileType
positionX: number positionX: number
positionY: number positionY: number
teleport?: ZoneEventTileTeleport teleport?: MapEventTileTeleport
} }
export type ZoneEventTileTeleport = { export type MapEventTileTeleport = {
id: UUID id: UUID
zoneEventTileId: UUID mapEventTile: MapEventTile
zoneEventTile: ZoneEventTile toMap: Map
toZoneId: UUID
toZone: Zone
toPositionX: number toPositionX: number
toPositionY: number toPositionY: number
toRotation: number toRotation: number
@ -155,8 +145,6 @@ export type CharacterType = {
gender: CharacterGender gender: CharacterGender
race: CharacterRace race: CharacterRace
isSelectable: boolean isSelectable: boolean
characters: Character[]
spriteId?: string
sprite?: Sprite sprite?: Sprite
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
@ -165,7 +153,7 @@ export type CharacterType = {
export type CharacterHair = { export type CharacterHair = {
id: UUID id: UUID
name: string name: string
sprite: Sprite sprite?: Sprite
gender: CharacterGender gender: CharacterGender
isSelectable: boolean isSelectable: boolean
} }
@ -184,27 +172,22 @@ export type Character = {
positionX: number positionX: number
positionY: number positionY: number
rotation: number rotation: number
characterTypeId: UUID | null characterType: UUID | null
characterType: CharacterType | null | string characterHair: UUID | null
characterHairId: UUID | null map: UUID
characterHair: CharacterHair | null
zoneId: UUID
zone: Zone
chats: Chat[] chats: Chat[]
items: CharacterItem[] items: CharacterItem[]
equipment: CharacterEquipment[] equipment: CharacterEquipment[]
} }
export type ZoneCharacter = { export type MapCharacter = {
character: Character character: Character
isMoving?: boolean isMoving: boolean
} }
export type CharacterItem = { export type CharacterItem = {
id: UUID id: UUID
characterId: UUID
character: Character character: Character
itemId: UUID
item: Item item: Item
quantity: number quantity: number
} }
@ -212,7 +195,6 @@ export type CharacterItem = {
export type CharacterEquipment = { export type CharacterEquipment = {
id: UUID id: UUID
slot: CharacterEquipmentSlotType slot: CharacterEquipmentSlotType
characterItemId: UUID
characterItem: CharacterItem characterItem: CharacterItem
} }
@ -234,45 +216,45 @@ export type Sprite = {
characterTypes: CharacterType[] characterTypes: CharacterType[]
} }
export interface SpriteImage {
url: string
offset: {
x: number
y: number
}
}
export type SpriteAction = { export type SpriteAction = {
id: UUID id: string
sprite: Sprite sprite: string
action: string action: string
sprites: string[] sprites: SpriteImage[]
originX: number originX: number
originY: number originY: number
isAnimated: boolean
isLooping: boolean
frameWidth: number frameWidth: number
frameHeight: number frameHeight: number
frameRate: number frameRate: number
} }
export type Chat = { export type Chat = {
id: UUID id: string
characterId: UUID
character: Character character: Character
zoneId: UUID map: Map
zone: Zone
message: string message: string
createdAt: Date createdAt: Date
} }
export type WorldSettings = { export type WorldSettings = {
date: Date date: Date
isRainEnabled: boolean weatherState: WeatherState
isFogEnabled: boolean
fogDensity: number
} }
export type WeatherState = { export type WeatherState = {
isRainEnabled: boolean
rainPercentage: number rainPercentage: number
isFogEnabled: boolean
fogDensity: number fogDensity: number
} }
export type zoneLoadData = { export type mapLoadData = {
zone: Zone mapId: UUID
characters: ZoneCharacter[] characters: MapCharacter[]
} }

View File

@ -1,3 +1,7 @@
import config from '@/application/config'
import type { HttpResponse } from '@/application/types'
import type { BaseStorage } from '@/storage/baseStorage'
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))
} }
@ -23,3 +27,26 @@ export function getDomain() {
return window.location.hostname.split('.').slice(-2).join('.') return window.location.hostname.split('.').slice(-2).join('.')
} }
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
const response = (await request.json()) as HttpResponse<T[]>
if (!response.success) {
console.error(`Failed to download ${endpoint}:`, response.message)
return
}
const items = response.data ?? []
for (const item of items) {
let overwrite = false
const existingItem = await storage.get(item.id)
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
overwrite = true
}
await storage.add(item, overwrite)
}
}

View File

@ -1,179 +0,0 @@
<template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
</template>
<script setup lang="ts">
import type { WeatherState } from '@/application/types'
import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore'
import { Scene } from 'phavuer'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
// Constants
const LIGHT_CONFIG = {
SUNRISE_HOUR: 6,
SUNSET_HOUR: 20,
DAY_STRENGTH: 100,
NIGHT_STRENGTH: 30
}
// Stores and refs
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const sceneRef = ref<Phaser.Scene | null>(null)
const zoneEffectsReady = ref(false)
// Effect objects
const effects = {
light: ref<Phaser.GameObjects.Graphics | null>(null),
rain: ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null),
fog: ref<Phaser.GameObjects.Sprite | null>(null)
}
// Weather state
const weatherState = ref<WeatherState>({
isRainEnabled: false,
rainPercentage: 0,
isFogEnabled: false,
fogDensity: 0
})
// Scene setup
const preloadScene = (scene: Phaser.Scene) => {
scene.load.image('raindrop', 'assets/raindrop.png')
scene.load.image('fog', 'assets/fog.png')
}
const createScene = (scene: Phaser.Scene) => {
sceneRef.value = scene
initializeEffects(scene)
setupSocketListeners()
}
const initializeEffects = (scene: Phaser.Scene) => {
// Light
effects.light.value = scene.add.graphics().setDepth(1000)
// Rain
effects.rain.value = scene.add
.particles(0, 0, 'raindrop', {
x: { min: 0, max: window.innerWidth },
y: -50,
quantity: 5,
lifespan: 2000,
speedY: { min: 300, max: 500 },
scale: { start: 0.005, end: 0.005 },
alpha: { start: 0.5, end: 0 },
blendMode: 'ADD'
})
.setDepth(900)
effects.rain.value.stop()
// Fog
effects.fog.value = scene.add
.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
.setScale(2)
.setAlpha(0)
.setDepth(950)
}
// Effect updates
const updateScene = () => {
const timeBasedLight = calculateLightStrength(gameStore.world.date)
const zoneEffects = zoneStore.zone?.zoneEffects?.reduce(
(acc, curr) => ({
...acc,
[curr.effect]: curr.strength
}),
{}
) as { [key: string]: number }
// Only update effects once zoneEffects are loaded
if (!zoneEffectsReady.value) {
if (zoneEffects && Object.keys(zoneEffects).length) {
zoneEffectsReady.value = true
} else {
return
}
}
const finalEffects =
zoneEffects && Object.keys(zoneEffects).length
? zoneEffects
: {
light: timeBasedLight,
rain: weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0,
fog: weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0
}
applyEffects(finalEffects)
}
const applyEffects = (effectValues: any) => {
if (effects.light.value) {
const darkness = 1 - (effectValues.light ?? 100) / 100
effects.light.value.clear().fillStyle(0x000000, darkness).fillRect(0, 0, window.innerWidth, window.innerHeight)
}
if (effects.rain.value) {
const strength = effectValues.rain ?? 0
strength > 0 ? effects.rain.value.start().setQuantity(Math.floor((strength / 100) * 10)) : effects.rain.value.stop()
}
if (effects.fog.value) {
effects.fog.value.setAlpha((effectValues.fog ?? 0) / 100)
}
}
const calculateLightStrength = (time: Date): number => {
const hour = time.getHours()
const minute = time.getMinutes()
if (hour >= LIGHT_CONFIG.SUNSET_HOUR || hour < LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH
if (hour > LIGHT_CONFIG.SUNRISE_HOUR && hour < LIGHT_CONFIG.SUNSET_HOUR - 2) return LIGHT_CONFIG.DAY_STRENGTH
if (hour === LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH + ((LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * minute) / 60
const totalMinutes = (hour - (LIGHT_CONFIG.SUNSET_HOUR - 2)) * 60 + minute
return LIGHT_CONFIG.DAY_STRENGTH - (LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * (totalMinutes / 120)
}
// Socket and window handlers
const setupSocketListeners = () => {
gameStore.connection?.emit('weather', (response: WeatherState) => {
weatherState.value = response
updateScene()
})
gameStore.connection!.on('weather', (data: WeatherState) => {
weatherState.value = data
updateScene()
})
gameStore.connection!.on('date', updateScene)
}
const handleResize = () => {
if (effects.rain.value) effects.rain.value.updateConfig({ x: { min: 0, max: window.innerWidth } })
if (effects.fog.value) effects.fog.value.setPosition(window.innerWidth / 2, window.innerHeight / 2)
}
// Lifecycle
watch(
() => zoneStore.zone,
() => {
zoneEffectsReady.value = false
updateScene()
},
{ deep: true }
)
onMounted(() => window.addEventListener('resize', handleResize))
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (sceneRef.value) sceneRef.value.scene.remove('effects')
gameStore.connection?.off('weather')
})
</script>

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="flex flex-wrap items-center input-field gap-1"> <div class="flex flex-wrap items-center input-field gap-1" @click="focusInput">
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2"> <div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2" role="listitem">
<span class="text-xs text-white">{{ chip }}</span> <span class="text-xs text-white">{{ chip }}</span>
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button> <button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click.stop="deleteChip(i)" aria-label="Remove tag">×</button>
</div> </div>
<input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" /> <input ref="inputRef" class="outline-none border-none p-1 text-gray-300 min-w-[60px] flex-grow" :placeholder="placeholder" v-model.trim="currentInput" @keydown="handleKeydown" @paste="handlePaste" :maxlength="maxChipLength" aria-label="Add new tag" />
</div> </div>
</template> </template>
@ -14,20 +14,29 @@ import type { Ref } from 'vue'
interface Props { interface Props {
modelValue?: string[] modelValue?: string[]
maxChips?: number
maxChipLength?: number
placeholder?: string
allowDuplicates?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: () => [] modelValue: () => [],
maxChips: 10,
maxChipLength: 20,
placeholder: 'Add tag',
allowDuplicates: false
}) })
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void (e: 'update:modelValue', value: string[]): void
(e: 'error', message: string): void
}>() }>()
const currentInput: Ref<string> = ref('') const currentInput: Ref<string> = ref('')
const internalValue = ref<string[]>([]) const internalValue = ref<string[]>([])
const inputRef = ref<HTMLInputElement | null>(null)
// Initialize internalValue with props.modelValue
watch( watch(
() => props.modelValue, () => props.modelValue,
(newValue) => { (newValue) => {
@ -36,9 +45,27 @@ watch(
{ immediate: true } { immediate: true }
) )
const validateChip = (chip: string): boolean => {
if (!chip) {
return false
}
if (!props.allowDuplicates && internalValue.value.includes(chip)) {
emit('error', 'Duplicate tags are not allowed')
return false
}
if (internalValue.value.length >= props.maxChips) {
emit('error', `Maximum ${props.maxChips} tags allowed`)
return false
}
return true
}
const addChip = () => { const addChip = () => {
const trimmedInput = currentInput.value.trim() const trimmedInput = currentInput.value.trim()
if (trimmedInput && !internalValue.value.includes(trimmedInput)) { if (validateChip(trimmedInput)) {
internalValue.value.push(trimmedInput) internalValue.value.push(trimmedInput)
emit('update:modelValue', internalValue.value) emit('update:modelValue', internalValue.value)
currentInput.value = '' currentInput.value = ''
@ -50,10 +77,36 @@ const deleteChip = (index: number) => {
emit('update:modelValue', internalValue.value) emit('update:modelValue', internalValue.value)
} }
const handleBackspace = (event: KeyboardEvent) => { const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Backspace' && currentInput.value === '' && internalValue.value.length > 0) { switch (event.key) {
internalValue.value.pop() case 'Enter':
emit('update:modelValue', internalValue.value) event.preventDefault()
addChip()
break
case 'Backspace':
if (currentInput.value === '' && internalValue.value.length > 0) {
deleteChip(internalValue.value.length - 1)
}
break
} }
} }
const handlePaste = (event: ClipboardEvent) => {
event.preventDefault()
const pastedText = event.clipboardData?.getData('text')
if (pastedText) {
const chips = pastedText
.split(/[,\n]/)
.map((chip) => chip.trim())
.filter(Boolean)
chips.forEach((chip) => {
currentInput.value = chip
addChip()
})
}
}
const focusInput = () => {
inputRef.value?.focus()
}
</script> </script>

View File

@ -1,28 +1,24 @@
<template> <template>
<ChatBubble :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" /> <ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<Healthbar :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" /> <Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY"> <CharacterHair :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<CharacterHair :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" /> <Sprite ref="charSprite" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY" :origin-y="1" :flipX="isFlippedX" />
<!-- <CharacterChest :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />-->
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
</Container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import config from '@/application/config' import config from '@/application/config'
import { type Sprite as SpriteT, type ZoneCharacter } from '@/application/types' import { type MapCharacter } from '@/application/types'
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue' import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue' import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
import Healthbar from '@/components/game/character/partials/Healthbar.vue' import Healthbar from '@/components/game/character/partials/Healthbar.vue'
import { loadSpriteTextures } from '@/composables/gameComposable' import { loadSpriteTextures } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable' import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { CharacterTypeStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore' import { useMapStore } from '@/stores/mapStore'
import { Container, refObj, Sprite, useScene } from 'phavuer' import { refObj, Sprite, useScene } from 'phavuer'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
// import CharacterChest from '@/components/game/character/partials/CharacterChest.vue'
enum Direction { enum Direction {
POSITIVE, POSITIVE,
NEGATIVE, NEGATIVE,
@ -31,162 +27,161 @@ enum Direction {
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tilemap: Phaser.Tilemaps.Tilemap
zoneCharacter: ZoneCharacter mapCharacter: MapCharacter
}>() }>()
const charContainer = refObj<Phaser.GameObjects.Container>()
const charSprite = refObj<Phaser.GameObjects.Sprite>() const charSprite = refObj<Phaser.GameObjects.Sprite>()
const charSpriteId = ref('')
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneStore = useZoneStore() const mapStore = useMapStore()
const scene = useScene() const scene = useScene()
const currentX = ref(0) const currentPositionX = ref(0)
const currentY = ref(0) const currentPositionY = ref(0)
const isometricDepth = ref(1) const isometricDepth = ref(1)
const isInitialPosition = ref(true) const isInitialPosition = ref(true)
const isMoving = ref(false) const tween = ref<Phaser.Tweens.Tween | null>(null)
let animationFrame: number | null = null
const moveSpeed = 5.7
const updateIsometricDepth = (x: number, y: number) => { const updateIsometricDepth = (positionX: number, positionY: number) => {
isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true) isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true)
} }
const stopMovement = () => { const updatePosition = (positionX: number, positionY: number, direction: Direction) => {
isMoving.value = false const newPositionX = tileToWorldX(props.tilemap, positionX, positionY)
if (animationFrame) { const newPositionY = tileToWorldY(props.tilemap, positionX, positionY)
cancelAnimationFrame(animationFrame)
animationFrame = null
}
}
const updatePosition = (x: number, y: number, direction: Direction) => {
const targetX = tileToWorldX(props.tilemap, x, y)
const targetY = tileToWorldY(props.tilemap, x, y)
if (isInitialPosition.value) { if (isInitialPosition.value) {
currentX.value = targetX currentPositionX.value = newPositionX
currentY.value = targetY currentPositionY.value = newPositionY
isInitialPosition.value = false isInitialPosition.value = false
return return
} }
if (isMoving.value) { if (tween.value?.isPlaying()) {
stopMovement() tween.value.stop()
} }
const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2)) const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2))
isMoving.value = true if (distance >= config.tile_size.width / 1.1) {
const startTime = performance.now() currentPositionX.value = newPositionX
const startX = currentX.value currentPositionY.value = newPositionY
const startY = currentY.value
const duration = distance * moveSpeed
if (direction === Direction.POSITIVE) {
updateIsometricDepth(x, y)
}
const animate = (currentTime: number) => {
if (!isMoving.value) return
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
currentX.value = startX + (targetX - startX) * progress
currentY.value = startY + (targetY - startY) * progress
if (progress < 1) {
animationFrame = requestAnimationFrame(animate)
} else {
isMoving.value = false
if (direction === Direction.NEGATIVE) {
updateIsometricDepth(x, y)
}
}
}
animationFrame = requestAnimationFrame(animate)
}
const calcDirection = (oldX: number, oldY: number, newX: number, newY: number): Direction => {
if (newY < oldY || newX < oldX) return Direction.NEGATIVE
if (newX > oldX || newY > oldY) return Direction.POSITIVE
return Direction.UNCHANGED
}
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
const charTexture = computed(() => {
const { rotation, characterType } = props.zoneCharacter.character
const spriteId = characterType?.sprite ?? 'idle_right_down'
const action = props.zoneCharacter.isMoving ? 'walk' : 'idle'
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
return `${spriteId}-${action}_${direction}`
})
const updateSprite = () => {
if (props.zoneCharacter.isMoving) {
charSprite.value!.anims.play(charTexture.value, true)
return return
} }
charSprite.value!.anims.stop() const duration = distance * 5.7
charSprite.value!.setFrame(0)
charSprite.value!.setTexture(charTexture.value) tween.value = props.tilemap.scene.tweens.add({
targets: { x: currentPositionX.value, y: currentPositionY.value },
x: newPositionX,
y: newPositionY,
duration,
ease: 'Linear',
onStart: () => {
if (direction === Direction.POSITIVE) {
updateIsometricDepth(positionX, positionY)
}
},
onUpdate: (tween) => {
// @ts-ignore
currentPositionX.value = tween.targets[0].x
// @ts-ignore
currentPositionY.value = tween.targets[0].y
},
onComplete: () => {
if (direction === Direction.NEGATIVE) {
updateIsometricDepth(positionX, positionY)
}
}
})
}
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
return Direction.UNCHANGED
}
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const currentDirection = computed(() => {
return [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
})
const currentAction = computed(() => {
return props.mapCharacter.isMoving ? 'walk' : 'idle'
})
const charTexture = computed(() => {
const spriteId = charSpriteId.value ?? 'idle_right_down'
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
})
const updateSprite = () => {
if (!charSprite.value) return
if (props.mapCharacter.isMoving) {
charSprite.value.anims.play(charTexture.value, true)
} else {
charSprite.value.anims.stop()
charSprite.value.setFrame(0)
charSprite.value.setTexture(charTexture.value)
}
}
const handlePositionUpdate = (newValues: any, oldValues: any) => {
if (!newValues) return
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.positionX, oldValues.positionY, newValues.positionX, newValues.positionY)
updatePosition(newValues.positionX, newValues.positionY, direction)
}
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite()
}
} }
watch( watch(
() => ({ () => ({
x: props.zoneCharacter.character.positionX, positionX: props.mapCharacter.character.positionX,
y: props.zoneCharacter.character.positionY, positionY: props.mapCharacter.character.positionY,
isMoving: props.zoneCharacter.isMoving, isMoving: props.mapCharacter.isMoving,
rotation: props.zoneCharacter.character.rotation rotation: props.mapCharacter.character.rotation
}), }),
(newValues, oldValues) => { handlePositionUpdate
if (!newValues) return
if (!oldValues || newValues.x !== oldValues.x || newValues.y !== oldValues.y) {
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.x, oldValues.y, newValues.x, newValues.y)
updatePosition(newValues.x, newValues.y, direction)
}
// Handle animation updates
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite()
}
},
{ deep: true }
) )
watch(() => props.zoneCharacter, updateSprite) onMounted(async () => {
let character = props.mapCharacter.character
loadSpriteTextures(scene, props.zoneCharacter.character.characterType?.sprite as string) const characterTypeStorage = new CharacterTypeStorage()
.then(() => {
charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value)
})
.catch((error) => {
console.error('Error loading texture:', error)
})
onMounted(() => { const spriteId = await characterTypeStorage.getSpriteId(character.characterType!)
charContainer.value!.setName(props.zoneCharacter.character!.name) if (!spriteId) return
if (props.zoneCharacter.character.id === gameStore.character!.id) { charSpriteId.value = spriteId
zoneStore.setCharacterLoaded(true)
// #146 : Set camera position to character, need to be improved still await loadSpriteTextures(scene, spriteId)
// scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
// scene.cameras.main.stopFollow() if (charSprite.value) {
charSprite.value.setTexture(charTexture.value)
charSprite.value.setFlipX(isFlippedX.value)
charSprite.value.setName(props.mapCharacter.character.name)
} }
updatePosition(props.zoneCharacter.character.positionX, props.zoneCharacter.character.positionY, props.zoneCharacter.character.rotation) if (character.id === gameStore.character!.id) {
mapStore.setCharacterLoaded(true)
// #146 : Set camera position to character, need to be improved still
scene.cameras.main.startFollow(charSprite.value as Phaser.GameObjects.Sprite)
}
updatePosition(character.positionX, character.positionY, character.rotation)
}) })
onUnmounted(() => { onUnmounted(() => {
stopMovement() tween.value?.stop()
}) })
</script> </script>

View File

@ -3,14 +3,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Sprite as SpriteT, ZoneCharacter } from '@/application/types' import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable' import { loadSpriteTextures } from '@/composables/gameComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps<{ const props = defineProps<{
zoneCharacter: ZoneCharacter mapCharacter: MapCharacter
currentX: number currentX: number
currentY: number currentY: number
}>() }>()
@ -19,19 +19,19 @@ const gameStore = useGameStore()
const scene = useScene() const scene = useScene()
const texture = computed(() => { const texture = computed(() => {
const { rotation, characterHair } = props.zoneCharacter.character const { rotation, characterHair } = props.mapCharacter.character
const spriteId = characterHair?.sprite?.id const spriteId = characterHair?.sprite?.id
const direction = [0, 6].includes(rotation) ? 'back' : 'front' const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${spriteId}-${direction}` return `${spriteId}-${direction}`
}) })
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0)) const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const imageProps = computed(() => { const imageProps = computed(() => {
// Get the current sprite action based on direction // Get the current sprite action based on direction
const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front' const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction) const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
return { return {
depth: 1, depth: 1,
@ -39,11 +39,11 @@ const imageProps = computed(() => {
originY: Number(spriteAction?.originY) ?? 0, originY: Number(spriteAction?.originY) ?? 0,
flipX: isFlippedX.value, flipX: isFlippedX.value,
texture: texture.value texture: texture.value
// y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0 // y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
} }
}) })
loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT) loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT)
.then(() => {}) .then(() => {})
.catch((error) => { .catch((error) => {
console.error('Error loading texture:', error) console.error('Error loading texture:', error)

View File

@ -1,51 +1,58 @@
<template> <template>
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" /> <Image v-bind="imageProps" v-if="gameStore.isTextureLoaded(texture)" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Sprite as SpriteT, ZoneCharacter } from '@/application/types' import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable' import { loadSpriteTextures } from '@/composables/gameComposable'
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { computed } from 'vue' import { computed, onMounted, ref } from 'vue'
const props = defineProps<{ const props = defineProps<{
zoneCharacter: ZoneCharacter mapCharacter: MapCharacter
currentX: number currentX: number
currentY: number currentY: number
}>() }>()
const gameStore = useGameStore() const gameStore = useGameStore()
const scene = useScene() const scene = useScene()
const hairSpriteId = ref('')
const sprite = ref<SpriteT | null>(null)
const texture = computed(() => { const texture = computed(() => {
const { rotation, characterHair } = props.zoneCharacter.character const { rotation } = props.mapCharacter.character
const spriteId = characterHair?.sprite?.id
const direction = [0, 6].includes(rotation) ? 'back' : 'front' const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${spriteId}-${direction}` return `${hairSpriteId.value}-${direction}`
}) })
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0)) const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const imageProps = computed(() => { const imageProps = computed(() => {
// Get the current sprite action based on direction const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front' const spriteAction = sprite.value?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
return { return {
depth: 1, depth: 9999,
originX: Number(spriteAction?.originX) ?? 0, originX: Number(spriteAction?.originX) ?? 0,
originY: Number(spriteAction?.originY) ?? 0, originY: Number(spriteAction?.originY) ?? 0,
flipX: isFlippedX.value, flipX: isFlippedX.value,
texture: texture.value, texture: texture.value,
y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0 y: props.currentY,
x: props.currentX
} }
}) })
loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT) onMounted(async () => {
.then(() => {}) const characterHairStorage = new CharacterHairStorage()
.catch((error) => { const spriteId = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair!)
console.error('Error loading texture:', error) if (!spriteId) return
})
hairSpriteId.value = spriteId
const spriteStorage = new SpriteStorage()
sprite.value = await spriteStorage.get(spriteId)
await loadSpriteTextures(scene, spriteId)
})
</script> </script>

View File

@ -6,12 +6,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ZoneCharacter } from '@/application/types' import type { MapCharacter } from '@/application/types'
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer' import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
import { onMounted } from 'vue' import { onMounted } from 'vue'
const props = defineProps<{ const props = defineProps<{
zoneCharacter: ZoneCharacter mapCharacter: MapCharacter
currentX: number currentX: number
currentY: number currentY: number
}>() }>()
@ -20,14 +20,15 @@ const game = useGame()
const charChatContainer = refObj<Phaser.GameObjects.Container>() const charChatContainer = refObj<Phaser.GameObjects.Container>()
const createChatBubble = (container: Phaser.GameObjects.Container) => { const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.zoneCharacter.character.name}_chatBubble`) container.setName(`${props.mapCharacter.character.name}_chatBubble`)
} }
const createChatText = (text: Phaser.GameObjects.Text) => { const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(`${props.zoneCharacter.character.name}_chatText`) text.setName(`${props.mapCharacter.character.name}_chatText`)
text.setFontSize(13) text.setFontSize(13)
text.setFontFamily('Arial') text.setFontFamily('Arial')
text.setOrigin(0.5, 10.9) text.setOrigin(0.5, 10.9)
text.setResolution(2)
// Fix text alignment on Windows and Android // Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) { if (game.device.os.windows || game.device.os.android) {
@ -40,7 +41,7 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
} }
onMounted(() => { onMounted(() => {
charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`) charChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false) charChatContainer.value!.setVisible(false)
}) })
</script> </script>

View File

@ -1,17 +1,17 @@
<template> <template>
<Container :depth="999" :x="currentX" :y="currentY"> <Container :depth="999" :x="currentX" :y="currentY">
<Text @create="createNicknameText" :text="props.zoneCharacter.character.name" /> <Text @create="createNicknameText" :text="props.mapCharacter.character.name" />
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
</Container> </Container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ZoneCharacter } from '@/application/types' import type { MapCharacter } from '@/application/types'
import { Container, RoundRectangle, Text, useGame } from 'phavuer' import { Container, RoundRectangle, Text, useGame } from 'phavuer'
const props = defineProps<{ const props = defineProps<{
zoneCharacter: ZoneCharacter mapCharacter: MapCharacter
currentX: number currentX: number
currentY: number currentY: number
}>() }>()
@ -22,6 +22,7 @@ const createNicknameText = (text: Phaser.GameObjects.Text) => {
text.setFontSize(13) text.setFontSize(13)
text.setFontFamily('Arial') text.setFontFamily('Arial')
text.setOrigin(0.5, 9) text.setOrigin(0.5, 9)
text.setResolution(2)
// Fix text alignment on Windows and Android // Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) { if (game.device.os.windows || game.device.os.android) {

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle"> <div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
<div class="relative"> <div class="relative">
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" /> <img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" /> <img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="" />
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative"> <div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span> <span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" /> <img draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" alt="Close button icon" />
</button> </button>
</div> </div>
<div class="py-4 px-6 flex flex-col gap-7 relative z-10"> <div class="py-4 px-6 flex flex-col gap-7 relative z-10">
@ -17,7 +17,7 @@
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span> <span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
</div> </div>
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]"> <a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" /> <img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" alt="Plus button icon" />
</a> </a>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
@ -37,20 +37,20 @@
</div> </div>
</div> </div>
</div> </div>
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" /> <img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" alt="Player character sprite" />
<div class="flex flex-col items-end gap-0.5"> <div class="flex flex-col items-end gap-0.5">
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" alt="Helmet icon" />
</div> </div>
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" alt="Chestplate icon" />
</div> </div>
<div class="flex gap-0.5 items-end"> <div class="flex gap-0.5 items-end">
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" /> <img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" alt="Boots icon" />
</div> </div>
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" alt="Legs icon" />
</div> </div>
</div> </div>
</div> </div>
@ -119,111 +119,44 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
let startX = 0 const width = ref(286)
let startY = 0 const height = ref(483)
let initialX = 0 const x = ref(window.innerWidth / 2 - 143)
let initialY = 0 const y = ref(window.innerHeight / 2 - 241)
let modalPositionX = 0
let modalPositionY = 0
let modalWidth = 286
let modalHeight = 483
const width = ref(modalWidth)
const height = ref(modalHeight)
const x = ref(0)
const y = ref(0)
const isDragging = ref(false) const isDragging = ref(false)
const modalStyle = computed(() => ({ const modalStyle = computed(() => ({
top: `${y.value}px`, top: `${y.value}px`,
left: `${x.value}px`, left: `${x.value}px`,
width: `${width.value}px`, width: `${width.value}px`,
height: `${height.value}px`, height: `${height.value}px`
maxWidth: '100vw',
maxHeight: '100vh'
})) }))
function startDrag(event: MouseEvent) { function startDrag(event: MouseEvent) {
isDragging.value = true isDragging.value = true
startX = event.clientX const startX = event.clientX - x.value
startY = event.clientY const startY = event.clientY - y.value
initialX = x.value
initialY = y.value function drag(event: MouseEvent) {
event.preventDefault() if (!isDragging.value) return
x.value = event.clientX - startX
y.value = event.clientY - startY
}
function stopDrag() {
isDragging.value = false
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
}
addEventListener('mousemove', drag)
addEventListener('mouseup', stopDrag)
} }
function drag(event: MouseEvent) {
if (!isDragging.value) return
const dx = event.clientX - startX
const dy = event.clientY - startY
x.value = initialX + dx
y.value = initialY + dy
adjustPosition()
}
function stopDrag() {
isDragging.value = false
}
function adjustPosition() {
x.value = Math.min(x.value, window.innerWidth - width.value)
y.value = Math.min(y.value, window.innerHeight - height.value)
}
function initializePosition() {
width.value = Math.min(modalWidth, window.innerWidth)
height.value = Math.min(modalHeight, window.innerHeight)
if (modalPositionX !== 0 && modalPositionY !== 0) {
x.value = modalPositionX
y.value = modalPositionY
} else {
x.value = (window.innerWidth - width.value) / 2
y.value = (window.innerHeight - height.value) / 2
}
}
watch(
() => gameStore.uiSettings.isCharacterProfileOpen,
(value) => {
gameStore.uiSettings.isCharacterProfileOpen = value
if (value) {
initializePosition()
}
}
)
watch(
() => modalWidth,
(value) => {
width.value = Math.min(value, window.innerWidth)
}
)
watch(
() => modalHeight,
(value) => {
height.value = Math.min(value, window.innerHeight)
}
)
watch(
() => modalPositionX,
(value) => {
x.value = value
}
)
watch(
() => modalPositionY,
(value) => {
y.value = value
}
)
function keyPress(event: KeyboardEvent) { function keyPress(event: KeyboardEvent) {
if (event.altKey && event.key === 'c') { if (event.altKey && event.key === 'c') {
gameStore.toggleCharacterProfile() gameStore.toggleCharacterProfile()
@ -232,14 +165,9 @@ function keyPress(event: KeyboardEvent) {
onMounted(() => { onMounted(() => {
addEventListener('keydown', keyPress) addEventListener('keydown', keyPress)
addEventListener('mousemove', drag)
addEventListener('mouseup', stopDrag)
initializePosition()
}) })
onUnmounted(() => { onUnmounted(() => {
removeEventListener('keydown', keyPress) removeEventListener('keydown', keyPress)
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
}) })
</script> </script>

View File

@ -7,7 +7,7 @@
</div> </div>
</div> </div>
<div class="w-96 mx-auto relative"> <div class="w-96 mx-auto relative">
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" /> <img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" alt="" />
<input <input
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800" class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
placeholder="Type something..." placeholder="Type something..."
@ -23,14 +23,14 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Chat } from '@/application/types' import type { Chat } from '@/application/types'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore' import { useMapStore } from '@/stores/mapStore'
import { onClickOutside, useFocus } from '@vueuse/core' import { onClickOutside, useFocus } from '@vueuse/core'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue' import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
const scene = useScene() const scene = useScene()
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneStore = useZoneStore() const mapStore = useMapStore()
const message = ref('') const message = ref('')
const chats = ref([] as Chat[]) const chats = ref([] as Chat[])
@ -79,11 +79,11 @@ const scrollToBottom = () => {
}) })
} }
gameStore.connection!.on('chat:message', (data: Chat) => { gameStore.connection?.on('chat:message', (data: Chat) => {
chats.value.push(data) chats.value.push(data)
scrollToBottom() scrollToBottom()
if (!zoneStore.characterLoaded) return if (!mapStore.characterLoaded) return
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
if (!charChatContainer) return if (!charChatContainer) return

View File

@ -11,7 +11,7 @@ import { onUnmounted } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
// Listen for new date from socket // Listen for new date from socket
gameStore.connection!.on('date', (data: Date) => { gameStore.connection?.on('date', (data: Date) => {
gameStore.world.date = new Date(data) gameStore.world.date = new Date(data)
}) })

View File

@ -6,7 +6,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative"> <a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" alt="Menu button icon" />
</a> </a>
</li> </li>
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile"> <li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
@ -15,7 +15,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative"> <a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" /> <img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" alt="User profile button icon" />
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p> <p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
</a> </a>
</li> </li>
@ -25,7 +25,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]"> <a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" alt="Open chat button icon" />
</a> </a>
</li> </li>
<li class="menu-item group relative"> <li class="menu-item group relative">
@ -34,7 +34,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]"> <a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" alt="World map button icon" />
</a> </a>
</li> </li>
<li class="menu-item group relative"> <li class="menu-item group relative">
@ -43,7 +43,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]"> <a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" alt="Users button icon" />
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -5,12 +5,12 @@
</div> </div>
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1"> <div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
<button class="w-6 h-6 relative p-0"> <button class="w-6 h-6 relative p-0">
<img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" /> <img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" alt="Zoom-in button icon"/>
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" /> <img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt=""/>
</button> </button>
<button class="w-6 h-6 relative p-0"> <button class="w-6 h-6 relative p-0">
<img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" /> <img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" alt="Zoom-out button icon"/>
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" /> <img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt=""/>
</button> </button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,14 @@
<template>
<Character v-for="item in mapStore.characters" :key="item.character.id" :tilemap="tilemap" :mapCharacter="item" />
</template>
<script setup lang="ts">
import Character from '@/components/game/character/Character.vue'
import { useMapStore } from '@/stores/mapStore'
const mapStore = useMapStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
</script>

View File

@ -0,0 +1,46 @@
<template>
<MapTiles :key="mapStore.mapId" @tileMap:create="tileMap = $event" />
<PlacedMapObjects v-if="tileMap" :key="mapStore.mapId" :tilemap="tileMap" />
<Characters v-if="tileMap && mapStore.characters" :tilemap="tileMap" />
</template>
<script setup lang="ts">
import type { MapCharacter, mapLoadData, UUID } from '@/application/types'
import Characters from '@/components/game/map/Characters.vue'
import MapTiles from '@/components/game/map/MapTiles.vue'
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { onUnmounted, shallowRef } from 'vue'
const gameStore = useGameStore()
const mapStore = useMapStore()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
// Event listeners
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => {
mapStore.setMapId(data.mapId)
mapStore.setCharacters(data.characters)
})
gameStore.connection?.on('map:character:join', async (data: MapCharacter) => {
mapStore.addCharacter(data)
})
gameStore.connection?.on('map:character:leave', (characterId: UUID) => {
mapStore.removeCharacter(characterId)
})
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
mapStore.updateCharacterPosition(data)
})
onUnmounted(() => {
mapStore.reset()
gameStore.connection?.off('map:character:teleport')
gameStore.connection?.off('map:character:join')
gameStore.connection?.off('map:character:leave')
gameStore.connection?.off('map:character:move')
})
</script>

View File

@ -0,0 +1,74 @@
<template>
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { Map as MapT, UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities'
import Controls from '@/components/utilities/Controls.vue'
import { loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable'
import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore'
import { useScene } from 'phavuer'
import { onBeforeUnmount, shallowRef } from 'vue'
import Tileset = Phaser.Tilemaps.Tileset
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
function createTileMap(map: MapT) {
const mapConfig = new Phaser.Tilemaps.MapData({
width: map.width,
height: map.height,
tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapConfig)
emit('tileMap:create', newTileMap)
return newTileMap
}
function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) {
const tilesArray = unduplicateArray(mapData?.tiles.flat())
const tilesetImages = tilesArray.map((tile: string, index: number) => {
return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
})
// Add blank tile
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
.then(() => mapStorage.get(mapStore.mapId))
.then((mapData) => {
if (!mapData || !mapData?.tiles) return
tileMap.value = createTileMap(mapData)
tileLayer.value = createTileLayer(tileMap.value, mapData)
setLayerTiles(tileMap.value, tileLayer.value, mapData.tiles)
})
.catch((error) => console.error('Failed to initialize map:', error))
onBeforeUnmount(() => {
if (!tileMap.value) return
tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
})
</script>

View File

@ -0,0 +1,28 @@
<template>
<PlacedMapObject v-for="placedMapObject in items" :tilemap="tilemap" :placedMapObject />
</template>
<script setup lang="ts">
import type { PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore'
import { onMounted, ref } from 'vue'
defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
const items = ref<PlacedMapObjectT[]>([])
onMounted(async () => {
if (!mapStore.mapId) return
const map = await mapStorage.get(mapStore.mapId)
if (!map) return
items.value = map.placedMapObjects
})
</script>

View File

@ -0,0 +1,43 @@
<template>
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { PlacedMapObject, TextureData } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed, onMounted } from 'vue'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
placedMapObject: PlacedMapObject
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
flipX: props.placedMapObject.isRotated,
texture: props.placedMapObject.mapObject.id,
originY: Number(props.placedMapObject.mapObject.originX),
originX: Number(props.placedMapObject.mapObject.originY)
}))
loadTexture(scene, {
key: props.placedMapObject.mapObject.id,
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
group: 'map_objects',
updatedAt: props.placedMapObject.mapObject.updatedAt,
frameWidth: props.placedMapObject.mapObject.frameWidth,
frameHeight: props.placedMapObject.mapObject.frameHeight
} as TextureData).catch((error) => {
console.error('Error loading texture:', error)
})
onMounted(async () => {})
</script>

View File

@ -1,14 +0,0 @@
<template>
<Character v-for="item in zoneStore.characters" :key="item.character.id" :tilemap="tilemap" :zoneCharacter="item" />
</template>
<script setup lang="ts">
import Character from '@/components/game/character/Character.vue'
import { useZoneStore } from '@/stores/zoneStore'
const zoneStore = useZoneStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
</script>

View File

@ -1,50 +0,0 @@
<template>
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
</template>
<script setup lang="ts">
import type { ZoneCharacter, zoneLoadData } from '@/application/types'
import Characters from '@/components/game/zone/Characters.vue'
import ZoneObjects from '@/components/game/zone/ZoneObjects.vue'
import ZoneTiles from '@/components/game/zone/ZoneTiles.vue'
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore'
import { useScene } from 'phavuer'
import { onUnmounted, ref } from 'vue'
const scene = useScene()
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
onUnmounted(() => {
zoneStore.reset()
gameStore.connection!.off('zone:character:teleport')
gameStore.connection!.off('zone:character:join')
gameStore.connection!.off('zone:character:leave')
gameStore.connection!.off('zone:character:move')
})
// Event listeners
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
await loadZoneTilesIntoScene(data.zone.id, scene)
zoneStore.setZone(data.zone)
zoneStore.setCharacters(data.characters)
})
gameStore.connection!.on('zone:character:join', async (data: ZoneCharacter) => {
zoneStore.addCharacter(data)
})
gameStore.connection!.on('zone:character:leave', (characterId: number) => {
zoneStore.removeCharacter(characterId)
})
gameStore.connection!.on('zone:character:move', (data: { characterId: number; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
zoneStore.updateCharacterPosition(data)
})
</script>

View File

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

View File

@ -1,69 +0,0 @@
<template>
<Controls :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import { unduplicateArray } from '@/application/utilities'
import Controls from '@/components/utilities/Controls.vue'
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
import { useZoneStore } from '@/stores/zoneStore'
import { useScene } from 'phavuer'
import { onBeforeUnmount } from 'vue'
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const zoneStore = useZoneStore()
const tileMap = createTileMap()
const tileLayer = 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() {
const zoneData = new Phaser.Tilemaps.MapData({
width: zoneStore.zone?.width,
height: zoneStore.zone?.height,
tileWidth: config.tile_size.x,
tileHeight: config.tile_size.y,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
emit('tileMap:create', newTileMap)
return newTileMap
}
/**
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
*/
function createTileLayer() {
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
return tileMap.addTilesetImage(tile, tile, 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(tileMap.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 = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
onBeforeUnmount(() => {
tileMap.destroyLayer('tiles')
tileMap.removeAllLayers()
tileMap.destroy()
})
</script>

View File

@ -1,41 +0,0 @@
<template>
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { AssetDataT, ZoneObject } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed } from 'vue'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
zoneObject: ZoneObject
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
flipX: props.zoneObject.isRotated,
texture: props.zoneObject.object.id,
originY: Number(props.zoneObject.object.originX),
originX: Number(props.zoneObject.object.originY)
}))
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

@ -6,7 +6,7 @@
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button> <button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Map editor</button> <button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="$emit('open-map-editor')">Map editor</button>
</div> </div>
</template> </template>
<template #modalBody> <template #modalBody>
@ -20,12 +20,13 @@
<script setup lang="ts"> <script setup lang="ts">
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue' import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { ref } from 'vue' import { ref } from 'vue'
defineEmits(['open-map-editor'])
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const mapEditor = useMapEditorComposable()
let toggle = ref('asset-manager') let toggle = ref('asset-manager')
</script> </script>

View File

@ -5,8 +5,8 @@
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'map_objects' }" @click="() => (selectedCategory = 'map_objects')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'map_objects' }">Map objects</span>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
@ -40,7 +40,7 @@
<!-- Assets list --> <!-- Assets list -->
<div class="overflow-auto h-full w-4/12 flex flex-col relative"> <div class="overflow-auto h-full w-4/12 flex flex-col relative">
<TileList v-if="selectedCategory === 'tiles'" /> <TileList v-if="selectedCategory === 'tiles'" />
<ObjectList v-if="selectedCategory === 'objects'" /> <MapObjectList v-if="selectedCategory === 'map_objects'" />
<SpriteList v-if="selectedCategory === 'sprites'" /> <SpriteList v-if="selectedCategory === 'sprites'" />
<ItemList v-if="selectedCategory === 'items'" /> <ItemList v-if="selectedCategory === 'items'" />
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" /> <CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
@ -50,7 +50,7 @@
<!-- Asset details --> <!-- Asset details -->
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto"> <div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" /> <TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" /> <MapObjectDetails v-if="selectedCategory === 'map_objects' && assetManagerStore.selectedMapObject" />
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" /> <SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" /> <ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" /> <CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
@ -66,8 +66,8 @@ import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue' import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue' import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue' import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
import ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue' import MapObjectDetails from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectDetails.vue'
import ObjectList from '@/components/gameMaster/assetManager/partials/object/ObjectList.vue' import MapObjectList from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectList.vue'
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue' import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue' import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue' import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'

View File

@ -59,7 +59,7 @@ if (selectedCharacterHair.value) {
characterName.value = selectedCharacterHair.value.name characterName.value = selectedCharacterHair.value.name
characterGender.value = selectedCharacterHair.value.gender characterGender.value = selectedCharacterHair.value.gender
characterIsSelectable.value = selectedCharacterHair.value.isSelectable characterIsSelectable.value = selectedCharacterHair.value.isSelectable
characterSpriteId.value = selectedCharacterHair.value.spriteId characterSpriteId.value = selectedCharacterHair.value.sprite?.id
} }
function removeCharacterHair() { function removeCharacterHair() {
@ -107,7 +107,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
characterName.value = characterHair.name characterName.value = characterHair.name
characterGender.value = characterHair.gender characterGender.value = characterHair.gender
characterIsSelectable.value = characterHair.isSelectable characterIsSelectable.value = characterHair.isSelectable
characterSpriteId.value = characterHair.spriteId characterSpriteId.value = characterHair.sprite?.id
}) })
onMounted(() => { onMounted(() => {

View File

@ -9,8 +9,8 @@
</button> </button>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a <a
v-for="{ data: characterHair } in list" v-for="{ data: characterHair } in list"
:key="characterHair.id" :key="characterHair.id"
@ -25,7 +25,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -68,7 +68,7 @@ if (selectedCharacterType.value) {
characterGender.value = selectedCharacterType.value.gender characterGender.value = selectedCharacterType.value.gender
characterRace.value = selectedCharacterType.value.race characterRace.value = selectedCharacterType.value.race
characterIsSelectable.value = selectedCharacterType.value.isSelectable characterIsSelectable.value = selectedCharacterType.value.isSelectable
characterSpriteId.value = selectedCharacterType.value.spriteId characterSpriteId.value = selectedCharacterType.value.sprite?.id
} }
function removeCharacterType() { function removeCharacterType() {
@ -118,7 +118,7 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
characterGender.value = characterType.gender characterGender.value = characterType.gender
characterRace.value = characterType.race characterRace.value = characterType.race
characterIsSelectable.value = characterType.isSelectable characterIsSelectable.value = characterType.isSelectable
characterSpriteId.value = characterType.spriteId characterSpriteId.value = characterType.sprite?.id
}) })
onMounted(() => { onMounted(() => {

View File

@ -9,8 +9,8 @@
</button> </button>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a <a
v-for="{ data: characterType } in list" v-for="{ data: characterType } in list"
:key="characterType.id" :key="characterType.id"
@ -25,7 +25,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -44,7 +44,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Item, ItemRarity, ItemType } from '@/application/types' import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'

View File

@ -9,8 +9,8 @@
</button> </button>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)"> <a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }"> <span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
@ -22,7 +22,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,146 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" />
</div>
<div class="mt-5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="mapObjectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
</div>
<div class="form-field-half">
<label for="origin-x">Origin X</label>
<input v-model="mapObjectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
</div>
<div class="form-field-half">
<label for="origin-y">Origin Y</label>
<input v-model="mapObjectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="form-field-full">
<label for="tags">Tags</label>
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
</div>
<div class="form-field-full">
<label for="frame-speed">Frame rate</label>
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
</div>
<div class="form-field-half">
<label for="frame-width">Frame width</label>
<input v-model="mapObjectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
</div>
<div class="form-field-half">
<label for="frame-height">Frame height</label>
<input v-model="mapObjectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
</div>
<div class="flex gap-4">
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { MapObject } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
const mapObjectName = ref('')
const mapObjectTags = ref<string[]>([])
const mapObjectOriginX = ref(0)
const mapObjectOriginY = ref(0)
const mapObjectFrameRate = ref(0)
const mapObjectFrameWidth = ref(0)
const mapObjectFrameHeight = ref(0)
if (!selectedMapObject.value) {
console.error('No map mapObject selected')
}
if (selectedMapObject.value) {
mapObjectName.value = selectedMapObject.value.name
mapObjectTags.value = selectedMapObject.value.tags
mapObjectOriginX.value = selectedMapObject.value.originX
mapObjectOriginY.value = selectedMapObject.value.originY
mapObjectFrameRate.value = selectedMapObject.value.frameRate
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
}
function removeObject() {
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove mapObject')
return
}
refreshObjectList()
})
}
function refreshObjectList(unsetSelectedMapObject = true) {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response)
if (unsetSelectedMapObject) {
assetManagerStore.setSelectedMapObject(null)
}
})
}
function saveObject() {
if (!selectedMapObject.value) {
console.error('No mapObject selected')
return
}
gameStore.connection?.emit(
'gm:mapObject:update',
{
id: selectedMapObject.value.id,
name: mapObjectName.value,
tags: mapObjectTags.value,
originX: mapObjectOriginX.value,
originY: mapObjectOriginY.value,
frameRate: mapObjectFrameRate.value,
frameWidth: mapObjectFrameWidth.value,
frameHeight: mapObjectFrameHeight.value
},
(response: boolean) => {
if (!response) {
console.error('Failed to save mapObject')
return
}
refreshObjectList(false)
}
)
}
watch(selectedMapObject, (mapObject: MapObject | null) => {
if (!mapObject) return
mapObjectName.value = mapObject.name
mapObjectTags.value = mapObject.tags
mapObjectOriginX.value = mapObject.originX
mapObjectOriginY.value = mapObject.originY
mapObjectFrameRate.value = mapObject.frameRate
mapObjectFrameWidth.value = mapObject.frameWidth
mapObjectFrameHeight.value = mapObject.frameHeight
})
onMounted(() => {
if (!selectedMapObject.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedMapObject(null)
})
</script>

View File

@ -8,20 +8,20 @@
</svg> </svg>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)"> <a v-for="{ data: mapObject } in list" :key="mapObject.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedMapObject?.id === mapObject.id }" @click="assetManagerStore.setSelectedMapObject(mapObject as MapObject)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<div class="h-7 w-16 max-w-16 flex justify-center"> <div class="h-7 w-16 max-w-16 flex justify-center">
<img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" /> <img class="h-7" :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" alt="Object" />
</div> </div>
<span :class="{ 'text-white': assetManagerStore.selectedObject?.id === object.id }">{{ object.name }}</span> <span :class="{ 'text-white': assetManagerStore.selectedMapObject?.id === mapObject.id }">{{ mapObject.name }}</span>
</div> </div>
</a> </a>
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>
@ -29,7 +29,7 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { Object } from '@/application/types' import type { MapObject } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
@ -47,14 +47,14 @@ const elementToScroll = ref()
const handleFileUpload = (e: Event) => { const handleFileUpload = (e: Event) => {
const files = (e.target as HTMLInputElement).files const files = (e.target as HTMLInputElement).files
if (!files) return if (!files) return
gameStore.connection?.emit('gm:object:upload', files, (response: boolean) => { gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => {
if (!response) { if (!response) {
if (config.development) console.error('Failed to upload object') if (config.development) console.error('Failed to upload object')
return return
} }
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => { gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
assetManagerStore.setObjectList(response) assetManagerStore.setMapObjectList(response)
}) })
}) })
} }
@ -66,9 +66,9 @@ const handleSearch = () => {
const filteredObjects = computed(() => { const filteredObjects = computed(() => {
if (!searchQuery.value) { if (!searchQuery.value) {
return assetManagerStore.objectList return assetManagerStore.mapObjectList
} }
return assetManagerStore.objectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase())) return assetManagerStore.mapObjectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
}) })
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, { const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, {
@ -92,8 +92,8 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => { gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
assetManagerStore.setObjectList(response) assetManagerStore.setMapObjectList(response)
}) })
}) })
</script> </script>

View File

@ -1,163 +0,0 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
</div>
<div class="mt-5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="objectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
</div>
<div class="form-field-half">
<label for="origin-x">Origin X</label>
<input v-model="objectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
</div>
<div class="form-field-half">
<label for="origin-y">Origin Y</label>
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="form-field-full">
<label for="tags">Tags</label>
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
</div>
<div class="form-field-full">
<label for="is-animated">Is animated</label>
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="frame-speed">Frame rate</label>
<input v-model="objectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
</div>
<div class="form-field-half">
<label for="frame-width">Frame width</label>
<input v-model="objectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
</div>
<div class="form-field-half">
<label for="frame-height">Frame height</label>
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
</div>
<div class="flex gap-4">
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { Object } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const zoneEditorStore = useZoneEditorStore()
const selectedObject = computed(() => assetManagerStore.selectedObject)
const objectName = ref('')
const objectTags = ref<string[]>([])
const objectOriginX = ref(0)
const objectOriginY = ref(0)
const objectIsAnimated = ref(false)
const objectFrameRate = ref(0)
const objectFrameWidth = ref(0)
const objectFrameHeight = ref(0)
if (!selectedObject.value) {
console.error('No object selected')
}
if (selectedObject.value) {
objectName.value = selectedObject.value.name
objectTags.value = selectedObject.value.tags
objectOriginX.value = selectedObject.value.originX
objectOriginY.value = selectedObject.value.originY
objectIsAnimated.value = selectedObject.value.isAnimated
objectFrameRate.value = selectedObject.value.frameRate
objectFrameWidth.value = selectedObject.value.frameWidth
objectFrameHeight.value = selectedObject.value.frameHeight
}
function removeObject() {
gameStore.connection?.emit('gm:object:remove', { object: selectedObject.value?.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove object')
return
}
refreshObjectList()
})
}
function refreshObjectList(unsetSelectedObject = true) {
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
assetManagerStore.setObjectList(response)
if (unsetSelectedObject) {
assetManagerStore.setSelectedObject(null)
}
if (zoneEditorStore.active) {
zoneEditorStore.setObjectList(response)
}
})
}
function saveObject() {
if (!selectedObject.value) {
console.error('No object selected')
return
}
gameStore.connection?.emit(
'gm:object:update',
{
id: selectedObject.value.id,
name: objectName.value,
tags: objectTags.value,
originX: objectOriginX.value,
originY: objectOriginY.value,
isAnimated: objectIsAnimated.value,
frameRate: objectFrameRate.value,
frameWidth: objectFrameWidth.value,
frameHeight: objectFrameHeight.value
},
(response: boolean) => {
if (!response) {
console.error('Failed to save object')
return
}
refreshObjectList(false)
}
)
}
watch(selectedObject, (object: Object | null) => {
if (!object) return
objectName.value = object.name
objectTags.value = object.tags
objectOriginX.value = object.originX
objectOriginY.value = object.originY
objectIsAnimated.value = object.isAnimated
objectFrameRate.value = object.frameRate
objectFrameWidth.value = object.frameWidth
objectFrameHeight.value = object.frameHeight
})
onMounted(() => {
if (!selectedObject.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedObject(null)
})
</script>

View File

@ -21,9 +21,12 @@
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button> <button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
<Accordion v-for="action in spriteActions" :key="action.id"> <Accordion v-for="action in spriteActions" :key="action.id">
<template #header> <template #header>
<div class="flex justify-between items-center"> <div class="flex items-center">
{{ action.action }} {{ action.action }}
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button> <div class="ml-auto space-x-2">
<button class="btn-cyan px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="openPreviewModal(action)">View</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
</div>
</div> </div>
</template> </template>
<template #content> <template #content>
@ -40,38 +43,38 @@
<label for="origin-y">Origin Y</label> <label for="origin-y">Origin Y</label>
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" /> <input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div> </div>
<div class="form-field-half"> <div class="form-field-full">
<label for="is-animated">Is animated</label>
<select v-model="action.isAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-half" v-if="action.isAnimated">
<label for="is-looping">Is looping</label>
<select v-model="action.isLooping" class="input-field" name="is-looping">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full" v-if="action.isAnimated">
<label for="frame-speed">Frame rate</label> <label for="frame-speed">Frame rate</label>
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" /> <input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<SpriteActionsInput v-model="action.sprites" /> <SpriteActionsInput
v-model="action.sprites"
@tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)"
/>
</div> </div>
</form> </form>
</template> </template>
</Accordion> </Accordion>
<SpritePreview
v-if="selectedAction"
:sprites="selectedAction.sprites"
:frame-rate="selectedAction.frameRate"
:is-modal-open="isModalOpen"
:temp-offset-index="tempOffsetData.index"
:temp-offset="tempOffsetData.offset"
@update:frame-rate="updateFrameRate"
@update:is-modal-open="isModalOpen = $event"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Sprite, SpriteAction } from '@/application/types' import type { Sprite, SpriteAction, UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue' import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
import SpritePreview from '@/components/gameMaster/assetManager/partials/sprite/partials/SpritePreview.vue'
import Accordion from '@/components/utilities/Accordion.vue' import Accordion from '@/components/utilities/Accordion.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -84,6 +87,8 @@ const selectedSprite = computed(() => assetManagerStore.selectedSprite)
const spriteName = ref('') const spriteName = ref('')
const spriteActions = ref<SpriteAction[]>([]) const spriteActions = ref<SpriteAction[]>([])
const isModalOpen = ref(false)
const selectedAction = ref<SpriteAction | null>(null)
if (!selectedSprite.value) { if (!selectedSprite.value) {
console.error('No sprite selected') console.error('No sprite selected')
@ -140,8 +145,6 @@ function saveSprite() {
sprites: action.sprites, sprites: action.sprites,
originX: action.originX, originX: action.originX,
originY: action.originY, originY: action.originY,
isAnimated: action.isAnimated,
isLooping: action.isLooping,
frameRate: action.frameRate, frameRate: action.frameRate,
frameWidth: action.frameWidth, frameWidth: action.frameWidth,
frameHeight: action.frameHeight frameHeight: action.frameHeight
@ -163,14 +166,11 @@ function addNewImage() {
const newImage: SpriteAction = { const newImage: SpriteAction = {
id: uuidv4(), id: uuidv4(),
spriteId: selectedSprite.value.id, sprite: selectedSprite.value.id,
sprite: selectedSprite.value,
action: 'new_action', action: 'new_action',
sprites: [], sprites: [],
originX: 0, originX: 0,
originY: 0, originY: 0,
isAnimated: false,
isLooping: false,
frameRate: 0, frameRate: 0,
frameWidth: 0, frameWidth: 0,
frameHeight: 0 frameHeight: 0
@ -184,15 +184,44 @@ function addNewImage() {
} }
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] { function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
if (!actions) return []
return [...actions].sort((a, b) => a.action.localeCompare(b.action)) return [...actions].sort((a, b) => a.action.localeCompare(b.action))
} }
function openPreviewModal(action: SpriteAction) {
selectedAction.value = action
isModalOpen.value = true
}
function updateFrameRate(value: number) {
if (selectedAction.value) {
selectedAction.value.frameRate = value
}
}
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
index: undefined,
offset: undefined
})
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
if (selectedAction.value === action) {
tempOffsetData.value = { index, offset }
}
}
watch(selectedSprite, (sprite: Sprite | null) => { watch(selectedSprite, (sprite: Sprite | null) => {
if (!sprite) return if (!sprite) return
spriteName.value = sprite.name spriteName.value = sprite.name
spriteActions.value = sortSpriteActions(sprite.spriteActions) spriteActions.value = sortSpriteActions(sprite.spriteActions)
}) })
watch(isModalOpen, (newValue) => {
if (!newValue) {
selectedAction.value = null
}
})
onMounted(() => { onMounted(() => {
if (!selectedSprite.value) return if (!selectedSprite.value) return
}) })

View File

@ -7,8 +7,8 @@
</svg> </svg>
</button> </button>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)"> <a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span> <span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span>
@ -17,7 +17,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,19 +1,46 @@
<template> <template>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)"> <div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
<img :src="image" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" /> <img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
<div v-if="image.dimensions" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">
{{ image.dimensions.width }}x{{ image.dimensions.height }}
</div>
<div class="absolute top-1 left-1 flex-row space-y-1"> <div class="absolute top-1 left-1 flex-row space-y-1">
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image"> <button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<button class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image"> <button @click.stop="openOffsetModal(index)" class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg width="50px" height="50px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path d="M8.29289 3.70711L1 11V15H5L12.2929 7.70711L8.29289 3.70711Z" fill="white" />
<path d="M9.70711 2.29289L13.7071 6.29289L15.1716 4.82843C15.702 4.29799 16 3.57857 16 2.82843C16 1.26633 14.7337 0 13.1716 0C12.4214 0 11.702 0.297995 11.1716 0.828428L9.70711 2.29289Z" fill="white" />
</svg> </svg>
</button> </button>
</div> </div>
<Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" :bg-style="'none'" @modal:close="closeOffsetModal">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="saveOffset(index)" class="inline">
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-half">
<label for="offsetX">Offset X</label>
<input class="input-field max-w-64" v-model="tempOffset.x" name="offsetX" id="offsetX" type="number" />
</div>
<div class="form-field-half">
<label for="offsetY">Offset Y</label>
<input class="input-field max-w-64" v-model="tempOffset.y" name="offsetY" id="offsetY" type="number" />
</div>
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</template>
</Modal>
</div> </div>
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent> <div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -25,10 +52,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import Modal from '@/components/utilities/Modal.vue'
import { ref, watch } from 'vue'
interface SpriteImage {
url: string
offset: {
x: number
y: number
}
dimensions?: {
width: number
height: number
}
}
interface Props { interface Props {
modelValue: string[] modelValue: SpriteImage[]
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -36,11 +76,15 @@ const props = withDefaults(defineProps<Props>(), {
}) })
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void (e: 'update:modelValue', value: SpriteImage[]): void
(e: 'close'): void
(e: 'tempOffsetChange', index: number, offset: { x: number, y: number }): void
}>() }>()
const fileInput = ref<HTMLInputElement | null>(null) const fileInput = ref<HTMLInputElement | null>(null)
const draggedIndex = ref<number | null>(null) const draggedIndex = ref<number | null>(null)
const selectedImageIndex = ref<number | null>(null)
const tempOffset = ref({ x: 0, y: 0 })
const triggerFileInput = () => { const triggerFileInput = () => {
fileInput.value?.click() fileInput.value?.click()
@ -61,19 +105,25 @@ const onDrop = (event: DragEvent) => {
const handleFiles = (files: FileList) => { const handleFiles = (files: FileList) => {
Array.from(files).forEach((file) => { Array.from(files).forEach((file) => {
if (file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
const reader = new FileReader() return
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
updateImages([...props.modelValue, e.target.result])
}
}
reader.readAsDataURL(file)
} }
const reader = new FileReader()
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
const newImage: SpriteImage = {
url: e.target.result,
offset: { x: 0, y: 0 }
}
updateImages([...props.modelValue, newImage])
}
}
reader.readAsDataURL(file)
}) })
} }
const updateImages = (newImages: string[]) => { const updateImages = (newImages: SpriteImage[]) => {
emit('update:modelValue', newImages) emit('update:modelValue', newImages)
} }
@ -101,4 +151,44 @@ const drop = (event: DragEvent, dropIndex: number) => {
} }
draggedIndex.value = null draggedIndex.value = null
} }
const openOffsetModal = (index: number) => {
selectedImageIndex.value = index
tempOffset.value = { ...props.modelValue[index].offset }
}
const closeOffsetModal = () => {
selectedImageIndex.value = null
}
const saveOffset = (index: number) => {
const newImages = [...props.modelValue]
newImages[index] = {
...newImages[index],
offset: { ...tempOffset.value }
}
updateImages(newImages)
closeOffsetModal()
}
const onOffsetChange = () => {
if (selectedImageIndex.value !== null) {
emit('tempOffsetChange', selectedImageIndex.value, tempOffset.value)
}
}
watch(tempOffset, onOffsetChange, { deep: true })
const updateImageDimensions = (event: Event, index: number) => {
const img = event.target as HTMLImageElement
const newImages = [...props.modelValue]
newImages[index] = {
...newImages[index],
dimensions: {
width: img.naturalWidth,
height: img.naturalHeight
}
}
updateImages(newImages)
}
</script> </script>

View File

@ -0,0 +1,154 @@
<template>
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" :bg-style="'none'" @modal:close="closeModal">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3>
</template>
<template #modalBody>
<div class="m-4 flex gap-8">
<div class="relative">
<div
class="sprite-container bg-gray-800"
:style="{
width: `${maxWidth}px`,
height: `${maxHeight}px`,
position: 'relative',
overflow: 'hidden'
}"
>
<img
v-for="(sprite, index) in spritesWithTempOffset"
:key="index"
:src="sprite.url"
alt="Sprite"
:style="{
position: 'absolute',
left: `${sprite.offset?.x || 0}px`,
bottom: `${sprite.offset?.y || 0}px`,
display: currentFrame === index ? 'block' : 'none',
transform: `scale(${zoomLevel / 100})`,
transformOrigin: 'bottom left'
}"
@load="updateContainerSize"
/>
</div>
</div>
<div class="flex flex-col justify-center gap-8 flex-1">
<div class="flex flex-col">
<label class="block mb-2 text-white">Frame Rate: {{ frameRate }} FPS</label>
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
</div>
<div class="flex flex-col">
<label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
<input
type="range"
v-model.number="currentFrame"
:min="0"
:max="sprites.length - 1"
step="1"
class="w-full accent-cyan-500"
@input="stopAnimation"
/>
</div>
<div class="flex flex-col">
<label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label>
<input type="range" v-model.number="zoomLevel" min="10" max="200" step="10" class="w-full accent-cyan-500" />
</div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import type { SpriteImage } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps<{
sprites: SpriteImage[]
frameRate: number
isModalOpen?: boolean
tempOffsetIndex?: number
tempOffset?: { x: number, y: number }
}>()
const emit = defineEmits<{
(e: 'update:frameRate', value: number): void
(e: 'update:isModalOpen', value: boolean): void
}>()
const currentFrame = ref(0)
const maxWidth = ref(250)
const maxHeight = ref(250)
const localFrameRate = ref(props.frameRate)
const zoomLevel = ref(100)
let animationInterval: number | null = null
const spritesWithTempOffset = computed(() => {
return props.sprites.map((sprite, index) => {
if (index === props.tempOffsetIndex && props.tempOffset) {
return { ...sprite, offset: props.tempOffset }
}
return sprite
})
})
function updateContainerSize(event: Event) {
const img = event.target as HTMLImageElement
maxWidth.value = Math.max(maxWidth.value, img.naturalWidth)
maxHeight.value = Math.max(maxHeight.value, img.naturalHeight)
}
function updateAnimation() {
stopAnimation()
if (props.frameRate <= 0 || props.sprites.length === 0) {
currentFrame.value = 0
return
}
animationInterval = window.setInterval(() => {
currentFrame.value = (currentFrame.value + 1) % props.sprites.length
}, 1000 / props.frameRate)
}
function stopAnimation() {
if (animationInterval) {
clearInterval(animationInterval)
animationInterval = null
}
}
function updateFrameRate() {
emit('update:frameRate', localFrameRate.value)
}
function closeModal() {
emit('update:isModalOpen', false)
}
watch(
() => props.frameRate,
(newValue) => {
localFrameRate.value = newValue
updateAnimation()
},
{ immediate: true }
)
watch(() => props.sprites, updateAnimation, { immediate: true })
onMounted(() => {
updateAnimation()
})
onUnmounted(() => {
stopAnimation()
})
</script>
<style scoped>
.sprite-container {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="h-full overflow-auto"> <div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray"> <div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" /> <img class="max-h-72" :src="`${config.server_endpoint}/textures/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
</div> </div>
<div class="mt-5 block"> <div class="mt-5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile"> <form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
@ -26,14 +26,14 @@
import config from '@/application/config' import config from '@/application/config'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue' import ChipsInput from '@/components/forms/ChipsInput.vue'
import { TileStorage } from '@/storage/storages'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
const zoneEditorStore = useZoneEditorStore() const tileStorage = new TileStorage()
const selectedTile = computed(() => assetManagerStore.selectedTile) const selectedTile = computed(() => assetManagerStore.selectedTile)
@ -55,12 +55,13 @@ watch(selectedTile, (tile: Tile | null) => {
tileTags.value = tile.tags tileTags.value = tile.tags
}) })
function deleteTile() { async function deleteTile() {
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, (response: boolean) => { gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, async (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to delete tile') console.error('Failed to delete tile')
return return
} }
await tileStorage.delete(selectedTile.value!.id)
refreshTileList() refreshTileList()
}) })
} }
@ -72,10 +73,6 @@ function refreshTileList(unsetSelectedTile = true) {
if (unsetSelectedTile) { if (unsetSelectedTile) {
assetManagerStore.setSelectedTile(null) assetManagerStore.setSelectedTile(null)
} }
if (zoneEditorStore.active) {
zoneEditorStore.setTileList(response)
}
}) })
} }

View File

@ -8,12 +8,12 @@
</svg> </svg>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)"> <a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<div class="h-7 w-16 max-w-16 flex justify-center"> <div class="h-7 w-16 max-w-16 flex justify-center">
<img class="h-7" :src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`" alt="Tile" /> <img class="h-7" :src="`${config.server_endpoint}/textures/tiles/${tile.id}.png`" alt="Tile" />
</div> </div>
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span> <span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span>
</div> </div>
@ -21,7 +21,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,54 @@
<template>
<MapTiles ref="mapTiles" @tileMap:create="tileMap = $event" />
<PlacedMapObjects ref="mapObjects" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
<MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
</template>
<script setup lang="ts">
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue'
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const mapEditor = useMapEditorComposable()
const scene = useScene()
const mapTiles = useTemplateRef('mapTiles')
const mapObjects = useTemplateRef('mapObjects')
const eventTiles = useTemplateRef('eventTiles')
function handlePointer(pointer: Phaser.Input.Pointer) {
if (!mapTiles.value || !mapObjects.value || !eventTiles.value) 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 draw mode is tile
switch (mapEditor.drawMode.value) {
case 'tile':
mapTiles.value.handlePointer(pointer)
case 'object':
mapObjects.value.handlePointer(pointer)
case 'teleport':
eventTiles.value.handlePointer(pointer)
}
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointer)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointer)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointer)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointer)
mapEditor.reset()
})
</script>

View File

@ -0,0 +1,100 @@
<template>
<Image v-for="tile in mapEditor.currentMap.value?.mapEventTiles" v-bind="getImageProps(tile)" />
</template>
<script setup lang="ts">
import { MapEventTileType, type MapEventTile, type Map as MapT } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { Image, useScene } from 'phavuer'
import { shallowRef } from 'vue'
const mapEditor = useMapEditorComposable()
defineExpose({ handlePointer })
const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap
}>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
function getImageProps(tile: MapEventTile) {
return {
x: tileToWorldX(props.tileMap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tileMap, tile.positionX, tile.positionY),
texture: tile.type,
depth: 999
}
}
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
if (!tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return
// If teleport, check if there is a selected map
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
const newEventTile = {
id: uuidv4(),
mapId: map?.id,
map: map?.id,
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
positionX: tile.x,
positionY: tile.y,
teleport:
mapEditor.drawMode.value === 'teleport'
? {
toMap: mapEditor.teleportSettings.value.toMapId,
toPositionX: mapEditor.teleportSettings.value.toPositionX,
toPositionY: mapEditor.teleportSettings.value.toPositionY,
toRotation: mapEditor.teleportSettings.value.toRotation
}
: undefined
}
map!.mapEventTiles = map!.mapEventTiles.concat(newEventTile as MapEventTile)
}
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
if (!tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return
// Remove existing event tile
map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
}
function handlePointer(pointer: Phaser.Input.Pointer) {
const map = mapEditor.currentMap.value
if (!map) 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
switch (mapEditor.tool.value) {
case 'pencil':
pencil(pointer, map)
break
case 'eraser':
erase(pointer, map)
break
}
}
</script>

View File

@ -0,0 +1,194 @@
<template>
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { TileStorage } from '@/storage/storages'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
import Tileset = Phaser.Tilemaps.Tileset
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const mapEditor = useMapEditorComposable()
const tileStorage = new TileStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
defineExpose({ handlePointer })
function createTileMap() {
const mapData = new Phaser.Tilemaps.MapData({
width: mapEditor.currentMap.value?.width,
height: mapEditor.currentMap.value?.height,
tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapData)
emit('tileMap:create', newTileMap)
return newTileMap
}
async function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap) {
const tiles = await tileStorage.getAll()
const tilesetImages = []
for (const tile of tiles) {
tilesetImages.push(currentTileMap.addTilesetImage(tile.id, tile.id, config.tile_size.width, config.tile_size.height, 1, 2, tilesetImages.length + 1, { x: 0, y: -config.tile_size.height }))
}
// Add blank tile
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
function pencil(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
// Check if there is a selected tile
if (!mapEditor.selectedTile.value) return
if (!tileMap.value || !tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value)
// Adjust mapEditorStore.map.tiles
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value
}
function eraser(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
if (!tileMap.value || !tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
// Adjust mapEditorStore.map.tiles
map.tiles[tile.y][tile.x] = 'blank_tile'
}
function paint(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Set new tileArray with selected tile
const tileArray = createTileArray(tileMap.value.width, tileMap.value.height, mapEditor.selectedTile.value)
setLayerTiles(tileMap.value, tileLayer.value, tileArray)
// Adjust mapEditorStore.map.tiles
if (mapEditor.currentMap.value) {
mapEditor.currentMap.value.tiles = tileArray
}
}
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
if (!tileMap.value || !tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
mapEditor.setSelectedTile(map.tiles[tile.y][tile.x])
}
watch(
() => mapEditor.shouldClearTiles,
(shouldClear) => {
if (shouldClear && mapEditor.currentMap.value && tileMap.value && tileLayer.value) {
const blankTiles = createTileArray(tileLayer.value.width, tileLayer.value.height, 'blank_tile')
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
mapEditor.currentMap.value.tiles = blankTiles
mapEditor.resetClearTilesFlag()
}
}
)
function handlePointer(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) 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) {
tilePicker(pointer)
return
}
// Check if draw mode is tile
switch (mapEditor.tool.value) {
case 'pencil':
pencil(pointer)
break
case 'eraser':
eraser(pointer)
break
case 'paint':
paint(pointer)
break
}
}
onMounted(async () => {
if (!mapEditor.currentMap.value) return
tileMap.value = createTileMap()
tileLayer.value = await createTileLayer(tileMap.value)
// First fill the entire map with blank tiles using current map dimensions
const blankTiles = createTileArray(mapEditor.currentMap.value.width, mapEditor.currentMap.value.height, 'blank_tile')
// Then overlay the map tiles, but only within the current map dimensions
const mapTiles = mapEditor.currentMap.value.tiles
for (let y = 0; y < mapEditor.currentMap.value.height; y++) {
for (let x = 0; x < mapEditor.currentMap.value.width; x++) {
if (mapTiles[y] && mapTiles[y][x] !== undefined) {
blankTiles[y][x] = mapTiles[y][x]
}
}
}
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
})
onUnmounted(() => {
if (tileMap.value) {
tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
}
})
</script>

View File

@ -0,0 +1,45 @@
<template>
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { PlacedMapObject, TextureData } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed } from 'vue'
const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap
placedMapObject: PlacedMapObject
selectedPlacedMapObject: PlacedMapObject | null
movingPlacedMapObject: PlacedMapObject | null
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
alpha: props.movingPlacedMapObject?.id === props.placedMapObject.id ? 0.5 : 1,
tint: props.selectedPlacedMapObject?.id === props.placedMapObject.id ? 0x00ff00 : 0xffffff,
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
x: tileToWorldX(props.tileMap, props.placedMapObject.positionX, props.placedMapObject.positionY),
y: tileToWorldY(props.tileMap, props.placedMapObject.positionX, props.placedMapObject.positionY),
flipX: props.placedMapObject.isRotated,
texture: props.placedMapObject.mapObject.id,
originY: Number(props.placedMapObject.mapObject.originX),
originX: Number(props.placedMapObject.mapObject.originY)
}))
loadTexture(scene, {
key: props.placedMapObject.mapObject.id,
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
group: 'map_objects',
updatedAt: props.placedMapObject.mapObject.updatedAt,
frameWidth: props.placedMapObject.mapObject.frameWidth,
frameHeight: props.placedMapObject.mapObject.frameHeight
} as TextureData).catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -0,0 +1,181 @@
<template>
<SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap="tileMap" :placedMapObject :selectedPlacedMapObject :movingPlacedMapObject @pointerup="clickPlacedMapObject(placedMapObject)" />
</template>
<script setup lang="ts">
import type { Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import PlacedMapObject from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObject.vue'
import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useScene } from 'phavuer'
import { ref, watch } from 'vue'
const scene = useScene()
const mapEditor = useMapEditorComposable()
const selectedPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const movingPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap
}>()
defineExpose({ handlePointer })
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if object already exists on position
const existingPlacedMapObject = findInMap(pointer, map)
if (existingPlacedMapObject) return
const newPlacedMapObject: PlacedMapObjectT = {
id: uuidv4(),
depth: 0,
map: map,
mapObject: mapEditor.selectedMapObject.value!,
isRotated: false,
positionX: pointer.x,
positionY: pointer.y
}
// Add new object to mapObjects
map.placedMapObjects.concat(newPlacedMapObject)
}
function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if object already exists on position
const existingPlacedMapObject = findInMap(pointer, map)
if (!existingPlacedMapObject) return
// Remove existing object
map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
}
function findInMap(pointer: Phaser.Input.Pointer, map: MapT) {
return map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === pointer.worldX && placedMapObject.positionY === pointer.worldY)
}
function objectPicker(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if object already exists on position
const existingPlacedMapObject = findInMap(pointer, map)
if (!existingPlacedMapObject) return
// Select the object
mapEditor.setSelectedMapObject(existingPlacedMapObject.mapObject)
}
function moveMapObject(id: string, map: MapT) {
movingPlacedMapObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingPlacedMapObject.value) return
movingPlacedMapObject.value.positionX = pointer.worldX
movingPlacedMapObject.value.positionY = pointer.worldY
}
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
function handlePointerUp() {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
movingPlacedMapObject.value = null
}
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
}
function rotatePlacedMapObject(id: string, map: MapT) {
map.placedMapObjects = map.placedMapObjects.map((placedMapObject) => {
if (placedMapObject.id === id) {
return {
...placedMapObject,
isRotated: !placedMapObject.isRotated
}
}
return placedMapObject
})
}
function deletePlacedMapObject(id: string, map: MapT) {
map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
selectedPlacedMapObject.value = null
}
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
selectedPlacedMapObject.value = placedMapObject
// If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) {
mapEditor.setSelectedMapObject(placedMapObject.mapObject)
}
}
function handlePointer(pointer: Phaser.Input.Pointer) {
const map = mapEditor.currentMap.value
if (!map) return
if (mapEditor.drawMode.value !== 'map_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
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if tool is pencil
switch (mapEditor.tool.value) {
case 'pencil':
if (mapEditor.selectedMapObject.value) pencil(pointer, map)
break
case 'eraser':
eraser(pointer, map)
break
case 'object picker':
objectPicker(pointer, map)
break
}
}
// watch mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects
watch(
() => mapEditor.currentMap.value,
() => {
const map = mapEditor.currentMap.value
if (!map) return
const updatedMapObjects = map.placedMapObjects.map((mapObject) => {
const updatedMapObject = map.placedMapObjects.find((obj) => obj.id === mapObject.mapObject.id)
if (updatedMapObject) {
return {
...mapObject,
mapObject: {
...mapObject.mapObject,
originX: updatedMapObject.positionX,
originY: updatedMapObject.positionY
}
}
}
return mapObject
})
// Update the map with the new mapObjects
map.placedMapObjects = [...map.placedMapObjects, ...updatedMapObjects]
// Update mapObject if it's set
if (mapEditor.selectedMapObject.value) {
const updatedMapObject = map.placedMapObjects.find((obj) => obj.id === mapEditor.selectedMapObject.value?.id)
if (updatedMapObject) {
mapEditor.setSelectedMapObject({
...mapEditor.selectedMapObject.value,
originX: updatedMapObject.positionX,
originY: updatedMapObject.positionY
})
}
}
}
// { deep: true }
)
</script>

View File

@ -1,9 +1,8 @@
<template> <template>
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'"> <Modal ref="modalRef" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3> <h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3>
</template> </template>
<template #modalBody> <template #modalBody>
<div class="m-4"> <div class="m-4">
<form method="post" @submit.prevent="submit" class="inline"> <form method="post" @submit.prevent="submit" class="inline">
@ -14,15 +13,15 @@
</div> </div>
<div class="form-field-half"> <div class="form-field-half">
<label for="name">Width</label> <label for="name">Width</label>
<input class="input-field max-w-64" v-model="width" name="name" id="name" type="number" /> <input class="input-field max-w-64" v-model="width" name="width" id="width" type="number" />
</div> </div>
<div class="form-field-half"> <div class="form-field-half">
<label for="name">Height</label> <label for="name">Height</label>
<input class="input-field max-w-64" v-model="height" name="name" id="name" type="number" /> <input class="input-field max-w-64" v-model="height" name="height" id="height" type="number" />
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<label for="name">PVP enabled</label> <label for="name">PVP enabled</label>
<select class="input-field" name="pvp" id="pvp"> <select class="input-field" v-model="pvp" name="pvp" id="pvp">
<option :value="false">No</option> <option :value="false">No</option>
<option :value="true">Yes</option> <option :value="true">Yes</option>
</select> </select>
@ -36,23 +35,52 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Zone } from '@/application/types' import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { ref, useTemplateRef } from 'vue'
import { ref } from 'vue'
const emit = defineEmits(['create'])
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const mapStorage = new MapStorage()
const modalRef = useTemplateRef('modalRef')
const name = ref('') const name = ref('')
const width = ref(0) const width = ref(0)
const height = ref(0) const height = ref(0)
const pvp = ref(false)
function submit() { defineExpose({ open: () => modalRef.value?.open() })
gameStore.connection.emit('gm:zone_editor:zone:create', { name: name.value, width: width.value, height: height.value }, (response: Zone[]) => {
zoneEditorStore.setZoneList(response) async function submit() {
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
if (!response) {
gameStore.addNotification({
title: 'Error',
message: 'Failed to create map.'
})
return
}
// Reset form
name.value = ''
width.value = 0
height.value = 0
pvp.value = false
// Add map to storage
console.log(response)
await mapStorage.add(response)
// Let list know to fetch new maps
emit('create')
}) })
zoneEditorStore.toggleCreateZoneModal()
// Close modal
modalRef.value?.close()
} }
</script> </script>

View File

@ -0,0 +1,84 @@
<template>
<Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'">
<template #modalHeader>
<h3 class="text-lg text-white">Maps</h3>
</template>
<template #modalBody>
<div class="my-4 mx-auto h-full">
<div class="text-center mb-4 px-2 flex gap-2.5">
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button>
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="createMapModal?.open">New</button>
</div>
<div class="overflow-y-auto h-[calc(100%-20px)]">
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapList" :key="map.id">
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
<span>{{ map.name }}</span>
<span class="ml-auto gap-1 flex">
<button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteMap(map.id)">x</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
</div>
</div>
</template>
</Modal>
<CreateMap ref="createMapModal" @create="fetchMaps" />
</template>
<script setup lang="ts">
import type { Map, UUID } from '@/application/types'
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onMounted, ref, useTemplateRef } from 'vue'
const gameStore = useGameStore()
const mapEditor = useMapEditorComposable()
const mapStorage = new MapStorage()
const mapList = ref<Map[]>([])
const modalRef = useTemplateRef('modalRef')
const createMapModal = useTemplateRef('createMapModal')
defineEmits(['open-create-map'])
defineExpose({
open: () => modalRef.value?.open()
})
onMounted(async () => {
await fetchMaps()
})
async function fetchMaps() {
mapList.value = await mapStorage.getAll()
}
function loadMap(id: UUID) {
gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => {
mapEditor.loadMap(response)
})
modalRef.value?.close()
}
async function deleteMap(id: UUID) {
gameStore.connection?.emit('gm:map:delete', { mapId: id }, async (response: boolean) => {
if (!response) {
gameStore.addNotification({
title: 'Error',
message: 'Failed to delete map.'
})
return
}
await mapStorage.delete(id)
await fetchMaps()
})
}
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)" :bg-style="'none'"> <Modal ref="modalRef" :modal-width="645" :modal-height="260" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="text-lg text-white">Objects</h3> <h3 class="text-lg text-white">Map objects</h3>
</template> </template>
<template #modalBody> <template #modalBody>
<div class="flex pt-4 pl-4"> <div class="flex pt-4 pl-4">
@ -20,16 +20,16 @@
</div> </div>
<div class="h-full overflow-auto"> <div class="h-full overflow-auto">
<div class="flex justify-between flex-wrap gap-2.5 items-center"> <div class="flex justify-between flex-wrap gap-2.5 items-center">
<div v-for="(object, index) in filteredObjects" :key="index" class="max-w-1/4 inline-block"> <div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
<img <img
class="border-2 border-solid max-w-full" class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/assets/objects/${object.id}.png`" :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
alt="Object" alt="Object"
@click="zoneEditorStore.setSelectedObject(object)" @click="mapEditor.setSelectedMapObject(mapObject)"
:class="{ :class="{
'cursor-pointer transition-all duration-300': true, 'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg scale-105': zoneEditorStore.selectedObject?.id === object.id, 'border-cyan shadow-lg scale-105': mapEditor.selectedMapObject.value?.id === mapObject.id,
'border-transparent hover:border-gray-300': zoneEditorStore.selectedObject?.id !== object.id 'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
}" }"
/> />
</div> </div>
@ -42,25 +42,33 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { Object, ZoneObject } from '@/application/types' import type { MapObject } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { MapObjectStorage } from '@/storage/storages'
import { computed, onMounted, ref } from 'vue' import { liveQuery } from 'dexie'
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
const gameStore = useGameStore() const mapObjectStorage = new MapObjectStorage()
const isModalOpen = ref(false) const isModalOpen = ref(false)
const zoneEditorStore = useZoneEditorStore() const mapEditor = useMapEditorComposable()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const mapObjectList = ref<MapObject[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open(),
close: () => modalRef.value?.close()
})
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || []) const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))
}) })
const filteredObjects = computed(() => { const filteredMapObjects = computed(() => {
return zoneEditorStore.objectList.filter((object) => { return mapObjectList.value.filter((object) => {
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase()) const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag))) const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
return matchesSearch && matchesTags return matchesSearch && matchesTags
@ -75,10 +83,23 @@ const toggleTag = (tag: string) => {
} }
} }
onMounted(async () => { let subscription: any = null
onMounted(() => {
isModalOpen.value = true isModalOpen.value = true
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => { subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
zoneEditorStore.setObjectList(response) next: (result) => {
mapObjectList.value = result
},
error: (error) => {
console.error('Failed to fetch tiles:', error)
}
}) })
}) })
onUnmounted(() => {
if (subscription) {
subscription.unsubscribe()
}
})
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="430" :bg-style="'none'"> <Modal ref="modalRef" :modal-width="600" :modal-height="430" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
</template> </template>
<template #modalBody> <template #modalBody>
@ -34,7 +34,7 @@
</div> </div>
</form> </form>
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'effects'"> <form method="post" @submit.prevent="" class="inline" v-if="screen === 'effects'">
<div v-for="(effect, index) in zoneEffects" :key="effect.id" class="mb-2 flex items-center space-x-2 mt-4"> <div v-for="(effect, index) in mapEffects" :key="effect.id" class="mb-2 flex items-center space-x-2 mt-4">
<input class="input-field flex-grow" v-model="effect.effect" placeholder="Effect name" /> <input class="input-field flex-grow" v-model="effect.effect" placeholder="Effect name" />
<input class="input-field w-20" v-model.number="effect.strength" type="number" placeholder="Strength" /> <input class="input-field w-20" v-model.number="effect.strength" type="number" placeholder="Strength" />
<button class="btn-red py-1 px-2" type="button" @click="removeEffect(index)">Delete</button> <button class="btn-red py-1 px-2" type="button" @click="removeEffect(index)">Delete</button>
@ -46,61 +46,57 @@
</Modal> </Modal>
</template> </template>
<script setup> <script setup lang="ts">
import type { UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { ref, watch } from 'vue' import { ref, useTemplateRef, watch } from 'vue'
const zoneEditorStore = useZoneEditorStore() const mapEditor = useMapEditorComposable()
const screen = ref('settings') const screen = ref('settings')
zoneEditorStore.setZoneName(zoneEditorStore.zone?.name) const name = ref(mapEditor.currentMap.value?.name)
zoneEditorStore.setZoneWidth(zoneEditorStore.zone?.width) const width = ref(mapEditor.currentMap.value?.width)
zoneEditorStore.setZoneHeight(zoneEditorStore.zone?.height) const height = ref(mapEditor.currentMap.value?.height)
zoneEditorStore.setZonePvp(zoneEditorStore.zone?.pvp) const pvp = ref(mapEditor.currentMap.value?.pvp)
zoneEditorStore.setZoneEffects(zoneEditorStore.zone?.zoneEffects) const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || [])
const modalRef = useTemplateRef('modalRef')
const name = ref(zoneEditorStore.zoneSettings?.name) defineExpose({
const width = ref(zoneEditorStore.zoneSettings?.width) open: () => modalRef.value?.open()
const height = ref(zoneEditorStore.zoneSettings?.height) })
const pvp = ref(zoneEditorStore.zoneSettings?.pvp)
const zoneEffects = ref(zoneEditorStore.zoneSettings?.zoneEffects || [])
watch(name, (value) => { watch(name, (value) => {
zoneEditorStore.setZoneName(value) mapEditor.updateProperty('name', value!)
}) })
watch(width, (value) => { watch(width, (value) => {
zoneEditorStore.setZoneWidth(value) mapEditor.updateProperty('width', value!)
}) })
watch(height, (value) => { watch(height, (value) => {
zoneEditorStore.setZoneHeight(value) mapEditor.updateProperty('height', value!)
}) })
watch(pvp, (value) => { watch(pvp, (value) => {
zoneEditorStore.setZonePvp(value) mapEditor.updateProperty('pvp', value!)
}) })
watch( watch(mapEffects, (value) => {
zoneEffects, mapEditor.updateProperty('mapEffects', value!)
(value) => { })
zoneEditorStore.setZoneEffects(value)
},
{ deep: true }
)
const addEffect = () => { const addEffect = () => {
zoneEffects.value.push({ mapEffects.value.push({
id: Date.now().toString(), // Simple unique id generation id: uuidv4() as UUID, // Simple unique id generation
zoneId: zoneEditorStore.zone?.id, map: mapEditor.currentMap.value!,
zone: zoneEditorStore.zone,
effect: '', effect: '',
strength: 1 strength: 1
}) })
} }
const removeEffect = (index) => { const removeEffect = (index: number) => {
zoneEffects.value.splice(index, 1) mapEffects.value.splice(index, 1)
} }
</script> </script>

View File

@ -11,23 +11,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ZoneObject } from '@/application/types' import type { PlacedMapObject } from '@/application/types'
const props = defineProps<{ const props = defineProps<{
zoneObject: ZoneObject placedMapObject: PlacedMapObject
}>() }>()
const emit = defineEmits(['move', 'rotate', 'delete']) const emit = defineEmits(['move', 'rotate', 'delete'])
const handleMove = () => { const handleMove = () => {
emit('move', props.zoneObject.id) emit('move', props.placedMapObject.id)
} }
const handleRotate = () => { const handleRotate = () => {
emit('rotate', props.zoneObject.id) emit('rotate', props.placedMapObject.id)
} }
const handleDelete = () => { const handleDelete = () => {
emit('delete', props.zoneObject.id) emit('delete', props.placedMapObject.id)
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :is-modal-open="showTeleportModal" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'"> <Modal ref="modalRef" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
</template> </template>
@ -25,10 +25,10 @@
</select> </select>
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<label for="toZoneId">Zone to teleport to</label> <label for="toMap">Map to teleport to</label>
<select v-model="toZoneId" class="input-field" name="toZoneId" id="toZoneId"> <select v-model="toMap" class="input-field" name="toMap" id="toMap">
<option :value="0">Select zone</option> <option :value="null">Select map</option>
<option v-for="zone in zoneEditorStore.zoneList" :key="zone.id" :value="zone.id">{{ zone.name }}</option> <option v-for="map in mapList" :key="map.id" :value="map">{{ map.name }}</option>
</select> </select>
</div> </div>
</div> </div>
@ -39,44 +39,50 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Zone } from '@/application/types' import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport') const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapList = ref<Map[]>([])
const modalRef = useTemplateRef('modalRef')
onMounted(fetchZones) defineExpose({
open: () => modalRef.value?.open()
})
function fetchZones() { onMounted(fetchMaps)
gameStore.connection?.emit('gm:zone_editor:zone:list', {}, (response: Zone[]) => {
zoneEditorStore.setZoneList(response) function fetchMaps() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
mapList.value = response
}) })
} }
const { toPositionX, toPositionY, toRotation, toZoneId } = useRefTeleportSettings() const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
function useRefTeleportSettings() { function useRefTeleportSettings() {
const settings = zoneEditorStore.teleportSettings const settings = mapEditorStore.teleportSettings
return { return {
toPositionX: ref(settings.toPositionX), toPositionX: ref(settings.toPositionX),
toPositionY: ref(settings.toPositionY), toPositionY: ref(settings.toPositionY),
toRotation: ref(settings.toRotation), toRotation: ref(settings.toRotation),
toZoneId: ref(settings.toZoneId) toMap: ref(settings.toMapId)
} }
} }
watch([toPositionX, toPositionY, toRotation, toZoneId], updateTeleportSettings) watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
function updateTeleportSettings() { function updateTeleportSettings() {
zoneEditorStore.setTeleportSettings({ mapEditorStore.setTeleportSettings({
toPositionX: toPositionX.value, toPositionX: toPositionX.value,
toPositionY: toPositionY.value, toPositionY: toPositionY.value,
toRotation: toRotation.value, toRotation: toRotation.value,
toZoneId: toZoneId.value toMapId: toMap.value
}) })
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)" :bg-style="'none'"> <Modal ref="modalRef" :modal-width="645" :modal-height="600" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="text-lg text-white">Tiles</h3> <h3 class="text-lg text-white">Tiles</h3>
</template> </template>
@ -24,7 +24,7 @@
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative"> <div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
<img <img
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/${group.parent.id}.png`" :src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
:alt="group.parent.name" :alt="group.parent.name"
@click="openGroup(group)" @click="openGroup(group)"
@load="() => processTile(group.parent)" @load="() => processTile(group.parent)"
@ -50,7 +50,7 @@
<div class="flex flex-col items-center justify-center"> <div class="flex flex-col items-center justify-center">
<img <img
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}/textures/tiles/${selectedGroup.parent.id}.png`"
:alt="selectedGroup.parent.name" :alt="selectedGroup.parent.name"
@click="selectTile(selectedGroup.parent.id)" @click="selectTile(selectedGroup.parent.id)"
:class="{ :class="{
@ -63,7 +63,7 @@
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center"> <div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
<img <img
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}/textures/tiles/${childTile.id}.png`"
:alt="childTile.name" :alt="childTile.name"
@click="selectTile(childTile.id)" @click="selectTile(childTile.id)"
:class="{ :class="{
@ -84,26 +84,33 @@
import config from '@/application/config' import config from '@/application/config'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { TileStorage } from '@/storage/storages'
import { computed, onMounted, ref } from 'vue' import { liveQuery } from 'dexie'
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
const gameStore = useGameStore() const tileStorage = new TileStorage()
const isModalOpen = ref(false) const mapEditor = useMapEditorComposable()
const zoneEditorStore = useZoneEditorStore()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const tileCategories = ref<Map<string, string>>(new Map()) const tileCategories = ref<Map<string, string>>(new Map())
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null) const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
const tiles = ref<Tile[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open(),
close: () => modalRef.value?.close()
})
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = zoneEditorStore.tileList.flatMap((tile) => tile.tags || []) const allTags = tiles.value.flatMap((tile) => tile.tags || [])
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))
}) })
const groupedTiles = computed(() => { const groupedTiles = computed(() => {
const groups: { parent: Tile; children: Tile[] }[] = [] const groups: { parent: Tile; children: Tile[] }[] = []
const filteredTiles = zoneEditorStore.tileList.filter((tile) => { const filteredTiles = tiles.value.filter((tile) => {
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase()) const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag))) const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
return matchesSearch && matchesTags return matchesSearch && matchesTags
@ -169,7 +176,7 @@ function processTile(tile: Tile) {
tileColorData.value.set(tile.id, getDominantColor(imageData)) tileColorData.value.set(tile.id, getDominantColor(imageData))
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData)) tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
} }
img.src = `${config.server_endpoint}/assets/tiles/${tile.id}.png` img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
} }
function getDominantColor(imageData: ImageData) { function getDominantColor(imageData: ImageData) {
@ -177,6 +184,7 @@ function getDominantColor(imageData: ImageData) {
g = 0, g = 0,
b = 0, b = 0,
total = 0 total = 0
for (let i = 0; i < imageData.data.length; i += 4) { for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] > 0) { if (imageData.data[i + 3] > 0) {
// Only consider non-transparent pixels // Only consider non-transparent pixels
@ -186,6 +194,7 @@ function getDominantColor(imageData: ImageData) {
total++ total++
} }
} }
return { return {
r: Math.round(r / total), r: Math.round(r / total),
g: Math.round(g / total), g: Math.round(g / total),
@ -219,18 +228,29 @@ function closeGroup() {
} }
function selectTile(tile: string) { function selectTile(tile: string) {
zoneEditorStore.setSelectedTile(tile) mapEditor.setSelectedTile(tile)
} }
function isActiveTile(tile: Tile): boolean { function isActiveTile(tile: Tile): boolean {
return zoneEditorStore.selectedTile === tile.id return mapEditor.selectedTile.value === tile.id
} }
onMounted(async () => { let subscription: any = null
isModalOpen.value = true
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { onMounted(() => {
zoneEditorStore.setTileList(response) subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({
response.forEach((tile) => processTile(tile)) next: (result) => {
tiles.value = result
},
error: (error) => {
console.error('Failed to fetch tiles:', error)
}
}) })
}) })
onUnmounted(() => {
if (subscription) {
subscription.unsubscribe()
}
})
</script> </script>

View File

@ -1,94 +1,99 @@
<template> <template>
<div class="flex justify-center p-5"> <div class="flex justify-center p-5">
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10"> <div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
<div ref="toolbar" class="tools flex gap-2.5" v-if="zoneEditorStore.zone"> <div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'move' }" @click="handleClick('move')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'move' }">(M)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'move' }">(M)</span>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'pencil' }" @click="handleClick('pencil')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'pencil' }" @click="handleClick('pencil')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'pencil' }">(P)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'pencil' }">(P)</span>
<div class="select" v-if="zoneEditorStore.tool === 'pencil'"> <div class="select" v-if="mapEditor.tool.value === 'pencil'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }"> <div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
{{ zoneEditorStore.drawMode }} {{ mapEditor.drawMode.value.replace('_', ' ') }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" /> <img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</div> </div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && zoneEditorStore.tool === 'pencil'"> <div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditor.tool.value === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('tile')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'pencil')">
Tile Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('object')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'pencil')">
Object Map object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'pencil')">
Teleport Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('blocking tile')">Blocking tile</span> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'pencil')">Blocking tile</span>
</div> </div>
</div> </div>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'eraser' }" @click="handleClick('eraser')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'eraser' }" @click="handleClick('eraser')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'eraser' }">(E)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'eraser' }">(E)</span>
<div class="select" v-if="zoneEditorStore.tool === 'eraser'"> <div class="select" v-if="mapEditor.tool.value === 'eraser'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }"> <div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
{{ zoneEditorStore.eraserMode }} {{ mapEditor.drawMode.value.replace('_', ' ') }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" /> <img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</div> </div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen"> <div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'eraser')">
Tile Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('object')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'eraser')">
Object Map object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'eraser')">
Teleport Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('blocking tile')">Blocking tile</span> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'eraser')">Blocking tile</span>
</div> </div>
</div> </div>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'paint' }" @click="handleClick('paint')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'paint' }" @click="handleClick('paint')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'paint' }">(B)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'paint' }">(B)</span>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="zoneEditorStore.zone"><img class="invert w-5 h-5" src="/assets/icons/zoneEditor/gear.svg" alt="Zone settings" /> <span class="h-5 ml-2.5">(Z)</span></button> <button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
<div class="w-px bg-cyan"></div>
<label class="my-auto gap-0" for="checkbox">Continuous Drawing</label>
<input type="checkbox" />
</div> </div>
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2"> <div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
<button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleZoneListModal()">Load</button> <button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="zoneEditorStore.zone">Save</button> <button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="zoneEditorStore.zone">Clear</button> <button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
<button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleActive()">Exit</button> <button class="btn-cyan px-3.5" @click="() => emit('close-editor')">Exit</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
const zoneEditorStore = useZoneEditorStore() const mapEditor = useMapEditorComposable()
const emit = defineEmits(['save', 'clear']) const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-tile-list', 'open-map-object-list', 'close-lists'])
// track when clicked outside of toolbar items // track when clicked outside of toolbar items
const toolbar = ref(null) const toolbar = ref(null)
@ -97,26 +102,45 @@ const toolbar = ref(null)
let selectPencilOpen = ref(false) let selectPencilOpen = ref(false)
let selectEraserOpen = ref(false) let selectEraserOpen = ref(false)
let tileListShown = ref(false)
let mapObjectListShown = ref(false)
defineExpose({ tileListShown, mapObjectListShown })
// drawMode // drawMode
function setDrawMode(value: string) { function setDrawMode(value: string) {
zoneEditorStore.isTileListModalShown = value === 'tile' if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil') {
zoneEditorStore.isObjectListModalShown = value === 'object' emit('close-lists')
if (value === 'tile') emit('open-tile-list')
if (value === 'map_object') emit('open-map-object-list')
}
zoneEditorStore.setDrawMode(value) mapEditor.setDrawMode(value)
selectPencilOpen.value = false
selectEraserOpen.value = false
}
function setPencilMode() {
mapEditor.setTool('pencil')
selectPencilOpen.value = false selectPencilOpen.value = false
} }
// drawMode // drawMode
function setEraserMode(value: string) { function setEraserMode() {
zoneEditorStore.setEraserMode(value) mapEditor.setTool('eraser')
selectEraserOpen.value = false selectEraserOpen.value = false
} }
function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
setDrawMode(mode)
type === 'pencil' ? setPencilMode() : setEraserMode()
}
function handleClick(tool: string) { function handleClick(tool: string) {
if (tool === 'settings') { if (tool === 'settings') {
zoneEditorStore.toggleSettingsModal() emit('open-settings')
} else { } else {
zoneEditorStore.setTool(tool) mapEditor.setTool(tool)
} }
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
@ -124,22 +148,17 @@ function handleClick(tool: string) {
} }
function cycleToolMode(tool: 'pencil' | 'eraser') { function cycleToolMode(tool: 'pencil' | 'eraser') {
const modes = ['tile', 'object', 'teleport', 'blocking tile'] const modes = ['tile', 'map_object', 'teleport', 'blocking tile']
const currentMode = tool === 'pencil' ? zoneEditorStore.drawMode : zoneEditorStore.eraserMode const currentIndex = modes.indexOf(mapEditor.drawMode.value)
const currentIndex = modes.indexOf(currentMode)
const nextIndex = (currentIndex + 1) % modes.length const nextIndex = (currentIndex + 1) % modes.length
const nextMode = modes[nextIndex] const nextMode = modes[nextIndex]
if (tool === 'pencil') { setDrawMode(nextMode)
setDrawMode(nextMode)
} else {
setEraserMode(nextMode)
}
} }
function initKeyShortcuts(event: KeyboardEvent) { function initKeyShortcuts(event: KeyboardEvent) {
// Check if zone is set // Check if map is set
if (!zoneEditorStore.zone) return if (!mapEditor.currentMap.value) return
// prevent if focused on composables // prevent if focused on composables
if (document.activeElement?.tagName === 'INPUT') return if (document.activeElement?.tagName === 'INPUT') return
@ -154,7 +173,7 @@ function initKeyShortcuts(event: KeyboardEvent) {
if (keyActions.hasOwnProperty(event.key)) { if (keyActions.hasOwnProperty(event.key)) {
const tool = keyActions[event.key] const tool = keyActions[event.key]
if ((tool === 'pencil' || tool === 'eraser') && zoneEditorStore.tool === tool) { if ((tool === 'pencil' || tool === 'eraser') && mapEditor.tool.value === tool) {
cycleToolMode(tool) cycleToolMode(tool)
} else { } else {
handleClick(tool) handleClick(tool)

View File

@ -1,73 +0,0 @@
<template>
<ZoneTiles @tileMap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Toolbar @save="save" @clear="clear" />
<ZoneList />
<TileList />
<ObjectList />
<ZoneSettings />
<TeleportModal />
</template>
<script setup lang="ts">
import { type Zone } from '@/application/types'
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
// Components
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.vue'
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onUnmounted, ref } from 'vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
function clear() {
if (!zoneEditorStore.zone) return
// Clear objects, event tiles and tiles
zoneEditorStore.zone.zoneObjects = []
zoneEditorStore.zone.zoneEventTiles = []
zoneEditorStore.triggerClearTiles()
}
function save() {
if (!zoneEditorStore.zone) return
const data = {
zoneId: zoneEditorStore.zone.id,
name: zoneEditorStore.zoneSettings.name,
width: zoneEditorStore.zoneSettings.width,
height: zoneEditorStore.zoneSettings.height,
tiles: zoneEditorStore.zone.tiles,
pvp: zoneEditorStore.zone.pvp,
zoneEffects: zoneEditorStore.zone.zoneEffects.map(({ id, zoneId, effect, strength }) => ({ id, zoneId, effect, strength })),
zoneEventTiles: zoneEditorStore.zone.zoneEventTiles.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
zoneObjects: zoneEditorStore.zone.zoneObjects.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
}
if (zoneEditorStore.isSettingsModalShown) {
zoneEditorStore.toggleSettingsModal()
}
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
zoneEditorStore.setZone(response)
})
}
onUnmounted(() => {
zoneEditorStore.reset()
})
</script>

View File

@ -1,61 +0,0 @@
<template>
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
<Modal :is-modal-open="zoneEditorStore.isZoneListModalShown" @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'">
<template #modalHeader>
<h3 class="text-lg text-white">Zones</h3>
</template>
<template #modalBody>
<div class="my-4 mx-auto">
<div class="text-center mb-4 px-2 flex gap-2.5">
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchZones">Refresh</button>
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => zoneEditorStore.toggleCreateZoneModal()">New</button>
</div>
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(zone, index) in zoneEditorStore.zoneList" :key="zone.id">
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
<span>{{ zone.name }}</span>
<span class="ml-auto gap-1 flex">
<button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteZone(zone.id)">x</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import type { Zone } from '@/application/types'
import CreateZone from '@/components/gameMaster/zoneEditor/partials/CreateZone.vue'
import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onMounted } from 'vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
onMounted(async () => {
fetchZones()
})
function fetchZones() {
gameStore.connection?.emit('gm:zone_editor:zone:list', {}, (response: Zone[]) => {
zoneEditorStore.setZoneList(response)
})
}
function loadZone(id: number) {
gameStore.connection?.emit('gm:zone_editor:zone:request', { zoneId: id }, (response: Zone) => {
zoneEditorStore.setZone(response)
})
zoneEditorStore.toggleZoneListModal()
}
function deleteZone(id: number) {
gameStore.connection?.emit('gm:zone_editor:zone:delete', { zoneId: id }, () => {
fetchZones()
})
}
</script>

View File

@ -1,118 +0,0 @@
<template>
<Image v-for="tile in zoneEditorStore.zone?.zoneEventTiles" v-bind="getImageProps(tile)" />
</template>
<script setup lang="ts">
import { ZoneEventTileType, type ZoneEventTile } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { Image, useScene } from 'phavuer'
import { onMounted, onUnmounted } from 'vue'
const scene = useScene()
const zoneEditorStore = useZoneEditorStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function getImageProps(tile: ZoneEventTile) {
return {
x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY),
texture: tile.type,
depth: 999
}
}
function pencil(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 blocking tile or teleport
if (zoneEditorStore.drawMode !== 'blocking tile' && zoneEditorStore.drawMode !== 'teleport') 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 there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return
// If teleport, check if there is a selected zone
if (zoneEditorStore.drawMode === 'teleport' && !zoneEditorStore.teleportSettings.toZoneId) return
const newEventTile = {
id: uuidv4(),
zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone,
type: zoneEditorStore.drawMode === 'blocking tile' ? ZoneEventTileType.BLOCK : ZoneEventTileType.TELEPORT,
positionX: tile.x,
positionY: tile.y,
teleport:
zoneEditorStore.drawMode === 'teleport'
? {
toZoneId: zoneEditorStore.teleportSettings.toZoneId,
toPositionX: zoneEditorStore.teleportSettings.toPositionX,
toPositionY: zoneEditorStore.teleportSettings.toPositionY,
toRotation: zoneEditorStore.teleportSettings.toRotation
}
: undefined
}
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.concat(newEventTile as ZoneEventTile)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is blocking tile or teleport
if (zoneEditorStore.eraserMode !== 'blocking tile' && zoneEditorStore.eraserMode !== 'teleport') 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 there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return
// Remove existing event tile
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
}
onMounted(() => {
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_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
})
onUnmounted(() => {
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_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
})
</script>

View File

@ -1,45 +0,0 @@
<template>
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { AssetDataT, ZoneObject } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed } from 'vue'
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

@ -1,248 +0,0 @@
<template>
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" :movingZoneObject="movingZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
<ZoneObject v-for="zoneObject in zoneEditorStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject :selectedZoneObject :movingZoneObject @pointerup="clickZoneObject(zoneObject)" />
</template>
<script setup lang="ts">
import type { ZoneObject as ZoneObjectT } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
import { getTile } from '@/composables/zoneComposable'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, ref, watch } from 'vue'
const scene = useScene()
const zoneEditorStore = useZoneEditorStore()
const selectedZoneObject = ref<ZoneObjectT | null>(null)
const movingZoneObject = ref<ZoneObjectT | null>(null)
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function pencil(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 there is a selected object
if (!zoneEditorStore.selectedObject) 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, this means we are selecting the object
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
const newObject = {
id: uuidv4(),
zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone,
objectId: zoneEditorStore.selectedObject.id,
object: zoneEditorStore.selectedObject,
depth: 0,
isRotated: false,
positionX: tile.x,
positionY: tile.y
}
// Add new object to zoneObjects
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.concat(newObject as ZoneObjectT)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is eraser
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is object
if (zoneEditorStore.eraserMode !== '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
// Check if alt is pressed, this means we are selecting the object
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
// Remove existing object
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) {
// Check if zone is set
if (!zoneEditorStore.zone) return
movingZoneObject.value = zoneEditorStore.zone.zoneObjects.find((object) => object.id === id) as ZoneObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingZoneObject.value) return
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
movingZoneObject.value.positionX = tile.x
movingZoneObject.value.positionY = tile.y
}
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
function handlePointerUp() {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
movingZoneObject.value = null
}
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
}
function rotateZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.map((object) => {
if (object.id === id) {
return {
...object,
isRotated: !object.isRotated
}
}
return object
})
}
function deleteZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== id)
selectedZoneObject.value = null
}
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_MOVE, pencil)
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_DOWN, objectPicker)
})
onUnmounted(() => {
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_DOWN, 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,
(newObjects) => {
if (!zoneEditorStore.zone) return
const updatedZoneObjects = zoneEditorStore.zone.zoneObjects.map((zoneObject) => {
const updatedObject = newObjects.find((obj) => obj.id === zoneObject.object.id)
if (updatedObject) {
return {
...zoneObject,
object: {
...zoneObject.object,
originX: updatedObject.originX,
originY: updatedObject.originY
}
}
}
return zoneObject
})
// Update the zone with the new zoneObjects
zoneEditorStore.setZone({
...zoneEditorStore.zone,
zoneObjects: updatedZoneObjects
})
// Update selectedObject if it's set
if (zoneEditorStore.selectedObject) {
const updatedObject = newObjects.find((obj) => obj.id === zoneEditorStore.selectedObject?.id)
if (updatedObject) {
zoneEditorStore.setSelectedObject({
...zoneEditorStore.selectedObject,
originX: updatedObject.originX,
originY: updatedObject.originY
})
}
}
},
{ deep: true }
)
</script>

View File

@ -1,227 +0,0 @@
<template>
<Controls :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { AssetDataT } from '@/application/types'
import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, watch } from 'vue'
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const tileMap = createTileMap()
const tileLayer = 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() {
const zoneData = new Phaser.Tilemaps.MapData({
width: zoneEditorStore.zone?.width,
height: zoneEditorStore.zone?.height,
tileWidth: config.tile_size.x,
tileHeight: config.tile_size.y,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
emit('tileMap:create', newTileMap)
return newTileMap
}
/**
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
*/
function createTileLayer() {
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => {
return tileMap.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(tileMap.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 = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
function pencil(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 there is a selected tile
if (!zoneEditorStore.selectedTile) 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 there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, zoneEditorStore.selectedTile)
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is tile
if (zoneEditorStore.eraserMode !== '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(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
}
function paint(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'paint') return
// Check if there is a selected tile
if (!zoneEditorStore.selectedTile) 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
// Set new tileArray with selected tile
setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile))
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles = createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile)
}
// 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(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
zoneEditorStore.setSelectedTile(zoneEditorStore.zone.tiles[tile.y][tile.x])
}
watch(
() => zoneEditorStore.shouldClearTiles,
(shouldClear) => {
if (shouldClear && zoneEditorStore.zone) {
const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile')
setLayerTiles(tileMap, tileLayer, blankTiles)
zoneEditorStore.zone.tiles = blankTiles
zoneEditorStore.resetClearTilesFlag()
}
}
)
onMounted(() => {
if (!zoneEditorStore.zone?.tiles) {
return
}
// First fill the entire map with blank tiles using current zone dimensions
const blankTiles = createTileArray(zoneEditorStore.zone.width, zoneEditorStore.zone.height, 'blank_tile')
// Then overlay the zone tiles, but only within the current zone dimensions
const zoneTiles = zoneEditorStore.zone.tiles
for (let y = 0; y < zoneEditorStore.zone.height; y++) {
for (let x = 0; x < zoneEditorStore.zone.width; x++) {
// Only copy if the source tiles array has this position
if (zoneTiles[y] && zoneTiles[y][x] !== undefined) {
blankTiles[y][x] = zoneTiles[y][x]
}
}
}
setLayerTiles(tileMap, tileLayer, blankTiles)
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_DOWN, paint)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
})
onUnmounted(() => {
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_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
tileMap.destroyLayer('tiles')
tileMap.removeAllLayers()
tileMap.destroy()
})
</script>

View File

@ -1,42 +0,0 @@
<template>
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false">
<div class="center-element max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-gray-500">
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
<div class="hidden sm:flex gap-1.5 flex-wrap">
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
</div>
<div class="flex gap-2.5">
<button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
</button>
</div>
<div class="flex sm:hidden gap-1.5 flex-wrap">
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
</div>
</div>
<Inventory v-show="userPanelScreen === 'inventory'" />
<Equipment v-show="userPanelScreen === 'equipment'" />
<CharacterScreen v-show="userPanelScreen === 'characterScreen'" />
<Settings v-show="userPanelScreen === 'settings'" />
</div>
</div>
</template>
<script setup lang="ts">
import CharacterScreen from '@/components/gui/partials/CharacterScreen.vue'
import Equipment from '@/components/gui/partials/Equipment.vue'
import Inventory from '@/components/gui/partials/Inventory.vue'
import Settings from '@/components/gui/partials/Settings.vue'
import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue'
const gameStore = useGameStore()
let userPanelScreen = ref('inventory')
</script>

View File

@ -1,68 +0,0 @@
<template>
<div class="grow flex flex-col w-full h-full relative overflow-auto">
<div class="m-4 relative">
<h4 class="font-medium text-lg max-w-[375px]">Character</h4>
<div class="flex justify-center flex-wrap gap-20">
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
<div class="h-full flex flex-col justify-center items-center">
<img class="h-72 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-3 mx-5 mt-2">
<h3>{{ gameStore.character?.name }}</h3>
<div class="flex gap-4 flex-wrap max-w-[375px]">
<ul class="p-0 m-0 list-none">
<li class="leading-6 text-lg">Class:</li>
<li class="leading-6 text-lg">Race:</li>
<li class="leading-6 text-lg">Hit Points:</li>
<li class="leading-6 text-lg">Mana Points:</li>
<li class="leading-6 text-lg">Level:</li>
<li class="leading-6 text-lg">Stat Points:</li>
</ul>
<ul class="p-0 m-0 list-none">
<li class="leading-6 text-lg min-h-6">Knight</li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.characterType?.race }}</li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.hitpoints }} <span class="text-green">(+15)</span></li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.mana }}</li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.level }}</li>
<li class="leading-6 text-lg min-h-6">3</li>
</ul>
</div>
</div>
<div class="flex flex-col gap-3 mx-5 mt-2">
<h3>Alignment</h3>
<div class="h-8 w-64 rounded border border-solid border-white bg-gradient-to-r from-red to-blue relative">
<!-- TODO: Give slider left value based on alignment (0-100), new characters start with 50 -->
<div class="rounded border-2 border-solid border-white h-10 w-2 absolute top-1/2 -translate-y-1/2 -translate-x-1/2" :style="{ left: gameStore.character?.alignment + '%' }"></div>
</div>
</div>
</div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="font-medium text-lg max-w-[375px]">Character stats</h4>
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
<ul class="p-0 m-0 list-none">
<li class="leading-6">Health:</li>
<li class="leading-6">Defense:</li>
<li class="leading-6">More stats:</li>
<li class="leading-6">Extra stats:</li>
</ul>
<ul class="p-0 m-0 list-none text-right">
<li class="leading-6">100 <span class="text-green">(+15)</span></li>
<li class="leading-6">1000 <span class="text-green">(+500)</span></li>
<li class="leading-6"></li>
<li class="leading-6"></li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -1,89 +0,0 @@
<template>
<div class="grow flex flex-col w-full h-full relative overflow-auto">
<div class="m-4 relative">
<h4 class="font-medium text-lg max-w-[375px]">Equipment</h4>
<div class="flex justify-center items-center flex-wrap gap-20">
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
<div class="h-full flex flex-col justify-center items-center">
<h3>{{ gameStore.character?.name }}</h3>
<img class="h-60 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
<span class="block text-sm">Level {{ gameStore.character?.level }}</span>
</div>
</div>
<div class="flex flex-col gap-3 mx-5 mt-2">
<div class="flex gap-3 justify-center">
<!-- Helmet -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/helmet.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Head charm -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/head_charm.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Bracers -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/bracers.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Chestplate -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/chestplate.svg" class="center-element w-10/12 opacity-20" />
</div>
<!-- Primary Weapon -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/primary_weapon.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Legs -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/legs.svg" class="center-element w-11/12 opacity-20" />
</div>
<div class="flex flex-col gap-3">
<!-- Belt/pouch -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/pouch.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Boots -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/boots.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
</div>
</div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="font-medium text-lg max-w-[375px]">Equipment Bonus</h4>
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
<ul class="p-0 m-0 list-none">
<li class="leading-6">Health:</li>
<li class="leading-6">Defense:</li>
<li class="leading-6">More stats:</li>
<li class="leading-6">Extra stats:</li>
</ul>
<ul class="p-0 m-0 list-none text-right">
<li class="leading-6">+15</li>
<li class="leading-6">500</li>
<li class="leading-6"></li>
<li class="leading-6"></li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -1,17 +0,0 @@
<template>
<div class="grow flex flex-col w-full h-full relative overflow-auto">
<div class="m-4 relative">
<h4 class="m-auto font-medium text-lg max-w-[375px]">Inventory</h4>
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
<div v-for="n in 24" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="m-auto font-medium text-lg max-w-[375px]">Chest items</h4>
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
<div v-for="n in 12" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
</div>
</div>
</div>
</template>

View File

@ -1,40 +0,0 @@
<template>
<div class="flex h-full w-full relative">
<div class="w-2/12 flex flex-col relative">
<!-- Settings Categories -->
<div class="relative p-2.5">
<h3>Settings</h3>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'character' }" @click.stop="settingCategory = 'character'">
<span>Character</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'account' }" @click.stop="settingCategory = 'account'">
<span>Account</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'audio' }" @click.stop="settingCategory = 'audio'">
<span>Audio</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'video' }" @click.stop="settingCategory = 'video'">
<span>Video</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
<!-- Assets list -->
<div class="overflow-auto h-full w-10/12 flex flex-col relative">
<CharacterSettings v-show="settingCategory == 'character'" />
</div>
</div>
</template>
<script setup lang="ts">
import CharacterSettings from '@/components/gui/partials/settings/CharacterSettings.vue'
import { ref } from 'vue'
let settingCategory = ref('character')
</script>

View File

@ -1,34 +0,0 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col gap-5 h-72">
<h3>Character details</h3>
<button @click="toggle" class="btn-cyan px-4 py-1.5 w-24">Edit</button>
<form class="flex gap-2.5 flex-wrap">
<div class="form-field-half max-w-[300px]">
<label for="name">Name</label>
<input class="input-field" :class="{ inactive: !editCharacter }" type="text" name="name" placeholder="Ethereal" :disabled="!editCharacter" />
</div>
<div class="form-field-half max-w-[300px] relative">
<label for="class">Class</label>
<select class="input-field" v-model="characterClass" :class="{ inactive: !editCharacter }" name="class" :disabled="!editCharacter">
<option value="Knight" :selected="characterClass == 'Knight'" :disabled="characterClass == 'Knight'">Knight</option>
<option value="Paladin" :selected="characterClass == 'Paladin'" :disabled="characterClass == 'Paladin'">Paladin</option>
</select>
<span v-if="!editCharacter" class="absolute bottom-[9px] left-[14px] text-sm text-gray-300/50">{{ characterClass }}</span>
</div>
<button v-if="editCharacter" @click="toggle" class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const editCharacter = ref(false)
const characterClass = ref('')
const toggle = () => {
editCharacter.value = !editCharacter.value
}
</script>

View File

@ -34,27 +34,23 @@
<button class="ml-6 w-4 h-8 p-0"> <button class="ml-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" /> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
</button> </button>
<img class="w-24 object-contain mb-3.5" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType?.id + '/' + (selectedHairId ?? 'default')" /> <img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" />
<button class="mr-6 w-4 h-8 p-0"> <button class="mr-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" /> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
</button> </button>
</div> </div>
<!-- <div class="flex justify-between w-[190px]">-->
<!-- &lt;!&ndash; TODO: replace with color swatches &ndash;&gt;-->
<!-- <button v-for="n in 9" class="w-4 h-4 rounded-sm bg-white"></button>-->
<!-- </div>-->
</div> </div>
<!-- TODO: update gender on (selected) character --> <!-- TODO: update gender on (selected) character -->
<div class="flex justify-between w-[190px]"> <!-- <div class="flex justify-between w-[190px]">-->
<button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'MALE' }"> <!-- <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'MALE' }">-->
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" /> <!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />-->
<span class="text-white">Male</span> <!-- <span class="text-white">Male</span>-->
</button> <!-- </button>-->
<button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'FEMALE' }"> <!-- <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'FEMALE' }">-->
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" /> <!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />-->
<span class="text-white">Female</span> <!-- <span class="text-white">Female</span>-->
</button> <!-- </button>-->
</div> <!-- </div>-->
</div> </div>
</div> </div>
</div> </div>
@ -74,7 +70,7 @@
v-for="hair in characterHairs" v-for="hair in characterHairs"
class="relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent" class="relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"
> >
<img class="h-4 object-contain" :src="config.server_endpoint + '/assets/sprites/' + hair.sprite.id + '/front.png'" alt="Hair sprite" /> <img class="h-4 object-contain" :src="config.server_endpoint + '/textures/sprites/' + hair.sprite + '/front.png'" alt="Hair sprite" />
<input type="radio" name="hair" :value="hair.id" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" /> <input type="radio" name="hair" :value="hair.id" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
</div> </div>
</div> </div>
@ -91,7 +87,7 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" /> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
</div> </div>
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading"> <div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
@ -126,34 +122,29 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { type CharacterHair, type Character as CharacterT, type Zone } from '@/application/types' import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { CharacterHairStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { onBeforeUnmount, ref, watch } from 'vue' import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoading = ref<boolean>(true) const isLoading = ref<boolean>(true)
const characters = ref<CharacterT[]>([]) const characters = ref<CharacterT[]>([])
const selectedCharacterId = ref<number | null>(null) const selectedCharacterId = ref<string | null>(null)
const isCreateNewCharacterModalOpen = ref<boolean>(false) const isCreateNewCharacterModalOpen = ref<boolean>(false)
const newCharacterName = ref<string>('') const newCharacterName = ref<string>('')
const characterHairs = ref<CharacterHair[]>([]) const characterHairs = ref<CharacterHair[]>([])
const selectedHairId = ref<number | null>(null) const selectedHairId = ref<string | null>(null)
// Fetch characters // Fetch characters
setTimeout(() => { setTimeout(() => {
gameStore.connection?.emit('character:list') gameStore.connection?.emit('character:list')
}, 750) }, 750)
gameStore.connection!.on('character:list', (data: any) => { gameStore.connection?.on('character:list', (data: any) => {
characters.value = data characters.value = data
isLoading.value = false isLoading.value = false
// Fetch hairs
// @TODO: This is hacky, we should have a better way to do this
gameStore.connection?.emit('character:hair:list', {}, (data: CharacterHair[]) => {
characterHairs.value = data
})
}) })
// Select character logics // Select character logics
@ -166,7 +157,7 @@ function loginWithCharacter() {
characterId: selectedCharacterId.value, characterId: selectedCharacterId.value,
characterHairId: selectedHairId.value characterHairId: selectedHairId.value
}, },
(response: { character: CharacterT; zone: Zone; characters: CharacterT[] }) => { (response: { character: CharacterT; map: Map; characters: CharacterT[] }) => {
gameStore.setCharacter(response.character) gameStore.setCharacter(response.character)
} }
) )
@ -174,7 +165,7 @@ function loginWithCharacter() {
// Create character logics // Create character logics
function createCharacter() { function createCharacter() {
gameStore.connection!.on('character:create:success', (data: CharacterT) => { gameStore.connection?.on('character:create:success', (data: CharacterT) => {
gameStore.setCharacter(data) gameStore.setCharacter(data)
isCreateNewCharacterModalOpen.value = false isCreateNewCharacterModalOpen.value = false
}) })
@ -184,7 +175,12 @@ function createCharacter() {
// Watch changes for selected character and update hairs // Watch changes for selected character and update hairs
watch(selectedCharacterId, (characterId) => { watch(selectedCharacterId, (characterId) => {
if (!characterId) return if (!characterId) return
selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null // selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null
})
onMounted(async () => {
const characterHairStorage = new CharacterHairStorage()
characterHairs.value = await characterHairStorage.getAll()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -6,12 +6,11 @@
<Hud /> <Hud />
<Hotkeys /> <Hotkeys />
<Clock /> <Clock />
<Zone /> <Map />
<Chat /> <Chat />
<ExpBar /> <ExpBar />
<CharacterProfile /> <CharacterProfile />
<Effects />
</Scene> </Scene>
</Game> </Game>
</div> </div>
@ -20,19 +19,17 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import 'phaser' import 'phaser'
import Effects from '@/components/Effects.vue' import CharacterProfile from '@/components/game/gui/CharacterProfile.vue'
import Zone from '@/components/game/zone/Zone.vue' import Chat from '@/components/game/gui/Chat.vue'
import CharacterProfile from '@/components/gui/CharacterProfile.vue' import Clock from '@/components/game/gui/Clock.vue'
import Chat from '@/components/gui/Chat.vue' import ExpBar from '@/components/game/gui/ExpBar.vue'
// import Minimap from '@/components/gui/Minimap.vue' import Hotkeys from '@/components/game/gui/Hotkeys.vue'
import Clock from '@/components/gui/Clock.vue' import Hud from '@/components/game/gui/Hud.vue'
import ExpBar from '@/components/gui/ExpBar.vue' import Menu from '@/components/game/gui/Menu.vue'
import Hotkeys from '@/components/gui/Hotkeys.vue' import Map from '@/components/game/map/Map.vue'
import Hud from '@/components/gui/Hud.vue'
import Menu from '@/components/gui/Menu.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'
import { onBeforeUnmount } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
@ -41,22 +38,11 @@ const gameConfig = {
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) => {
/** // Resize the game when the window is resized
* Resize the game when the window is resized
*/
addEventListener('resize', () => { addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight) game.scale.resize(window.innerWidth, window.innerHeight)
}) })
@ -72,12 +58,12 @@ const createGame = (game: Phaser.Game) => {
} }
function preloadScene(scene: Phaser.Scene) { function preloadScene(scene: Phaser.Scene) {
/** // Load the base assets into the Phaser scene
* Load the base assets into the Phaser scene scene.load.image('blank_tile', '/assets/map/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')
} }
function createScene(scene: Phaser.Scene) {} function createScene(scene: Phaser.Scene) {}
onBeforeUnmount(() => {})
</script> </script>

View File

@ -1,25 +1,28 @@
<template> <template>
<div class="flex flex-col justify-center items-center h-dvh relative"> <div class="flex flex-col justify-center items-center h-dvh relative col">
<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"> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
<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> </div>
</template> </template>
<script setup lang="ts" async> <script setup lang="ts" async>
import { downloadCache } from '@/application/utilities'
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
function continueBtnClick() { const totalItems = ref(0)
// Play music const currentItem = ref(0)
const audio = new Audio('/assets/music/login.mp3')
audio.play()
// Set isLoaded to true Promise.all([
downloadCache('tiles', new TileStorage()),
downloadCache('maps', new MapStorage()),
downloadCache('map_objects', new MapObjectStorage()),
downloadCache('sprites', new SpriteStorage()),
downloadCache('character_types', new CharacterTypeStorage()),
downloadCache('character_hair', new CharacterHairStorage())
]).then(() => {
gameStore.game.isLoaded = true gameStore.game.isLoaded = true
} })
</script> </script>

View File

@ -8,8 +8,8 @@
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20"> <div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />--> <!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />-->
<div class="relative"> <div class="relative">
<img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" /> <img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
<img src="/assets/ui-elements/login-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" /> <img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)]" alt="" />
<!-- Login Form --> <!-- Login Form -->
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" /> <LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />

View File

@ -0,0 +1,123 @@
<template>
<div class="flex justify-center items-center h-dvh relative">
<Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene">
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
<div v-else>
<Map :key="mapEditor.currentMap.value?.id" />
<Toolbar
ref="toolbar"
@save="save"
@clear="clear"
@open-maps="mapModal?.open"
@open-settings="mapSettingsModal?.open"
@close-editor="mapEditor.toggleActive"
@close-lists="tileModal?.close"
@closeLists="objectModal?.close"
@open-tile-list="tileModal?.open"
@open-map-object-list="objectModal?.open"
/>
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
<TileList ref="tileModal" />
<ObjectList ref="objectModal" />
<MapSettings ref="mapSettingsModal" />
<TeleportModal ref="teleportModal" />
</div>
</Scene>
</Game>
</div>
</template>
<script setup lang="ts">
import config from '@/application/config'
import 'phaser'
import type { Map as MapT } from '@/application/types'
import Map from '@/components/gameMaster/mapEditor/Map.vue'
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
import ObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
import { loadAllTilesIntoScene } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { Game, Scene } from 'phavuer'
import { ref, useTemplateRef, watch } from 'vue'
const mapStorage = new MapStorage()
const mapEditor = useMapEditorComposable()
const gameStore = useGameStore()
const toolbar = useTemplateRef('toolbar')
const mapModal = useTemplateRef('mapModal')
const tileModal = useTemplateRef('tileModal')
const objectModal = useTemplateRef('objectModal')
const mapSettingsModal = useTemplateRef('mapSettingsModal')
const teleportModal = useTemplateRef('teleportModal')
const isLoaded = ref(false)
const gameConfig = {
name: config.name,
width: window.innerWidth,
height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5
}
const createGame = (game: Phaser.Game) => {
// Resize the game when the window is resized
window.addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight)
})
}
const preloadScene = async (scene: Phaser.Scene) => {
// Load the base assets into the Phaser scene
scene.load.image('BLOCK', '/assets/map/bt_tile.png')
scene.load.image('TELEPORT', '/assets/map/tp_tile.png')
scene.load.image('blank_tile', '/assets/map/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png')
// Get all tiles from IndexedDB and load them into the scene
await loadAllTilesIntoScene(scene)
// Wait for all assets to be loaded before continuing
await new Promise<void>((resolve) => {
scene.load.on(Phaser.Loader.Events.COMPLETE, () => {
resolve()
})
isLoaded.value = true
})
}
function save() {
const currentMap = mapEditor.currentMap.value
if (!currentMap) return
const data = {
mapId: currentMap.id,
name: currentMap.name,
width: currentMap.width,
height: currentMap.height,
tiles: currentMap.tiles,
pvp: currentMap.pvp,
mapEffects: currentMap.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
mapEventTiles: currentMap.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
placedMapObjects: currentMap.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
}
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => {
mapStorage.update(response.id, response)
})
}
function clear() {
if (!mapEditor.currentMap.value) return
// Clear placed objects, event tiles and tiles
mapEditor.clearMap()
}
</script>

View File

@ -1,85 +0,0 @@
<template>
<div class="flex justify-center items-center h-dvh relative">
<Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene">
<ZoneEditor :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt ?? ''}`)" />
</Scene>
</Game>
</div>
</template>
<script setup lang="ts">
import config from '@/application/config'
import 'phaser'
import type { AssetDataT } from '@/application/types'
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
import { loadTexture } from '@/composables/gameComposable'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { Game, Scene } from 'phavuer'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const gameConfig = {
name: config.name,
width: window.innerWidth,
height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5,
plugins: {
global: [
{
key: 'rexAwaitLoader',
plugin: AwaitLoaderPlugin,
start: true
}
]
}
}
const createGame = (game: Phaser.Game) => {
/**
* Resize the game when the window is resized
*/
addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight)
})
// We don't support canvas mode, only WebGL
if (game.renderer.type === Phaser.CANVAS) {
gameStore.addNotification({
title: 'Warning',
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
})
gameStore.disconnectSocket()
}
}
const preloadScene = async (scene: Phaser.Scene) => {
/**
* Load the base assets into the Phaser scene
*/
scene.load.image('BLOCK', '/assets/zone/bt_tile.png')
scene.load.image('TELEPORT', '/assets/zone/tp_tile.png')
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png')
/**
* Because Phaser can't load tiles after the scene with map in it is created,
* we need to load and cache all the tiles first.
* Then load them into the scene.
*/
scene.load.rexAwait(async function (successCallback: any) {
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)
}
successCallback()
})
}
const createScene = async (scene: Phaser.Scene) => {}
</script>

View File

@ -0,0 +1,46 @@
<template></template>
<script setup lang="ts">
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
import { TextureStorage } from '@/storage/textureStorage'
import { onMounted, onUnmounted } from 'vue'
const mapStorage = new MapStorage()
const tileStorage = new TileStorage()
const mapObjectStorage = new MapObjectStorage()
const spriteStorage = new SpriteStorage()
const characterTypeStorage = new CharacterTypeStorage()
const characterHairStorage = new CharacterHairStorage()
const textureStorage = new TextureStorage()
let currentString = ''
async function handleKeyPress(event: KeyboardEvent) {
// Ignore if typing in input/textarea
if (document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
return
}
currentString += event.key
// Do something when string matches
if (currentString.includes('reset')) {
await mapStorage.destroy()
await tileStorage.destroy()
await mapObjectStorage.destroy()
await spriteStorage.destroy()
await characterTypeStorage.destroy()
await characterHairStorage.destroy()
await textureStorage.destroy()
currentString = '' // Reset
}
// Reset string after a certain amount of time
setTimeout(() => {
currentString = ''
}, 60000)
}
onMounted(() => window.addEventListener('keydown', handleKeyPress))
onUnmounted(() => window.removeEventListener('keydown', handleKeyPress))
</script>

View File

@ -17,7 +17,7 @@
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out"> <button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out">
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" /> <img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
</button> </button>
<button v-if="closable" @click="emit('modal:close')" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button v-if="closable" @click="closeModal" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" /> <img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" />
</button> </button>
</div> </div>
@ -46,7 +46,7 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
interface ModalProps { interface ModalProps {
isModalOpen: boolean isModalOpen?: boolean
closable?: boolean closable?: boolean
isResizable?: boolean isResizable?: boolean
isFullScreen?: boolean isFullScreen?: boolean
@ -79,10 +79,16 @@ const props = withDefaults(defineProps<ModalProps>(), {
}) })
const emit = defineEmits<{ const emit = defineEmits<{
'modal:open': []
'modal:close': [] 'modal:close': []
'character:create': []
}>() }>()
defineExpose({
open: () => (isModalOpenRef.value = true),
close: () => (isModalOpenRef.value = false),
toggle: () => (isModalOpenRef.value = !isModalOpenRef.value)
})
const isModalOpenRef = ref(props.isModalOpen) const isModalOpenRef = ref(props.isModalOpen)
const width = ref(props.modalWidth) const width = ref(props.modalWidth)
const height = ref(props.modalHeight) const height = ref(props.modalHeight)
@ -150,6 +156,11 @@ function drag(event: MouseEvent) {
y.value = dragState.initialY + (event.clientY - dragState.startY) y.value = dragState.initialY + (event.clientY - dragState.startY)
} }
function closeModal() {
isModalOpenRef.value = false
emit('modal:close')
}
function toggleFullScreen() { function toggleFullScreen() {
if (isFullScreen.value) { if (isFullScreen.value) {
Object.assign({ x, y, width, height }, preFullScreenState) Object.assign({ x, y, width, height }, preFullScreenState)

View File

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

View File

@ -1,34 +0,0 @@
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

@ -1,96 +1,101 @@
import { Assets } from '@/application/assets' import type { TextureData } from '@/application/types'
import config from '@/application/config' import { SpriteStorage } from '@/storage/storages'
import type { AssetDataT, HttpResponse, Sprite, SpriteAction } from '@/application/types' import { TextureStorage } from '@/storage/textureStorage'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
const textureLoadingPromises = new Map<string, Promise<boolean>>() const textureLoadingPromises = new Map<string, Promise<boolean>>()
export async function loadTexture(scene: Phaser.Scene, assetData: AssetDataT): Promise<boolean> { export async function loadTexture(scene: Phaser.Scene, textureData: TextureData): Promise<boolean> {
const gameStore = useGameStore() const gameStore = useGameStore()
const assetStorage = new Assets() const textureStorage = new TextureStorage()
// Check if the texture is already loaded in Phaser // Check if the texture is already loaded in Phaser
if (gameStore.game.loadedAssets.find((asset) => asset.key === assetData.key)) { if (gameStore.game.loadedTextures.find((texture) => texture === textureData.key)) {
return Promise.resolve(true) return true
} }
// If there's already a loading promise for this texture, return it // If there's already a loading promise for this texture, return it
if (textureLoadingPromises.has(assetData.key)) { if (textureLoadingPromises.has(textureData.key)) {
return await textureLoadingPromises.get(assetData.key)! return await textureLoadingPromises.get(textureData.key)!
} }
// Create new loading promise // Create new loading promise
const loadingPromise = (async () => { const loadingPromise = (async () => {
// Check if the asset is already cached // Check if the asset is already cached
let asset = await assetStorage.get(assetData.key) let texture = await textureStorage.get(textureData.key)
// If asset is not found, download it // If asset is not found, download it
if (!asset) { if (!texture) {
await assetStorage.download(assetData) await textureStorage.download(textureData)
asset = await assetStorage.get(assetData.key) texture = await textureStorage.get(textureData.key)
} }
// If asset is found, add it to the scene // If asset is found, add it to the scene
if (asset) { if (texture) {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
// Remove existing texture if it exists // Remove existing texture if it exists
if (scene.textures.exists(asset.key)) { if (scene.textures.exists(texture.key)) {
scene.textures.remove(asset.key) scene.textures.remove(texture.key)
} }
scene.textures.addBase64(asset.key, asset.data) scene.textures.addBase64(texture.key, texture.data)
scene.textures.once(`addtexture-${asset.key}`, () => { scene.textures.once(`addtexture-${texture.key}`, () => {
gameStore.game.loadedAssets.push(assetData) gameStore.game.loadedTextures.push(textureData.key)
textureLoadingPromises.delete(assetData.key) // Clean up the promise textureLoadingPromises.delete(textureData.key) // Clean up the promise
resolve(true) resolve(true)
}) })
}) })
} }
textureLoadingPromises.delete(assetData.key) // Clean up the promise textureLoadingPromises.delete(textureData.key) // Clean up the promise
return Promise.resolve(false) return false
})() })()
// Store the loading promise // Store the loading promise
textureLoadingPromises.set(assetData.key, loadingPromise) textureLoadingPromises.set(textureData.key, loadingPromise)
return loadingPromise return loadingPromise
} }
export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) { export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) {
if (!sprite_id) return if (!sprite_id) return false
// @TODO: Fix this const spriteStorage = new SpriteStorage()
const sprite_actions: HttpResponse<any[]> = await fetch(config.server_endpoint + '/assets/list_sprite_actions/' + sprite_id).then((response) => response.json()) const sprite = await spriteStorage.get(sprite_id)
for await (const sprite_action of sprite_actions.data ?? []) { if (!sprite) {
console.error('Failed to load sprite:', sprite_id)
return false
}
for await (const sprite_action of sprite.spriteActions) {
const key = sprite.id + '-' + sprite_action.action
await loadTexture(scene, { await loadTexture(scene, {
key: sprite_action.key, key,
data: sprite_action.data, data: '/textures/sprites/' + sprite.id + '/' + sprite_action.action + '.png',
group: sprite_action.isAnimated ? 'sprite_animations' : 'sprites', group: sprite_action.frameCount > 1 ? 'sprite_animations' : 'sprites',
updatedAt: sprite_action.updatedAt, updatedAt: sprite_action.updatedAt,
originX: sprite_action.originX ?? 0, originX: sprite_action.originX,
originY: sprite_action.originY ?? 0, originY: sprite_action.originY,
isAnimated: sprite_action.isAnimated,
frameWidth: sprite_action.frameWidth, frameWidth: sprite_action.frameWidth,
frameHeight: sprite_action.frameHeight, frameHeight: sprite_action.frameHeight,
frameRate: sprite_action.frameRate frameRate: sprite_action.frameRate
} as AssetDataT) } as TextureData)
// If the sprite is not animated, skip // If the sprite has no more than one frame, skip
if (!sprite_action.isAnimated) continue if (sprite_action.frameCount <= 1) continue
// Check if animation already exists // Check if animation already exists
if (scene.anims.get(sprite_action.key)) continue if (scene.anims.get(key)) continue
// Add the animation to the scene // Add the animation to the scene
const anim = scene.textures.get(sprite_action.key) const anim = scene.textures.get(key)
scene.textures.addSpriteSheet(sprite_action.key, anim, { frameWidth: sprite_action.frameWidth ?? 0, frameHeight: sprite_action.frameHeight ?? 0 }) scene.textures.addSpriteSheet(key, anim, { frameWidth: sprite_action.frameWidth ?? 0, frameHeight: sprite_action.frameHeight ?? 0 })
scene.anims.create({ scene.anims.create({
key: sprite_action.key, key: key,
frameRate: sprite_action.frameRate, frameRate: sprite_action.frameRate,
frames: scene.anims.generateFrameNumbers(sprite_action.key, { start: 0, end: sprite_action.frameCount! - 1 }), frames: scene.anims.generateFrameNumbers(key, { start: 0, end: sprite_action.frameCount! - 1 }),
repeat: -1 repeat: -1
}) })
} }
return Promise.resolve(true) return true
} }

Some files were not shown because too many files have changed in this diff Show More