Compare commits

..

107 Commits

Author SHA1 Message Date
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
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
63758e67b3 Removed redundant field 2025-01-01 23:01:58 +01:00
b51aa29bd8 Updated types 2025-01-01 20:59:38 +01:00
f9bfbdf735 Renamed property for better consistency 2025-01-01 20:46:02 +01:00
2abce7a7e7 Sorted imports 2025-01-01 19:05:24 +01:00
6ec9f8a7bc Loading char. texture works again 2025-01-01 18:16:31 +01:00
8191a039c9 Walking works again but needs to be improved 2025-01-01 17:50:07 +01:00
540425ca44 Minor change 2025-01-01 04:48:57 +01:00
1c2e642fe3 Typo 2025-01-01 02:59:26 +01:00
8355c83dc8 Renamed class 2024-12-31 16:15:28 +01:00
5fcb336835 Moved file 2024-12-30 17:44:56 +01:00
90bdf43b64 Small change 2024-12-30 02:47:49 +01:00
e9dfcf7870 Minor changes 2024-12-29 02:36:04 +01:00
d0c08c25fd #286 - Added global class for fully absolute-centering elements 2024-12-29 02:21:21 +01:00
7bb7af9476 #295 + #296 - Changed login boxes and char select styling 2024-12-29 02:10:18 +01:00
e4186a1bf5 WIP zone loading 2024-12-29 00:40:02 +01:00
8c664d7774 Started working on improved connect method 2024-12-28 23:48:52 +01:00
744df2e2dc Re-enabled vue dev tools, moved type into types.ts, added temp. logging 2024-12-28 17:27:00 +01:00
b4f9b11143 Removed console log 2024-12-27 19:04:33 +01:00
18b07d2f46 Several fixes, improvements, refactor for authentication 2024-12-27 19:00:53 +01:00
9d0f810ab3 Double field fix 2024-12-27 00:54:31 +01:00
cf3f17dfef Disabled vue dev tools for testing purposes 2024-12-27 00:54:23 +01:00
6be1134c8c Minor improvement 2024-12-27 00:49:57 +01:00
6dad7bc9dd http improvements, fixed link 2024-12-27 00:48:36 +01:00
231f19a30f Added characterChest component for chest equipment, moved some files 2024-12-27 00:27:54 +01:00
9c105d6df6 Returned data update 2024-12-26 23:55:47 +01:00
179ceb0ca0 Cleaned up assets, added default border values to main.scss 2024-12-26 20:03:42 +01:00
680661f07c #258 - Put update effects in the timeout corner until zoneEffects is ready
Reverted latest changes due to zoneEffects needing to fully overwrite
2024-12-25 00:40:56 +01:00
c54d2a2da8 Merge branch 'main' of ssh://gitea.directonline.io:29417/sylvan-quest/client 2024-12-24 00:54:27 +01:00
85f0fca2ae Added copy sprite button, changed asset manager layout, updated packages 2024-12-24 00:54:20 +01:00
420e63b724 #258 - Made it so zoneEffects only overrides defined effects instead of all 2024-12-23 23:23:16 +01:00
5d9b4fd19a #187 - Enter to focus chat when not focused 2024-12-22 20:56:06 +01:00
b3d68ef562 Merge branch 'main' of ssh://gitea.directonline.io:29417/sylvan-quest/client 2024-12-22 20:08:49 +01:00
baae737d6b CRUD components for items 2024-12-22 20:08:45 +01:00
03f8b327c5 #258 - Fixed zone effects when set in settings 2024-12-22 20:06:51 +01:00
b9a1ce5ab5 Adjusted sorting 2024-12-22 02:36:14 +01:00
1b650bd733 #16: Show updates made to character in real time 2024-12-22 00:13:15 +01:00
b867250580 Updated name 2024-12-21 22:10:37 +01:00
2c7a1e27be Fixes for origin being string, styling bug hair select and wrong label tags 2024-12-21 17:48:20 +01:00
0e455f8ffc Use originX and Y for hair 2024-12-21 03:00:09 +01:00
8005bc1318 Small fix 2024-12-21 02:29:48 +01:00
11e978121f Renamed frame speed > frame rate 2024-12-21 02:27:47 +01:00
727ca99b73 #262 : Use frameRate is value is set in sprite settings 2024-12-21 02:20:03 +01:00
97080d7380 Better anim. timing 2024-12-21 02:09:18 +01:00
1a3a53a229 Timing for animations 2024-12-20 21:29:33 +01:00
a926de8466 Hair anim. adjustment 2024-12-20 20:36:13 +01:00
5eabb39ec8 Improved hair animation 2024-12-20 01:28:48 +01:00
03313cb092 #259 - Changed bg options for modals, restyled GM Panel 2024-12-15 15:24:10 +01:00
d58cfa668d More finetuning of hair positioning 2024-12-15 14:40:47 +01:00
e3e40dd083 updated packages 2024-12-13 00:47:44 +01:00
facdd2d1b4 Minor styling changes 2024-12-13 00:47:36 +01:00
7d6bd39f29 GmPanel is full screen by default now 2024-12-12 01:12:33 +01:00
608932300f Improved proof of concept hair customisation & sprite anchor points 2024-12-12 00:42:56 +01:00
b5c1c92b04 Worked on character customisation 2024-12-10 02:22:03 +01:00
c68b129da8 #260 - Fix Characterprofile start position 2024-12-08 19:58:43 +01:00
963c593a1f #260 - Add min height when no character is selected 2024-12-08 19:45:11 +01:00
a299e22f88 #260 - added responsive styling character select 2024-12-08 19:29:07 +01:00
2007bfd7c5 Altered menu a bit 2024-12-08 00:53:07 +01:00
4cae045d0d Effect improvement 2024-12-08 00:35:00 +01:00
1fa8b8f06e You can now zoom in/out with key combination (shift + arrow uo/down)
my 5 EUR Action gaming mouse doesnt let me scroll on MacOS 💩
2024-12-07 23:24:11 +01:00
4095184b27 updated packages 2024-12-07 22:50:36 +01:00
857d56a878 Overriding zone effects now works 2024-12-07 22:50:27 +01:00
8087f754b0 Merge branch 'feature/#245-character-hair' of ssh://gitea.directonline.io:29417/sylvan-quest/client into feature/#245-character-hair 2024-12-05 09:56:17 +01:00
1479d96162 npm update, proof of concept code for character hair 2024-12-05 09:55:20 +01:00
606e220a9f Slightly adjusted styling radio hair color buttons 2024-11-27 20:39:09 +01:00
6988565484 Removed redundant code 2024-11-25 00:42:07 +01:00
fbc4a3dcdb Better typescript 2024-11-25 00:41:12 +01:00
924d5bdd13 Bug fixes and improvements to character select screen 2024-11-25 00:40:21 +01:00
25a2fd24f3 Prep hair color radio btns, adjust styling hairstyles to make it tab friendly 2024-11-24 20:46:29 +01:00
64f5ac45dd Removed console.log, renamed hars to characterHairs 2024-11-24 20:28:50 +01:00
937ce939d1 Preselect hairstyle that's set on character
Removed unused watch
2024-11-24 20:19:45 +01:00
7d89364104 Fix styling conditions for radio select
WIP
2024-11-24 17:15:57 +01:00
f7b8c235d8 Renamed hair > characterHair, split character logic (chat bubble, healthbar etc) into separate components for better DX 2024-11-24 15:13:11 +01:00
89d83efca4 Mess 2024-11-23 22:55:43 +01:00
ab97e27f27 Moved game components into a new folder, working proof of concept hair customisation 2024-11-23 16:47:41 +01:00
ee3e1b55cb Huehue wups 2024-11-23 15:43:57 +01:00
5e109e2a39 Removed ID from radio 2024-11-23 15:39:47 +01:00
a8e50c993a Commented out color swatch, added justify-center 2024-11-23 15:38:39 +01:00
ba8af589a7 Merge remote-tracking branch 'origin/feature/#245-character-hair' into feature/#250
# Conflicts:
#	src/components/screens/Characters.vue
2024-11-23 15:34:42 +01:00
301340327a Added components to manage hair types 2024-11-23 15:29:49 +01:00
1e4c58c79e npm update 2024-11-23 15:29:38 +01:00
f87cd063ee Center elements in characters screen 2024-11-23 15:29:28 +01:00
9593298389 #250 - Changed focus styling, adjusted where needed 2024-11-23 00:27:11 +01:00
ad4651844d Updated TS types 2024-11-21 03:00:09 +01:00
132 changed files with 4078 additions and 4017 deletions

View File

@ -1,5 +1,5 @@
VITE_NAME=Sylvan Quest 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

@ -4,5 +4,8 @@
"tabWidth": 2, "tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"printWidth": 300, "printWidth": 300,
"trailingComma": "none" "trailingComma": "none",
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
"importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy", "classProperties"],
"importOrderCaseSensitive": false
} }

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Sylvan Quest - Play</title> <title>Noxious - Play</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

