Compare commits

..

No commits in common. "main" and "feature/#245-character-hair" have entirely different histories.

202 changed files with 7687 additions and 12907 deletions

View File

@ -1,6 +1,5 @@
VITE_NAME=Noxious
VITE_DOMAIN=localhost
VITE_ENVIRONMENT=development
VITE_NAME=Sylvan Quest
VITE_DEVELOPMENT=true
VITE_SERVER_ENDPOINT=http://localhost:4000
VITE_TILE_SIZE_WIDTH=64
VITE_TILE_SIZE_HEIGHT=32
VITE_TILE_SIZE_X=64
VITE_TILE_SIZE_Y=32

13
.eslintrc.cjs Normal file
View File

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

View File

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

View File

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

View File

@ -1,70 +0,0 @@
{
# Global options
admin off # Disable admin API
# Global logging configuration
log {
output file /var/log/caddy/access.log
format json
level INFO
}
}
noxious.gg {
# Root directory for your Vue app
root * ./dist
# Enable compression with optimal settings
encode zstd gzip
# Handle SPA routing
try_files {path} /index.html
# Serve static files with optimizations
file_server
# Enhanced security headers
header {
# Existing headers with improvements
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
# Additional security headers
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
# Remove server information
-Server
}
# Improved cache configuration for static assets
@static {
file
path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot
}
header @static {
Cache-Control "public, max-age=31536000, immutable"
Vary Accept-Encoding
}
# Cache control for HTML files
@html {
file
path *.html
}
header @html {
Cache-Control "no-cache, must-revalidate"
}
# Handle errors
handle_errors {
respond "{http.error.status_code} {http.error.status_text}" {http.error.status_code}
}
}
# Improved redirect configuration
www.noxious.gg {
redir https://noxious.gg{uri} permanent
}

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
# Build stage
FROM node:22.4.1-alpine as builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
# Set environment variables
ARG VITE_NAME=${VITE_NAME}
ENV VITE_NAME=${VITE_NAME}
ARG VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
ENV VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
ARG VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
ENV VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
ARG VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
ENV VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
ARG VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
ENV VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
# Build the application
RUN npm run build-ntc
# Production stage
FROM nginx:1.26.1-alpine
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

4
captain-definition Normal file
View File

@ -0,0 +1,4 @@
{
"schemaVersion": 2,
"dockerfilePath" :"./Dockerfile"
}

View File

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

16
nginx.conf Normal file
View File

@ -0,0 +1,16 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Redirect example
location /discord {
return 301 https://discord.gg/JTev3nzeDa;
}
# Serve static files
location / {
try_files $uri $uri/ /index.html;
}
}

4751
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,36 +11,39 @@
"test:unit": "vitest",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0",
"axios": "^1.7.9",
"dexie": "^4.0.11",
"phaser": "^3.88.2",
"phaser3-rex-plugins": "^1.80.13",
"phavuer": "^0.16.5",
"pinia": "^2.3.1",
"sharp": "^0.33.5",
"socket.io-client": "^4.8.1",
"axios": "^1.7.7",
"dexie": "^4.0.8",
"phaser": "^3.86.0",
"pinia": "^2.1.6",
"socket.io-client": "^4.8.0",
"universal-cookie": "^6.1.3",
"vite-plugin-image-optimizer": "^1.1.8",
"vue": "^3.5.13",
"zod": "^3.24.2"
"vue": "^3.5.12",
"zod": "^3.22.2"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@tauri-apps/cli": "^2.2.7",
"@rushstack/eslint-patch": "^1.10.3",
"@tsconfig/node20": "^20.1.4",
"@types/jsdom": "^21.1.7",
"@types/node": "^20.14.11",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.19",
"easystarjs": "^0.4.4",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0",
"jsdom": "^24.1.1",
"npm-run-all2": "^6.2.3",
"phaser3-rex-plugins": "^1.80.8",
"phavuer": "^0.16.1",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"sass": "^1.79.4",
@ -48,6 +51,7 @@
"typescript": "~5.6.2",
"vite": "^5.4.9",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "^7.5.2",
"vitest": "^2.0.3",
"vue-tsc": "^1.6.5"
}

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.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
public/assets/fog.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
public/assets/galaxy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 9.5L12 14.5L7 9.5" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 325 B

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.3333 17.5L11.0833 12.25C10.6667 12.5833 10.1875 12.8472 9.64583 13.0417C9.10417 13.2361 8.52778 13.3333 7.91667 13.3333C6.40278 13.3333 5.12153 12.809 4.07292 11.7604C3.02431 10.7118 2.5 9.43056 2.5 7.91667C2.5 6.40278 3.02431 5.12153 4.07292 4.07292C5.12153 3.02431 6.40278 2.5 7.91667 2.5C9.43056 2.5 10.7118 3.02431 11.7604 4.07292C12.809 5.12153 13.3333 6.40278 13.3333 7.91667C13.3333 8.52778 13.2361 9.10417 13.0417 9.64583C12.8472 10.1875 12.5833 10.6667 12.25 11.0833L17.5 16.3333L16.3333 17.5ZM7.91667 11.6667C8.95833 11.6667 9.84375 11.3021 10.5729 10.5729C11.3021 9.84375 11.6667 8.95833 11.6667 7.91667C11.6667 6.875 11.3021 5.98958 10.5729 5.26042C9.84375 4.53125 8.95833 4.16667 7.91667 4.16667C6.875 4.16667 5.98958 4.53125 5.26042 5.26042C4.53125 5.98958 4.16667 6.875 4.16667 7.91667C4.16667 8.95833 4.53125 9.84375 5.26042 10.5729C5.98958 11.3021 6.875 11.6667 7.91667 11.6667Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,59 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M490.667,405.333h-56.811C424.619,374.592,396.373,352,362.667,352s-61.931,22.592-71.189,53.333H21.333
C9.557,405.333,0,414.891,0,426.667S9.557,448,21.333,448h270.144c9.237,30.741,37.483,53.333,71.189,53.333
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,405.333,490.667,405.333z M362.667,458.667
c-17.643,0-32-14.357-32-32s14.357-32,32-32s32,14.357,32,32S380.309,458.667,362.667,458.667z"/>
</g>
</g>
<g>
<g>
<path d="M490.667,64h-56.811c-9.259-30.741-37.483-53.333-71.189-53.333S300.736,33.259,291.477,64H21.333
C9.557,64,0,73.557,0,85.333s9.557,21.333,21.333,21.333h270.144C300.736,137.408,328.96,160,362.667,160
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,64,490.667,64z M362.667,117.333
c-17.643,0-32-14.357-32-32c0-17.643,14.357-32,32-32s32,14.357,32,32C394.667,102.976,380.309,117.333,362.667,117.333z"/>
</g>
</g>
<g>
<g>
<path d="M490.667,234.667H220.523c-9.259-30.741-37.483-53.333-71.189-53.333s-61.931,22.592-71.189,53.333H21.333
C9.557,234.667,0,244.224,0,256c0,11.776,9.557,21.333,21.333,21.333h56.811c9.259,30.741,37.483,53.333,71.189,53.333
s61.931-22.592,71.189-53.333h270.144c11.797,0,21.333-9.557,21.333-21.333C512,244.224,502.464,234.667,490.667,234.667z
M149.333,288c-17.643,0-32-14.357-32-32s14.357-32,32-32c17.643,0,32,14.357,32,32S166.976,288,149.333,288z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 946 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Binary file not shown.