2224
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@
"zod": "^3.22.2" "zod": "^3.22.2"
}, },
"devDependencies": { "devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@rushstack/eslint-patch": "^1.10.3", "@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",
@ -37,7 +38,6 @@
"@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",
"easystarjs": "^0.4.4",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0", "eslint-plugin-vue": "^9.27.0",
"jsdom": "^24.1.1", "jsdom": "^24.1.1",
@ -51,7 +51,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"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

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

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

BIN
public/assets/tlogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 302 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 400 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,4 +1,5 @@
<template> <template>
<Debug />
<Notifications /> <Notifications />
<BackgroundImageLoader /> <BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
@ -6,37 +7,43 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import Notifications from '@/components/utilities/Notifications.vue'
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue' import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Login from '@/components/screens/Login.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 ZoneEditor from '@/components/screens/ZoneEditor.vue' import Loading from '@/components/screens/Loading.vue'
import { computed, watch } from 'vue' import Login from '@/components/screens/Login.vue'
import MapEditor from '@/components/screens/MapEditor.vue'
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import Debug from '@/components/utilities/Debug.vue'
import Notifications from '@/components/utilities/Notifications.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, onUnmounted, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
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 (mapEditorStore.active) return MapEditor
return Game return Game
}) })
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets // Watch mapEditorStore.active and empty gameStore.game.loadedAssets
watch( watch(
() => zoneEditorStore.active, () => mapEditorStore.active,
() => { () => {
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

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

@ -1,119 +1,128 @@
export type UUID = `${string}-${string}-${string}-${string}-${string}`
export type Notification = { export type Notification = {
id?: string id?: string
title?: string title?: string
message?: string message?: string
} }
export type AssetDataT = { export type HttpResponse<T> = {
success: boolean
message?: string
data?: T
}
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
originY?: number
isAnimated?: boolean isAnimated?: boolean
frameCount?: number frameRate?: number
frameWidth?: number frameWidth?: number
frameHeight?: number frameHeight?: number
frameCount?: number
} }
export type Tile = { export type Tile = {
id: string id: UUID
name: string name: string
tags: any | null tags: any | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
} }
export type Object = { export type MapObject = {
id: string id: UUID
name: string name: string
tags: any | null tags: any | null
originX: number originX: number
originY: number originY: number
isAnimated: boolean isAnimated: boolean
frameSpeed: 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 = {
id: string id: UUID
name: string name: string
description: string | null description: string | null
itemType: ItemType
stackable: boolean stackable: boolean
rarity: ItemRarity
sprite?: Sprite
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
characters: CharacterItem[]
} }
export type Zone = { export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
id: number export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
export type Map = {
id: UUID
name: string name: string
width: number width: number
height: number height: number
tiles: any | null tiles: any | null
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: string id: UUID
zoneId: number map: Map
zone: Zone
effect: string effect: string
strength: number strength: number
} }
export type ZoneObject = { export type PlacedMapObject = {
id: string id: UUID
zoneId: number map: Map
zone: Zone mapObject: MapObject
objectId: string
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: string id: UUID
zoneId: number map: Map
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: string id: UUID
zoneEventTileId: string mapEventTile: MapEventTile
zoneEventTile: ZoneEventTile toMap: Map
toZoneId: number
toZone: Zone
toPositionX: number toPositionX: number
toPositionY: number toPositionY: number
toRotation: number toRotation: number
} }
export type User = { export type User = {
id: number id: UUID
username: string username: string
password: string password: string
characters: Character[] characters: Character[]
@ -133,21 +142,27 @@ export enum CharacterRace {
} }
export type CharacterType = { export type CharacterType = {
id: number id: UUID
name: string name: string
gender: CharacterGender gender: CharacterGender
race: CharacterRace race: CharacterRace
isEnabledForCharCreation: boolean isSelectable: boolean
characters: Character[]
spriteId?: string
sprite?: Sprite sprite?: Sprite
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
} }
export type CharacterHair = {
id: UUID
name: string
sprite?: Sprite
gender: CharacterGender
isSelectable: boolean
}
export type Character = { export type Character = {
id: number id: UUID
userId: number userId: UUID
user: User user: User
name: string name: string
hitpoints: number hitpoints: number
@ -159,34 +174,43 @@ export type Character = {
positionX: number positionX: number
positionY: number positionY: number
rotation: number rotation: number
zoneId: number characterType: UUID | null
zone: Zone characterHair: UUID | null
characterTypeId: number | null map: UUID
characterType: CharacterType | null
chats: Chat[] chats: Chat[]
items: CharacterItem[] items: CharacterItem[]
equipment: CharacterEquipment[]
} }
export type ZoneCharacter = { export type MapCharacter = {
character: Character character: Character
isMoving?: boolean isMoving: boolean
}
export type ExtendedCharacter = Character & {
isMoving?: boolean
} }
export type CharacterItem = { export type CharacterItem = {
id: number id: UUID
characterId: number
character: Character character: Character
itemId: string
item: Item item: Item
quantity: number quantity: number
} }
export type CharacterEquipment = {
id: UUID
slot: CharacterEquipmentSlotType
characterItem: CharacterItem
}
export enum CharacterEquipmentSlotType {
HEAD = 'HEAD',
BODY = 'BODY',
ARMS = 'ARMS',
LEGS = 'LEGS',
NECK = 'NECK',
RING = 'RING'
}
export type Sprite = { export type Sprite = {
id: string id: UUID
name: string name: string
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
@ -195,8 +219,7 @@ export type Sprite = {
} }
export type SpriteAction = { export type SpriteAction = {
id: string id: UUID
spriteId: string
sprite: Sprite sprite: Sprite
action: string action: string
sprites: string[] sprites: string[]
@ -206,15 +229,13 @@ export type SpriteAction = {
isLooping: boolean isLooping: boolean
frameWidth: number frameWidth: number
frameHeight: number frameHeight: number
frameSpeed: number frameRate: number
} }
export type Chat = { export type Chat = {
id: number id: UUID
characterId: number
character: Character character: Character
zoneId: number map: Map
zone: Zone
message: string message: string
createdAt: Date createdAt: Date
} }
@ -232,3 +253,8 @@ export type WeatherState = {
isFogEnabled: boolean isFogEnabled: boolean
fogDensity: number fogDensity: number
} }
export type mapLoadData = {
mapId: UUID
characters: MapCharacter[]
}

Binary file not shown.

View File

@ -23,6 +23,14 @@ body {
// Disable pinch zoom // Disable pinch zoom
touch-action: pan-x pan-y; touch-action: pan-x pan-y;
// Add custom focus outline
*:focus-visible {
@apply outline-gray-300;
@apply outline;
@apply outline-offset-2;
@apply rounded-sm;
}
} }
h1, h1,
@ -45,7 +53,7 @@ label {
button, button,
a { a {
@apply font-medium drop-shadow-20; @apply font-medium;
} }
button, button,
@ -58,13 +66,20 @@ input {
appearance: textfield; appearance: textfield;
} }
&[type='number']::-webkit-inner-spin-button, &[type='number']::-webkit-inner-spin-button,
&[type='number']::-webkit-outer-spin-button { &[type='number']::-webkit-outer-spin-button,
&[type='radio'] {
-webkit-appearance: none; -webkit-appearance: none;
} }
} }
.input-field { .input-field {
@apply px-4 py-2.5 text-base leading-5 focus-visible:outline-none bg-gray border border-solid border-gray-500 rounded text-gray-300; @apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300;
&:focus-visible {
@apply outline-none border-cyan rounded bg-gray-900;
}
&::placeholder {
@apply focus-visible:text-gray-300/50;
}
&.inactive { &.inactive {
@apply bg-gray-600/50 hover:cursor-not-allowed; @apply bg-gray-600/50 hover:cursor-not-allowed;
&::placeholder { &::placeholder {
@ -99,11 +114,11 @@ button {
} }
&.btn-red { &.btn-red {
@apply bg-red text-gray-50 text-base leading-5 rounded py-2.5; @apply bg-red-300 text-gray-50 text-base leading-5 rounded py-2.5;
&.active, &.active,
&:hover { &:hover {
@apply bg-red-300; @apply bg-red-400;
} }
} }
@ -113,7 +128,7 @@ button {
&.active, &.active,
&.selected, &.selected,
&:hover { &:hover {
@apply bg-gray-700 border-gray-700; @apply bg-gray border-gray;
} }
} }
@ -130,10 +145,22 @@ button {
} }
} }
.character { .character.active {
&.active { @apply bg-gray bg-none;
@apply pr-px border-r-0;
} }
.hair-deselect:has(:checked) {
img {
@apply brightness-200;
}
}
.default-border {
@apply border border-solid border-gray-500;
}
.center-element {
@apply absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2;
} }
.text-pixel { .text-pixel {

View File

@ -3,31 +3,34 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Scene } from 'phavuer' import type { WeatherState } from '@/application/types'
import { useZoneStore } from '@/stores/zoneStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { Scene } from 'phavuer'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue' import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { WeatherState } from '@/types'
// Constants // Constants
const SUNRISE_HOUR = 6 const LIGHT_CONFIG = {
const SUNSET_HOUR = 20 SUNRISE_HOUR: 6,
const DAY_STRENGTH = 100 SUNSET_HOUR: 20,
const NIGHT_STRENGTH = 30 DAY_STRENGTH: 100,
NIGHT_STRENGTH: 30
}
// Stores // Stores and refs
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneStore = useZoneStore() const mapStore = useMapStore()
// Scene ref
const sceneRef = ref<Phaser.Scene | null>(null) const sceneRef = ref<Phaser.Scene | null>(null)
const mapEffectsReady = ref(false)
// Effect refs // Effect objects
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null) const effects = {
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null) light: ref<Phaser.GameObjects.Graphics | null>(null),
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null) rain: ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null),
fog: ref<Phaser.GameObjects.Sprite | null>(null)
}
// State refs // Weather state
const weatherState = ref<WeatherState>({ const weatherState = ref<WeatherState>({
isRainEnabled: false, isRainEnabled: false,
rainPercentage: 0, rainPercentage: 0,
@ -35,36 +38,25 @@ const weatherState = ref<WeatherState>({
fogDensity: 0 fogDensity: 0
}) })
// Scene lifecycle methods // Scene setup
const preloadScene = async (scene: Phaser.Scene) => { const preloadScene = (scene: Phaser.Scene) => {
scene.load.image('raindrop', 'assets/raindrop.png') scene.load.image('raindrop', 'assets/raindrop.png')
scene.load.image('fog', 'assets/fog.png') scene.load.image('fog', 'assets/fog.png')
} }
const createScene = async (scene: Phaser.Scene) => { const createScene = (scene: Phaser.Scene) => {
sceneRef.value = scene sceneRef.value = scene
setupEffects(scene) initializeEffects(scene)
setupSocketListeners() setupSocketListeners()
} }
const updateScene = () => { const initializeEffects = (scene: Phaser.Scene) => {
updateEffects() // Light
} effects.light.value = scene.add.graphics().setDepth(1000)
// Effect setup // Rain
const setupEffects = (scene: Phaser.Scene) => { effects.rain.value = scene.add
createLightEffect(scene) .particles(0, 0, 'raindrop', {
createRainEffect(scene)
createFogEffect(scene)
}
const createLightEffect = (scene: Phaser.Scene) => {
lightEffect.value = scene.add.graphics()
lightEffect.value.setDepth(1000)
}
const createRainEffect = (scene: Phaser.Scene) => {
rainEmitter.value = scene.add.particles(0, 0, 'raindrop', {
x: { min: 0, max: window.innerWidth }, x: { min: 0, max: window.innerWidth },
y: -50, y: -50,
quantity: 5, quantity: 5,
@ -74,144 +66,113 @@ const createRainEffect = (scene: Phaser.Scene) => {
alpha: { start: 0.5, end: 0 }, alpha: { start: 0.5, end: 0 },
blendMode: 'ADD' blendMode: 'ADD'
}) })
rainEmitter.value.setDepth(900) .setDepth(900)
rainEmitter.value.stop() effects.rain.value.stop()
// Fog
effects.fog.value = scene.add
.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
.setScale(2)
.setAlpha(0)
.setDepth(950)
} }
const createFogEffect = (scene: Phaser.Scene) => { // Effect updates
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog') const updateScene = () => {
fogSprite.value.setScale(2) const timeBasedLight = calculateLightStrength(gameStore.world.date)
fogSprite.value.setAlpha(0) const mapEffects = mapStore.map?.mapEffects?.reduce(
fogSprite.value.setDepth(950) (acc, curr) => ({
...acc,
[curr.effect]: curr.strength
}),
{}
) as { [key: string]: number }
// Only update effects once mapEffects are loaded
if (!mapEffectsReady.value) {
if (mapEffects && Object.keys(mapEffects).length) {
mapEffectsReady.value = true
} else {
return
}
}
const finalEffects =
mapEffects && Object.keys(mapEffects).length
? mapEffects
: {
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)
}
} }
// Lighting calculations
const calculateLightStrength = (time: Date): number => { const calculateLightStrength = (time: Date): number => {
const hour = time.getHours() const hour = time.getHours()
const minute = time.getMinutes() const minute = time.getMinutes()
let strength = DAY_STRENGTH if (hour >= LIGHT_CONFIG.SUNSET_HOUR || hour < LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH
// Night time (10 PM - 6 AM) if (hour > LIGHT_CONFIG.SUNRISE_HOUR && hour < LIGHT_CONFIG.SUNSET_HOUR - 2) return LIGHT_CONFIG.DAY_STRENGTH
if (hour >= SUNSET_HOUR || hour < SUNRISE_HOUR) {
strength = NIGHT_STRENGTH if (hour === LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH + ((LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * minute) / 60
}
// Full daylight (7 AM - 7 PM) const totalMinutes = (hour - (LIGHT_CONFIG.SUNSET_HOUR - 2)) * 60 + minute
else if (hour > SUNRISE_HOUR && hour < SUNSET_HOUR - 2) { return LIGHT_CONFIG.DAY_STRENGTH - (LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * (totalMinutes / 120)
strength = DAY_STRENGTH
}
// Sunrise transition (6 AM - 7 AM)
else if (hour === SUNRISE_HOUR) {
strength = NIGHT_STRENGTH + ((DAY_STRENGTH - NIGHT_STRENGTH) * minute) / 60
}
// Sunset transition (8 PM - 10 PM)
else if (hour >= SUNSET_HOUR - 2 && hour < SUNSET_HOUR) {
const totalMinutes = (hour - (SUNSET_HOUR - 2)) * 60 + minute
const transitionProgress = totalMinutes / 120 // 2 hours = 120 minutes
strength = DAY_STRENGTH - (DAY_STRENGTH - NIGHT_STRENGTH) * transitionProgress
} }
return strength // Socket and window handlers
}
// Effect updates
const updateEffects = () => {
const effects = zoneStore.zone?.zoneEffects || []
if (effects.length > 0) {
updateZoneEffects(effects)
} else {
// Make sure we're getting the current time
const lightStrength = calculateLightStrength(gameStore.world.date)
updateLightEffect(lightStrength)
updateWeatherEffects()
}
}
const updateZoneEffects = (effects: any[]) => {
// Always update light based on time when zone effects are present
updateLightEffect(calculateLightStrength(gameStore.world.date))
effects.forEach((effect) => {
switch (effect.effect) {
case 'rain':
updateRainEffect(effect.strength)
break
case 'fog':
updateFogEffect(effect.strength)
break
}
})
}
const updateWeatherEffects = () => {
updateRainEffect(weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0)
updateFogEffect(weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0)
}
const updateLightEffect = (strength: number) => {
if (!lightEffect.value) return
const darkness = 1 - strength / 100
lightEffect.value.clear()
lightEffect.value.fillStyle(0x000000, darkness)
lightEffect.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
}
const updateRainEffect = (strength: number) => {
if (!rainEmitter.value) return
if (strength > 0) {
rainEmitter.value.start()
rainEmitter.value.setQuantity(Math.floor((strength / 100) * 10))
} else {
rainEmitter.value.stop()
}
}
const updateFogEffect = (strength: number) => {
if (!fogSprite.value) return
fogSprite.value.setAlpha(strength / 100)
}
// Socket handlers
const setupSocketListeners = () => { const setupSocketListeners = () => {
// Initial weather state
gameStore.connection?.emit('weather', (response: WeatherState) => { gameStore.connection?.emit('weather', (response: WeatherState) => {
if (zoneStore.zone?.zoneEffects) return
weatherState.value = response weatherState.value = response
updateEffects() updateScene()
}) })
// Weather updates
gameStore.connection?.on('weather', (data: WeatherState) => { gameStore.connection?.on('weather', (data: WeatherState) => {
weatherState.value = data weatherState.value = data
updateEffects() updateScene()
}) })
// Time updates gameStore.connection?.on('date', updateScene)
gameStore.connection?.on('date', () => {
if (zoneStore.zone?.zoneEffects) return
updateEffects()
})
} }
const updateEffectWindowSize = () => { const handleResize = () => {
if (rainEmitter.value) rainEmitter.value.updateConfig({ x: { min: 0, max: window.innerWidth } }) if (effects.rain.value) effects.rain.value.updateConfig({ x: { min: 0, max: window.innerWidth } })
if (fogSprite.value) { if (effects.fog.value) effects.fog.value.setPosition(window.innerWidth / 2, window.innerHeight / 2)
fogSprite.value.setX(window.innerWidth / 2)
fogSprite.value.setY(window.innerHeight / 2)
}
} }
// Watchers // Lifecycle
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true }) watch(
() => mapStore.map,
() => {
mapEffectsReady.value = false
updateScene()
},
{ deep: true }
)
onMounted(() => { onMounted(() => window.addEventListener('resize', handleResize))
window.addEventListener('resize', updateEffectWindowSize)
})
// Cleanup
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', updateEffectWindowSize) window.removeEventListener('resize', handleResize)
if (sceneRef.value) sceneRef.value.scene.remove('effects') if (sceneRef.value) sceneRef.value.scene.remove('effects')
gameStore.connection?.off('weather') gameStore.connection?.off('weather')
}) })

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

@ -0,0 +1,184 @@
<template>
<ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<Container ref="charContainer" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY">
<!-- <CharacterHair :mapCharacter="props.mapCharacter" :currentX="currentX" :currentY="currentY" />-->
<!-- <CharacterChest :mapCharacter="props.mapCharacter" :currentX="currentX" :currentY="currentY" />-->
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
</Container>
</template>
<script lang="ts" setup>
import config from '@/application/config'
import { type MapCharacter, type Sprite as SpriteT } from '@/application/types'
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
import Healthbar from '@/components/game/character/partials/Healthbar.vue'
import { loadSpriteTextures } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { CharacterTypeStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { Container, refObj, Sprite, useScene } from 'phavuer'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
// import CharacterChest from '@/components/game/character/partials/CharacterChest.vue'
enum Direction {
POSITIVE,
NEGATIVE,
UNCHANGED
}
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
mapCharacter: MapCharacter
}>()
const charContainer = refObj<Phaser.GameObjects.Container>()
const charSprite = refObj<Phaser.GameObjects.Sprite>()
const charSpriteId = ref('')
const gameStore = useGameStore()
const mapStore = useMapStore()
const scene = useScene()
const currentPositionX = ref(0)
const currentPositionY = ref(0)
const isometricDepth = ref(1)
const isInitialPosition = ref(true)
const tween = ref<Phaser.Tweens.Tween | null>(null)
const updateIsometricDepth = (positionX: number, positionY: number) => {
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true)
}
const updatePosition = (positionX: number, positionY: number, direction: Direction) => {
const newPositionX = tileToWorldX(props.tilemap, positionX, positionY)
const newPositionY = tileToWorldY(props.tilemap, positionX, positionY)
if (isInitialPosition.value) {
currentPositionX.value = newPositionX
currentPositionY.value = newPositionY
isInitialPosition.value = false
return
}
if (tween.value?.isPlaying()) {
tween.value.stop()
}
const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2))
if (distance >= config.tile_size.width / 1.1) {
currentPositionX.value = newPositionX
currentPositionY.value = newPositionY
return
}
const duration = distance * 5.7
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 charTexture = computed(() => {
const spriteId = charSpriteId.value ?? 'idle_right_down'
const action = props.mapCharacter.isMoving ? 'walk' : 'idle'
const direction = [0, 6].includes(props.mapCharacter.character.rotation) ? 'left_up' : 'right_down'
return `${spriteId}-${action}_${direction}`
})
const updateSprite = () => {
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)
}
}
watch(
() => ({
positionX: props.mapCharacter.character.positionX,
positionY: props.mapCharacter.character.positionY,
isMoving: props.mapCharacter.isMoving,
rotation: props.mapCharacter.character.rotation
}),
(newValues, oldValues) => {
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)
}
// Handle animation updates
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite()
}
}
)
const characterTypeStorage = new CharacterTypeStorage()
characterTypeStorage.getSpriteId(props.mapCharacter.character.characterType!).then((spriteId) => {
console.log(spriteId)
charSpriteId.value = spriteId
loadSpriteTextures(scene, spriteId)
.then(() => {
charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value)
})
.catch((error) => {
console.error('Error loading texture:', error)
})
})
onMounted(() => {
charContainer.value!.setName(props.mapCharacter.character!.name)
if (props.mapCharacter.character.id === gameStore.character!.id) {
mapStore.setCharacterLoaded(true)
// #146 : Set camera position to character, need to be improved still
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
}
updatePosition(props.mapCharacter.character.positionX, props.mapCharacter.character.positionY, props.mapCharacter.character.rotation)
})
onUnmounted(() => {
tween.value?.stop()
})
</script>