BIN
public/assets/raindrop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 301 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: 454 KiB

After

Width:  |  Height:  |  Size: 453 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

Before

Width:  |  Height:  |  Size: 708 B

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,4 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

5149
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +0,0 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.4", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.2.4", features = [] }
tauri-plugin-log = "2.0.0-rc"

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@ -1,11 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1,16 +0,0 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -1,6 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

View File

@ -1,37 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "noxious",
"version": "0.1.0",
"identifier": "com.noxious.app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build-only"
},
"app": {
"windows": [
{
"title": "Noxious",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View File

@ -1,55 +1,48 @@
<template>
<Debug />
<Notifications />
<BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" />
<component :is="currentScreen" />
</template>
<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 Login from '@/components/screens/Login.vue'
import Characters from '@/components/screens/Characters.vue'
import Game from '@/components/screens/Game.vue'
import Loading from '@/components/screens/Loading.vue'
import Login from '@/components/screens/Login.vue'
import MapEditor from '@/components/screens/MapEditor.vue'
import Debug from '@/components/utilities/Debug.vue'
import Notifications from '@/components/utilities/Notifications.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useSoundComposable } from '@/composables/useSoundComposable'
import { socketManager } from '@/managers/SocketManager'
import { useGameStore } from '@/stores/gameStore'
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
import { computed, watch } from 'vue'
const gameStore = useGameStore()
const mapEditor = useMapEditorComposable()
const { playSound } = useSoundComposable()
const zoneEditorStore = useZoneEditorStore()
const currentScreen = computed(() => {
if (!gameStore.game.isLoaded) return Loading
if (!socketManager.connection) return Login
if (!socketManager.token) return Login
if (!gameStore.connection) return Login
if (!gameStore.token) return Login
if (!gameStore.character) return Characters
if (mapEditor.active.value) return MapEditor
if (zoneEditorStore.active) return ZoneEditor
return Game
})
// Watch mapEditor.active and empty gameStore.game.loadedAssets
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
watch(
() => mapEditor.active.value,
() => zoneEditorStore.active,
() => {
gameStore.game.loadedTextures = []
gameStore.game.loadedAssets = []
}
)
// #209: Play sound when a button is pressed
addEventListener('click', (event) => {
const classList = ['btn-cyan', 'btn-red', 'btn-indigo', 'btn-empty', 'btn-sound']
const target = event.target as HTMLElement
// console.log(target) // Uncomment to log the clicked element
if (classList.some((className) => target.classList.contains(className))) {
playSound('/assets/sounds/button-click.wav')
if (!(event.target instanceof HTMLButtonElement)) {
return
}
const audio = new Audio('/assets/music/click-btn.mp3')
audio.play()
})
// Watch for "G" key press and toggle the gm panel
@ -57,9 +50,7 @@ addEventListener('keydown', (event) => {
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
// Check if no input is active or focus is on an input
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
return
}
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return
if (event.key === 'G') {
gameStore.toggleGmPanel()

View File

@ -1,10 +0,0 @@
export default {
name: import.meta.env.VITE_NAME,
domain: import.meta.env.VITE_DOMAIN,
environment: import.meta.env.VITE_ENVIRONMENT,
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
tile_size: {
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
height: Number(import.meta.env.VITE_TILE_SIZE_HEIGHT)
}
}

View File

@ -1,63 +0,0 @@
export enum Direction {
POSITIVE,
NEGATIVE,
UNCHANGED
}
export enum SocketEvent {
CONNECT_ERROR = 'connect_error',
RECONNECT_FAILED = 'reconnect_failed',
CLOSE = '52',
DATA = '51',
CHARACTER_CONNECT = '50',
CHARACTER_CREATE = '49',
CHARACTER_DELETE = '48',
CHARACTER_LIST = '47',
GM_CHARACTERHAIR_CREATE = '46',
GM_CHARACTERHAIR_REMOVE = '45',
GM_CHARACTERHAIR_LIST = '44',
GM_CHARACTERHAIR_UPDATE = '43',
GM_CHARACTERTYPE_CREATE = '42',
GM_CHARACTERTYPE_REMOVE = '41',
GM_CHARACTERTYPE_LIST = '40',
GM_CHARACTERTYPE_UPDATE = '39',
GM_ITEM_CREATE = '38',
GM_ITEM_REMOVE = '37',
GM_ITEM_LIST = '36',
GM_ITEM_UPDATE = '35',
GM_MAPOBJECT_LIST = '34',
GM_MAPOBJECT_REMOVE = '33',
GM_MAPOBJECT_UPDATE = '32',
GM_MAPOBJECT_UPLOAD = '31',
GM_SPRITE_COPY = '30',
GM_SPRITE_CREATE = '29',
GM_SPRITE_DELETE = '28',
GM_SPRITE_LIST = '27',
GM_SPRITE_UPDATE = '26',
GM_TILE_DELETE = '25',
GM_TILE_LIST = '24',
GM_TILE_UPDATE = '23',
GM_TILE_UPLOAD = '22',
GM_MAP_CREATE = '21',
GM_MAP_DELETE = '20',
GM_MAP_REQUEST = '19',
GM_MAP_UPDATE = '18',
MAP_CHARACTER_MOVEERROR = '17',
DISCONNECT = 'disconnect',
USER_DISCONNECT = '15',
LOGIN = '14',
LOGGED_IN = '13',
NOTIFICATION = '12',
DATE = '11',
FAILED = '10',
COMPLETED = '9',
CONNECTION = 'connection',
WEATHER = '7',
CHARACTER_DISCONNECT = '6',
MAP_CHARACTER_ATTACK = '5',
MAP_CHARACTER_TELEPORT = '4',
MAP_CHARACTER_JOIN = '3',
MAP_CHARACTER_LEAVE = '2',
MAP_CHARACTER_MOVE = '1',
CHAT_MESSAGE = '0'
}

View File

@ -1,45 +0,0 @@
import config from '@/application/config'
import type { HttpResponse } from '@/application/types'
import type { BaseStorage } from '@/storage/baseStorage'
export function uuidv4() {
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
}
export function unduplicateArray(array: any[]) {
const arrayToProcess = typeof array.flat === 'function' ? array.flat() : array
return [...new Set(arrayToProcess)]
}
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
const response = (await request.json()) as HttpResponse<T[]>
if (!response.success) {
console.error(`Failed to download ${endpoint}:`, response.message)
return
}
const items = response.data ?? []
const serverItemIds = new Set(items.map((item) => item.id))
// Remove items that don't exist on server
const existingItems = await storage.getAll()
for (const existingItem of existingItems) {
if (!serverItemIds.has(existingItem.id)) {
await storage.delete(existingItem.id)
}
}
// Update or add new items
for (const item of items) {
let overwrite = false
const existingItem = await storage.getById(item.id)
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
overwrite = true
}
await storage.add(item, overwrite)
}
}

Binary file not shown.

View File

@ -73,7 +73,7 @@ input {
}
.input-field {
@apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300 font-default;
@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;
}
@ -88,12 +88,6 @@ input {
}
}
select {
&.input-field {
@apply appearance-none bg-[url('/assets/icons/mapEditor/dropdown-chevron.svg')] bg-no-repeat bg-[calc(100%_-_10px)_center] bg-[length:20px] text-white;
}
}
.form-field-full {
@apply w-full flex flex-col mb-5;
label {
@ -124,16 +118,7 @@ button {
&.active,
&:hover {
@apply bg-red-500;
}
}
&.btn-indigo {
@apply bg-indigo-500 text-gray-50 text-base leading-5 rounded py-2.5;
&.active,
&:hover {
@apply bg-indigo-600;
@apply bg-red-400;
}
}
@ -143,7 +128,7 @@ button {
&.active,
&.selected,
&:hover {
@apply bg-gray border-gray;
@apply bg-gray-700 border-gray-700;
}
}
@ -160,12 +145,10 @@ button {
}
}
.character.active {
@apply bg-gray bg-none;
.character {
&.active {
@apply pr-px border-r-0;
}
.list-open {
@apply w-[calc(75%_-_40px)] max-xl:w-[calc(100%_-_360px)];
}
.hair-deselect:has(:checked) {
@ -174,14 +157,6 @@ button {
}
}
.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 {
@apply text-white font-ui drop-shadow-pixel-black;
}

163
src/components/Effects.vue Normal file
View File

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

View File

@ -1,10 +1,10 @@
<template>
<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" role="listitem">
<div class="flex flex-wrap items-center input-field gap-1">
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
<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.stop="deleteChip(i)" aria-label="Remove tag">×</button>
<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>
</div>
<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" />
<input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
</div>
</template>
@ -14,29 +14,20 @@ import type { Ref } from 'vue'
interface Props {
modelValue?: string[]
maxChips?: number
maxChipLength?: number
placeholder?: string
allowDuplicates?: boolean
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => [],
maxChips: 10,
maxChipLength: 20,
placeholder: 'Add tag',
allowDuplicates: false
modelValue: () => []
})
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
(e: 'error', message: string): void
}>()
const currentInput: Ref<string> = ref('')
const internalValue = ref<string[]>([])
const inputRef = ref<HTMLInputElement | null>(null)
// Initialize internalValue with props.modelValue
watch(
() => props.modelValue,
(newValue) => {
@ -45,27 +36,9 @@ watch(
{ 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 trimmedInput = currentInput.value.trim()
if (validateChip(trimmedInput)) {
if (trimmedInput && !internalValue.value.includes(trimmedInput)) {
internalValue.value.push(trimmedInput)
emit('update:modelValue', internalValue.value)
currentInput.value = ''
@ -77,36 +50,10 @@ const deleteChip = (index: number) => {
emit('update:modelValue', internalValue.value)
}
const handleKeydown = (event: KeyboardEvent) => {
switch (event.key) {
case 'Enter':
event.preventDefault()
addChip()
break
case 'Backspace':
if (currentInput.value === '' && internalValue.value.length > 0) {
deleteChip(internalValue.value.length - 1)
const handleBackspace = (event: KeyboardEvent) => {
if (event.key === 'Backspace' && currentInput.value === '' && internalValue.value.length > 0) {
internalValue.value.pop()
emit('update:modelValue', internalValue.value)
}
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>

View File

@ -1,103 +1,167 @@
<template>
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
<ChatBubble :mapCharacter="props.mapCharacter" />
<HealthBar :mapCharacter="props.mapCharacter" />
<CharacterHair :mapCharacter="props.mapCharacter" :flipX="isFlippedX" />
<Sprite ref="characterSprite" :flipX="isFlippedX" />
<ChatBubble :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
<Healthbar :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
<CharacterHair :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
</Container>
</template>
<script lang="ts" setup>
import { type MapCharacter } 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 { useCharacterSpriteComposable } from '@/composables/useCharacterSpriteComposable'
import { useSoundComposable } from '@/composables/useSoundComposable'
import config from '@/config'
import { type Sprite as SpriteT, type ZoneCharacter } from '@/types'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { Container, Sprite, useScene } from 'phavuer'
import { onMounted, onUnmounted, watch } from 'vue'
import { useZoneStore } from '@/stores/zoneStore'
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
import { Container, Image, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { loadSpriteTextures } from '@/composables/gameComposable'
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
import Healthbar from '@/components/game/character/partials/Healthbar.vue'
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
enum Direction {
POSITIVE,
NEGATIVE,
UNCHANGED
}
const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap
mapCharacter: MapCharacter
layer: Phaser.Tilemaps.TilemapLayer
zoneCharacter: ZoneCharacter
}>()
const charContainer = refObj<Phaser.GameObjects.Container>()
const charSprite = refObj<Phaser.GameObjects.Sprite>()
const gameStore = useGameStore()
const mapStore = useMapStore()
const zoneStore = useZoneStore()
const scene = useScene()
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
const { playSound, stopSound } = useSoundComposable()
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 handlePositionUpdate = (newValues: any, oldValues: any) => {
if (!newValues) return
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
updatePosition(newValues.positionX, newValues.positionY)
const updateIsometricDepth = (x: number, y: number) => {
isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
}
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite()
}
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 * 5.7
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)
}
/**
* Plays walk sound when character is moving
*/
watch(
() => props.mapCharacter.isMoving,
(newValue) => {
if (newValue) {
playSound('/assets/sounds/walk.wav', false, true)
} else {
stopSound('/assets/sounds/walk.wav')
() => 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)
}
}
)
/**
* Plays attack animation and sound when character is attacking
*/
watch(
() => props.mapCharacter.isAttacking,
(newValue) => {
if (newValue) {
playAnimation('attack')
playSound('/assets/sounds/attack.wav', false, true)
} else {
stopSound('/assets/sounds/attack.wav')
}
mapStore.updateCharacterProperty(props.mapCharacter.character.id, 'isAttacking', false)
}
)
watch(() => props.zoneCharacter, updateSprite)
/**
* Handles position updates and movement delay
*/
watch(
() => ({
positionX: props.mapCharacter.character.positionX,
positionY: props.mapCharacter.character.positionY,
isMoving: props.mapCharacter.isMoving,
rotation: props.mapCharacter.character.rotation,
isAttacking: props.mapCharacter.isAttacking
}),
async (oldValues, newValues) => {
handlePositionUpdate(oldValues, newValues)
}
)
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(async () => {
await initializeSprite()
onMounted(() => {
charContainer.value!.setName(props.zoneCharacter.character!.name)
if (props.mapCharacter.character.id === gameStore.character!.id) {
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
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(() => {
cleanup()
tween.value?.stop()
})
</script>

View File

@ -1,63 +1,47 @@
<template>
<Image ref="image" v-if="hairSpriteId" />
<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 '@/services/textureService'
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
import { Image, refObj, useScene } from 'phavuer'
import { computed, onMounted, ref, watch } from 'vue'
import { computed } from 'vue'
import { Image, useScene } from 'phavuer'
import type { Sprite as SpriteT, ZoneCharacter } from '@/types'
import { loadSpriteTextures } from '@/composables/gameComposable'
import { useGameStore } from '@/stores/gameStore'
const props = defineProps<{
mapCharacter: MapCharacter
zoneCharacter: ZoneCharacter
currentX: number
currentY: number
}>()
const gameStore = useGameStore()
const scene = useScene()
const hairSpriteId = ref('')
const hairSprite = ref<SpriteT | null>(null)
const characterSpriteHeight = ref(0)
const image = refObj<Phaser.GameObjects.Image>()
const flipX = computed(() => [6, 0].includes(props.mapCharacter.character.rotation ?? 0))
const texture = computed(() => {
const direction = flipX.value ? 'back' : 'front'
const { rotation, characterHair } = props.zoneCharacter.character
const spriteId = characterHair?.sprite?.id
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${hairSpriteId.value}-${direction}`
return `${spriteId}-${direction}`
})
watch(
() => props.mapCharacter.character,
(newValue) => {
if (!image.value) return
image.value.setTexture(texture.value)
},
{ deep: true }
)
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
onMounted(async () => {
if (!props.mapCharacter.character.characterType || !props.mapCharacter.character.characterHair) return
const ANIMATION_MS = 500 // Animation duration in milliseconds
const imageProps = computed(() => ({
depth: 1,
originY: [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 4.30 : 5.30,
flipX: isFlippedX.value,
texture: texture.value,
y: props.zoneCharacter.isMoving ? (scene.time.now % ANIMATION_MS < (ANIMATION_MS / 2) ? 0.5 : -0.5) : 0,
x: props.zoneCharacter.isMoving ? (scene.time.now % ANIMATION_MS < (ANIMATION_MS / 2) ? -0.5 : 0.5) : 0,
}))
const characterTypeStorage = new CharacterTypeStorage()
const characterHairStorage = new CharacterHairStorage()
const spriteStorage = new SpriteStorage()
const characterType = await characterTypeStorage.getById(props.mapCharacter.character.characterType!)
if (!characterType) return
characterSpriteHeight.value = 100
hairSpriteId.value = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair)
if (!hairSpriteId.value) return
hairSprite.value = await spriteStorage.getById(hairSpriteId.value)
if (!hairSprite.value) return
await loadSpriteTextures(scene, hairSpriteId.value)
if (!image.value) return
image.value.setOrigin(0.5, 2.15)
image.value.setTexture(texture.value)
image.value.setSize(30, 40)
loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT)
.then(() => {})
.catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -1,32 +1,33 @@
<template>
<Container ref="characterChatContainer">
<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 type { ZoneCharacter } from '@/types'
import { onMounted } from 'vue'
const props = defineProps<{
mapCharacter: MapCharacter
zoneCharacter: ZoneCharacter
currentX: number
currentY: number
}>()
const game = useGame()
const characterChatContainer = refObj<Phaser.GameObjects.Container>()
const charChatContainer = refObj<Phaser.GameObjects.Container>()
const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
}
const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(`${props.mapCharacter.character.name}_chatText`)
text.setName(`${props.zoneCharacter.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) {
@ -39,7 +40,7 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
}
onMounted(() => {
characterChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
characterChatContainer.value!.setVisible(false)
charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false)
})
</script>

View File

@ -1,17 +1,19 @@
<template>
<Container :depth="999">
<Text @create="createNicknameText" :text="props.mapCharacter.character.name" />
<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>
</template>
<script setup lang="ts">
import type { MapCharacter } from '@/application/types'
import { Container, RoundRectangle, Text, useGame } from 'phavuer'
import type { ZoneCharacter } from '@/types'
const props = defineProps<{
mapCharacter: MapCharacter
zoneCharacter: ZoneCharacter
currentX: number
currentY: number
}>()
const game = useGame()
@ -20,7 +22,6 @@ 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) {

View File

@ -1,21 +0,0 @@
<template>
<div class="absolute top-0 right-4 hidden lg:block" v-if="gameStore.world.date && typeof gameStore.world.date === 'object'">
<p class="text-white text-lg">
{{ useDateFormat(gameStore.world.date, 'YYYY/MM/DD HH:mm') }}
</p>
</div>
</template>
<script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import { socketManager } from '@/managers/SocketManager'
import { useGameStore } from '@/stores/gameStore'
import { useDateFormat } from '@vueuse/core'
import { onUnmounted } from 'vue'
const gameStore = useGameStore()
onUnmounted(() => {
socketManager.off(SocketEvent.DATE)
})
</script>

View File

@ -1,49 +0,0 @@
<template>
<Character v-for="item in mapStore.characters" :key="item.character.id" :tileMap :mapCharacter="item" />
</template>
<script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { MapCharacter, UUID } from '@/application/types'
import Character from '@/components/game/character/Character.vue'
import { socketManager } from '@/managers/SocketManager'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { onUnmounted } from 'vue'
const gameStore = useGameStore()
const mapStore = useMapStore()
const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap
}>()
socketManager.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
mapStore.addCharacter(data)
})
socketManager.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
mapStore.removeCharacter(characterId)
})
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) => {
mapStore.updateCharacterPosition([characterId, posX, posY, rot, isMoving])
if (characterId === gameStore.character?.id) {
gameStore.character!.positionX = posX
gameStore.character!.positionY = posY
gameStore.character!.rotation = rot
}
})
socketManager.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
})
onUnmounted(() => {
socketManager.off(SocketEvent.MAP_CHARACTER_ATTACK)
socketManager.off(SocketEvent.MAP_CHARACTER_MOVE)
socketManager.off(SocketEvent.MAP_CHARACTER_JOIN)
socketManager.off(SocketEvent.MAP_CHARACTER_LEAVE)
})
</script>

View File

@ -1,71 +0,0 @@
<template>
<MapTiles v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<PlacedMapObjects v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<Characters v-if="tileMap && mapStore.characters" :tileMap />
</template>
<script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { mapLoadData } from '@/application/types'
import { unduplicateArray } from '@/application/utilities'
import Characters from '@/components/game/map/Characters.vue'
import MapTiles from '@/components/game/map/MapTiles.vue'
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
import { socketManager } from '@/managers/SocketManager'
import { createTileLayer, createTileMap, loadTileTexturesFromMapTileArray } from '@/services/mapService'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
const scene = useScene()
const gameStore = useGameStore()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
// Event listeners
socketManager.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
mapStore.setMapId(data.mapId)
mapStore.setCharacters(data.characters)
})
async function initialize() {
if (!mapStore.mapId) return
const map = await mapStorage.getById(mapStore.mapId)
if (!map) return
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
tileMap.value = createTileMap(scene, map)
tileMapLayer.value = createTileLayer(tileMap.value, unduplicateArray(map.tiles))
}
watch(
() => mapStore.mapId,
async () => {
await initialize()
}
)
onMounted(async () => {
if (!mapStore.mapId) return
await initialize()
})
onUnmounted(() => {
if (tileMap.value) {
tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
}
socketManager.off(SocketEvent.MAP_CHARACTER_TELEPORT)
})
</script>

View File

@ -1,32 +0,0 @@
<template>
<Controls v-if="tileMapLayer" :layer="tileMapLayer" :depth="0" />
</template>
<script setup lang="ts">
import Controls from '@/components/utilities/Controls.vue'
import { loadTileTexturesFromMapTileArray, placeTiles } from '@/services/mapService'
import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore'
import { useScene } from 'phavuer'
import { onMounted } from 'vue'
const scene = useScene()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap
tileMapLayer: Phaser.Tilemaps.TilemapLayer
}>()
onMounted(async () => {
if (!mapStore.mapId) return
const map = await mapStorage.getById(mapStore.mapId)
if (!map) return
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
placeTiles(props.tileMap, props.tileMapLayer, map.tiles)
})
</script>

View File

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

View File

@ -1,102 +0,0 @@
<template>
<Zone :depth="baseDepth" :origin-x="mapObj?.originX" :origin-y="mapObj?.originY" :width="mapObj?.frameWidth" :height="mapObj?.frameHeight" :x="x" :y="y" />
</template>
<script setup lang="ts">
import type { MapObject, PlacedMapObject } from '@/application/types'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { calculateIsometricDepth } from '@/services/mapService'
import { onPreUpdate, useScene, Zone } from 'phavuer'
import { computed, onUnmounted } from 'vue'
interface Props {
obj?: PlacedMapObject
mapObj?: MapObject
x?: number
y?: number
}
const props = defineProps<Props>()
const mapEditor = useMapEditorComposable()
const scene = useScene()
const group = scene.add.group()
const partitionPoints = computed(() => {
if (!props.mapObj?.frameWidth || !props.mapObj?.depthOffsets.length) return []
const sliceCount = props.mapObj.depthOffsets.length
return Array.from({ length: sliceCount + 1 }, (_, i) => i * (props.mapObj!.frameWidth / sliceCount))
})
let baseDepth = 0
const createImagePartition = (startX: number, endX: number, depthOffset: number): void => {
if (!props.mapObj?.id) return
const img = scene.add.image(0, 0, props.mapObj.id)
img.setOrigin(props.mapObj.originX, props.mapObj.originY)
img.setCrop(startX, 0, endX, props.mapObj.frameHeight)
img.setDepth(baseDepth + depthOffset)
group.add(img)
}
const updateGroupProperties = (): void => {
if (!props.obj || !props.x || !props.y) return
const isMoving = mapEditor.movingPlacedObject.value?.id === props.obj.id
const isSelected = mapEditor.selectedMapObject.value?.id === props.obj.id
const isPlacedSelected = mapEditor.selectedPlacedObject.value?.id === props.obj.id
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
group.setXY(props.x, props.y)
group.setAlpha(isMoving || isSelected ? 0.5 : 1)
group.setTint(isPlacedSelected ? 0x00ff00 : 0xffffff)
group.setDepth(baseDepth)
}
const updateImageProperties = (): void => {
const orderedImages = group.getChildren() as Phaser.GameObjects.Image[]
orderedImages.forEach((image, index) => {
if (!props.obj || !props.mapObj || !props.x) return
image.flipX = props.obj.isRotated
if (props.obj.isRotated) {
const offsetNum = props.mapObj.depthOffsets.length
const xOffset = props.mapObj.frameWidth / offsetNum
image.x = props.x + (index < offsetNum / 2 ? -xOffset : xOffset)
image.setDepth(baseDepth - props.mapObj.depthOffsets[index])
} else {
image.x = props.x
image.setDepth(baseDepth + props.mapObj.depthOffsets[index])
}
})
}
onPreUpdate(() => {
updateGroupProperties()
updateImageProperties()
})
// Initial setup
const initializeGroup = (): void => {
if (!props.mapObj || !props.x || !props.y || !props.obj) return
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
group.setXY(props.x, props.y)
group.setOrigin(props.mapObj.originX, props.mapObj.originY)
const points = partitionPoints.value
for (let i = 0; i < points.length - 1; i++) {
createImagePartition(points[i], points[i + 1], props.mapObj.depthOffsets[i])
}
}
initializeGroup()
onUnmounted(() => {
group.destroy(true, true)
})
</script>

View File

@ -1,70 +0,0 @@
<template>
<ImageGroup v-bind="groupProps" v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" />
</template>
<script setup lang="ts">
import type { MapObject, PlacedMapObject } from '@/application/types'
import ImageGroup from '@/components/game/map/partials/ImageGroup.vue'
import { loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
import { MapObjectStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { useScene } from 'phavuer'
import { computed, onMounted, ref } from 'vue'
import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
const props = defineProps<{
placedMapObject: PlacedMapObject
tileMap: Tilemap
tileMapLayer: TilemapLayer
}>()
const scene = useScene()
const gameStore = useGameStore()
const mapObject = ref<MapObject>()
const groupProps = computed(() => ({
...calculateObjectPlacement(props.placedMapObject),
mapObj: mapObject.value,
obj: props.placedMapObject
}))
async function initialize() {
if (!props.placedMapObject.mapObject) return
/**
* Check if mapObject is an string or object, if its an object we assume its a mapObject and change it to a string
* We do this because this component is shared with the map editor, which gets sent the mapObject as an object by the server
*/
if (typeof props.placedMapObject.mapObject === 'object') {
// @ts-ignore
props.placedMapObject.mapObject = props.placedMapObject.mapObject.id
}
const mapObjectStorage = new MapObjectStorage()
const _mapObject = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
if (!_mapObject) return
console.log(_mapObject)
mapObject.value = _mapObject
await loadMapObjectTextures([_mapObject], scene)
}
function calculateObjectPlacement(mapObj: PlacedMapObject): { x: number; y: number } {
let position = tileToWorldXY(props.tileMapLayer, mapObj.positionX, mapObj.positionY)
return {
x: position.worldPositionX,
y: position.worldPositionY
}
}
onMounted(async () => {
await initialize()
})
</script>

View File

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

View File

@ -0,0 +1,72 @@
<template>
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore'
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
import type { Zone as ZoneT, ZoneCharacter } from '@/types'
import ZoneTiles from '@/components/game/zone/ZoneTiles.vue'
import ZoneObjects from '@/components/game/zone/ZoneObjects.vue'
import Characters from '@/components/game/zone/Characters.vue'
const scene = useScene()
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
type zoneLoadData = {
zone: ZoneT
characters: ZoneCharacter[]
}
// Event listeners
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
/**
* @TODO : Update character via global event server-side, remove this and listen for it somewhere not here
*/
gameStore.setCharacter({
...gameStore.character!,
zoneId: data.zone.id
})
await loadZoneTilesIntoScene(data.zone, scene)
zoneStore.setZone(data.zone)
zoneStore.setCharacters(data.characters)
})
gameStore.connection!.on('zone:character:join', async (data: ZoneCharacter) => {
// If data is from the current user, don't add it to the store
if (data.character.id === gameStore.character?.id) return
zoneStore.addCharacter(data)
})
gameStore.connection!.on('zone:character:leave', (characterId: number) => {
zoneStore.removeCharacter(characterId)
})
gameStore.connection!.on('character:move', (data: ZoneCharacter) => {
zoneStore.updateCharacter(data)
})
// Emit zone:character:join event to server and wait for response, then set zone and characters
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
await loadZoneTilesIntoScene(response.zone, scene)
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)
})
onUnmounted(() => {
zoneStore.reset()
gameStore.connection!.off('zone:character:teleport')
gameStore.connection!.off('zone:character:join')
gameStore.connection!.off('zone:character:leave')
gameStore.connection!.off('character:move')
})
</script>

View File

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

View File

@ -0,0 +1,69 @@
<template>
<Controls :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/config'
import { useScene } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeUnmount } from 'vue'
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue'
import { unduplicateArray } from '@/utilities'
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const zoneStore = useZoneStore()
const tileMap = createTileMap()
const tileLayer = createTileLayer()
/**
* A Tilemap is a container for Tilemap data.
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/
function createTileMap() {
const zoneData = new Phaser.Tilemaps.MapData({
width: zoneStore.zone?.width,
height: zoneStore.zone?.height,
tileWidth: config.tile_size.x,
tileHeight: config.tile_size.y,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
emit('tileMap:create', newTileMap)
return newTileMap
}
/**
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
*/
function createTileLayer() {
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
return tileMap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any
// Add blank tile
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
onBeforeUnmount(() => {
tileMap.destroyLayer('tiles')
tileMap.removeAllLayers()
tileMap.destroy()
})
</script>

View File

@ -0,0 +1,41 @@
<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
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
flipX: props.zoneObject.isRotated,
texture: props.zoneObject.object.id,
originY: Number(props.zoneObject.object.originX),
originX: Number(props.zoneObject.object.originY)
}))
loadTexture(scene, {
key: props.zoneObject.object.id,
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
group: 'objects',
updatedAt: props.zoneObject.object.updatedAt,
frameWidth: props.zoneObject.object.frameWidth,
frameHeight: props.zoneObject.object.frameHeight
} as AssetDataT).catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -6,7 +6,7 @@
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</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="mapEditor.toggleActive()">Map editor</button>
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Map editor</button>
</div>
</template>
<template #modalBody>
@ -18,14 +18,14 @@
</template>
<script setup lang="ts">
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue'
import Modal from '@/components/utilities/Modal.vue'
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
const mapEditor = useMapEditorComposable()
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
let toggle = ref('asset-manager')
</script>

View File

@ -1,18 +1,18 @@
<template>
<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 rounded-md default-border bg-gray p-2.5">
<div class="w-2/12 flex flex-col relative overflow-auto rounded-md border border-solid border-gray-500 bg-gray-700 p-2.5">
<!-- Asset Categories -->
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'map_objects' }" @click="() => (selectedCategory = 'map_objects')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'map_objects' }">Map objects</span>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'items' }" @click="() => (selectedCategory = 'items')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'items' }">Items</span>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span class="group-hover:text-white">Items</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span class="group-hover:text-white">NPC's</span>
@ -40,19 +40,17 @@
<!-- Assets list -->
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
<TileList v-if="selectedCategory === 'tiles'" />
<MapObjectList v-if="selectedCategory === 'map_objects'" />
<ObjectList v-if="selectedCategory === 'objects'" />
<SpriteList v-if="selectedCategory === 'sprites'" />
<ItemList v-if="selectedCategory === 'items'" />
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
<CharacterHairList v-if="selectedCategory === 'characterHair'" />
</div>
<!-- Asset details -->
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
<div class="flex w-4/12 after:hidden flex-col relative overflow-auto">
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
<MapObjectDetails v-if="selectedCategory === 'map_objects' && assetManagerStore.selectedMapObject" />
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
<CharacterHairDetails v-if="selectedCategory === 'characterHair' && assetManagerStore.selectedCharacterHair" />
</div>
@ -60,20 +58,18 @@
</template>
<script setup lang="ts">
import CharacterHairDetails from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairDetails.vue'
import CharacterHairList from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairList.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'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
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 CharacterHairList from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairList.vue'
import CharacterHairDetails from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairDetails.vue'
const assetManagerStore = useAssetManagerStore()
const selectedCategory = ref('tiles')