View File

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

View File

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

View File

@ -0,0 +1,47 @@
<template>
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
</Container>
</template>
<script setup lang="ts">
import type { MapCharacter } from '@/application/types'
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
import { onMounted } from 'vue'
const props = defineProps<{
mapCharacter: MapCharacter
currentX: number
currentY: number
}>()
const game = useGame()
const charChatContainer = refObj<Phaser.GameObjects.Container>()
const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
}
const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(`${props.mapCharacter.character.name}_chatText`)
text.setFontSize(13)
text.setFontFamily('Arial')
text.setOrigin(0.5, 10.9)
text.setResolution(2)
// Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) {
text.setOrigin(0.5, 9.75)
if (game.device.browser.firefox) {
text.setOrigin(0.5, 10.9)
}
}
}
onMounted(() => {
charChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false)
})
</script>

View File

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

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" />
<!-- <MapObjects 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 { MapCharacter, mapLoadData, UUID } from '@/application/types'
import Characters from '@/components/game/map/Characters.vue'
import MapTiles from '@/components/game/map/MapTiles.vue'
import MapObjects 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>()
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')
})
// 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)
})
</script>

View File

@ -0,0 +1,75 @@
<template>
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities'
import Controls from '@/components/utilities/Controls.vue'
import { FlattenMapArray, loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable'
import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore'
import { useScene } from 'phavuer'
import { onBeforeUnmount, onMounted, 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(mapData: any) {
const mapConfig = new Phaser.Tilemaps.MapData({
width: mapData?.width,
height: mapData?.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(FlattenMapArray(mapData?.tiles ?? []))
const tilesetImages = tilesArray.map((tile: any, 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
}
onMounted(() => {
loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
.then(() => mapStorage.get(mapStore.mapId))
.then((mapData) => {
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,14 @@
<template>
<PlacedMapObject v-for="placedMapObject in mapStore.map?.placedMapObjects" :tilemap="tilemap" :placedMapObject />
</template>
<script setup lang="ts">
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
import { useMapStore } from '@/stores/mapStore'
const mapStore = useMapStore()
defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
</script>

View File

@ -0,0 +1,41 @@
<template>
<Image v-if="gameStore.isAssetLoaded(props.placedMapObject.mapObject)" 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
}>()
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)
})
</script>

View File

@ -1,12 +1,12 @@
<template> <template>
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :can-full-screen="true" :disable-bg-texture="true"> <Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :is-full-screen="true" :bg-style="'dark'">
<template #modalHeader> <template #modalHeader>
<div class="flex gap-1.5 flex-wrap"> <div class="flex gap-1.5 flex-wrap">
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</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">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()">Zone manager</button> <button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => mapEditorStore.toggleActive()">Map editor</button>
</div> </div>
</template> </template>
<template #modalBody> <template #modalBody>
@ -18,14 +18,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import Modal from '@/components/utilities/Modal.vue'
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue' import AssetManager from '@/components/gameMaster/assetManager/AssetManager.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 { ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
let toggle = ref('asset-manager') let toggle = ref('asset-manager')
</script> </script>

View File

@ -1,85 +1,79 @@
<template> <template>
<div class="flex h-full w-full relative"> <div class="flex gap-4 h-[calc(100%_-_32px)] w-[calc(100%_-_32px)] relative m-4">
<div class="w-2/12 flex flex-col relative overflow-auto"> <div class="w-2/12 flex flex-col relative overflow-auto rounded-md default-border bg-gray p-2.5">
<!-- Asset Categories --> <!-- Asset Categories -->
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': 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="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': 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="{ 'text-white': selectedCategory === 'objects' }">Objects</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'map_objects' }">Map objects</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': 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="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'items' }" @click="() => (selectedCategory = 'items')">
<span>Items</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'items' }">Items</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span>NPC's</span> <span class="group-hover:text-white">NPC's</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
<span :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
<span :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
<span :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span>Mounts</span> <span class="group-hover:text-white">Mounts</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span>Pets</span> <span class="group-hover:text-white">Pets</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span>Emoticons</span> <span class="group-hover:text-white">Emoticons</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
</div> </div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
<!-- 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'" />
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" /> <CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
<CharacterHairList v-if="selectedCategory === 'characterHair'" />
</div> </div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
<!-- Asset details --> <!-- Asset details -->
<div class="flex w-1/2 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" />
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" /> <CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
<CharacterHairDetails v-if="selectedCategory === 'characterHair' && assetManagerStore.selectedCharacterHair" />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import CharacterHairDetails from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairDetails.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import CharacterHairList from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairList.vue'
import TileList from '@/components/gameMaster/assetManager/partials/tile/TileList.vue'
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
import ObjectList from '@/components/gameMaster/assetManager/partials/object/ObjectList.vue'
import ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.vue' import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.vue'
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
import MapObjectDetails from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectDetails.vue'
import MapObjectList from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectList.vue'
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
import TileList from '@/components/gameMaster/assetManager/partials/tile/TileList.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { ref } from 'vue'
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
const selectedCategory = ref('tiles') const selectedCategory = ref('tiles')

View File

@ -0,0 +1,124 @@
<template>
<div class="h-full overflow-auto">
<div class="p-2.5 block rounded-md default-border bg-gray">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterHair">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="characterName" class="input-field" type="text" name="name" placeholder="Character Type Name" />
</div>
<div class="form-field-full">
<label for="gender">Gender</label>
<select v-model="characterGender" class="input-field" name="gender">
<option v-for="gender in genderOptions" :key="gender" :value="gender">{{ gender }}</option>
</select>
</div>
<div class="form-field-full">
<label for="isSelectable">Is selectable</label>
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="spriteId">Sprite</label>
<select v-model="characterSpriteId" class="input-field" name="spriteId">
<option disabled selected value="">Select sprite</option>
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
</select>
</div>
<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="removeCharacterHair">Remove</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
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 selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
const characterName = ref('')
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
const characterIsSelectable = ref<boolean>(false)
const characterSpriteId = ref<string | null | undefined>(null)
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
if (!selectedCharacterHair.value) {
console.error('No character hair selected')
}
if (selectedCharacterHair.value) {
characterName.value = selectedCharacterHair.value.name
characterGender.value = selectedCharacterHair.value.gender
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
}
function removeCharacterHair() {
if (!selectedCharacterHair.value) return
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove character hair')
return
}
refreshCharacterHairList()
})
}
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response)
if (unsetSelectedCharacterHair) {
assetManagerStore.setSelectedCharacterHair(null)
}
})
}
function saveCharacterHair() {
const characterHairData = {
id: selectedCharacterHair.value!.id,
name: characterName.value,
gender: characterGender.value,
isSelectable: characterIsSelectable.value,
spriteId: characterSpriteId.value
}
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
if (!response) {
console.error('Failed to save character type')
return
}
refreshCharacterHairList(false)
})
}
watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
if (!characterHair) return
characterName.value = characterHair.name
characterGender.value = characterHair.gender
characterIsSelectable.value = characterHair.isSelectable
characterSpriteId.value = characterHair.sprite?.id
})
onMounted(() => {
if (!selectedCharacterHair.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedCharacterHair(null)
})
</script>

View File

@ -0,0 +1,100 @@
<template>
<div class="relative mb-5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<button class="p-0 h-5" id="create-character" @click="createNewCharacterHair">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</button>
</label>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a
v-for="{ data: characterHair } in list"
:key="characterHair.id"
class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group"
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterHair?.id === characterHair.id }"
@click="assetManagerStore.setSelectedCharacterHair(characterHair as CharacterHair)"
>
<div class="flex items-center gap-2.5">
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterHair?.id === characterHair.id }">{{ characterHair.name }}</span>
</div>
</a>
</div>
<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">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { CharacterHair } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const searchQuery = ref('')
const hasScrolled = ref(false)
const elementToScroll = ref()
const handleSearch = () => {
// Trigger a re-render of the virtual list
virtualList.value?.scrollTo(0)
}
const createNewCharacterHair = () => {
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => {
if (!response) {
console.error('Failed to create new character type')
return
}
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response)
})
})
}
const filteredCharacterHairs = computed(() => {
if (!searchQuery.value) {
return assetManagerStore.characterHairList
}
return assetManagerStore.characterHairList.filter((character) => character.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredCharacterHairs, {
itemHeight: 48
})
const virtualList = ref({ scrollTo })
const onScroll = () => {
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
if (scrollTop > 80) {
hasScrolled.value = true
} else if (scrollTop <= 80) {
hasScrolled.value = false
}
}
function toTop() {
virtualList.value?.scrollTo(0)
}
onMounted(() => {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
console.log(response)
assetManagerStore.setCharacterHairList(response)
})
})
</script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="h-full overflow-auto"> <div class="h-full overflow-auto">
<div class="m-2.5 p-2.5 block"> <div class="p-2.5 block rounded-md default-border bg-gray">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType"> <form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
<div class="form-field-full"> <div class="form-field-full">
<label for="name">Name</label> <label for="name">Name</label>
@ -19,8 +19,8 @@
</select> </select>
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<label for="isEnabledForCharCreation">Is enabled for character creation</label> <label for="isSelectable">Is selectable</label>
<select v-model="characterIsEnabledForCharCreation" class="input-field" name="isEnabledForCharCreation"> <select v-model="characterIsSelectable" class="input-field" name="isSelectable">
<option :value="false">No</option> <option :value="false">No</option>
<option :value="true">Yes</option> <option :value="true">Yes</option>
</select> </select>
@ -40,10 +40,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { CharacterType, CharacterGender, CharacterRace, Sprite } from '@/types' import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
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'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
@ -53,7 +53,7 @@ const selectedCharacterType = computed(() => assetManagerStore.selectedCharacter
const characterName = ref('') const characterName = ref('')
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE) const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN) const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN)
const characterIsEnabledForCharCreation = ref<boolean>(false) const characterIsSelectable = ref<boolean>(false)
const characterSpriteId = ref<string | null | undefined>(null) const characterSpriteId = ref<string | null | undefined>(null)
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE] const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
@ -67,8 +67,8 @@ if (selectedCharacterType.value) {
characterName.value = selectedCharacterType.value.name characterName.value = selectedCharacterType.value.name
characterGender.value = selectedCharacterType.value.gender characterGender.value = selectedCharacterType.value.gender
characterRace.value = selectedCharacterType.value.race characterRace.value = selectedCharacterType.value.race
characterIsEnabledForCharCreation.value = selectedCharacterType.value.isEnabledForCharCreation characterIsSelectable.value = selectedCharacterType.value.isSelectable
characterSpriteId.value = selectedCharacterType.value.spriteId characterSpriteId.value = selectedCharacterType.value.sprite?.id
} }
function removeCharacterType() { function removeCharacterType() {
@ -99,7 +99,7 @@ function saveCharacterType() {
name: characterName.value, name: characterName.value,
gender: characterGender.value, gender: characterGender.value,
race: characterRace.value, race: characterRace.value,
isEnabledForCharCreation: characterIsEnabledForCharCreation.value, isSelectable: characterIsSelectable.value,
spriteId: characterSpriteId.value spriteId: characterSpriteId.value
} }
@ -117,8 +117,8 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
characterName.value = characterType.name characterName.value = characterType.name
characterGender.value = characterType.gender characterGender.value = characterType.gender
characterRace.value = characterType.race characterRace.value = characterType.race
characterIsEnabledForCharCreation.value = characterType.isEnabledForCharCreation characterIsSelectable.value = characterType.isSelectable
characterSpriteId.value = characterType.spriteId characterSpriteId.value = characterType.sprite?.id
}) })
onMounted(() => { onMounted(() => {

View File

@ -1,36 +1,42 @@
<template> <template>
<div class="relative p-2.5 flex items-center gap-x-2.5"> <div class="relative mb-5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" /> <input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer"> <label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<button class="p-0 h-5" id="create-character" @click="createNewCharacterType"> <button class="p-0 h-5" id="create-character" @click="createNewCharacterType">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
</button> </button>
</label> </label>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll"> <div v-bind="containerProps" class="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: characterType } in list" :key="characterType.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedCharacterType?.id === characterType.id }" @click="assetManagerStore.setSelectedCharacterType(characterType as CharacterType)"> <a
v-for="{ data: characterType } in list"
:key="characterType.id"
class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group"
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterType?.id === characterType.id }"
@click="assetManagerStore.setSelectedCharacterType(characterType as CharacterType)"
>
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<span :class="{ 'text-white': assetManagerStore.selectedCharacterType?.id === characterType.id }">{{ characterType.name }}</span> <span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterType?.id === characterType.id }">{{ characterType.name }}</span>
</div> </div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
</div> </div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <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/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useGameStore } from '@/stores/gameStore' import type { CharacterType } from '@/application/types'
import { onMounted, ref, computed } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { CharacterType } from '@/types' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
@ -87,6 +93,7 @@ function toTop() {
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => { gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
console.log(response)
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
}) })
}) })

View File