View File

@ -1,6 +1,6 @@
<template>
<div class="h-full overflow-auto">
<div class="p-2.5 block rounded-md default-border bg-gray">
<div class="p-2.5 block rounded-md border border-solid border-gray-500 bg-gray-700">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterHair">
<div class="form-field-full">
<label for="name">Name</label>
@ -13,36 +13,19 @@
</select>
</div>
<div class="form-field-full">
<label for="isSelectable">Is selectable</label>
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
<label for="isEnabledForCharCreation">Is enabled for character creation</label>
<select v-model="characterIsEnabledForCharCreation" class="input-field" name="isEnabledForCharCreation">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<div class="space-x-6 flex items-center">
<label for="color">Color</label>
<input v-model="characterColor" class="input-field" type="text" name="color" placeholder="Character Hair Color" />
<div class="h-[38px] w-[38px] rounded" :style="{ backgroundColor: characterColor }"></div>
</div>
</div>
<div class="form-field-half">
<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>
<div class="form-field-half">
<label>Preview</label>
<div v-if="characterSpriteId" class="flex flex-col">
<div class="p-3 pb-5 min-h-32 block rounded-md default-border bg-gray-800">
<div class="flex items-center justify-center p-1 h-full bg-gray-700 rounded">
<img :src="config.server_endpoint + '/textures/sprites/' + characterSpriteId + '/front.png'" class="max-w-[200px] max-h-[200px] object-contain" />
</div>
</div>
</div>
</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>
@ -51,50 +34,48 @@
</template>
<script setup lang="ts">
import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
import { downloadCache } from '@/application/utilities'
import { socketManager } from '@/managers/SocketManager'
import { CharacterHairStorage } from '@/storage/storages'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { CharacterHair, CharacterGender, Sprite } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
const characterName = ref('')
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
const characterColor = ref<string>('#000000')
const characterIsSelectable = ref<boolean>(false)
const characterIsEnabledForCharCreation = 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
characterColor.value = selectedCharacterHair.value.color
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
characterIsEnabledForCharCreation.value = selectedCharacterHair.value.isEnabledForCharCreation
characterSpriteId.value = selectedCharacterHair.value.spriteId
}
async function removeCharacterHair() {
function removeCharacterHair() {
if (!selectedCharacterHair.value) return
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, async (response: boolean) => {
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove character hair')
return
}
await downloadCache('character_hair', new CharacterHairStorage())
await refreshCharacterHairList()
refreshCharacterHairList()
})
}
async function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response)
if (unsetSelectedCharacterHair) {
@ -103,24 +84,21 @@ async function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
})
}
async function saveCharacterHair() {
function saveCharacterHair() {
const characterHairData = {
id: selectedCharacterHair.value!.id,
name: characterName.value,
gender: characterGender.value,
color: characterColor.value,
isSelectable: characterIsSelectable.value,
isEnabledForCharCreation: characterIsEnabledForCharCreation.value,
spriteId: characterSpriteId.value
}
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, async (response: boolean) => {
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
if (!response) {
console.error('Failed to save character type')
return
}
await downloadCache('character_hair', new CharacterHairStorage())
await refreshCharacterHairList(false)
refreshCharacterHairList(false)
})
}
@ -128,15 +106,14 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
if (!characterHair) return
characterName.value = characterHair.name
characterGender.value = characterHair.gender
characterColor.value = characterHair.color
characterIsSelectable.value = characterHair.isSelectable
characterSpriteId.value = characterHair.sprite?.id
characterIsEnabledForCharCreation.value = characterHair.isEnabledForCharCreation
characterSpriteId.value = characterHair.spriteId
})
onMounted(() => {
if (!selectedCharacterHair.value) return
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})

View File

@ -9,15 +9,9 @@
</button>
</label>
</div>
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" 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 v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md border border-solid border-gray-500 bg-gray-700" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll">
<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>
@ -25,20 +19,18 @@
</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="" />
<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>
</div>
</div>
</template>
<script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterHair } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref, computed } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { CharacterHair } from '@/types'
import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
@ -54,13 +46,13 @@ const handleSearch = () => {
}
const createNewCharacterHair = () => {
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => {
if (!response) {
console.error('Failed to create new character type')
return
}
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response)
})
})
@ -94,7 +86,7 @@ function toTop() {
}
onMounted(() => {
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response)
})
})

View File

@ -1,6 +1,6 @@
<template>
<div class="h-full overflow-auto">
<div class="p-2.5 block rounded-md default-border bg-gray">
<div class="p-2.5 block rounded-md border border-solid border-gray-500 bg-gray-700">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
<div class="form-field-full">
<label for="name">Name</label>
@ -19,8 +19,8 @@
</select>
</div>
<div class="form-field-full">
<label for="isSelectable">Is selectable</label>
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
<label for="isEnabledForCharCreation">Is enabled for character creation</label>
<select v-model="characterIsEnabledForCharCreation" class="input-field" name="isEnabledForCharCreation">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
@ -40,14 +40,12 @@
</template>
<script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
import { downloadCache } from '@/application/utilities'
import { socketManager } from '@/managers/SocketManager'
import { CharacterTypeStorage } from '@/storage/storages'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { CharacterType, CharacterGender, CharacterRace, Sprite } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
@ -55,7 +53,7 @@ const selectedCharacterType = computed(() => assetManagerStore.selectedCharacter
const characterName = ref('')
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN)
const characterIsSelectable = ref<boolean>(false)
const characterIsEnabledForCharCreation = ref<boolean>(false)
const characterSpriteId = ref<string | null | undefined>(null)
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
@ -69,26 +67,24 @@ if (selectedCharacterType.value) {
characterName.value = selectedCharacterType.value.name
characterGender.value = selectedCharacterType.value.gender
characterRace.value = selectedCharacterType.value.race
characterIsSelectable.value = selectedCharacterType.value.isSelectable
characterSpriteId.value = selectedCharacterType.value.sprite?.id
characterIsEnabledForCharCreation.value = selectedCharacterType.value.isEnabledForCharCreation
characterSpriteId.value = selectedCharacterType.value.spriteId
}
async function removeCharacterType() {
function removeCharacterType() {
if (!selectedCharacterType.value) return
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, async (response: boolean) => {
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove character type')
return
}
await downloadCache('character_types', new CharacterTypeStorage())
await refreshCharacterTypeList()
refreshCharacterTypeList()
})
}
async function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response)
if (unsetSelectedCharacterType) {
@ -97,24 +93,22 @@ async function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
})
}
async function saveCharacterType() {
function saveCharacterType() {
const characterTypeData = {
id: selectedCharacterType.value!.id,
name: characterName.value,
gender: characterGender.value,
race: characterRace.value,
isSelectable: characterIsSelectable.value,
isEnabledForCharCreation: characterIsEnabledForCharCreation.value,
spriteId: characterSpriteId.value
}
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, async (response: boolean) => {
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => {
if (!response) {
console.error('Failed to save character type')
return
}
await downloadCache('character_types', new CharacterTypeStorage())
await refreshCharacterTypeList(false)
refreshCharacterTypeList(false)
})
}
@ -123,14 +117,14 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
characterName.value = characterType.name
characterGender.value = characterType.gender
characterRace.value = characterType.race
characterIsSelectable.value = characterType.isSelectable
characterSpriteId.value = characterType.sprite?.id
characterIsEnabledForCharCreation.value = characterType.isEnabledForCharCreation
characterSpriteId.value = characterType.spriteId
})
onMounted(() => {
if (!selectedCharacterType.value) return
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})

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