@ -0,0 +1,143 @@
<template>
<div class="h-full overflow-auto">
<div class="p-2.5 block rounded-md default-border bg-gray">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveItem">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="itemName" class="input-field" type="text" name="name" placeholder="Item Name" />
</div>
<div class="form-field-full">
<label for="description">Description</label>
<input v-model="itemDescription" class="input-field" type="text" name="description" placeholder="Item Description" />
</div>
<div class="form-field-full">
<label for="itemType">Type</label>
<select v-model="itemType" class="input-field" name="itemType">
<option v-for="type in itemTypeOptions" :key="type" :value="type">{{ type }}</option>
</select>
</div>
<div class="form-field-full">
<label for="rarity">Rarity</label>
<select v-model="itemRarity" class="input-field" name="rarity">
<option v-for="rarity in rarityOptions" :key="rarity" :value="rarity">{{ rarity }}</option>
</select>
</div>
<div class="form-field-full">
<label for="stackable">Stackable</label>
<select v-model="itemStackable" class="input-field" name="stackable">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="spriteId">Sprite</label>
<select v-model="itemSpriteId" class="input-field" name="spriteId">
<option disabled selected value="">Select sprite</option>
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
</select>
</div>
<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="removeItem">Remove</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
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 selectedItem = computed(() => assetManagerStore.selectedItem)
const itemName = ref('')
const itemDescription = ref('')
const itemType = ref<ItemType>('WEAPON' as ItemType)
const itemRarity = ref<ItemRarity>('COMMON' as ItemRarity)
const itemStackable = ref<boolean>(false)
const itemSpriteId = ref<string | null | undefined>(null)
const itemTypeOptions: ItemType[] = ['WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE']
const rarityOptions: ItemRarity[] = ['COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY']
if (!selectedItem.value) {
console.error('No item selected')
}
if (selectedItem.value) {
itemName.value = selectedItem.value.name
itemDescription.value = selectedItem.value.description || ''
itemType.value = selectedItem.value.itemType
itemRarity.value = selectedItem.value.rarity
itemStackable.value = selectedItem.value.stackable
itemSpriteId.value = selectedItem.value.spriteId
}
function removeItem() {
if (!selectedItem.value) return
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove item')
return
}
refreshItemList()
})
}
function refreshItemList(unsetSelectedItem = true) {
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
assetManagerStore.setItemList(response)
if (unsetSelectedItem) {
assetManagerStore.setSelectedItem(null)
}
})
}
function saveItem() {
const itemData = {
id: selectedItem.value!.id,
name: itemName.value,
description: itemDescription.value,
itemType: itemType.value,
rarity: itemRarity.value,
stackable: itemStackable.value,
spriteId: itemSpriteId.value
}
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => {
if (!response) {
console.error('Failed to save item')
return
}
refreshItemList(false)
})
}
watch(selectedItem, (item: Item | null) => {
if (!item) return
itemName.value = item.name
itemDescription.value = item.description || ''
itemType.value = item.itemType
itemRarity.value = item.rarity
itemStackable.value = item.stackable
itemSpriteId.value = item.spriteId
})
onMounted(() => {
if (!selectedItem.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedItem(null)
})
</script>

View File

@ -0,0 +1,95 @@
<template>
<div class="relative mb-5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="create-item" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<button class="p-0 h-5" id="create-item" @click="createNewItem">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</button>
</label>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<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)">
<div class="flex items-center gap-2.5">
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
{{ item.name }}
<small class="text-gray-400">({{ item.itemType }})</small>
</span>
</div>
</a>
</div>
<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">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { Item } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const searchQuery = ref('')
const hasScrolled = ref(false)
const elementToScroll = ref()
const handleSearch = () => {
virtualList.value?.scrollTo(0)
}
const createNewItem = () => {
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => {
if (!response) {
console.error('Failed to create new item')
return
}
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
assetManagerStore.setItemList(response)
})
})
}
const filteredItems = computed(() => {
if (!searchQuery.value) {
return assetManagerStore.itemList
}
return assetManagerStore.itemList.filter((item) => item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || item.itemType.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredItems, {
itemHeight: 48
})
const virtualList = ref({ scrollTo })
const onScroll = () => {
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
if (scrollTop > 80) {
hasScrolled.value = true
} else if (scrollTop <= 80) {
hasScrolled.value = false
}
}
function toTop() {
virtualList.value?.scrollTo(0)
}
onMounted(() => {
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
assetManagerStore.setItemList(response)
})
})
</script>

View File

@ -0,0 +1,163 @@
<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="is-animated">Is animated</label>
<select v-model="mapObjectIsAnimated" 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="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 { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const mapEditorStore = useMapEditorStore()
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
const mapObjectName = ref('')
const mapObjectTags = ref<string[]>([])
const mapObjectOriginX = ref(0)
const mapObjectOriginY = ref(0)
const mapObjectIsAnimated = ref(false)
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
mapObjectIsAnimated.value = selectedMapObject.value.isAnimated
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)
}
if (mapEditorStore.active) {
mapEditorStore.setMapObjectList(response)
}
})
}
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,
isAnimated: mapObjectIsAnimated.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
mapObjectIsAnimated.value = mapObject.isAnimated
mapObjectFrameRate.value = mapObject.frameRate
mapObjectFrameWidth.value = mapObject.frameWidth
mapObjectFrameHeight.value = mapObject.frameHeight
})
onMounted(() => {
if (!selectedMapObject.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedMapObject(null)
})
</script>

View File

@ -1,39 +1,39 @@
<template> <template>
<div class="relative p-2.5 flex items-center gap-x-2.5"> <div class="relative mb-5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" /> <input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer"> <label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" /> <input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
</label> </label>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll"> <div v-bind="containerProps" class="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" :class="{ 'bg-cyan/80': 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>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
</div> </div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <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/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/application/config'
import { useGameStore } from '@/stores/gameStore' import type { MapObject } from '@/application/types'
import { onMounted, ref, computed } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { Object } from '@/types' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const objectUploadField = ref(null) const objectUploadField = ref(null)
@ -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-between h-72">
<div class="filler"></div>
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div class="m-2.5 p-2.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="origin-x">Tags</label>
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
</div>
<div class="form-field-full">
<label for="origin-x">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 speed</label>
<input v-model="objectFrameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
</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>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { Object } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import config from '@/config'
import ChipsInput from '@/components/forms/ChipsInput.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 objectFrameSpeed = 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
objectFrameSpeed.value = selectedObject.value.frameSpeed
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,
frameSpeed: objectFrameSpeed.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
objectFrameSpeed.value = object.frameSpeed
objectFrameWidth.value = object.frameWidth
objectFrameHeight.value = object.frameHeight
})
onMounted(() => {
if (!selectedObject.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedObject(null)
})
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="h-full overflow-auto"> <div class="h-full overflow-auto">
<div class="relative p-4 flex flex-col"> <div class="relative flex flex-col">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<label class="mb-1.5 font-titles" for="name">Name</label> <label class="mb-1.5 font-titles" for="name">Name</label>
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" /> <input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
@ -10,7 +10,11 @@
<div class="w-full flex gap-2 mt-2 pb-4 relative"> <div class="w-full flex gap-2 mt-2 pb-4 relative">
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button> <button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button> <button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
<div class="w-[calc(100%_+_32px)] absolute left-[-15px] bottom-0 h-px bg-gray-500"></div> <button class="btn bg-indigo-500 hover:bg-indigo-600 rounded text-white px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div> </div>
</div> </div>
@ -51,8 +55,8 @@
</select> </select>
</div> </div>
<div class="form-field-full" v-if="action.isAnimated"> <div class="form-field-full" v-if="action.isAnimated">
<label for="frame-speed">Frame speed</label> <label for="frame-speed">Frame rate</label>
<input v-model.number="action.frameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" /> <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" />
@ -65,13 +69,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Sprite, SpriteAction } from '@/types' import type { Sprite, SpriteAction } from '@/application/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { uuidv4 } from '@/application/utilities'
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.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'
import Accordion from '@/components/utilities/Accordion.vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
import { uuidv4 } from '@/utilities'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
@ -87,7 +91,7 @@ if (!selectedSprite.value) {
if (selectedSprite.value) { if (selectedSprite.value) {
spriteName.value = selectedSprite.value.name spriteName.value = selectedSprite.value.name
spriteActions.value = selectedSprite.value.spriteActions spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
} }
function deleteSprite() { function deleteSprite() {
@ -100,6 +104,16 @@ function deleteSprite() {
}) })
} }
function copySprite() {
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => {
if (!response) {
console.error('Failed to copy sprite')
return
}
refreshSpriteList(false)
})
}
function refreshSpriteList(unsetSelectedSprite = true) { function refreshSpriteList(unsetSelectedSprite = true) {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
@ -128,7 +142,7 @@ function saveSprite() {
originY: action.originY, originY: action.originY,
isAnimated: action.isAnimated, isAnimated: action.isAnimated,
isLooping: action.isLooping, isLooping: action.isLooping,
frameSpeed: action.frameSpeed, frameRate: action.frameRate,
frameWidth: action.frameWidth, frameWidth: action.frameWidth,
frameHeight: action.frameHeight frameHeight: action.frameHeight
} }
@ -148,7 +162,7 @@ function addNewImage() {
if (!selectedSprite.value) return if (!selectedSprite.value) return
const newImage: SpriteAction = { const newImage: SpriteAction = {
id: uuidv4(), // Temporary ID, should be replaced by server-generated ID id: uuidv4(),
spriteId: selectedSprite.value.id, spriteId: selectedSprite.value.id,
sprite: selectedSprite.value, sprite: selectedSprite.value,
action: 'new_action', action: 'new_action',
@ -157,7 +171,7 @@ function addNewImage() {
originY: 0, originY: 0,
isAnimated: false, isAnimated: false,
isLooping: false, isLooping: false,
frameSpeed: 0, frameRate: 0,
frameWidth: 0, frameWidth: 0,
frameHeight: 0 frameHeight: 0
} }
@ -166,13 +180,18 @@ function addNewImage() {
spriteActions.value = [] spriteActions.value = []
} }
spriteActions.value.push(newImage) spriteActions.value = sortSpriteActions([...spriteActions.value, newImage])
}
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
if (!actions) return []
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
} }
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 = sprite.spriteActions spriteActions.value = sortSpriteActions(sprite.spriteActions)
}) })
onMounted(() => { onMounted(() => {

View File

@ -1,35 +1,35 @@
<template> <template>
<div class="relative p-2.5 flex items-center gap-x-2.5"> <div class="relative mb-5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" /> <input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer"> <button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
</button> </button>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll"> <div v-bind="containerProps" class="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" :class="{ 'bg-cyan/80': 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>
</div> </div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
</div> </div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <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/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/application/config'
import { useGameStore } from '@/stores/gameStore' import type { Sprite } from '@/application/types'
import { onMounted, ref, computed } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
import type { Sprite } from '@/types' import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()

View File

@ -1,14 +1,21 @@
<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-50 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" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
<button @click.stop="deleteImage(index)" class="absolute top-1 right-1 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"> <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">
<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">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</div> </div>
<div class="h-20 w-20 p-4 bg-gray-100 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent> </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>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg> </svg>

View File

@ -1,12 +1,9 @@
<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-between h-72"> <div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<div class="filler"></div> <img class="max-h-72" :src="`${config.server_endpoint}/textures/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div> </div>
<div class="m-2.5 p-2.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">
<div class="form-field-full"> <div class="form-field-full">
<label for="name">Name</label> <label for="name">Name</label>
@ -16,24 +13,27 @@
<label for="origin-x">Tags</label> <label for="origin-x">Tags</label>
<ChipsInput v-model="tileTags" @update:modelValue="tileTags = $event" /> <ChipsInput v-model="tileTags" @update:modelValue="tileTags = $event" />
</div> </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-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="deleteTile">Delete</button>
</div>
</form> </form>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Tile } from '@/types' import config from '@/application/config'
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue' import type { Tile } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import config from '@/config'
import ChipsInput from '@/components/forms/ChipsInput.vue' import ChipsInput from '@/components/forms/ChipsInput.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
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 mapEditorStore = useMapEditorStore()
const selectedTile = computed(() => assetManagerStore.selectedTile) const selectedTile = computed(() => assetManagerStore.selectedTile)
@ -73,8 +73,8 @@ function refreshTileList(unsetSelectedTile = true) {
assetManagerStore.setSelectedTile(null) assetManagerStore.setSelectedTile(null)
} }
if (zoneEditorStore.active) { if (mapEditorStore.active) {
zoneEditorStore.setTileList(response) mapEditorStore.setTileList(response)
} }
}) })
} }

View File

@ -1,39 +1,39 @@
<template> <template>
<div class="relative p-2.5 flex items-center gap-x-2.5"> <div class="relative mb-5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" /> <input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer"> <label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" /> <input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
</label> </label>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll"> <div v-bind="containerProps" class="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" :class="{ 'bg-cyan/80': 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="{ '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>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>
</div> </div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" /> <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/mapEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/application/config'
import { useGameStore } from '@/stores/gameStore' import type { Tile } from '@/application/types'
import { onMounted, ref, computed } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { Tile } from '@/types' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const tileUploadField = ref(null) const tileUploadField = ref(null)

View File

@ -0,0 +1,75 @@
<template>
<MapTiles @tileMap:create="tileMap = $event" />
<MapObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<MapEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Toolbar @save="save" @clear="clear" />
<MapList />
<TileList />
<ObjectList />
<MapSettings />
<TeleportModal />
</template>
<script setup lang="ts">
import { type Map } from '@/application/types'
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import MapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.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'
// Components
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onUnmounted, ref, shallowRef } from 'vue'
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
function clear() {
if (!mapEditorStore.map) return
// Clear objects, event tiles and tiles
mapEditorStore.map.placedMapObjects = []
mapEditorStore.map.mapEventTiles = []
mapEditorStore.triggerClearTiles()
}
function save() {
if (!mapEditorStore.map) return
const data = {
mapId: mapEditorStore.map.id,
name: mapEditorStore.mapSettings.name,
width: mapEditorStore.mapSettings.width,
height: mapEditorStore.mapSettings.height,
tiles: mapEditorStore.map.tiles,
pvp: mapEditorStore.map.pvp,
mapEffects: mapEditorStore.map.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
mapEventTiles: mapEditorStore.map.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
placedMapObjects: mapEditorStore.map.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
}
console.log(data.mapEventTiles)
if (mapEditorStore.isSettingsModalShown) {
mapEditorStore.toggleSettingsModal()
}
gameStore.connection?.emit('gm:map:update', data, (response: Map) => {
mapEditorStore.setMap(response)
})
}
onUnmounted(() => {
mapEditorStore.reset()
})
</script>

View File

@ -1,23 +1,23 @@
<template> <template>
<Image v-for="tile in zoneEditorStore.zone?.zoneEventTiles" v-bind="getImageProps(tile)" /> <Image v-for="tile in mapEditorStore.map?.mapEventTiles" v-bind="getImageProps(tile)" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { type ZoneEventTile, ZoneEventTileType } from '@/types' import { MapEventTileType, type MapEventTile } from '@/application/types'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { uuidv4 } from '@/application/utilities'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { uuidv4 } from '@/utilities'
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
const scene = useScene() const scene = useScene()
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tilemap: Phaser.Tilemaps.Tilemap
}>() }>()
function getImageProps(tile: ZoneEventTile) { function getImageProps(tile: MapEventTile) {
return { return {
x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY), x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY), y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY),
@ -27,14 +27,14 @@ function getImageProps(tile: ZoneEventTile) {
} }
function pencil(pointer: Phaser.Input.Pointer) { function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set // Check if map is set
if (!zoneEditorStore.zone) return if (!mapEditorStore.map) return
// Check if tool is pencil // Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is blocking tile or teleport // Check if draw mode is blocking tile or teleport
if (zoneEditorStore.drawMode !== 'blocking tile' && zoneEditorStore.drawMode !== 'teleport') return if (mapEditorStore.drawMode !== 'blocking tile' && mapEditorStore.drawMode !== 'teleport') return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -47,42 +47,42 @@ function pencil(pointer: Phaser.Input.Pointer) {
if (!tile) return if (!tile) return
// Check if event tile already exists on position // Check if event tile already exists on position
const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y) const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return if (existingEventTile) return
// If teleport, check if there is a selected zone // If teleport, check if there is a selected map
if (zoneEditorStore.drawMode === 'teleport' && !zoneEditorStore.teleportSettings.toZoneId) return if (mapEditorStore.drawMode === 'teleport' && !mapEditorStore.teleportSettings.toMap) return
const newEventTile = { const newEventTile = {
id: uuidv4(), id: uuidv4(),
zoneId: zoneEditorStore.zone.id, mapId: mapEditorStore.map.id,
zone: zoneEditorStore.zone, map: mapEditorStore.map,
type: zoneEditorStore.drawMode === 'blocking tile' ? ZoneEventTileType.BLOCK : ZoneEventTileType.TELEPORT, type: mapEditorStore.drawMode === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
positionX: tile.x, positionX: tile.x,
positionY: tile.y, positionY: tile.y,
teleport: teleport:
zoneEditorStore.drawMode === 'teleport' mapEditorStore.drawMode === 'teleport'
? { ? {
toZoneId: zoneEditorStore.teleportSettings.toZoneId, toMap: mapEditorStore.teleportSettings.toMap,
toPositionX: zoneEditorStore.teleportSettings.toPositionX, toPositionX: mapEditorStore.teleportSettings.toPositionX,
toPositionY: zoneEditorStore.teleportSettings.toPositionY, toPositionY: mapEditorStore.teleportSettings.toPositionY,
toRotation: zoneEditorStore.teleportSettings.toRotation toRotation: mapEditorStore.teleportSettings.toRotation
} }
: undefined : undefined
} }
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.concat(newEventTile as ZoneEventTile) mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.concat(newEventTile as MapEventTile)
} }
function eraser(pointer: Phaser.Input.Pointer) { function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set // Check if map is set
if (!zoneEditorStore.zone) return if (!mapEditorStore.map) return
// Check if tool is pencil // Check if tool is pencil
if (zoneEditorStore.tool !== 'eraser') return if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is blocking tile or teleport // Check if draw mode is blocking tile or teleport
if (zoneEditorStore.eraserMode !== 'blocking tile' && zoneEditorStore.eraserMode !== 'teleport') return if (mapEditorStore.eraserMode !== 'blocking tile' && mapEditorStore.eraserMode !== 'teleport') return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -95,11 +95,11 @@ function eraser(pointer: Phaser.Input.Pointer) {
if (!tile) return if (!tile) return
// Check if event tile already exists on position // Check if event tile already exists on position
const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y) const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return if (!existingEventTile) return
// Remove existing event tile // Remove existing event tile
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id) mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
} }
onMounted(() => { onMounted(() => {

View File

@ -3,20 +3,20 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/application/config'
import { useScene } from 'phavuer' import type { TextureData } from '@/application/types'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onMounted, onUnmounted, watch } from 'vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import type { AssetDataT } from '@/types' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, watch } from 'vue'
const emit = defineEmits(['tileMap:create']) const emit = defineEmits(['tileMap:create'])
const scene = useScene() const scene = useScene()
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
const tileMap = createTileMap() const tileMap = createTileMap()
const tileLayer = createTileLayer() const tileLayer = createTileLayer()
@ -26,16 +26,16 @@ const tileLayer = createTileLayer()
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles. * A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/ */
function createTileMap() { function createTileMap() {
const zoneData = new Phaser.Tilemaps.MapData({ const mapData = new Phaser.Tilemaps.MapData({
width: zoneEditorStore.zone?.width, width: mapEditorStore.map?.width,
height: zoneEditorStore.zone?.height, height: mapEditorStore.map?.height,
tileWidth: config.tile_size.x, tileWidth: config.tile_size.width,
tileHeight: config.tile_size.y, tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC, orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D format: Phaser.Tilemaps.Formats.ARRAY_2D
}) })
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData) const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapData)
emit('tileMap:create', newTileMap) emit('tileMap:create', newTileMap)
return newTileMap return newTileMap
@ -47,13 +47,13 @@ function createTileMap() {
function createTileLayer() { function createTileLayer() {
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles') const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => { const tilesetImages = Array.from(tilesArray).map((tile: TextureData, 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 }) return tileMap.addTilesetImage(tile.key, tile.key, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
}) as any }) as any
// Add blank tile // 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 })) tilesetImages.push(tileMap.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 = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0) layer.setDepth(0)
layer.setCullPadding(2, 2) layer.setCullPadding(2, 2)
@ -62,17 +62,17 @@ function createTileLayer() {
} }
function pencil(pointer: Phaser.Input.Pointer) { function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set // Check if map is set
if (!zoneEditorStore.zone) return if (!mapEditorStore.map) return
// Check if tool is pencil // Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is tile // Check if draw mode is tile
if (zoneEditorStore.drawMode !== 'tile') return if (mapEditorStore.drawMode !== 'tile') return
// Check if there is a selected tile // Check if there is a selected tile
if (!zoneEditorStore.selectedTile) return if (!mapEditorStore.selectedTile) return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -85,21 +85,21 @@ function pencil(pointer: Phaser.Input.Pointer) {
if (!tile) return if (!tile) return
// Place tile // Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, zoneEditorStore.selectedTile) placeTile(tileMap, tileLayer, tile.x, tile.y, mapEditorStore.selectedTile)
// Adjust zoneEditorStore.zone.tiles // Adjust mapEditorStore.map.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile mapEditorStore.map.tiles[tile.y][tile.x] = mapEditorStore.selectedTile
} }
function eraser(pointer: Phaser.Input.Pointer) { function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set // Check if map is set
if (!zoneEditorStore.zone) return if (!mapEditorStore.map) return
// Check if tool is pencil // Check if tool is pencil
if (zoneEditorStore.tool !== 'eraser') return if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is tile // Check if draw mode is tile
if (zoneEditorStore.eraserMode !== 'tile') return if (mapEditorStore.eraserMode !== 'tile') return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -117,19 +117,19 @@ function eraser(pointer: Phaser.Input.Pointer) {
// Place tile // Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile') placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
// Adjust zoneEditorStore.zone.tiles // Adjust mapEditorStore.map.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile' mapEditorStore.map.tiles[tile.y][tile.x] = 'blank_tile'
} }
function paint(pointer: Phaser.Input.Pointer) { function paint(pointer: Phaser.Input.Pointer) {
// Check if zone is set // Check if map is set
if (!zoneEditorStore.zone) return if (!mapEditorStore.map) return
// Check if tool is pencil // Check if tool is pencil
if (zoneEditorStore.tool !== 'paint') return if (mapEditorStore.tool !== 'paint') return
// Check if there is a selected tile // Check if there is a selected tile
if (!zoneEditorStore.selectedTile) return if (!mapEditorStore.selectedTile) return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -141,22 +141,22 @@ function paint(pointer: Phaser.Input.Pointer) {
if (pointer.event.altKey) return if (pointer.event.altKey) return
// Set new tileArray with selected tile // Set new tileArray with selected tile
setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile)) setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, mapEditorStore.selectedTile))
// Adjust zoneEditorStore.zone.tiles // Adjust mapEditorStore.map.tiles
zoneEditorStore.zone.tiles = createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile) mapEditorStore.map.tiles = createTileArray(tileMap.width, tileMap.height, mapEditorStore.selectedTile)
} }
// When alt is pressed, and the pointer is down, select the tile that the pointer is over // When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(pointer: Phaser.Input.Pointer) { function tilePicker(pointer: Phaser.Input.Pointer) {
// Check if zone is set // Check if map is set
if (!zoneEditorStore.zone) return if (!mapEditorStore.map) return
// Check if tool is pencil // Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is tile // Check if draw mode is tile
if (zoneEditorStore.drawMode !== 'tile') return if (mapEditorStore.drawMode !== 'tile') return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -172,36 +172,36 @@ function tilePicker(pointer: Phaser.Input.Pointer) {
if (!tile) return if (!tile) return
// Select the tile // Select the tile
zoneEditorStore.setSelectedTile(zoneEditorStore.zone.tiles[tile.y][tile.x]) mapEditorStore.setSelectedTile(mapEditorStore.map.tiles[tile.y][tile.x])
} }
watch( watch(
() => zoneEditorStore.shouldClearTiles, () => mapEditorStore.shouldClearTiles,
(shouldClear) => { (shouldClear) => {
if (shouldClear && zoneEditorStore.zone) { if (shouldClear && mapEditorStore.map) {
const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile') const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile')
setLayerTiles(tileMap, tileLayer, blankTiles) setLayerTiles(tileMap, tileLayer, blankTiles)
zoneEditorStore.zone.tiles = blankTiles mapEditorStore.map.tiles = blankTiles
zoneEditorStore.resetClearTilesFlag() mapEditorStore.resetClearTilesFlag()
} }
} }
) )
onMounted(() => { onMounted(() => {
if (!zoneEditorStore.zone?.tiles) { if (!mapEditorStore.map?.tiles) {
return return
} }
// First fill the entire map with blank tiles using current zone dimensions // First fill the entire map with blank tiles using current map dimensions
const blankTiles = createTileArray(zoneEditorStore.zone.width, zoneEditorStore.zone.height, 'blank_tile') const blankTiles = createTileArray(mapEditorStore.map.width, mapEditorStore.map.height, 'blank_tile')
// Then overlay the zone tiles, but only within the current zone dimensions // Then overlay the map tiles, but only within the current map dimensions
const zoneTiles = zoneEditorStore.zone.tiles const mapTiles = mapEditorStore.map.tiles
for (let y = 0; y < zoneEditorStore.zone.height; y++) { for (let y = 0; y < mapEditorStore.map.height; y++) {
for (let x = 0; x < zoneEditorStore.zone.width; x++) { for (let x = 0; x < mapEditorStore.map.width; x++) {
// Only copy if the source tiles array has this position // Only copy if the source tiles array has this position
if (zoneTiles[y] && zoneTiles[y][x] !== undefined) { if (mapTiles[y] && mapTiles[y][x] !== undefined) {
blankTiles[y][x] = zoneTiles[y][x] blankTiles[y][x] = mapTiles[y][x]
} }
} }
} }

View File

@ -0,0 +1,45 @@
<template>
<Image v-if="gameStore.getLoadedAsset(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,246 @@
<template>
<SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
<PlacedMapObject v-for="placedMapObject in mapEditorStore.map?.placedMapObjects" :tilemap="tilemap" :placedMapObject :selectedPlacedMapObject :movingPlacedMapObject @pointerup="clickPlacedMapObject(placedMapObject)" />
</template>
<script setup lang="ts">
import type { MapObject, 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 { getTile } from '@/composables/mapComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, ref, watch } from 'vue'
const scene = useScene()
const mapEditorStore = useMapEditorStore()
const selectedPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const movingPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function pencil(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is map_object
if (mapEditorStore.drawMode !== 'map_object') return
// Check if there is a selected object
if (!mapEditorStore.selectedMapObject) 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 existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
if (existingPlacedMapObject) return
const newPlacedMapObject = {
id: uuidv4(),
map: mapEditorStore.map,
mapObject: mapEditorStore.selectedMapObject,
depth: 0,
isRotated: false,
positionX: tile.x,
positionY: tile.y
}
// Add new object to mapObjects
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.concat(newPlacedMapObject as PlacedMapObjectT)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is eraser
if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is map_object
if (mapEditorStore.eraserMode !== '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 there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
if (!existingPlacedMapObject) return
// Remove existing object
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
}
function objectPicker(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is map_object
if (mapEditorStore.drawMode !== '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
// 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 existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
if (!existingPlacedMapObject) return
// Select the object
mapEditorStore.setSelectedMapObject(existingPlacedMapObject.mapObject)
}
function moveMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
movingPlacedMapObject.value = mapEditorStore.map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingPlacedMapObject.value) return
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
movingPlacedMapObject.value.positionX = tile.x
movingPlacedMapObject.value.positionY = tile.y
}
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) {
// Check if map is set
if (!mapEditorStore.map) return
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.map((placedMapObject) => {
if (placedMapObject.id === id) {
return {
...placedMapObject,
isRotated: !placedMapObject.isRotated
}
}
return placedMapObject
})
}
function deletePlacedMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
mapEditorStore.map.placedMapObjects = mapEditorStore.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) {
mapEditorStore.setSelectedMapObject(placedMapObject.mapObject)
}
}
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 mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects
watch(
() => mapEditorStore.mapObjectList,
(newMapObjects) => {
if (!mapEditorStore.map) return
const updatedMapObjects = mapEditorStore.map.placedMapObjects.map((mapObject) => {
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapObject.mapObject.id)
if (updatedMapObject) {
return {
...mapObject,
mapObject: {
...mapObject.mapObject,
originX: updatedMapObject.originX,
originY: updatedMapObject.originY
}
}
}
return mapObject
})
// Update the map with the new mapObjects
mapEditorStore.setMap({
...mapEditorStore.map,
placedMapObjects: updatedMapObjects
})
// Update selectedMapObject if it's set
if (mapEditorStore.selectedMapObject) {
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapEditorStore.selectedMapObject?.id)
if (updatedMapObject) {
mapEditorStore.setSelectedMapObject({
...mapEditorStore.selectedMapObject,
originX: updatedMapObject.originX,
originY: updatedMapObject.originY
})
}
}
}
// { deep: true }
)
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :disable-bg-texture="true"> <Modal :isModalOpen="true" @modal:close="() => mapEditorStore.toggleCreateMapModal()" :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>
@ -36,23 +36,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' 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 type { Zone } from '@/types' import { ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
const name = ref('') const name = ref('')
const width = ref(0) const width = ref(0)
const height = ref(0) const height = ref(0)
function submit() { function submit() {
gameStore.connection.emit('gm:zone_editor:zone:create', { name: name.value, width: width.value, height: height.value }, (response: Zone[]) => { gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, (response: Map[]) => {
zoneEditorStore.setZoneList(response) mapEditorStore.setMapList(response)
}) })
zoneEditorStore.toggleCreateZoneModal() mapEditorStore.toggleCreateMapModal()
} }
</script> </script>

View File

@ -0,0 +1,61 @@
<template>
<CreateMap v-if="mapEditorStore.isCreateMapModalShown" />
<Modal :is-modal-open="mapEditorStore.isMapListModalShown" @modal:close="() => mapEditorStore.toggleMapListModal()" :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">
<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="() => mapEditorStore.toggleCreateMapModal()">New</button>
</div>
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapEditorStore.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>
</template>
</Modal>
</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 { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onMounted } from 'vue'
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
onMounted(async () => {
fetchMaps()
})
function fetchMaps() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
mapEditorStore.setMapList(response)
})
}
function loadMap(id: UUID) {
gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => {
mapEditorStore.setMap(response)
})
mapEditorStore.toggleMapListModal()
}
function deleteMap(id: UUID) {
gameStore.connection?.emit('gm:map:delete', { mapId: id }, () => {
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)" :disable-bg-texture="true"> <Modal :isModalOpen="mapEditorStore.isMapObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (mapEditorStore.isMapObjectListModalShown = false)" :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="mapEditorStore.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': mapEditorStore.selectedMapObject?.id === mapObject.id,
'border-transparent hover:border-gray-300': zoneEditorStore.selectedObject?.id !== object.id 'border-transparent hover:border-gray-300': mapEditorStore.selectedMapObject?.id !== mapObject.id
}" }"
/> />
</div> </div>
@ -41,26 +41,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/application/config'
import { ref, onMounted, computed } from 'vue' import type { MapObject } from '@/application/types'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import type { Object, ZoneObject } from '@/types' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const isModalOpen = ref(false) const isModalOpen = ref(false)
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || []) const allTags = mapEditorStore.mapObjectList.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 mapEditorStore.mapObjectList.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
@ -77,8 +77,8 @@ const toggleTag = (tag: string) => {
onMounted(async () => { onMounted(async () => {
isModalOpen.value = true isModalOpen.value = true
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => { gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
zoneEditorStore.setObjectList(response) mapEditorStore.setMapObjectList(response)
}) })
}) })
</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="350" :disable-bg-texture="true"> <Modal :is-modal-open="mapEditorStore.isSettingsModalShown" @modal:close="() => mapEditorStore.toggleSettingsModal()" :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>
@ -47,60 +47,60 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { ref, watch } from 'vue'
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
const screen = ref('settings') const screen = ref('settings')
zoneEditorStore.setZoneName(zoneEditorStore.zone?.name) mapEditorStore.setMapName(mapEditorStore.map?.name)
zoneEditorStore.setZoneWidth(zoneEditorStore.zone?.width) mapEditorStore.setMapWidth(mapEditorStore.map?.width)
zoneEditorStore.setZoneHeight(zoneEditorStore.zone?.height) mapEditorStore.setMapHeight(mapEditorStore.map?.height)
zoneEditorStore.setZonePvp(zoneEditorStore.zone?.pvp) mapEditorStore.setMapPvp(mapEditorStore.map?.pvp)
zoneEditorStore.setZoneEffects(zoneEditorStore.zone?.zoneEffects) mapEditorStore.setMapEffects(mapEditorStore.map?.mapEffects)
const name = ref(zoneEditorStore.zoneSettings?.name) const name = ref(mapEditorStore.mapSettings?.name)
const width = ref(zoneEditorStore.zoneSettings?.width) const width = ref(mapEditorStore.mapSettings?.width)
const height = ref(zoneEditorStore.zoneSettings?.height) const height = ref(mapEditorStore.mapSettings?.height)
const pvp = ref(zoneEditorStore.zoneSettings?.pvp) const pvp = ref(mapEditorStore.mapSettings?.pvp)
const zoneEffects = ref(zoneEditorStore.zoneSettings?.zoneEffects || []) const mapEffects = ref(mapEditorStore.mapSettings?.mapEffects || [])
watch(name, (value) => { watch(name, (value) => {
zoneEditorStore.setZoneName(value) mapEditorStore.setMapName(value)
}) })
watch(width, (value) => { watch(width, (value) => {
zoneEditorStore.setZoneWidth(value) mapEditorStore.setMapWidth(value)
}) })
watch(height, (value) => { watch(height, (value) => {
zoneEditorStore.setZoneHeight(value) mapEditorStore.setMapHeight(value)
}) })
watch(pvp, (value) => { watch(pvp, (value) => {
zoneEditorStore.setZonePvp(value) mapEditorStore.setMapPvp(value)
}) })
watch( watch(
zoneEffects, mapEffects,
(value) => { (value) => {
zoneEditorStore.setZoneEffects(value) mapEditorStore.setMapEffects(value)
}, },
{ deep: true } { deep: true }
) )
const addEffect = () => { const addEffect = () => {
zoneEffects.value.push({ mapEffects.value.push({
id: Date.now().toString(), // Simple unique id generation id: Date.now().toString(), // Simple unique id generation
zoneId: zoneEditorStore.zone?.id, mapId: mapEditorStore.map?.id,
zone: zoneEditorStore.zone, map: mapEditorStore.map,
effect: '', effect: '',
strength: 1 strength: 1
}) })
} }
const removeEffect = (index) => { const removeEffect = (index) => {
zoneEffects.value.splice(index, 1) mapEffects.value.splice(index, 1)
} }
</script> </script>

View File

@ -11,23 +11,25 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ZoneObject } from '@/types' import type { PlacedMapObject } from '@/application/types'
const props = defineProps<{ const props = defineProps<{
zoneObject: ZoneObject placedMapObject: PlacedMapObject
}>() }>()
console.log(props.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" :disable-bg-texture="true"> <Modal :is-modal-open="showTeleportModal" @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 mapEditorStore.mapList" :key="map.id" :value="map">{{ map.name }}</option>
</select> </select>
</div> </div>
</div> </div>
@ -39,44 +39,44 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue' import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import type { Zone } from '@/types' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref, 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()
onMounted(fetchZones) onMounted(fetchMaps)
function fetchZones() { function fetchMaps() {
gameStore.connection?.emit('gm:zone_editor:zone:list', {}, (response: Zone[]) => { gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
zoneEditorStore.setZoneList(response) mapEditorStore.setMapList(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.toMap)
} }
} }
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 toMap: 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)" :disable-bg-texture="true"> <Modal :isModalOpen="mapEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (mapEditorStore.isTileListModalShown = false)" :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="{
@ -81,29 +81,29 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/application/config'
import { ref, onMounted, computed } from 'vue' import type { Tile } from '@/application/types'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import type { Tile } from '@/types' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const isModalOpen = ref(false) const isModalOpen = ref(false)
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
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 uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = zoneEditorStore.tileList.flatMap((tile) => tile.tags || []) const allTags = mapEditorStore.tileList.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 = mapEditorStore.tileList.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 +169,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) {
@ -219,17 +219,17 @@ function closeGroup() {
} }
function selectTile(tile: string) { function selectTile(tile: string) {
zoneEditorStore.setSelectedTile(tile) mapEditorStore.setSelectedTile(tile)
} }
function isActiveTile(tile: Tile): boolean { function isActiveTile(tile: Tile): boolean {
return zoneEditorStore.selectedTile === tile.id return mapEditorStore.selectedTile === tile.id
} }
onMounted(async () => { onMounted(async () => {
isModalOpen.value = true isModalOpen.value = true
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
zoneEditorStore.setTileList(response) mapEditorStore.setTileList(response)
response.forEach((tile) => processTile(tile)) response.forEach((tile) => processTile(tile))
}) })
}) })

View File

@ -1,27 +1,27 @@
<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="mapEditorStore.map">
<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': mapEditorStore.tool === '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': mapEditorStore.tool !== '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': mapEditorStore.tool === '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': mapEditorStore.tool !== 'pencil' }">(P)</span>
<div class="select" v-if="zoneEditorStore.tool === 'pencil'"> <div class="select" v-if="mapEditorStore.tool === '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 }} {{ mapEditorStore.drawMode.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 && mapEditorStore.tool === '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="setDrawMode('tile')">
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="setDrawMode('map_object')">
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="setDrawMode('teleport')">
@ -35,20 +35,20 @@
<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': mapEditorStore.tool === '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': mapEditorStore.tool !== 'eraser' }">(E)</span>
<div class="select" v-if="zoneEditorStore.tool === 'eraser'"> <div class="select" v-if="mapEditorStore.tool === '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 }} {{ mapEditorStore.eraserMode.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" />
</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="setEraserMode('tile')">
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="setEraserMode('map_object')">
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="setEraserMode('teleport')">
@ -62,31 +62,31 @@
<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': mapEditorStore.tool === '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': mapEditorStore.tool !== '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')" v-if="mapEditorStore.map"><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> </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="() => mapEditorStore.toggleMapListModal()">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="mapEditorStore.map">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="mapEditorStore.map">Clear</button>
<button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleActive()">Exit</button> <button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleActive()">Exit</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref } from 'vue'
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
const emit = defineEmits(['save', 'clear']) const emit = defineEmits(['save', 'clear'])
@ -99,24 +99,24 @@ let selectEraserOpen = ref(false)
// drawMode // drawMode
function setDrawMode(value: string) { function setDrawMode(value: string) {
zoneEditorStore.isTileListModalShown = value === 'tile' mapEditorStore.isTileListModalShown = value === 'tile'
zoneEditorStore.isObjectListModalShown = value === 'object' mapEditorStore.isMapObjectListModalShown = value === 'map_object'
zoneEditorStore.setDrawMode(value) mapEditorStore.setDrawMode(value)
selectPencilOpen.value = false selectPencilOpen.value = false
} }
// drawMode // drawMode
function setEraserMode(value: string) { function setEraserMode(value: string) {
zoneEditorStore.setEraserMode(value) mapEditorStore.setEraserMode(value)
selectEraserOpen.value = false selectEraserOpen.value = false
} }
function handleClick(tool: string) { function handleClick(tool: string) {
if (tool === 'settings') { if (tool === 'settings') {
zoneEditorStore.toggleSettingsModal() mapEditorStore.toggleSettingsModal()
} else { } else {
zoneEditorStore.setTool(tool) mapEditorStore.setTool(tool)
} }
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
@ -125,7 +125,7 @@ function handleClick(tool: string) {
function cycleToolMode(tool: 'pencil' | 'eraser') { function cycleToolMode(tool: 'pencil' | 'eraser') {
const modes = ['tile', 'object', 'teleport', 'blocking tile'] const modes = ['tile', 'object', 'teleport', 'blocking tile']
const currentMode = tool === 'pencil' ? zoneEditorStore.drawMode : zoneEditorStore.eraserMode const currentMode = tool === 'pencil' ? mapEditorStore.drawMode : mapEditorStore.eraserMode
const currentIndex = modes.indexOf(currentMode) 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]
@ -138,8 +138,8 @@ function cycleToolMode(tool: 'pencil' | 'eraser') {
} }
function initKeyShortcuts(event: KeyboardEvent) { function initKeyShortcuts(event: KeyboardEvent) {
// Check if zone is set // Check if map is set
if (!zoneEditorStore.zone) return if (!mapEditorStore.map) 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 +154,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') && mapEditorStore.tool === 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 { onUnmounted, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { type Zone } from '@/types'
// Components
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.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" :disable-bg-texture="true">
<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 { onMounted } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue'
import type { Zone } from '@/types'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import CreateZone from '@/components/gameMaster/zoneEditor/partials/CreateZone.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,45 +0,0 @@
<template>
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Image, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { loadTexture } from '@/composables/gameComposable'
import type { AssetDataT, ZoneObject } from '@/types'
import { useGameStore } from '@/stores/gameStore'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
zoneObject: ZoneObject
selectedZoneObject: ZoneObject | null
movingZoneObject: ZoneObject | null
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
alpha: props.movingZoneObject?.id === props.zoneObject.id ? 0.5 : 1,
tint: props.selectedZoneObject?.id === props.zoneObject.id ? 0x00ff00 : 0xffffff,
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
flipX: props.zoneObject.isRotated,
texture: props.zoneObject.object.id,
originY: Number(props.zoneObject.object.originX),
originX: Number(props.zoneObject.object.originY)
}))
loadTexture(scene, {
key: props.zoneObject.object.id,
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
group: 'objects',
updatedAt: props.zoneObject.object.updatedAt,
frameWidth: props.zoneObject.object.frameWidth,
frameHeight: props.zoneObject.object.frameHeight
} as AssetDataT).catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -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 { uuidv4 } from '@/utilities'
import { getTile } from '@/composables/zoneComposable'
import { useScene } from 'phavuer'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
import type { ZoneObject as ZoneObjectT } from '@/types'
const scene = useScene()
const 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,5 +1,6 @@
<template> <template>
<div class="relative" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle"> <div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
<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" />
<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)]" />
<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">
@ -21,35 +22,35 @@
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex flex-col gap-0.5"> <div class="flex flex-col gap-0.5">
<div class="w-9 h-9 border border-solid border-gray-500 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">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">CROWN</span> <span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">CROWN</span>
</div> </div>
<div class="w-9 h-9 border border-solid border-gray-500 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">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">R-HAND</span> <span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">R-HAND</span>
</div> </div>
<div class="flex gap-0.5 items-end"> <div class="flex gap-0.5 items-end">
<div class="w-9 h-9 border border-solid border-gray-500 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">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">L-HAND</span> <span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">L-HAND</span>
</div> </div>
<div class="w-6 h-6 border border-solid border-gray-500 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">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">RING</span> <span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">RING</span>
</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" />
<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 border border-solid border-gray-500 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="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/helmet.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" />
</div> </div>
<div class="w-9 h-9 border border-solid border-gray-500 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="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/chestplate.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" />
</div> </div>
<div class="flex gap-0.5 items-end"> <div class="flex gap-0.5 items-end">
<div class="w-6 h-6 border border-solid border-gray-500 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="absolute w-4 h-4 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/boots.svg" /> <img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" />
</div> </div>
<div class="w-9 h-9 border border-solid border-gray-500 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="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/legs.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" />
</div> </div>
</div> </div>
</div> </div>
@ -109,15 +110,16 @@
</div> </div>
</div> </div>
<div class="grid grid-rows-4 grid-cols-6 gap-0.5"> <div class="grid grid-rows-4 grid-cols-6 gap-0.5">
<div v-for="n in 24" class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600"></div> <div v-for="n in 24" class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted, ref, watch, computed } from 'vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()

View File

@ -21,27 +21,35 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, ref, nextTick } from 'vue' import type { Chat } from '@/application/types'
import { onClickOutside } from '@vueuse/core'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import type { Chat } from '@/types' import { useMapStore } from '@/stores/mapStore'
import { useZoneStore } from '@/stores/zoneStore' import { onClickOutside, useFocus } from '@vueuse/core'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
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[])
const chatWindow = ref<HTMLElement | null>(null) const chatWindow = ref<HTMLElement | null>(null)
const chatInput = ref<HTMLElement | null>(null) const chatInput = ref<HTMLElement | null>(null)
onClickOutside(chatInput, event => unfocusChat(event, chatInput.value as HTMLElement)) const { focused } = useFocus(chatInput)
function focusChat(event: KeyboardEvent) {
if (event.key === 'Enter' && !focused.value) {
focused.value = true
}
}
onClickOutside(chatInput, (event) => unfocusChat(event, chatInput.value as HTMLElement))
function unfocusChat(event: Event, targetElement: HTMLElement) { function unfocusChat(event: Event, targetElement: HTMLElement) {
if (!(event.target instanceof Node) || !targetElement.contains(event.target)) { if (!(event.target instanceof Node) || !targetElement.contains(event.target)) {
targetElement.blur(); targetElement.blur()
} }
} }
@ -75,7 +83,7 @@ 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
@ -128,7 +136,12 @@ gameStore.connection?.on('chat:message', (data: Chat) => {
}) })
scrollToBottom() scrollToBottom()
onMounted(() => {
addEventListener('keydown', focusChat)
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
gameStore.connection?.off('chat:message') gameStore.connection?.off('chat:message')
removeEventListener('keydown', focusChat)
}) })
</script> </script>

View File

@ -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/avatar/default/head.png" /> <img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" />
<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>
@ -24,7 +24,7 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open Chat</p> <p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open Chat</p>
<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 border-b-4 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" />
</a> </a>
</li> </li>
@ -33,7 +33,7 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">World map</p> <p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">World map</p>
<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 border-b-4 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" />
</a> </a>
</li> </li>
@ -42,7 +42,7 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Users</p> <p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Users</p>
<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 border-b-4 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" />
</a> </a>
</li> </li>

View File

@ -1,15 +1,15 @@
<template> <template>
<div class="absolute top-4 right-4 hidden lg:block"> <div class="absolute top-4 right-4 hidden lg:block">
<div class="w-40 h-40 rounded-full border border-solid border-gray-500 bg-[url('/assets/ui-texture.png')] bg-no-repeat"> <div class="w-40 h-40 rounded-full default-border bg-[url('/assets/ui-texture.png')] bg-no-repeat">
<div class="w-40 h-40 rounded-full shadow-inner"></div> <div class="w-40 h-40 rounded-full shadow-inner"></div>
</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="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/plus-icon.svg" /> <img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" />
<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" />
</button> </button>
<button class="w-6 h-6 relative p-0"> <button class="w-6 h-6 relative p-0">
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/minus-icon.svg" /> <img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" />
<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" />
</button> </button>
</div> </div>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false"> <div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false">
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray-700 border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg"> <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"> <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> <h3 class="m-0 font-medium shrink-0">Game menu</h3>
<div class="hidden sm:flex gap-1.5 flex-wrap"> <div class="hidden sm:flex gap-1.5 flex-wrap">
@ -30,12 +30,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import Inventory from '@/components/gui/partials/Inventory.vue'
import Equipment from '@/components/gui/partials/Equipment.vue'
import CharacterScreen from '@/components/gui/partials/CharacterScreen.vue' 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 Settings from '@/components/gui/partials/Settings.vue'
import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
let userPanelScreen = ref('inventory') let userPanelScreen = ref('inventory')

View File

@ -14,47 +14,47 @@
<div class="flex gap-3 justify-center"> <div class="flex gap-3 justify-center">
<!-- Helmet --> <!-- 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"> <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="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" /> <img src="/assets/icons/inventory/helmet.svg" class="center-element w-11/12 opacity-20" />
</div> </div>
<!-- Head charm --> <!-- 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"> <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="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" /> <img src="/assets/icons/inventory/head_charm.svg" class="center-element w-11/12 opacity-20" />
</div> </div>
</div> </div>
<div class="flex gap-3 justify-center"> <div class="flex gap-3 justify-center">
<!-- Bracers --> <!-- 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"> <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="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" /> <img src="/assets/icons/inventory/bracers.svg" class="center-element w-11/12 opacity-20" />
</div> </div>
<!-- Chestplate --> <!-- 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"> <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="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-10/12 opacity-20" /> <img src="/assets/icons/inventory/chestplate.svg" class="center-element w-10/12 opacity-20" />
</div> </div>
<!-- Primary Weapon --> <!-- 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"> <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="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" /> <img src="/assets/icons/inventory/primary_weapon.svg" class="center-element w-11/12 opacity-20" />
</div> </div>
</div> </div>
<div class="flex gap-3 justify-center"> <div class="flex gap-3 justify-center">
<!-- Legs --> <!-- 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"> <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="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" /> <img src="/assets/icons/inventory/legs.svg" class="center-element w-11/12 opacity-20" />
</div> </div>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<!-- Belt/pouch --> <!-- 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"> <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="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" /> <img src="/assets/icons/inventory/pouch.svg" class="center-element w-11/12 opacity-20" />
</div> </div>
<!-- Boots --> <!-- 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"> <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="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" /> <img src="/assets/icons/inventory/boots.svg" class="center-element w-11/12 opacity-20" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -33,8 +33,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import CharacterSettings from '@/components/gui/partials/settings/CharacterSettings.vue' import CharacterSettings from '@/components/gui/partials/settings/CharacterSettings.vue'
import { ref } from 'vue'
let settingCategory = ref('character') let settingCategory = ref('character')
</script> </script>

View File

@ -1,13 +1,13 @@
<template> <template>
<form @submit.prevent="loginFunc" class="relative px-6 py-11"> <form @submit.prevent="submit" class="relative px-6 py-[70px]">
<div class="flex flex-col gap-5 p-2 mb-8 relative"> <div class="flex flex-col gap-5 p-2 mb-8 relative">
<div class="w-full grid gap-3 relative"> <div class="w-full grid gap-3 relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus /> <input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
<div class="relative"> <div class="relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required /> <input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button> <button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-5 h-4 top-1/2 -translate-y-1/2 bg-no-repeat bg-center"></button>
</div> </div>
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span> <span v-if="formError" class="text-red-200 text-xs absolute top-full mt-1">{{ formError }}</span>
</div> </div>
<button @click.stop="() => emit('openResetPasswordModal')" type="button" class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button> <button @click.stop="() => emit('openResetPasswordModal')" type="button" class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button> <button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
@ -26,17 +26,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'
import { login } from '@/services/authentication' import { login } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import { onMounted, ref } from 'vue'
const emit = defineEmits(['openResetPasswordModal', 'switchToRegister']) const emit = defineEmits(['openResetPasswordModal', 'switchToRegister'])
const gameStore = useGameStore() const gameStore = useGameStore()
const username = ref('') const username = ref('')
const password = ref('') const password = ref('')
const loginError = ref('') const formError = ref('')
const showPassword = ref(false) const showPassword = ref(false)
// automatic login because of development // automatic login because of development
@ -48,10 +48,10 @@ onMounted(async () => {
} }
}) })
async function loginFunc() { async function submit() {
// check if username and password are valid // check if username and password are valid
if (username.value === '' || password.value === '') { if (username.value === '' || password.value === '') {
loginError.value = 'Please enter a valid username and password' formError.value = 'Please enter a valid username and password'
return return
} }
@ -59,7 +59,7 @@ async function loginFunc() {
const response = await login(username.value, password.value) const response = await login(username.value, password.value)
if (response.success === undefined) { if (response.success === undefined) {
loginError.value = response.error formError.value = response.error
return return
} }
gameStore.setToken(response.token) gameStore.setToken(response.token)

View File

@ -22,10 +22,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'
import { newPassword } from '@/services/authentication' import { newPassword } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import { onMounted, ref } from 'vue'
const emit = defineEmits(['switchToLogin']) const emit = defineEmits(['switchToLogin'])

View File

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

View File

@ -29,10 +29,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import { resetPassword } from '@/services/authentication'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { resetPassword } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue'
const emit = defineEmits(['close']) const emit = defineEmits(['close'])

View File

@ -1,80 +1,85 @@
<template> <template>
<div class="relative max-lg:h-dvh flex flex-row-reverse"> <div class="relative max-lg:h-dvh flex flex-row-reverse">
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div> <div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div> <div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div>
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative"></div> <div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh relative"></div>
<div class="absolute top-8 right-0 py-[18px] pr-[15px] pl-32 bg-gradient-to-r from-transparent to-cyan-900 z-20"> <div class="max-lg:hidden absolute top-8 right-0 py-[18px] pr-[15px] pl-32 bg-gradient-to-r from-transparent to-cyan-900 z-20">
<h2 class="text-white">CHARACTER SELECTION</h2> <h2 class="text-white">CHARACTER SELECTION</h2>
</div> </div>
<div class="ui-wrapper h-dvh w-[calc(100%_-_80px)] sm:w-[calc(100%_-_160px)] absolute flex flex-col justify-center items-center gap-14 px-10 sm:px-20 z-30"> <div class="ui-wrapper h-dvh w-[calc(100%_-_80px)] sm:w-[calc(100%_-_160px)] absolute flex flex-col justify-center items-center gap-8 lg:gap-14 px-10 sm:px-20 z-30">
<div class="filler"></div> <div class="filler"></div>
<div class="w-2/3 max-w-[860px]" v-if="!isLoading"> <div class="w-2/3 max-w-[860px]" v-if="!isLoading">
<div class="mb-5 flex flex-col gap-1"> <div class="mb-5 flex flex-col gap-1">
<h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1> <h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1>
<p class="m-0">Maximum of 4 characters can be created per player</p> <p class="m-0">Maximum of 4 characters can be created per player</p>
</div> </div>
<div class="flex w-full h-[400px] border border-solid border-gray-500 rounded-md rounded-tl-none bg-gray"> <div class="flex w-full max-lg:flex-col lg:h-[400px] default-border rounded-md bg-gray">
<div class="w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 border-r border-solid border-gray-500 rounded-bl-md relative"> <div class="lg:min-w-[285px] max-lg:min-h-[383px] lg:w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 max-lg:border-b lg:border-r border-solid border-gray-500 max-lg:rounded-t-md lg:rounded-l-md relative">
<div class="absolute right-full -top-px flex gap-1 flex-col"> <div class="absolute right-[calc(100%_+_16px)] -top-px flex gap-2 flex-col">
<div v-for="character in characters" :key="character.id" class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')]" :class="{ active: selected_character == character.id }"> <div v-for="character in characters" :key="character.id" class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')] after:absolute after:w-full after:h-px after:bg-gray-500" :class="{ active: selectedCharacterId === character.id }">
<img src="/assets/avatar/default/head.png" class="w-9 h-9 object-contain absolute top-1/2 -translate-y-1/2" alt="Player head" /> <img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" />
<input class="opacity-0 h-full w-full absolute m-0 z-10 hover:cursor-pointer" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" /> <input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" />
</div> </div>
<div class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4"> <div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4">
<button class="p-0 h-full w-full flex flex-col justify-between" @click="isModalOpen = true"> <button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0" @click="isCreateNewCharacterModalOpen = true">
<img class="w-6 h-6 object-contain absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2" draggable="false" src="/assets/icons/plus-icon.svg" /> <img class="w-6 h-6 object-contain center-element" draggable="false" src="/assets/icons/plus-icon.svg" />
</button> </button>
</div> </div>
</div> </div>
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6" v-if="selected_character"> <div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center" v-if="selectedCharacterId">
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selected_character)?.name" /> <input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selectedCharacterId)?.name" />
<div class="flex flex-col gap-4 items-center"> <div class="flex flex-col gap-4 items-center">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-between"> <div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-between">
<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-12 object-contain mb-3.5" src="/assets/avatar/default/0.png" alt="Player avatar" /> <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 + '/' + (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]">
<!-- TODO: replace with color swatches -->
<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 == selected_character)?.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 == selected_character)?.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> <div class="flex-1 lg:w-2/3 max-lg:min-h-[212px] h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center max-lg:rounded-bl-md rounded-r-md">
<div class="w-2/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center rounded-r-md"> <div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selectedCharacterId">
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selected_character">
<div class="flex flex-col gap-3 w-full"> <div class="flex flex-col gap-3 w-full">
<span class="text-sm">Hairstyle</span> <span class="text-sm">Hairstyle</span>
<div class="flex gap-2 flex-wrap max-h-20 overflow-y-auto scrollbar"> <div class="flex gap-2 flex-wrap max-h-20 overflow-y-auto scrollbar">
<button class="bg-gray border border-solid border-gray-500 min-w-9 max-w-9 min-h-9 max-h-9 p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400"> <div
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4 m-auto" alt="Male symbol" /> class="hair-deselect 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-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"
</button> >
<!-- TODO: replace with hairstyles --> <img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" />
<button v-for="n in 30" class="bg-gray border border-solid border-gray-500 min-w-9 max-w-9 min-h-9 max-h-9 p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400"></button> <input type="radio" name="hair" :value="null" 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>
<!-- TODO #255: make radio button so we can set a value, do the same with swatches -->
<div
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"
>
<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" />
</div>
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 w-full"> <div class="flex flex-col gap-3 w-full">
<span class="text-sm">Hair color</span> <span class="text-sm">Hair color</span>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
<!-- TODO: replace with hairstyles --> <!-- TODO: replace with hair colors -->
<button v-for="n in 10" class="bg-white w-6 h-6 rounded-sm"></button> <input type="radio" name="hair-color" v-for="n in 10" class="bg-red w-6 h-6 m-0 rounded-sm hover:cursor-pointer checked:outline checked:outline-1 checked:outline-white" />
</div> </div>
</div> </div>
</div> </div>
@ -85,101 +90,99 @@
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" /> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" />
</div> </div>
<div class="button-wrapper flex self-center justify-end gap-4 max-w-[860px] w-full" 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">
<button class="btn-empty min-w-48" @click.stop="gameStore.disconnectSocket()">Back</button> <button class="btn-empty min-w-48" @click.stop="gameStore.disconnectSocket()">Back</button>
<button class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed" :disabled="!selected_character" @click="select_character()">Play now</button> <button class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed" :disabled="!selectedCharacterId" @click="loginWithCharacter()">Play now</button>
</div> </div>
</div> </div>
</div> </div>
<!-- CREATE CHARACTER MODAL --> <!-- CREATE CHARACTER MODAL -->
<Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false" :modal-width="430" :modal-height="275"> <Modal :isModalOpen="isCreateNewCharacterModalOpen" @modal:close="isCreateNewCharacterModalOpen = false" :modal-width="430" :modal-height="275">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium text-white">Create your character</h3> <h3 class="m-0 font-medium text-white">Create your character</h3>
</template> </template>
<template #modalBody> <template #modalBody>
<div class="p-4 h-[calc(100%_-_32px)]"> <div class="p-4 h-[calc(100%_-_32px)]">
<form method="post" @submit.prevent="create" class="h-full flex flex-col justify-between"> <form method="post" @submit.prevent="createCharacter" class="h-full flex flex-col justify-between">
<div class="form-field-full"> <div class="form-field-full">
<label for="name" class="text-white">Nickname</label> <label for="name" class="text-white">Nickname</label>
<input class="input-field" v-model="name" name="name" id="name" placeholder="Enter a nickname.." /> <input class="input-field" v-model="newCharacterName" name="name" id="name" placeholder="Enter a nickname..." />
</div> </div>
<div class="grid grid-flow-col justify-stretch gap-4"> <div class="grid grid-flow-col justify-stretch gap-4">
<button type="button" class="btn-empty py-1.5 px-4 inline-block" @click.prevent="isModalOpen = false">Cancel</button> <button type="button" class="btn-empty py-1.5 px-4 inline-block" @click.prevent="isCreateNewCharacterModalOpen = false">Cancel</button>
<button class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button> <button class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button>
</div> </div>
</form> </form>
</div> </div>
</template> </template>
</Modal> </Modal>
<!-- DELETE CHARACTER MODAL -->
<ConfirmationModal v-if="deletingCharacter != null" :confirm-function="delete_character.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete">
<template #modalHeader>
<h3 class="m-0 font-medium text-white">Delete character?</h3>
</template>
<template #modalBody>
<p class="mt-0 mb-5 text-white text-lg">
Do you want to permanently delete <span class="font-extrabold text-white">{{ deletingCharacter.name }}</span
>?
</p>
</template>
</ConfirmationModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useGameStore } from '@/stores/gameStore' import config from '@/application/config'
import { onBeforeUnmount, onMounted, ref } from 'vue' 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 { type Character as CharacterT } from '@/types' import { CharacterHairStorage } from '@/storage/storages'
import ConfirmationModal from '@/components/utilities/ConfirmationModal.vue' import { useGameStore } from '@/stores/gameStore'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoading = ref(true) const isLoading = ref<boolean>(true)
const characters = ref([] as CharacterT[]) const characters = ref<CharacterT[]>([])
const deletingCharacter = ref(null as CharacterT | null) const selectedCharacterId = ref<number | null>(null)
const isCreateNewCharacterModalOpen = ref<boolean>(false)
const newCharacterName = ref<string>('')
const characterHairs = ref<CharacterHair[]>([])
const selectedHairId = ref<number | null>(null)
// Fetch characters // Fetch characters
gameStore.connection?.on('character:list', (data: any) => {
characters.value = data
})
onMounted(async () => {
// wait 0.75 sec
setTimeout(() => { setTimeout(() => {
gameStore.connection?.emit('character:list') gameStore.connection?.emit('character:list')
isLoading.value = false
}, 750) }, 750)
gameStore.connection?.on('character:list', (data: any) => {
characters.value = data
isLoading.value = false
}) })
// Select character logics // Select character logics
const selected_character = ref(null) function loginWithCharacter() {
function select_character() { if (!selectedCharacterId.value) return
if (!selected_character.value) return
deletingCharacter.value = null
gameStore.connection?.emit('character:connect', { characterId: selected_character.value })
gameStore.connection?.on('character:connect', (data: CharacterT) => gameStore.setCharacter(data))
}
// Delete character logics gameStore.connection?.emit(
function delete_character(characterId: number) { 'character:connect',
if (!characterId) return {
deletingCharacter.value = null characterId: selectedCharacterId.value,
gameStore.connection?.emit('character:delete', { characterId: characterId }) characterHairId: selectedHairId.value
},
(response: { character: CharacterT; map: Map; characters: CharacterT[] }) => {
gameStore.setCharacter(response.character)
}
)
} }
// Create character logics // Create character logics
const isModalOpen = ref(false) function createCharacter() {
const name = ref('')
function create() {
gameStore.connection?.on('character:create:success', (data: CharacterT) => { gameStore.connection?.on('character:create:success', (data: CharacterT) => {
gameStore.setCharacter(data) gameStore.setCharacter(data)
isModalOpen.value = false isCreateNewCharacterModalOpen.value = false
}) })
gameStore.connection?.emit('character:create', { name: name.value }) gameStore.connection?.emit('character:create', { name: newCharacterName.value })
} }
// Watch changes for selected character and update hairs
watch(selectedCharacterId, (characterId) => {
if (!characterId) return
// selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null
})
onMounted(async () => {
const characterHairStorage = new CharacterHairStorage()
characterHairs.value = await characterHairStorage.getAll()
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
gameStore.connection?.off('character:list') gameStore.connection?.off('character:list')
gameStore.connection?.off('character:connect') gameStore.connection?.off('character:connect')

View File

@ -6,7 +6,7 @@
<Hud /> <Hud />
<Hotkeys /> <Hotkeys />
<Clock /> <Clock />
<Zone /> <Map />
<Chat /> <Chat />
<ExpBar /> <ExpBar />
@ -18,21 +18,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/application/config'
import 'phaser' import 'phaser'
import { Game, Scene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import Menu from '@/components/gui/Menu.vue'
import ExpBar from '@/components/gui/ExpBar.vue'
import Hud from '@/components/gui/Hud.vue'
import Zone from '@/components/zone/Zone.vue'
import Hotkeys from '@/components/gui/Hotkeys.vue'
import Chat from '@/components/gui/Chat.vue'
import CharacterProfile from '@/components/gui/CharacterProfile.vue'
import Effects from '@/components/Effects.vue' import Effects from '@/components/Effects.vue'
import Map from '@/components/game/map/Map.vue'
import CharacterProfile from '@/components/gui/CharacterProfile.vue'
import Chat from '@/components/gui/Chat.vue'
// import Minimap from '@/components/gui/Minimap.vue' // import Minimap from '@/components/gui/Minimap.vue'
import Clock from '@/components/gui/Clock.vue' import Clock from '@/components/gui/Clock.vue'
import ExpBar from '@/components/gui/ExpBar.vue'
import Hotkeys from '@/components/gui/Hotkeys.vue'
import Hud from '@/components/gui/Hud.vue'
import Menu from '@/components/gui/Menu.vue'
import { useGameStore } from '@/stores/gameStore'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin' import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { Game, Scene } from 'phavuer'
import { onBeforeUnmount } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
@ -75,9 +76,11 @@ 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/zone/blank_tile.png') scene.load.image('blank_tile', '/assets/map/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,60 @@
<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"> <svg width="40" height="40" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<span class="text-white text-lg flex-1 text-center">Play</span> <circle cx="4" cy="12" r="3" fill="white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <animate id="spinner_qFRN" begin="0;spinner_OcgL.end+0.25s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> </circle>
<circle cx="12" cy="12" r="3" fill="white">
<animate begin="spinner_qFRN.begin+0.1s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
<circle cx="20" cy="12" r="3" fill="white">
<animate id="spinner_OcgL" begin="spinner_qFRN.begin+0.2s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
</svg> </svg>
</button>
</div> </div>
</template> </template>
<script setup lang="ts" async> <script setup lang="ts" async>
import config from '@/application/config'
import type { HttpResponse, MapObject } from '@/application/types'
import type { BaseStorage } from '@/storage/baseStorage'
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
// import type { Map } from '@/application/types'
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 async function downloadAndStore<T extends { id: string }>(endpoint: string, storage: BaseStorage<T>) {
gameStore.game.isLoaded = true 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) {
await storage.add(item)
}
}
const tileStorage = new TileStorage()
const mapStorage = new MapStorage()
const mapObjectStorage = new MapObjectStorage()
Promise.all([
downloadAndStore('tiles', tileStorage),
downloadAndStore('maps', mapStorage),
downloadAndStore('map_objects', mapObjectStorage),
downloadAndStore('sprites', new SpriteStorage()),
downloadAndStore('character_types', new CharacterTypeStorage()),
downloadAndStore('character_hair', new CharacterHairStorage())
]).then(() => {
gameStore.game.isLoaded = true
})
</script> </script>

View File

@ -6,7 +6,7 @@
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div> <div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative"> <div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20"> <div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" alt="Sylvan Quest logo" /> <!-- <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="UI box outer" />
<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)] max-lg:hidden" alt="UI box inner" />
@ -26,13 +26,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import LoginForm from '@/components/login/LoginForm.vue'
import NewPasswordForm from '@/components/login/NewPasswordForm.vue'
import RegisterForm from '@/components/login/RegisterForm.vue'
import ResetPassword from '@/components/login/ResetPasswordModal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import LoginForm from '@/components/login/LoginForm.vue' import { onMounted, ref } from 'vue'
import RegisterForm from '@/components/login/RegisterForm.vue'
import NewPasswordForm from '@/components/login/NewPasswordForm.vue'
import ResetPassword from '@/components/login/ResetPasswordModal.vue'
const isPasswordResetFormShown = ref(false) const isPasswordResetFormShown = ref(false)
const doesUrlHaveToken = ref(window.location.hash !== '') const doesUrlHaveToken = ref(window.location.hash !== '')

View File

@ -2,25 +2,25 @@
<div class="flex justify-center items-center h-dvh relative"> <div class="flex justify-center items-center h-dvh relative">
<Game :config="gameConfig" @create="createGame"> <Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene"> <Scene name="main" @preload="preloadScene" @create="createScene">
<ZoneEditor :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt ?? ''}`)" /> <MapEditor :key="JSON.stringify(`${mapEditorStore.map?.id}_${mapEditorStore.map?.createdAt}_${mapEditorStore.map?.updatedAt ?? ''}`)" />
</Scene> </Scene>
</Game> </Game>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/application/config'
import 'phaser' import 'phaser'
import { Game, Scene } from 'phavuer' import type { TextureData } from '@/application/types'
import { useGameStore } from '@/stores/gameStore' import MapEditor from '@/components/gameMaster/mapEditor/MapEditor.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { loadTexture } from '@/composables/gameComposable' import { loadTexture } from '@/composables/gameComposable'
import type { AssetDataT } from '@/types' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { Game, Scene } from 'phavuer'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const mapEditorStore = useMapEditorStore()
const gameConfig = { const gameConfig = {
name: config.name, name: config.name,
@ -61,9 +61,9 @@ const preloadScene = async (scene: Phaser.Scene) => {
/** /**
* Load the base assets into the Phaser scene * Load the base assets into the Phaser scene
*/ */
scene.load.image('BLOCK', '/assets/zone/bt_tile.png') scene.load.image('BLOCK', '/assets/map/bt_tile.png')
scene.load.image('TELEPORT', '/assets/zone/tp_tile.png') scene.load.image('TELEPORT', '/assets/map/tp_tile.png')
scene.load.image('blank_tile', '/assets/zone/blank_tile.png') scene.load.image('blank_tile', '/assets/map/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png') scene.load.image('waypoint', '/assets/waypoint.png')
/** /**
@ -72,8 +72,9 @@ const preloadScene = async (scene: Phaser.Scene) => {
* Then load them into the scene. * Then load them into the scene.
*/ */
scene.load.rexAwait(async function (successCallback: any) { scene.load.rexAwait(async function (successCallback: any) {
const tiles: AssetDataT[] = await fetch(config.server_endpoint + '/assets/list_tiles').then((response) => response.json()) const tiles: { data: TextureData[] } = await fetch(config.server_endpoint + '/assets/list_tiles').then((response) => response.json())
for await (const tile of tiles) {
for await (const tile of tiles?.data ?? []) {
await loadTexture(scene, tile) await loadTexture(scene, tile)
} }

View File

@ -1,212 +0,0 @@
<template>
<!-- Chat bubble -->
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
</Container>
<!-- Character name and health -->
<Container :depth="999" :x="currentX" :y="currentY">
<Text @create="createNicknameText" :text="props.zoneCharacter.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="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
</Container>
<!-- Character sprite -->
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" :flipY="false" />
</Container>
</template>
<script lang="ts" setup>
import config from '@/config'
import { type Sprite as SpriteT, type ZoneCharacter } from '@/types'
import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore'
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { loadSpriteTextures } from '@/composables/gameComposable'
enum Direction {
POSITIVE,
NEGATIVE,
UNCHANGED
}
const props = defineProps<{
layer: Phaser.Tilemaps.TilemapLayer
zoneCharacter: ZoneCharacter
}>()
const charChatContainer = refObj<Phaser.GameObjects.Container>()
const charContainer = refObj<Phaser.GameObjects.Container>()
const charSprite = refObj<Phaser.GameObjects.Sprite>()
const game = useGame()
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const scene = useScene()
const currentX = ref(0)
const currentY = ref(0)
const isometricDepth = ref(1)
const isInitialPosition = ref(true)
const tween = ref<Phaser.Tweens.Tween | null>(null)
const updateIsometricDepth = (x: number, y: number) => {
isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
}
const updatePosition = (x: number, y: number, direction: Direction) => {
const targetX = tileToWorldX(props.layer, x, y)
const targetY = tileToWorldY(props.layer, x, y)
if (isInitialPosition.value) {
currentX.value = targetX
currentY.value = targetY
isInitialPosition.value = false
return
}
if (tween.value?.isPlaying()) {
tween.value.stop()
}
const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2))
if (distance >= config.tile_size.x / 1.1) {
currentX.value = targetX
currentY.value = targetY
return
}
const duration = distance * 6
tween.value = props.layer.scene.tweens.add({
targets: { x: currentX.value, y: currentY.value },
x: targetX,
y: targetY,
duration,
ease: 'Linear',
onStart: () => {
if (direction === Direction.POSITIVE) {
updateIsometricDepth(x, y)
}
},
onUpdate: (tween) => {
currentX.value = tween.targets[0].x
currentY.value = tween.targets[0].y
},
onComplete: () => {
if (direction === Direction.NEGATIVE) {
updateIsometricDepth(x, y)
}
}
})
}
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?.id ?? '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
}
charSprite.value!.anims.stop()
charSprite.value!.setFrame(0)
charSprite.value!.setTexture(charTexture.value)
}
const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
}
const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(`${props.zoneCharacter.character.name}_chatText`)
text.setFontSize(13)
text.setFontFamily('Arial')
text.setOrigin(0.5, 10.9)
// Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) {
text.setOrigin(0.5, 9.75)
if (game.device.browser.firefox) {
text.setOrigin(0.5, 10.9)
}
}
}
const createNicknameText = (text: Phaser.GameObjects.Text) => {
text.setFontSize(13)
text.setFontFamily('Arial')
text.setOrigin(0.5, 9)
// Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) {
text.setOrigin(0.5, 8)
if (game.device.browser.firefox) {
text.setOrigin(0.5, 9)
}
}
}
watch(
() => props.zoneCharacter.character,
(newChar, oldChar) => {
if (!newChar) return
if (!oldChar || newChar.positionX !== oldChar.positionX || newChar.positionY !== oldChar.positionY) {
const direction = !oldChar ? Direction.POSITIVE : calcDirection(oldChar.positionX, oldChar.positionY, newChar.positionX, newChar.positionY)
updatePosition(newChar.positionX, newChar.positionY, direction)
}
}
)
watch(() => props.zoneCharacter, updateSprite)
loadSpriteTextures(scene, props.zoneCharacter.character.characterType?.sprite as SpriteT)
.then(() => {
charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value)
})
.catch((error) => {
console.error('Error loading texture:', error)
})
onMounted(() => {
charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false)
charContainer.value!.setName(props.zoneCharacter.character!.name)
if (props.zoneCharacter.character.id === gameStore.character!.id) {
zoneStore.setCharacterLoaded(true)
// #146 : Set camera position to character, need to be improved still
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
scene.cameras.main.stopFollow()
}
updatePosition(props.zoneCharacter.character.positionX, props.zoneCharacter.character.positionY, props.zoneCharacter.character.rotation)
})
onUnmounted(() => {
tween.value?.stop()
})
</script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="mb-4 flex flex-col gap-3"> <div class="mb-4 flex flex-col gap-3">
<div @click="toggle" class="p-3 bg-gray-200 bg-opacity-50 rounded hover:bg-gray-300 text-white font-default cursor-pointer"> <div @click="toggle" class="p-3 bg-gray-300 bg-opacity-50 rounded hover:bg-gray-400 text-white font-default cursor-pointer">
<slot name="header" /> <slot name="header" />
</div> </div>
<transition enter-active-class="transition-all duration-300 ease-in-out" leave-active-class="transition-all duration-300 ease-in-out" enter-from-class="opacity-0 max-h-0" enter-to-class="opacity-100 max-h-96" leave-from-class="opacity-100 max-h-96" leave-to-class="opacity-0 max-h-0"> <transition enter-active-class="transition-all duration-300 ease-in-out" leave-active-class="transition-all duration-300 ease-in-out" enter-from-class="opacity-0 max-h-0" enter-to-class="opacity-100 max-h-96" leave-from-class="opacity-100 max-h-96" leave-to-class="opacity-0 max-h-0">

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