Compare commits

..

1 Commits

Author SHA1 Message Date
175d7c6199 Added overlay on portrait screens to force switching to landscape
(Needs better styling/design)
2025-01-22 22:36:08 +01:00
50 changed files with 3055 additions and 1605 deletions

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

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

2082
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

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

@ -2,7 +2,7 @@
<Debug /> <Debug />
<Notifications /> <Notifications />
<BackgroundImageLoader /> <BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" @open-map-editor="mapEditor.toggleActive" /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
<component :is="currentScreen" /> <component :is="currentScreen" />
</template> </template>
@ -16,33 +16,34 @@ import MapEditor from '@/components/screens/MapEditor.vue'
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue' import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import Debug from '@/components/utilities/Debug.vue' import Debug from '@/components/utilities/Debug.vue'
import Notifications from '@/components/utilities/Notifications.vue' import Notifications from '@/components/utilities/Notifications.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, ref, useTemplateRef, watch } from 'vue' import { computed, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditor = useMapEditorComposable() const mapEditorStore = useMapEditorStore()
const currentScreen = computed(() => { const currentScreen = computed(() => {
if (!gameStore.game.isLoaded) return Loading 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 (mapEditor.active.value) return MapEditor if (mapEditorStore.active) return MapEditor
return Game return Game
}) })
// Watch mapEditor.active and empty gameStore.game.loadedAssets // Watch mapEditorStore.active and empty gameStore.game.loadedAssets
watch( watch(
() => mapEditor.active.value, () => mapEditorStore.active,
() => { () => {
gameStore.game.loadedTextures = [] 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 /**
* @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

@ -19,6 +19,7 @@ export type TextureData = {
updatedAt: Date updatedAt: Date
originX?: number originX?: number
originY?: number originY?: number
isAnimated?: boolean
frameRate?: number frameRate?: number
frameWidth?: number frameWidth?: number
frameHeight?: number frameHeight?: number
@ -39,6 +40,7 @@ export type MapObject = {
tags: any | null tags: any | null
originX: number originX: number
originY: number originY: number
isAnimated: boolean
frameRate: number frameRate: number
frameWidth: number frameWidth: number
frameHeight: number frameHeight: number
@ -66,7 +68,7 @@ export type Map = {
name: string name: string
width: number width: number
height: number height: number
tiles: string[][] tiles: any | null
pvp: boolean pvp: boolean
mapEffects: MapEffect[] mapEffects: MapEffect[]
mapEventTiles: MapEventTile[] mapEventTiles: MapEventTile[]
@ -103,7 +105,7 @@ export enum MapEventTileType {
export type MapEventTile = { export type MapEventTile = {
id: UUID id: UUID
mapId: UUID map: Map
type: MapEventTileType type: MapEventTileType
positionX: number positionX: number
positionY: number positionY: number
@ -216,28 +218,22 @@ export type Sprite = {
characterTypes: CharacterType[] characterTypes: CharacterType[]
} }
export interface SpriteImage {
url: string
offset: {
x: number
y: number
}
}
export type SpriteAction = { export type SpriteAction = {
id: string id: UUID
sprite: string sprite: Sprite
action: string action: string
sprites: SpriteImage[] sprites: string[]
originX: number originX: number
originY: number originY: number
isAnimated: boolean
isLooping: boolean
frameWidth: number frameWidth: number
frameHeight: number frameHeight: number
frameRate: number frameRate: number
} }
export type Chat = { export type Chat = {
id: string id: UUID
character: Character character: Character
map: Map map: Map
message: string message: string
@ -246,11 +242,15 @@ export type Chat = {
export type WorldSettings = { export type WorldSettings = {
date: Date date: Date
weatherState: WeatherState isRainEnabled: boolean
isFogEnabled: boolean
fogDensity: number
} }
export type WeatherState = { export type WeatherState = {
isRainEnabled: boolean
rainPercentage: number rainPercentage: number
isFogEnabled: boolean
fogDensity: number fogDensity: number
} }

View File

@ -1,7 +1,3 @@
import config from '@/application/config'
import type { HttpResponse } from '@/application/types'
import type { BaseStorage } from '@/storage/baseStorage'
export function uuidv4() { export function uuidv4() {
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16)) return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
} }
@ -27,26 +23,3 @@ export function getDomain() {
return window.location.hostname.split('.').slice(-2).join('.') return window.location.hostname.split('.').slice(-2).join('.')
} }
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
const response = (await request.json()) as HttpResponse<T[]>
if (!response.success) {
console.error(`Failed to download ${endpoint}:`, response.message)
return
}
const items = response.data ?? []
for (const item of items) {
let overwrite = false
const existingItem = await storage.get(item.id)
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
overwrite = true
}
await storage.add(item, overwrite)
}
}

View File

@ -31,6 +31,12 @@ body {
@apply outline-offset-2; @apply outline-offset-2;
@apply rounded-sm; @apply rounded-sm;
} }
@media only screen and (orientation:portrait) and (max-width: 768px) {
.portrait-mode-notice {
@apply block;
}
}
} }
h1, h1,

View File

@ -1,14 +1,14 @@
<template> <template>
<ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<CharacterHair :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <Container ref="charContainer" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY">
<Sprite ref="charSprite" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY" :origin-y="1" :flipX="isFlippedX" /> <Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
</Container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import config from '@/application/config' import config from '@/application/config'
import { type MapCharacter } from '@/application/types' 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 ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
import Healthbar from '@/components/game/character/partials/Healthbar.vue' import Healthbar from '@/components/game/character/partials/Healthbar.vue'
import { loadSpriteTextures } from '@/composables/gameComposable' import { loadSpriteTextures } from '@/composables/gameComposable'
@ -16,7 +16,7 @@ import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composabl
import { CharacterTypeStorage } from '@/storage/storages' import { CharacterTypeStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { refObj, Sprite, useScene } from 'phavuer' import { Container, refObj, Sprite, useScene } from 'phavuer'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
enum Direction { enum Direction {
@ -30,6 +30,7 @@ const props = defineProps<{
mapCharacter: MapCharacter mapCharacter: MapCharacter
}>() }>()
const charContainer = refObj<Phaser.GameObjects.Container>()
const charSprite = refObj<Phaser.GameObjects.Sprite>() const charSprite = refObj<Phaser.GameObjects.Sprite>()
const charSpriteId = ref('') const charSpriteId = ref('')
@ -105,41 +106,21 @@ const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX:
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0)) const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const currentDirection = computed(() => {
return [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
})
const currentAction = computed(() => {
return props.mapCharacter.isMoving ? 'walk' : 'idle'
})
const charTexture = computed(() => { const charTexture = computed(() => {
const spriteId = charSpriteId.value ?? 'idle_right_down' const spriteId = charSpriteId.value ?? 'idle_right_down'
return `${spriteId}-${currentAction.value}_${currentDirection.value}` 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 = () => { const updateSprite = () => {
if (!charSprite.value) return
if (props.mapCharacter.isMoving) { if (props.mapCharacter.isMoving) {
charSprite.value.anims.play(charTexture.value, true) charSprite.value!.anims.play(charTexture.value, true)
} else { } else {
charSprite.value.anims.stop() charSprite.value!.anims.stop()
charSprite.value.setFrame(0) charSprite.value!.setFrame(0)
charSprite.value.setTexture(charTexture.value) charSprite.value!.setTexture(charTexture.value)
}
}
const handlePositionUpdate = (newValues: any, oldValues: any) => {
if (!newValues) return
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.positionX, oldValues.positionY, newValues.positionX, newValues.positionY)
updatePosition(newValues.positionX, newValues.positionY, direction)
}
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite()
} }
} }
@ -150,35 +131,44 @@ watch(
isMoving: props.mapCharacter.isMoving, isMoving: props.mapCharacter.isMoving,
rotation: props.mapCharacter.character.rotation rotation: props.mapCharacter.character.rotation
}), }),
handlePositionUpdate (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()
}
}
) )
onMounted(async () => { onMounted(async () => {
let character = props.mapCharacter.character
const characterTypeStorage = new CharacterTypeStorage() const characterTypeStorage = new CharacterTypeStorage()
const spriteId = await characterTypeStorage.getSpriteId(character.characterType!) const spriteId = await characterTypeStorage.getSpriteId(props.mapCharacter.character.characterType!)
if (!spriteId) return if (!spriteId) return
charSpriteId.value = spriteId charSpriteId.value = spriteId
await loadSpriteTextures(scene, spriteId) await loadSpriteTextures(scene, spriteId)
if (charSprite.value) { charSprite.value!.setTexture(charTexture.value)
charSprite.value.setTexture(charTexture.value) charSprite.value!.setFlipX(isFlippedX.value)
charSprite.value.setFlipX(isFlippedX.value)
charSprite.value.setName(props.mapCharacter.character.name)
}
if (character.id === gameStore.character!.id) { charContainer.value!.setName(props.mapCharacter.character!.name)
if (props.mapCharacter.character.id === gameStore.character!.id) {
mapStore.setCharacterLoaded(true) mapStore.setCharacterLoaded(true)
// #146 : Set camera position to character, need to be improved still // #146 : Set camera position to character, need to be improved still
scene.cameras.main.startFollow(charSprite.value as Phaser.GameObjects.Sprite) scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
} }
updatePosition(character.positionX, character.positionY, character.rotation) updatePosition(props.mapCharacter.character.positionX, props.mapCharacter.character.positionY, props.mapCharacter.character.rotation)
}) })
onUnmounted(() => { onUnmounted(() => {

View File

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

View File

@ -119,44 +119,111 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const width = ref(286) let startX = 0
const height = ref(483) let startY = 0
const x = ref(window.innerWidth / 2 - 143) let initialX = 0
const y = ref(window.innerHeight / 2 - 241) let initialY = 0
let modalPositionX = 0
let modalPositionY = 0
let modalWidth = 286
let modalHeight = 483
const width = ref(modalWidth)
const height = ref(modalHeight)
const x = ref(0)
const y = ref(0)
const isDragging = ref(false) const isDragging = ref(false)
const modalStyle = computed(() => ({ const modalStyle = computed(() => ({
top: `${y.value}px`, top: `${y.value}px`,
left: `${x.value}px`, left: `${x.value}px`,
width: `${width.value}px`, width: `${width.value}px`,
height: `${height.value}px` height: `${height.value}px`,
maxWidth: '100vw',
maxHeight: '100vh'
})) }))
function startDrag(event: MouseEvent) { function startDrag(event: MouseEvent) {
isDragging.value = true isDragging.value = true
const startX = event.clientX - x.value startX = event.clientX
const startY = event.clientY - y.value startY = event.clientY
initialX = x.value
initialY = y.value
event.preventDefault()
}
function drag(event: MouseEvent) { function drag(event: MouseEvent) {
if (!isDragging.value) return if (!isDragging.value) return
x.value = event.clientX - startX const dx = event.clientX - startX
y.value = event.clientY - startY const dy = event.clientY - startY
x.value = initialX + dx
y.value = initialY + dy
adjustPosition()
} }
function stopDrag() { function stopDrag() {
isDragging.value = false isDragging.value = false
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
} }
addEventListener('mousemove', drag) function adjustPosition() {
addEventListener('mouseup', stopDrag) x.value = Math.min(x.value, window.innerWidth - width.value)
y.value = Math.min(y.value, window.innerHeight - height.value)
} }
function initializePosition() {
width.value = Math.min(modalWidth, window.innerWidth)
height.value = Math.min(modalHeight, window.innerHeight)
if (modalPositionX !== 0 && modalPositionY !== 0) {
x.value = modalPositionX
y.value = modalPositionY
} else {
x.value = (window.innerWidth - width.value) / 2
y.value = (window.innerHeight - height.value) / 2
}
}
watch(
() => gameStore.uiSettings.isCharacterProfileOpen,
(value) => {
gameStore.uiSettings.isCharacterProfileOpen = value
if (value) {
initializePosition()
}
}
)
watch(
() => modalWidth,
(value) => {
width.value = Math.min(value, window.innerWidth)
}
)
watch(
() => modalHeight,
(value) => {
height.value = Math.min(value, window.innerHeight)
}
)
watch(
() => modalPositionX,
(value) => {
x.value = value
}
)
watch(
() => modalPositionY,
(value) => {
y.value = value
}
)
function keyPress(event: KeyboardEvent) { function keyPress(event: KeyboardEvent) {
if (event.altKey && event.key === 'c') { if (event.altKey && event.key === 'c') {
gameStore.toggleCharacterProfile() gameStore.toggleCharacterProfile()
@ -165,9 +232,14 @@ function keyPress(event: KeyboardEvent) {
onMounted(() => { onMounted(() => {
addEventListener('keydown', keyPress) addEventListener('keydown', keyPress)
addEventListener('mousemove', drag)
addEventListener('mouseup', stopDrag)
initializePosition()
}) })
onUnmounted(() => { onUnmounted(() => {
removeEventListener('keydown', keyPress) removeEventListener('keydown', keyPress)
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
}) })
</script> </script>

View File

@ -0,0 +1,199 @@
<template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
</template>
<script setup lang="ts">
import type { Map, WeatherState } from '@/application/types'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { Scene } from 'phavuer'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
// Constants
const LIGHT_CONFIG = {
SUNRISE_HOUR: 6,
SUNSET_HOUR: 20,
DAY_STRENGTH: 100,
NIGHT_STRENGTH: 30
}
// Stores and refs
const gameStore = useGameStore()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
const sceneRef = ref<Phaser.Scene | null>(null)
const mapEffectsReady = ref(false)
const mapObject = ref<Map | 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 loadMap = async () => {
if (!mapStore.mapId) return
mapObject.value = await mapStorage.get(mapStore.mapId)
}
// Watch for mapId changes and load map when it's available
watch(
() => mapStore.mapId,
async (newMapId) => {
if (newMapId) {
mapEffectsReady.value = false
await loadMap()
updateScene()
}
},
{ immediate: true }
)
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 mapEffects = mapObject.value?.mapEffects?.reduce(
(acc, curr) => ({
...acc,
[curr.effect]: curr.strength
}),
{}
) as { [key: string]: number }
// Only update effects once mapEffects are loaded
if (!mapEffectsReady.value) {
if (mapObject.value) {
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)
}
}
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(
() => mapObject.value,
() => {
mapEffectsReady.value = false
updateScene()
},
{ deep: true }
)
onMounted(() => window.addEventListener('resize', handleResize))
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (sceneRef.value) sceneRef.value.scene.remove('effects')
gameStore.connection?.off('weather')
})
</script>

View File

@ -4,10 +4,10 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { Map as MapT, UUID } from '@/application/types' import type { UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities' import { unduplicateArray } from '@/application/utilities'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable' import { FlattenMapArray, loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
@ -23,10 +23,10 @@ const mapStorage = new MapStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
function createTileMap(map: MapT) { function createTileMap(mapData: any) {
const mapConfig = new Phaser.Tilemaps.MapData({ const mapConfig = new Phaser.Tilemaps.MapData({
width: map.width, width: mapData?.width,
height: map.height, height: mapData?.height,
tileWidth: config.tile_size.width, tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height, tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC, orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
@ -39,7 +39,7 @@ function createTileMap(map: MapT) {
} }
function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) { function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) {
const tilesArray = unduplicateArray(mapData?.tiles.flat()) const tilesArray = unduplicateArray(FlattenMapArray(mapData?.tiles ?? []))
const tilesetImages = tilesArray.map((tile: string, index: number) => { const tilesetImages = tilesArray.map((tile: string, index: number) => {
return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height }) return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
@ -58,10 +58,9 @@ function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any)
loadMapTilesIntoScene(mapStore.mapId as UUID, scene) loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
.then(() => mapStorage.get(mapStore.mapId)) .then(() => mapStorage.get(mapStore.mapId))
.then((mapData) => { .then((mapData) => {
if (!mapData || !mapData?.tiles) return
tileMap.value = createTileMap(mapData) tileMap.value = createTileMap(mapData)
tileLayer.value = createTileLayer(tileMap.value, mapData) tileLayer.value = createTileLayer(tileMap.value, mapData)
setLayerTiles(tileMap.value, tileLayer.value, mapData.tiles) setLayerTiles(tileMap.value, tileLayer.value, mapData?.tiles)
}) })
.catch((error) => console.error('Failed to initialize map:', error)) .catch((error) => console.error('Failed to initialize map:', error))

View File

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

View File

@ -21,6 +21,13 @@
<label for="tags">Tags</label> <label for="tags">Tags</label>
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" /> <ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
</div> </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"> <div class="form-field-full">
<label for="frame-speed">Frame rate</label> <label for="frame-speed">Frame rate</label>
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" /> <input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
@ -48,10 +55,12 @@ import type { MapObject } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue' import ChipsInput from '@/components/forms/ChipsInput.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
const mapEditorStore = useMapEditorStore()
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject) const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
@ -59,6 +68,7 @@ const mapObjectName = ref('')
const mapObjectTags = ref<string[]>([]) const mapObjectTags = ref<string[]>([])
const mapObjectOriginX = ref(0) const mapObjectOriginX = ref(0)
const mapObjectOriginY = ref(0) const mapObjectOriginY = ref(0)
const mapObjectIsAnimated = ref(false)
const mapObjectFrameRate = ref(0) const mapObjectFrameRate = ref(0)
const mapObjectFrameWidth = ref(0) const mapObjectFrameWidth = ref(0)
const mapObjectFrameHeight = ref(0) const mapObjectFrameHeight = ref(0)
@ -72,6 +82,7 @@ if (selectedMapObject.value) {
mapObjectTags.value = selectedMapObject.value.tags mapObjectTags.value = selectedMapObject.value.tags
mapObjectOriginX.value = selectedMapObject.value.originX mapObjectOriginX.value = selectedMapObject.value.originX
mapObjectOriginY.value = selectedMapObject.value.originY mapObjectOriginY.value = selectedMapObject.value.originY
mapObjectIsAnimated.value = selectedMapObject.value.isAnimated
mapObjectFrameRate.value = selectedMapObject.value.frameRate mapObjectFrameRate.value = selectedMapObject.value.frameRate
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
@ -94,6 +105,10 @@ function refreshObjectList(unsetSelectedMapObject = true) {
if (unsetSelectedMapObject) { if (unsetSelectedMapObject) {
assetManagerStore.setSelectedMapObject(null) assetManagerStore.setSelectedMapObject(null)
} }
if (mapEditorStore.active) {
mapEditorStore.setMapObjectList(response)
}
}) })
} }
@ -111,6 +126,7 @@ function saveObject() {
tags: mapObjectTags.value, tags: mapObjectTags.value,
originX: mapObjectOriginX.value, originX: mapObjectOriginX.value,
originY: mapObjectOriginY.value, originY: mapObjectOriginY.value,
isAnimated: mapObjectIsAnimated.value,
frameRate: mapObjectFrameRate.value, frameRate: mapObjectFrameRate.value,
frameWidth: mapObjectFrameWidth.value, frameWidth: mapObjectFrameWidth.value,
frameHeight: mapObjectFrameHeight.value frameHeight: mapObjectFrameHeight.value
@ -131,6 +147,7 @@ watch(selectedMapObject, (mapObject: MapObject | null) => {
mapObjectTags.value = mapObject.tags mapObjectTags.value = mapObject.tags
mapObjectOriginX.value = mapObject.originX mapObjectOriginX.value = mapObject.originX
mapObjectOriginY.value = mapObject.originY mapObjectOriginY.value = mapObject.originY
mapObjectIsAnimated.value = mapObject.isAnimated
mapObjectFrameRate.value = mapObject.frameRate mapObjectFrameRate.value = mapObject.frameRate
mapObjectFrameWidth.value = mapObject.frameWidth mapObjectFrameWidth.value = mapObject.frameWidth
mapObjectFrameHeight.value = mapObject.frameHeight mapObjectFrameHeight.value = mapObject.frameHeight

View File

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

View File

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

View File

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

View File

@ -26,14 +26,14 @@
import config from '@/application/config' import config from '@/application/config'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue' import ChipsInput from '@/components/forms/ChipsInput.vue'
import { TileStorage } from '@/storage/storages'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
const tileStorage = new TileStorage() const mapEditorStore = useMapEditorStore()
const selectedTile = computed(() => assetManagerStore.selectedTile) const selectedTile = computed(() => assetManagerStore.selectedTile)
@ -55,13 +55,12 @@ watch(selectedTile, (tile: Tile | null) => {
tileTags.value = tile.tags tileTags.value = tile.tags
}) })
async function deleteTile() { function deleteTile() {
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, async (response: boolean) => { gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to delete tile') console.error('Failed to delete tile')
return return
} }
await tileStorage.delete(selectedTile.value!.id)
refreshTileList() refreshTileList()
}) })
} }
@ -73,6 +72,10 @@ function refreshTileList(unsetSelectedTile = true) {
if (unsetSelectedTile) { if (unsetSelectedTile) {
assetManagerStore.setSelectedTile(null) assetManagerStore.setSelectedTile(null)
} }
if (mapEditorStore.active) {
mapEditorStore.setTileList(response)
}
}) })
} }

View File

@ -1,54 +0,0 @@
<template>
<MapTiles ref="mapTiles" @tileMap:create="tileMap = $event" />
<PlacedMapObjects ref="mapObjects" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
<MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
</template>
<script setup lang="ts">
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue'
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const mapEditor = useMapEditorComposable()
const scene = useScene()
const mapTiles = useTemplateRef('mapTiles')
const mapObjects = useTemplateRef('mapObjects')
const eventTiles = useTemplateRef('eventTiles')
function handlePointer(pointer: Phaser.Input.Pointer) {
if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if draw mode is tile
switch (mapEditor.drawMode.value) {
case 'tile':
mapTiles.value.handlePointer(pointer)
case 'object':
mapObjects.value.handlePointer(pointer)
case 'teleport':
eventTiles.value.handlePointer(pointer)
}
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointer)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointer)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointer)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointer)
mapEditor.reset()
})
</script>

View File

@ -0,0 +1,72 @@
<template>
<MapTiles @tileMap:create="tileMap = $event" />
<PlacedMapObjects 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 PlacedMapObjects 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'
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onUnmounted, 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 })) ?? []
}
if (mapEditorStore.isSettingsModalShown) {
mapEditorStore.toggleSettingsModal()
}
gameStore.connection?.emit('gm:map:update', data, (response: Map) => {
mapEditorStore.setMap(response)
})
}
onUnmounted(() => {
mapEditorStore.reset()
})
</script>

View File

@ -1,86 +1,40 @@
<template> <template>
<Image v-for="tile in mapEditor.currentMap.value?.mapEventTiles" 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 { MapEventTileType, type MapEventTile, type Map as MapT } from '@/application/types' import { MapEventTileType, type MapEventTile } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable' import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { shallowRef } from 'vue' import { onMounted, onUnmounted } from 'vue'
const mapEditor = useMapEditorComposable() const scene = useScene()
const mapEditorStore = useMapEditorStore()
defineExpose({ handlePointer })
const props = defineProps<{ const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap tilemap: Phaser.Tilemaps.Tilemap
}>() }>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
function getImageProps(tile: MapEventTile) { 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),
texture: tile.type, texture: tile.type,
depth: 999 depth: 999
} }
} }
function pencil(pointer: Phaser.Input.Pointer, map: MapT) { function pencil(pointer: Phaser.Input.Pointer) {
if (!tileLayer.value) return // Check if map is set
if (!mapEditorStore.map) return
// Check if there is a tile // Check if tool is pencil
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) if (mapEditorStore.tool !== 'pencil') return
if (!tile) return
// Check if event tile already exists on position // Check if draw mode is blocking tile or teleport
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y) if (mapEditorStore.drawMode !== 'blocking tile' && mapEditorStore.drawMode !== 'teleport') return
if (existingEventTile) return
// If teleport, check if there is a selected map
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
const newEventTile = {
id: uuidv4(),
mapId: map?.id,
map: map?.id,
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
positionX: tile.x,
positionY: tile.y,
teleport:
mapEditor.drawMode.value === 'teleport'
? {
toMap: mapEditor.teleportSettings.value.toMapId,
toPositionX: mapEditor.teleportSettings.value.toPositionX,
toPositionY: mapEditor.teleportSettings.value.toPositionY,
toRotation: mapEditor.teleportSettings.value.toRotation
}
: undefined
}
map!.mapEventTiles = map!.mapEventTiles.concat(newEventTile as MapEventTile)
}
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
if (!tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return
// Remove existing event tile
map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
}
function handlePointer(pointer: Phaser.Input.Pointer) {
const map = mapEditor.currentMap.value
if (!map) return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -88,13 +42,77 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
// Check if shift is not pressed, this means we are moving the camera // Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return if (pointer.event.shiftKey) return
switch (mapEditor.tool.value) { // Check if there is a tile
case 'pencil': const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
pencil(pointer, map) if (!tile) return
break
case 'eraser': // Check if event tile already exists on position
erase(pointer, map) const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
break if (existingEventTile) return
// If teleport, check if there is a selected map
if (mapEditorStore.drawMode === 'teleport' && !mapEditorStore.teleportSettings.toMap) return
const newEventTile = {
id: uuidv4(),
mapId: mapEditorStore.map.id,
map: mapEditorStore.map,
type: mapEditorStore.drawMode === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
positionX: tile.x,
positionY: tile.y,
teleport:
mapEditorStore.drawMode === 'teleport'
? {
toMap: mapEditorStore.teleportSettings.toMap,
toPositionX: mapEditorStore.teleportSettings.toPositionX,
toPositionY: mapEditorStore.teleportSettings.toPositionY,
toRotation: mapEditorStore.teleportSettings.toRotation
} }
: undefined
} }
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.concat(newEventTile as MapEventTile)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is blocking tile or teleport
if (mapEditorStore.eraserMode !== 'blocking tile' && mapEditorStore.eraserMode !== 'teleport') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return
// Remove existing event tile
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
})
</script> </script>

View File

@ -5,29 +5,27 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable' import { createTileArray, getTile, loadAllTilesIntoScene, placeTile, setLayerTiles } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { TileStorage } from '@/storage/storages' import { TileStorage } from '@/storage/storages'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, watch } from 'vue' import { onBeforeMount, onMounted, onUnmounted, shallowRef, watch } from 'vue'
import Tileset = Phaser.Tilemaps.Tileset import Tileset = Phaser.Tilemaps.Tileset
const emit = defineEmits(['tileMap:create']) const emit = defineEmits(['tileMap:create'])
const scene = useScene() const scene = useScene()
const mapEditor = useMapEditorComposable() const mapEditorStore = useMapEditorStore()
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
defineExpose({ handlePointer })
function createTileMap() { function createTileMap() {
const mapData = new Phaser.Tilemaps.MapData({ const mapData = new Phaser.Tilemaps.MapData({
width: mapEditor.currentMap.value?.width, width: mapEditorStore.map?.width,
height: mapEditor.currentMap.value?.height, height: mapEditorStore.map?.height,
tileWidth: config.tile_size.width, tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height, tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC, orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
@ -58,31 +56,58 @@ async function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap) {
} }
function pencil(pointer: Phaser.Input.Pointer) { function pencil(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value if (!tileMap.value || !tileLayer.value) return
if (!map) return
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (mapEditorStore.drawMode !== 'tile') return
// Check if there is a selected tile // Check if there is a selected tile
if (!mapEditor.selectedTile.value) return if (!mapEditorStore.selectedTile) return
if (!tileMap.value || !tileLayer.value) return // Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Place tile // Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value) placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditorStore.selectedTile)
// Adjust mapEditorStore.map.tiles // Adjust mapEditorStore.map.tiles
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value mapEditorStore.map.tiles[tile.y][tile.x] = mapEditorStore.selectedTile
} }
function eraser(pointer: Phaser.Input.Pointer) { function eraser(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
if (!tileMap.value || !tileLayer.value) return if (!tileMap.value || !tileLayer.value) return
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is tile
if (mapEditorStore.eraserMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
@ -91,50 +116,20 @@ function eraser(pointer: Phaser.Input.Pointer) {
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile') placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
// Adjust mapEditorStore.map.tiles // Adjust mapEditorStore.map.tiles
map.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) {
if (!tileMap.value || !tileLayer.value) return if (!tileMap.value || !tileLayer.value) return
// Set new tileArray with selected tile
const tileArray = createTileArray(tileMap.value.width, tileMap.value.height, mapEditor.selectedTile.value)
setLayerTiles(tileMap.value, tileLayer.value, tileArray)
// Adjust mapEditorStore.map.tiles // Check if map is set
if (mapEditor.currentMap.value) { if (!mapEditorStore.map) return
mapEditor.currentMap.value.tiles = tileArray
}
}
// When alt is pressed, and the pointer is down, select the tile that the pointer is over // Check if tool is pencil
function tilePicker(pointer: Phaser.Input.Pointer) { if (mapEditorStore.tool !== 'paint') return
let map = mapEditor.currentMap.value
if (!map) return
if (!tileMap.value || !tileLayer.value) return // Check if there is a selected tile
if (!mapEditorStore.selectedTile) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
mapEditor.setSelectedTile(map.tiles[tile.y][tile.x])
}
watch(
() => mapEditor.shouldClearTiles,
(shouldClear) => {
if (shouldClear && mapEditor.currentMap.value && tileMap.value && tileLayer.value) {
const blankTiles = createTileArray(tileLayer.value.width, tileLayer.value.height, 'blank_tile')
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
mapEditor.currentMap.value.tiles = blankTiles
mapEditor.resetClearTilesFlag()
}
}
)
function handlePointer(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -143,38 +138,70 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
if (pointer.event.shiftKey) return if (pointer.event.shiftKey) return
// Check if alt is pressed // Check if alt is pressed
if (pointer.event.altKey) { if (pointer.event.altKey) return
tilePicker(pointer)
return // Set new tileArray with selected tile
setLayerTiles(tileMap.value, tileLayer.value, createTileArray(tileMap.value.width, tileMap.value.height, mapEditorStore.selectedTile))
// Adjust mapEditorStore.map.tiles
mapEditorStore.map.tiles = createTileArray(tileMap.value.width, tileMap.value.height, mapEditorStore.selectedTile)
} }
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is tile // Check if draw mode is tile
switch (mapEditor.tool.value) { if (mapEditorStore.drawMode !== 'tile') return
case 'pencil':
pencil(pointer) // Check if left mouse button is pressed
break if (!pointer.isDown) return
case 'eraser':
eraser(pointer) // Check if shift is not pressed, this means we are moving the camera
break if (pointer.event.shiftKey) return
case 'paint':
paint(pointer) // Check if alt is pressed
break if (!pointer.event.altKey) return
}
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
mapEditorStore.setSelectedTile(mapEditorStore.map.tiles[tile.y][tile.x])
} }
watch(
() => mapEditorStore.shouldClearTiles,
(shouldClear) => {
if (shouldClear && mapEditorStore.map && tileMap.value && tileLayer.value) {
const blankTiles = createTileArray(tileMap.value.width, tileMap.value.height, 'blank_tile')
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
mapEditorStore.map.tiles = blankTiles
mapEditorStore.resetClearTilesFlag()
}
}
)
onMounted(async () => { onMounted(async () => {
if (!mapEditor.currentMap.value) return if (!mapEditorStore.map?.tiles) return
tileMap.value = createTileMap() tileMap.value = createTileMap()
tileLayer.value = await createTileLayer(tileMap.value) tileLayer.value = await createTileLayer(tileMap.value)
// First fill the entire map with blank tiles using current map dimensions // First fill the entire map with blank tiles using current map dimensions
const blankTiles = createTileArray(mapEditor.currentMap.value.width, mapEditor.currentMap.value.height, 'blank_tile') const blankTiles = createTileArray(mapEditorStore.map.width, mapEditorStore.map.height, 'blank_tile')
// Then overlay the map tiles, but only within the current map dimensions // Then overlay the map tiles, but only within the current map dimensions
const mapTiles = mapEditor.currentMap.value.tiles const mapTiles = mapEditorStore.map.tiles
for (let y = 0; y < mapEditor.currentMap.value.height; y++) { for (let y = 0; y < mapEditorStore.map.height; y++) {
for (let x = 0; x < mapEditor.currentMap.value.width; x++) { for (let x = 0; x < mapEditorStore.map.width; x++) {
if (mapTiles[y] && mapTiles[y][x] !== undefined) { if (mapTiles[y] && mapTiles[y][x] !== undefined) {
blankTiles[y][x] = mapTiles[y][x] blankTiles[y][x] = mapTiles[y][x]
} }
@ -182,9 +209,19 @@ onMounted(async () => {
} }
setLayerTiles(tileMap.value, tileLayer.value, blankTiles) setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
}) })
onUnmounted(() => { onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
if (tileMap.value) { if (tileMap.value) {
tileMap.value.destroyLayer('tiles') tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers() tileMap.value.removeAllLayers()

View File

@ -11,7 +11,7 @@ import { Image, useScene } from 'phavuer'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps<{ const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap tilemap: Phaser.Tilemaps.Tilemap
placedMapObject: PlacedMapObject placedMapObject: PlacedMapObject
selectedPlacedMapObject: PlacedMapObject | null selectedPlacedMapObject: PlacedMapObject | null
movingPlacedMapObject: PlacedMapObject | null movingPlacedMapObject: PlacedMapObject | null
@ -24,8 +24,8 @@ const imageProps = computed(() => ({
alpha: props.movingPlacedMapObject?.id === props.placedMapObject.id ? 0.5 : 1, alpha: props.movingPlacedMapObject?.id === props.placedMapObject.id ? 0.5 : 1,
tint: props.selectedPlacedMapObject?.id === props.placedMapObject.id ? 0x00ff00 : 0xffffff, 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), 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), x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
y: tileToWorldY(props.tileMap, props.placedMapObject.positionX, props.placedMapObject.positionY), y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
flipX: props.placedMapObject.isRotated, flipX: props.placedMapObject.isRotated,
texture: props.placedMapObject.mapObject.id, texture: props.placedMapObject.mapObject.id,
originY: Number(props.placedMapObject.mapObject.originX), originY: Number(props.placedMapObject.mapObject.originX),

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal ref="modalRef" :modal-width="645" :modal-height="260" :bg-style="'none'"> <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">Map objects</h3> <h3 class="text-lg text-white">Map objects</h3>
</template> </template>
@ -25,11 +25,11 @@
class="border-2 border-solid max-w-full" class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
alt="Object" alt="Object"
@click="mapEditor.setSelectedMapObject(mapObject)" @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': mapEditor.selectedMapObject.value?.id === mapObject.id, 'border-cyan shadow-lg scale-105': mapEditorStore.selectedMapObject?.id === mapObject.id,
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id 'border-transparent hover:border-gray-300': mapEditorStore.selectedMapObject?.id !== mapObject.id
}" }"
/> />
</div> </div>
@ -44,31 +44,23 @@
import config from '@/application/config' import config from '@/application/config'
import type { MapObject } from '@/application/types' import type { MapObject } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useGameStore } from '@/stores/gameStore'
import { MapObjectStorage } from '@/storage/storages' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { liveQuery } from 'dexie' import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
const mapObjectStorage = new MapObjectStorage() const gameStore = useGameStore()
const isModalOpen = ref(false) const isModalOpen = ref(false)
const mapEditor = useMapEditorComposable() const mapEditorStore = useMapEditorStore()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const mapObjectList = ref<MapObject[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open(),
close: () => modalRef.value?.close()
})
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = mapObjectList.value.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 filteredMapObjects = computed(() => { const filteredMapObjects = computed(() => {
return mapObjectList.value.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
@ -83,23 +75,10 @@ const toggleTag = (tag: string) => {
} }
} }
let subscription: any = null onMounted(async () => {
onMounted(() => {
isModalOpen.value = true isModalOpen.value = true
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({ gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
next: (result) => { mapEditorStore.setMapObjectList(response)
mapObjectList.value = result
},
error: (error) => {
console.error('Failed to fetch tiles:', error)
}
}) })
}) })
onUnmounted(() => {
if (subscription) {
subscription.unsubscribe()
}
})
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal ref="modalRef" :modal-width="600" :modal-height="430" :bg-style="'none'"> <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">Map settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
</template> </template>
@ -47,56 +47,60 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { ref, useTemplateRef, watch } from 'vue' import { ref, watch } from 'vue'
const mapEditor = useMapEditorComposable() const mapEditorStore = useMapEditorStore()
const screen = ref('settings') const screen = ref('settings')
const name = ref(mapEditor.currentMap.value?.name) mapEditorStore.setMapName(mapEditorStore.map?.name)
const width = ref(mapEditor.currentMap.value?.width) mapEditorStore.setMapWidth(mapEditorStore.map?.width)
const height = ref(mapEditor.currentMap.value?.height) mapEditorStore.setMapHeight(mapEditorStore.map?.height)
const pvp = ref(mapEditor.currentMap.value?.pvp) mapEditorStore.setMapPvp(mapEditorStore.map?.pvp)
const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || []) mapEditorStore.setMapEffects(mapEditorStore.map?.mapEffects)
const modalRef = useTemplateRef('modalRef')
defineExpose({ const name = ref(mapEditorStore.mapSettings?.name)
open: () => modalRef.value?.open() const width = ref(mapEditorStore.mapSettings?.width)
}) const height = ref(mapEditorStore.mapSettings?.height)
const pvp = ref(mapEditorStore.mapSettings?.pvp)
const mapEffects = ref(mapEditorStore.mapSettings?.mapEffects || [])
watch(name, (value) => { watch(name, (value) => {
mapEditor.updateProperty('name', value!) mapEditorStore.setMapName(value)
}) })
watch(width, (value) => { watch(width, (value) => {
mapEditor.updateProperty('width', value!) mapEditorStore.setMapWidth(value)
}) })
watch(height, (value) => { watch(height, (value) => {
mapEditor.updateProperty('height', value!) mapEditorStore.setMapHeight(value)
}) })
watch(pvp, (value) => { watch(pvp, (value) => {
mapEditor.updateProperty('pvp', value!) mapEditorStore.setMapPvp(value)
}) })
watch(mapEffects, (value) => { watch(
mapEditor.updateProperty('mapEffects', value!) mapEffects,
}) (value) => {
mapEditorStore.setMapEffects(value)
},
{ deep: true }
)
const addEffect = () => { const addEffect = () => {
mapEffects.value.push({ mapEffects.value.push({
id: uuidv4() as UUID, // Simple unique id generation id: Date.now().toString(), // Simple unique id generation
map: mapEditor.currentMap.value!, mapId: mapEditorStore.map?.id,
map: mapEditorStore.map,
effect: '', effect: '',
strength: 1 strength: 1
}) })
} }
const removeEffect = (index: number) => { const removeEffect = (index) => {
mapEffects.value.splice(index, 1) mapEffects.value.splice(index, 1)
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal ref="modalRef" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'"> <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>
@ -28,7 +28,7 @@
<label for="toMap">Map to teleport to</label> <label for="toMap">Map to teleport to</label>
<select v-model="toMap" class="input-field" name="toMap" id="toMap"> <select v-model="toMap" class="input-field" name="toMap" id="toMap">
<option :value="null">Select map</option> <option :value="null">Select map</option>
<option v-for="map in mapList" :key="map.id" :value="map">{{ map.name }}</option> <option v-for="map in mapEditorStore.mapList" :key="map.id" :value="map">{{ map.name }}</option>
</select> </select>
</div> </div>
</div> </div>
@ -43,23 +43,17 @@ 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 { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport') const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
const mapEditorStore = useMapEditorStore() const mapEditorStore = useMapEditorStore()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapList = ref<Map[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open()
})
onMounted(fetchMaps) onMounted(fetchMaps)
function fetchMaps() { function fetchMaps() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => { gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
mapList.value = response mapEditorStore.setMapList(response)
}) })
} }
@ -71,7 +65,7 @@ function useRefTeleportSettings() {
toPositionX: ref(settings.toPositionX), toPositionX: ref(settings.toPositionX),
toPositionY: ref(settings.toPositionY), toPositionY: ref(settings.toPositionY),
toRotation: ref(settings.toRotation), toRotation: ref(settings.toRotation),
toMap: ref(settings.toMapId) toMap: ref(settings.toMap)
} }
} }
@ -82,7 +76,7 @@ function updateTeleportSettings() {
toPositionX: toPositionX.value, toPositionX: toPositionX.value,
toPositionY: toPositionY.value, toPositionY: toPositionY.value,
toRotation: toRotation.value, toRotation: toRotation.value,
toMapId: toMap.value toMap: toMap.value
}) })
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal ref="modalRef" :modal-width="645" :modal-height="600" :bg-style="'none'"> <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>
@ -84,33 +84,26 @@
import config from '@/application/config' import config from '@/application/config'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useGameStore } from '@/stores/gameStore'
import { TileStorage } from '@/storage/storages' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { liveQuery } from 'dexie' import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
const tileStorage = new TileStorage() const gameStore = useGameStore()
const mapEditor = useMapEditorComposable() const isModalOpen = ref(false)
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 tiles = ref<Tile[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open(),
close: () => modalRef.value?.close()
})
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = tiles.value.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 = tiles.value.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
@ -184,7 +177,6 @@ function getDominantColor(imageData: ImageData) {
g = 0, g = 0,
b = 0, b = 0,
total = 0 total = 0
for (let i = 0; i < imageData.data.length; i += 4) { for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] > 0) { if (imageData.data[i + 3] > 0) {
// Only consider non-transparent pixels // Only consider non-transparent pixels
@ -194,7 +186,6 @@ function getDominantColor(imageData: ImageData) {
total++ total++
} }
} }
return { return {
r: Math.round(r / total), r: Math.round(r / total),
g: Math.round(g / total), g: Math.round(g / total),
@ -228,29 +219,18 @@ function closeGroup() {
} }
function selectTile(tile: string) { function selectTile(tile: string) {
mapEditor.setSelectedTile(tile) mapEditorStore.setSelectedTile(tile)
} }
function isActiveTile(tile: Tile): boolean { function isActiveTile(tile: Tile): boolean {
return mapEditor.selectedTile.value === tile.id return mapEditorStore.selectedTile === tile.id
} }
let subscription: any = null onMounted(async () => {
isModalOpen.value = true
onMounted(() => { gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({ mapEditorStore.setTileList(response)
next: (result) => { response.forEach((tile) => processTile(tile))
tiles.value = result
},
error: (error) => {
console.error('Failed to fetch tiles:', error)
}
}) })
}) })
onUnmounted(() => {
if (subscription) {
subscription.unsubscribe()
}
})
</script> </script>

View File

@ -1,99 +1,94 @@
<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="mapEditor.currentMap.value"> <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': mapEditor.tool.value === '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/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== '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': mapEditor.tool.value === '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/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== '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="mapEditor.tool.value === '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 }">
{{ mapEditor.drawMode.value.replace('_', ' ') }} {{ 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/mapEditor/chevron.svg" alt="" /> <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 && mapEditor.tool.value === '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="() => handleModeClick('tile', 'pencil')"> <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="() => handleModeClick('map_object', 'pencil')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('map_object')">
Map 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="() => handleModeClick('teleport', 'pencil')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')">
Teleport Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'pencil')">Blocking tile</span> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('blocking tile')">Blocking tile</span>
</div> </div>
</div> </div>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === '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/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== '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="mapEditor.tool.value === '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 }">
{{ mapEditor.drawMode.value.replace('_', ' ') }} {{ 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/mapEditor/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="() => handleModeClick('tile', 'eraser')"> <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="() => handleModeClick('map_object', 'eraser')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('map_object')">
Map 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="() => handleModeClick('teleport', 'eraser')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')">
Teleport Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'eraser')">Blocking tile</span> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('blocking tile')">Blocking tile</span>
</div> </div>
</div> </div>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === '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/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== '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')"><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> <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 class="w-px bg-cyan"></div>
<label class="my-auto gap-0" for="checkbox">Continuous Drawing</label>
<input type="checkbox" />
</div> </div>
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2"> <div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
<button class="btn-cyan px-3.5" @click="() => emit('open-maps')">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="mapEditor.currentMap.value">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="mapEditor.currentMap.value">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="() => emit('close-editor')">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 { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
const mapEditor = useMapEditorComposable() const mapEditorStore = useMapEditorStore()
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-tile-list', 'open-map-object-list', 'close-lists']) const emit = defineEmits(['save', 'clear'])
// track when clicked outside of toolbar items // track when clicked outside of toolbar items
const toolbar = ref(null) const toolbar = ref(null)
@ -102,45 +97,26 @@ const toolbar = ref(null)
let selectPencilOpen = ref(false) let selectPencilOpen = ref(false)
let selectEraserOpen = ref(false) let selectEraserOpen = ref(false)
let tileListShown = ref(false)
let mapObjectListShown = ref(false)
defineExpose({ tileListShown, mapObjectListShown })
// drawMode // drawMode
function setDrawMode(value: string) { function setDrawMode(value: string) {
if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil') { mapEditorStore.isTileListModalShown = value === 'tile'
emit('close-lists') mapEditorStore.isMapObjectListModalShown = value === 'map_object'
if (value === 'tile') emit('open-tile-list')
if (value === 'map_object') emit('open-map-object-list')
}
mapEditor.setDrawMode(value) mapEditorStore.setDrawMode(value)
selectPencilOpen.value = false
selectEraserOpen.value = false
}
function setPencilMode() {
mapEditor.setTool('pencil')
selectPencilOpen.value = false selectPencilOpen.value = false
} }
// drawMode // drawMode
function setEraserMode() { function setEraserMode(value: string) {
mapEditor.setTool('eraser') mapEditorStore.setEraserMode(value)
selectEraserOpen.value = false selectEraserOpen.value = false
} }
function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
setDrawMode(mode)
type === 'pencil' ? setPencilMode() : setEraserMode()
}
function handleClick(tool: string) { function handleClick(tool: string) {
if (tool === 'settings') { if (tool === 'settings') {
emit('open-settings') mapEditorStore.toggleSettingsModal()
} else { } else {
mapEditor.setTool(tool) mapEditorStore.setTool(tool)
} }
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
@ -148,17 +124,22 @@ function handleClick(tool: string) {
} }
function cycleToolMode(tool: 'pencil' | 'eraser') { function cycleToolMode(tool: 'pencil' | 'eraser') {
const modes = ['tile', 'map_object', 'teleport', 'blocking tile'] const modes = ['tile', 'object', 'teleport', 'blocking tile']
const currentIndex = modes.indexOf(mapEditor.drawMode.value) const currentMode = tool === 'pencil' ? mapEditorStore.drawMode : mapEditorStore.eraserMode
const currentIndex = modes.indexOf(currentMode)
const nextIndex = (currentIndex + 1) % modes.length const nextIndex = (currentIndex + 1) % modes.length
const nextMode = modes[nextIndex] const nextMode = modes[nextIndex]
if (tool === 'pencil') {
setDrawMode(nextMode) setDrawMode(nextMode)
} else {
setEraserMode(nextMode)
}
} }
function initKeyShortcuts(event: KeyboardEvent) { function initKeyShortcuts(event: KeyboardEvent) {
// Check if map is set // Check if map is set
if (!mapEditor.currentMap.value) 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
@ -173,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') && mapEditor.tool.value === tool) { if ((tool === 'pencil' || tool === 'eraser') && mapEditorStore.tool === tool) {
cycleToolMode(tool) cycleToolMode(tool)
} else { } else {
handleClick(tool) handleClick(tool)

View File

@ -1,5 +1,8 @@
<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="portrait-mode-notice hidden absolute h-[calc(100%_-_80px)] w-[calc(100%_-_80px)] left-0 top-0 bg-gray z-50 p-10 text-center">
<span class="text-lg">Noxious is not compatible with portrait mode on smaller screens. Please switch to landscape mode to play.</span>
</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="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-[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-[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-[35dvh] 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>
@ -34,7 +37,7 @@
<button class="ml-6 w-4 h-8 p-0"> <button class="ml-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" /> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
</button> </button>
<img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" /> <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>
@ -87,7 +90,7 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" /> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" />
</div> </div>
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading"> <div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
@ -131,11 +134,11 @@ import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoading = ref<boolean>(true) const isLoading = ref<boolean>(true)
const characters = ref<CharacterT[]>([]) const characters = ref<CharacterT[]>([])
const selectedCharacterId = ref<string | null>(null) const selectedCharacterId = ref<number | null>(null)
const isCreateNewCharacterModalOpen = ref<boolean>(false) const isCreateNewCharacterModalOpen = ref<boolean>(false)
const newCharacterName = ref<string>('') const newCharacterName = ref<string>('')
const characterHairs = ref<CharacterHair[]>([]) const characterHairs = ref<CharacterHair[]>([])
const selectedHairId = ref<string | null>(null) const selectedHairId = ref<number | null>(null)
// Fetch characters // Fetch characters
setTimeout(() => { setTimeout(() => {

View File

@ -1,5 +1,8 @@
<template> <template>
<div class="flex justify-center items-center h-dvh relative"> <div class="flex justify-center items-center h-dvh relative">
<div class="portrait-mode-notice hidden absolute h-[calc(100%_-_80px)] w-[calc(100%_-_80px)] left-0 top-0 bg-gray z-50 p-10 text-center">
<span class="text-lg">Noxious is not compatible with portrait mode on smaller screens. Please switch to landscape mode to play.</span>
</div>
<Game :config="gameConfig" @create="createGame"> <Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene"> <Scene name="main" @preload="preloadScene" @create="createScene">
<Menu /> <Menu />
@ -11,6 +14,7 @@
<ExpBar /> <ExpBar />
<CharacterProfile /> <CharacterProfile />
<Effects />
</Scene> </Scene>
</Game> </Game>
</div> </div>
@ -26,6 +30,7 @@ import ExpBar from '@/components/game/gui/ExpBar.vue'
import Hotkeys from '@/components/game/gui/Hotkeys.vue' import Hotkeys from '@/components/game/gui/Hotkeys.vue'
import Hud from '@/components/game/gui/Hud.vue' import Hud from '@/components/game/gui/Hud.vue'
import Menu from '@/components/game/gui/Menu.vue' import Menu from '@/components/game/gui/Menu.vue'
import Effects from '@/components/game/map/Effects.vue'
import Map from '@/components/game/map/Map.vue' import Map from '@/components/game/map/Map.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'

View File

@ -1,12 +1,25 @@
<template> <template>
<div class="flex flex-col justify-center items-center h-dvh relative col"> <div class="flex flex-col justify-center items-center h-dvh relative col">
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" /> <svg width="40" height="40" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="12" r="3" fill="white">
<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" />
</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>
</div> </div>
</template> </template>
<script setup lang="ts" async> <script setup lang="ts" async>
import { downloadCache } from '@/application/utilities' 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 { 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' import { ref } from 'vue'
@ -15,13 +28,32 @@ const gameStore = useGameStore()
const totalItems = ref(0) const totalItems = ref(0)
const currentItem = ref(0) const currentItem = ref(0)
async function downloadAndStore<T extends { id: string }>(endpoint: string, storage: BaseStorage<T>) {
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
const response = (await request.json()) as HttpResponse<T[]>
if (!response.success) {
console.error(`Failed to download ${endpoint}:`, response.message)
return
}
const items = response.data ?? []
for (const item of items) {
await storage.add(item)
}
}
const tileStorage = new TileStorage()
const mapStorage = new MapStorage()
const mapObjectStorage = new MapObjectStorage()
Promise.all([ Promise.all([
downloadCache('tiles', new TileStorage()), downloadAndStore('tiles', tileStorage),
downloadCache('maps', new MapStorage()), downloadAndStore('maps', mapStorage),
downloadCache('map_objects', new MapObjectStorage()), downloadAndStore('map_objects', mapObjectStorage),
downloadCache('sprites', new SpriteStorage()), downloadAndStore('sprites', new SpriteStorage()),
downloadCache('character_types', new CharacterTypeStorage()), downloadAndStore('character_types', new CharacterTypeStorage()),
downloadCache('character_hair', new CharacterHairStorage()) downloadAndStore('character_hair', new CharacterHairStorage())
]).then(() => { ]).then(() => {
gameStore.game.isLoaded = true gameStore.game.isLoaded = true
}) })

View File

@ -1,28 +1,9 @@
<template> <template>
<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"> <Scene name="main" @preload="preloadScene" @create="createScene">
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div> <MapEditor :key="JSON.stringify(`${mapEditorStore.map?.id}_${mapEditorStore.map?.createdAt}_${mapEditorStore.map?.updatedAt}`)" v-if="isLoaded" />
<div v-else> <div v-else class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
<Map :key="mapEditor.currentMap.value?.id" />
<Toolbar
ref="toolbar"
@save="save"
@clear="clear"
@open-maps="mapModal?.open"
@open-settings="mapSettingsModal?.open"
@close-editor="mapEditor.toggleActive"
@close-lists="tileModal?.close"
@closeLists="objectModal?.close"
@open-tile-list="tileModal?.open"
@open-map-object-list="objectModal?.open"
/>
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
<TileList ref="tileModal" />
<ObjectList ref="objectModal" />
<MapSettings ref="mapSettingsModal" />
<TeleportModal ref="teleportModal" />
</div>
</Scene> </Scene>
</Game> </Game>
</div> </div>
@ -31,31 +12,15 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import 'phaser' import 'phaser'
import type { Map as MapT } from '@/application/types' import MapEditor from '@/components/gameMaster/mapEditor/MapEditor.vue'
import Map from '@/components/gameMaster/mapEditor/Map.vue'
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
import ObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
import { loadAllTilesIntoScene } from '@/composables/mapComposable' import { loadAllTilesIntoScene } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'
import { ref, useTemplateRef, watch } from 'vue' import { ref } from 'vue'
const mapStorage = new MapStorage()
const mapEditor = useMapEditorComposable()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const toolbar = useTemplateRef('toolbar')
const mapModal = useTemplateRef('mapModal')
const tileModal = useTemplateRef('tileModal')
const objectModal = useTemplateRef('objectModal')
const mapSettingsModal = useTemplateRef('mapSettingsModal')
const teleportModal = useTemplateRef('teleportModal')
const isLoaded = ref(false) const isLoaded = ref(false)
@ -69,9 +34,18 @@ const gameConfig = {
const createGame = (game: Phaser.Game) => { const createGame = (game: Phaser.Game) => {
// Resize the game when the window is resized // Resize the game when the window is resized
window.addEventListener('resize', () => { addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight) game.scale.resize(window.innerWidth, window.innerHeight)
}) })
// We don't support canvas mode, only WebGL
if (game.renderer.type === Phaser.CANVAS) {
gameStore.addNotification({
title: 'Warning',
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
})
gameStore.disconnectSocket()
}
} }
const preloadScene = async (scene: Phaser.Scene) => { const preloadScene = async (scene: Phaser.Scene) => {
@ -81,10 +55,8 @@ const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('blank_tile', '/assets/map/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')
// Get all tiles from IndexedDB and load them into the scene
await loadAllTilesIntoScene(scene) await loadAllTilesIntoScene(scene)
// Wait for all assets to be loaded before continuing
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
scene.load.on(Phaser.Loader.Events.COMPLETE, () => { scene.load.on(Phaser.Loader.Events.COMPLETE, () => {
resolve() resolve()
@ -93,31 +65,5 @@ const preloadScene = async (scene: Phaser.Scene) => {
}) })
} }
function save() { const createScene = async (scene: Phaser.Scene) => {}
const currentMap = mapEditor.currentMap.value
if (!currentMap) return
const data = {
mapId: currentMap.id,
name: currentMap.name,
width: currentMap.width,
height: currentMap.height,
tiles: currentMap.tiles,
pvp: currentMap.pvp,
mapEffects: currentMap.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
mapEventTiles: currentMap.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
placedMapObjects: currentMap.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
}
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => {
mapStorage.update(response.id, response)
})
}
function clear() {
if (!mapEditor.currentMap.value) return
// Clear placed objects, event tiles and tiles
mapEditor.clearMap()
}
</script> </script>

View File

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

View File

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

View File

@ -59,6 +59,7 @@ export async function loadTexture(scene: Phaser.Scene, textureData: TextureData)
export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) { export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) {
if (!sprite_id) return false if (!sprite_id) return false
// @TODO: Fix this
const spriteStorage = new SpriteStorage() const spriteStorage = new SpriteStorage()
const sprite = await spriteStorage.get(sprite_id) const sprite = await spriteStorage.get(sprite_id)
@ -72,17 +73,18 @@ export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string)
await loadTexture(scene, { await loadTexture(scene, {
key, key,
data: '/textures/sprites/' + sprite.id + '/' + sprite_action.action + '.png', data: '/textures/sprites/' + sprite.id + '/' + sprite_action.action + '.png',
group: sprite_action.frameCount > 1 ? 'sprite_animations' : 'sprites', group: sprite_action.isAnimated ? 'sprite_animations' : 'sprites',
updatedAt: sprite_action.updatedAt, updatedAt: sprite_action.updatedAt,
originX: sprite_action.originX, originX: sprite_action.originX,
originY: sprite_action.originY, originY: sprite_action.originY,
isAnimated: sprite_action.isAnimated,
frameWidth: sprite_action.frameWidth, frameWidth: sprite_action.frameWidth,
frameHeight: sprite_action.frameHeight, frameHeight: sprite_action.frameHeight,
frameRate: sprite_action.frameRate frameRate: sprite_action.frameRate
} as TextureData) } as TextureData)
// If the sprite has no more than one frame, skip // If the sprite is not animated, skip
if (sprite_action.frameCount <= 1) continue if (!sprite_action.isAnimated) continue
// Check if animation already exists // Check if animation already exists
if (scene.anims.get(key)) continue if (scene.anims.get(key)) continue

View File

@ -1,5 +1,5 @@
import config from '@/application/config' import config from '@/application/config'
import type { HttpResponse, TextureData, Tile as TileT, UUID } from '@/application/types' import type { HttpResponse, TextureData, UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities' import { unduplicateArray } from '@/application/utilities'
import { loadTexture } from '@/composables/gameComposable' import { loadTexture } from '@/composables/gameComposable'
import { MapStorage, TileStorage } from '@/storage/storages' import { MapStorage, TileStorage } from '@/storage/storages'
@ -10,7 +10,9 @@ import Tileset = Phaser.Tilemaps.Tileset
import Tile = Phaser.Tilemaps.Tile import Tile = Phaser.Tilemaps.Tile
export function getTile(layer: TilemapLayer | Tilemap, positionX: number, positionY: number): Tile | null { export function getTile(layer: TilemapLayer | Tilemap, positionX: number, positionY: number): Tile | null {
return layer.getTileAtWorldXY(positionX, positionY) const tile = layer?.getTileAtWorldXY(positionX, positionY)
if (!tile) return null
return tile
} }
export function tileToWorldXY(layer: TilemapLayer | Tilemap, positionX: number, positionY: number) { export function tileToWorldXY(layer: TilemapLayer | Tilemap, positionX: number, positionY: number) {
@ -75,10 +77,27 @@ export const calculateIsometricDepth = (positionX: number, positionY: number, wi
return baseDepth + (width + height) / (2 * config.tile_size.width) return baseDepth + (width + height) / (2 * config.tile_size.width)
} }
async function getTiles(tiles: TileT[], scene: Phaser.Scene) { export function FlattenMapArray(tiles: string[][]) {
const normalArray = []
for (const row of tiles) {
normalArray.push(...row)
}
return normalArray
}
export async function loadMapTilesIntoScene(map_id: UUID, scene: Phaser.Scene) {
const tileStorage = new TileStorage()
const mapStorage = new MapStorage()
const map = await mapStorage.get(map_id)
if (!map) return
const tileArray = unduplicateArray(FlattenMapArray(map.tiles))
const tiles = await tileStorage.getByIds(tileArray)
// Load each tile into the scene // Load each tile into the scene
for (const tile of tiles) { for (const tile of tiles) {
if (!tile) continue
const textureData = { const textureData = {
key: tile.id, key: tile.id,
data: '/textures/tiles/' + tile.id + '.png', data: '/textures/tiles/' + tile.id + '.png',
@ -89,28 +108,35 @@ async function getTiles(tiles: TileT[], scene: Phaser.Scene) {
} }
} }
export async function loadMapTilesIntoScene(map_id: UUID, scene: Phaser.Scene) {
const tileStorage = new TileStorage()
const mapStorage = new MapStorage()
const map = await mapStorage.get(map_id)
if (!map) return
const tileArray = unduplicateArray(map.tiles)
const tiles = await tileStorage.getByIds(tileArray)
await getTiles(tiles, scene)
}
export async function loadTilesIntoScene(tileIds: string[], scene: Phaser.Scene) { export async function loadTilesIntoScene(tileIds: string[], scene: Phaser.Scene) {
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const tiles = await tileStorage.getByIds(tileIds) const tiles = await tileStorage.getByIds(tileIds)
await getTiles(tiles, scene) // Load each tile into the scene
for (const tile of tiles) {
const textureData = {
key: tile.id,
data: '/textures/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as TextureData
await loadTexture(scene, textureData)
}
} }
export async function loadAllTilesIntoScene(scene: Phaser.Scene) { export async function loadAllTilesIntoScene(scene: Phaser.Scene) {
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const tiles = await tileStorage.getAll() const tiles = await tileStorage.getAll()
await getTiles(tiles, scene) // Load each tile into the scene
for (const tile of tiles) {
const textureData = {
key: tile.id,
data: '/textures/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as TextureData
await loadTexture(scene, textureData)
}
} }

View File

@ -25,7 +25,7 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
} }
function handlePointerDown(pointer: Phaser.Input.Pointer) { function handlePointerDown(pointer: Phaser.Input.Pointer) {
pointerStartPosition.value = pointer.position pointerStartPosition.value = { x: pointer.x, y: pointer.y }
gameStore.setPlayerDraggingCamera(true) gameStore.setPlayerDraggingCamera(true)
} }
@ -34,9 +34,10 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
if (!gameStore.game.isPlayerDraggingCamera) return if (!gameStore.game.isPlayerDraggingCamera) return
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
// If the distance is less than the drag threshold, return // If the distance is less than the drag threshold, return
// We do this to prevent the camera from scrolling too quickly // We do this to prevent the camera from scrolling too quickly
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
if (distance <= dragThreshold) return if (distance <= dragThreshold) return
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom) camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom)
@ -45,6 +46,12 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
function handlePointerUp(pointer: Phaser.Input.Pointer) { function handlePointerUp(pointer: Phaser.Input.Pointer) {
gameStore.setPlayerDraggingCamera(false) gameStore.setPlayerDraggingCamera(false)
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
// If the distance is greater than the drag threshold, return
// We do this to prevent the camera from scrolling too quickly
if (distance > dragThreshold) return
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY) const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
if (!pointerTile) return if (!pointerTile) return

View File

@ -1,13 +1,13 @@
import config from '@/application/config' import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/composables/mapComposable' import { getTile, tileToWorldXY } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, ref, type Ref } from 'vue' import { computed, ref, type Ref } from 'vue'
export function useMapEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) { export function useMapEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditor = useMapEditorComposable() const mapEditorStore = useMapEditorStore()
const isMoveTool = computed(() => mapEditor.tool.value === 'move') const isMoveTool = computed(() => mapEditorStore.tool === 'move')
const pointerStartPosition = ref({ x: 0, y: 0 }) const pointerStartPosition = ref({ x: 0, y: 0 })
const dragThreshold = 5 // pixels const dragThreshold = 5 // pixels
@ -39,9 +39,6 @@ export function useMapEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.T
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y) const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
// If the distance is less than the drag threshold, return
// We do this to prevent the camera from scrolling too quickly
if (distance <= dragThreshold) return if (distance <= dragThreshold) return
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom) camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom)

View File

@ -1,109 +0,0 @@
import type { Map, MapObject } from '@/application/types'
import { ref } from 'vue'
export type TeleportSettings = {
toMapId: string
toPositionX: number
toPositionY: number
toRotation: number
}
const currentMap = ref<Map | null>(null)
const active = ref(false)
const tool = ref('move')
const drawMode = ref('tile')
const selectedTile = ref('')
const selectedMapObject = ref<MapObject | null>(null)
const shouldClearTiles = ref(false)
const teleportSettings = ref<TeleportSettings>({
toMapId: '',
toPositionX: 0,
toPositionY: 0,
toRotation: 0
})
export function useMapEditorComposable() {
const loadMap = (map: Map) => {
currentMap.value = map
}
const updateProperty = <K extends keyof Map>(property: K, value: Map[K]) => {
if (currentMap.value) {
currentMap.value[property] = value
}
}
const clearMap = () => {
if (!currentMap.value) return
currentMap.value.placedMapObjects = []
currentMap.value.mapEventTiles = []
currentMap.value.tiles = []
}
const toggleActive = () => {
if (active.value) reset()
active.value = !active.value
}
const setTool = (newTool: string) => {
tool.value = newTool
}
const setDrawMode = (mode: string) => {
drawMode.value = mode
}
const setSelectedTile = (tile: string) => {
selectedTile.value = tile
}
const setSelectedMapObject = (object: MapObject) => {
selectedMapObject.value = object
}
const setTeleportSettings = (settings: TeleportSettings) => {
teleportSettings.value = settings
}
const triggerClearTiles = () => {
shouldClearTiles.value = true
}
const resetClearTilesFlag = () => {
shouldClearTiles.value = false
}
const reset = () => {
tool.value = 'move'
drawMode.value = 'tile'
selectedTile.value = ''
selectedMapObject.value = null
shouldClearTiles.value = false
}
return {
// State
currentMap,
active,
tool,
drawMode,
selectedTile,
selectedMapObject,
shouldClearTiles,
teleportSettings,
// Methods
loadMap,
updateProperty,
clearMap,
toggleActive,
setTool,
setDrawMode,
setSelectedTile,
setSelectedMapObject,
setTeleportSettings,
triggerClearTiles,
resetClearTilesFlag,
reset
}
}

View File

@ -1,20 +1,20 @@
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, watch, type Ref } from 'vue' import { computed, watch, type Ref } from 'vue'
import { useGamePointerHandlers } from './pointerHandlers/useGamePointerHandlers' import { useGamePointerHandlers } from './pointerHandlers/useGamePointerHandlers'
import { useMapEditorPointerHandlers } from './pointerHandlers/useMapEditorPointerHandlers' import { useMapEditorPointerHandlers } from './pointerHandlers/useMapEditorPointerHandlers'
export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) { export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const mapEditor = useMapEditorComposable() const mapEditorStore = useMapEditorStore()
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera) const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera)
const mapEditorHandlers = useMapEditorPointerHandlers(scene, layer, waypoint, camera) const mapEditorHandlers = useMapEditorPointerHandlers(scene, layer, waypoint, camera)
const currentHandlers = computed(() => (mapEditor.active.value ? mapEditorHandlers : gameHandlers)) const currentHandlers = computed(() => (mapEditorStore.active ? mapEditorHandlers : gameHandlers))
const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers() const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers()
const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers() const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers()
watch( watch(
() => mapEditor.active.value, () => mapEditorStore.active,
() => { () => {
cleanupPointerHandlers() cleanupPointerHandlers()
setupPointerHandlers() setupPointerHandlers()

View File

@ -23,22 +23,6 @@ export class BaseStorage<T extends { id: string }> {
} }
} }
async update(id: string, item: Partial<T>) {
try {
await this.dexie.table(this.tableName).update(id, item)
} catch (error) {
console.error(`Failed to update ${this.tableName} ${id}:`, error)
}
}
async delete(id: string) {
try {
await this.dexie.table(this.tableName).delete(id)
} catch (error) {
console.error(`Failed to delete ${this.tableName} ${id}:`, error)
}
}
async get(id: string): Promise<T | null> { async get(id: string): Promise<T | null> {
try { try {
const item = await this.dexie.table(this.tableName).get(id) const item = await this.dexie.table(this.tableName).get(id)
@ -68,10 +52,6 @@ export class BaseStorage<T extends { id: string }> {
} }
} }
liveQuery() {
return this.dexie.table(this.tableName).toArray()
}
async reset() { async reset() {
try { try {
await this.dexie.table(this.tableName).clear() await this.dexie.table(this.tableName).clear()

View File

@ -40,9 +40,4 @@ export class CharacterHairStorage extends BaseStorage<any> {
constructor() { constructor() {
super('characterHairs', 'id, name, createdAt, updatedAt') super('characterHairs', 'id, name, createdAt, updatedAt')
} }
async getSpriteId(characterTypeId: string) {
const characterType = await this.get(characterTypeId)
return characterType?.sprite
}
} }

View File

@ -32,6 +32,7 @@ export class TextureStorage {
updatedAt: texture.updatedAt, updatedAt: texture.updatedAt,
originX: texture.originX, originX: texture.originX,
originY: texture.originY, originY: texture.originY,
isAnimated: texture.isAnimated,
frameRate: texture.frameRate, frameRate: texture.frameRate,
frameWidth: texture.frameWidth, frameWidth: texture.frameWidth,
frameHeight: texture.frameHeight, frameHeight: texture.frameHeight,

View File

@ -15,10 +15,9 @@ export const useGameStore = defineStore('game', {
character: null as Character | null, character: null as Character | null,
world: { world: {
date: new Date(), date: new Date(),
weatherState: { isRainEnabled: false,
rainPercentage: 0, isFogEnabled: false,
fogDensity: 0 fogDensity: 0
}
} as WorldSettings, } as WorldSettings,
game: { game: {
isLoading: false, isLoading: false,
@ -120,8 +119,9 @@ export const useGameStore = defineStore('game', {
this.uiSettings.isGmPanelOpen = false this.uiSettings.isGmPanelOpen = false
this.world.date = new Date() this.world.date = new Date()
this.world.weatherState.rainPercentage = 0 this.world.isRainEnabled = false
this.world.weatherState.fogDensity = 0 this.world.isFogEnabled = false
this.world.fogDensity = 0
} }
} }
}) })

View File

@ -1,8 +1,9 @@
import type { MapObject, Map as MapT } from '@/application/types' import type { Map, MapEffect, MapObject, PlacedMapObject, Tile } from '@/application/types'
import { useGameStore } from '@/stores/gameStore'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export type TeleportSettings = { export type TeleportSettings = {
toMapId: string toMap: Map | null
toPositionX: number toPositionX: number
toPositionY: number toPositionY: number
toRotation: number toRotation: number
@ -11,14 +12,31 @@ export type TeleportSettings = {
export const useMapEditorStore = defineStore('mapEditor', { export const useMapEditorStore = defineStore('mapEditor', {
state: () => { state: () => {
return { return {
active: true, active: false,
map: null as Map | null,
tool: 'move', tool: 'move',
drawMode: 'tile', drawMode: 'tile',
eraserMode: 'tile',
mapList: [] as Map[],
tileList: [] as Tile[],
mapObjectList: [] as MapObject[],
selectedTile: '', selectedTile: '',
selectedMapObject: null as MapObject | null, selectedMapObject: null as MapObject | null,
isTileListModalShown: false,
isMapObjectListModalShown: false,
isMapListModalShown: false,
isCreateMapModalShown: false,
isSettingsModalShown: false,
shouldClearTiles: false, shouldClearTiles: false,
mapSettings: {
name: '',
width: 0,
height: 0,
pvp: false,
mapEffects: [] as MapEffect[]
},
teleportSettings: { teleportSettings: {
toMapId: '', toMap: null,
toPositionX: 0, toPositionX: 0,
toPositionY: 0, toPositionY: 0,
toRotation: 0 toRotation: 0
@ -26,18 +44,66 @@ export const useMapEditorStore = defineStore('mapEditor', {
} }
}, },
actions: { actions: {
toggleActive() {
const gameStore = useGameStore()
if (!this.active) gameStore.connection?.emit('map:character:leave')
if (this.active) this.reset()
this.active = !this.active
},
setMap(map: Map | null) {
this.map = map
},
setMapName(name: string) {
this.mapSettings.name = name
},
setMapWidth(width: number) {
this.mapSettings.width = width
},
setMapHeight(height: number) {
this.mapSettings.height = height
},
setMapPvp(pvp: boolean) {
if (!this.map) return
this.map.pvp = pvp
},
setMapEffects(mapEffects: MapEffect[]) {
if (!this.map) return
this.mapSettings.mapEffects = mapEffects
},
setTool(tool: string) { setTool(tool: string) {
this.tool = tool this.tool = tool
}, },
setDrawMode(mode: string) { setDrawMode(mode: string) {
this.drawMode = mode this.drawMode = mode
}, },
setEraserMode(mode: string) {
this.eraserMode = mode
},
setMapList(maps: Map[]) {
this.mapList = maps
},
setTileList(tiles: Tile[]) {
this.tileList = tiles
},
setMapObjectList(objects: MapObject[]) {
this.mapObjectList = objects
},
setSelectedTile(tile: string) { setSelectedTile(tile: string) {
this.selectedTile = tile this.selectedTile = tile
}, },
setSelectedMapObject(object: MapObject) { setSelectedMapObject(object: MapObject) {
this.selectedMapObject = object this.selectedMapObject = object
}, },
toggleSettingsModal() {
this.isSettingsModalShown = !this.isSettingsModalShown
},
toggleMapListModal() {
this.isMapListModalShown = !this.isMapListModalShown
this.isCreateMapModalShown = false
},
toggleCreateMapModal() {
this.isCreateMapModalShown = !this.isCreateMapModalShown
},
setTeleportSettings(teleportSettings: TeleportSettings) { setTeleportSettings(teleportSettings: TeleportSettings) {
this.teleportSettings = teleportSettings this.teleportSettings = teleportSettings
}, },
@ -47,11 +113,20 @@ export const useMapEditorStore = defineStore('mapEditor', {
resetClearTilesFlag() { resetClearTilesFlag() {
this.shouldClearTiles = false this.shouldClearTiles = false
}, },
reset() { reset(resetMap = false) {
if (resetMap) this.map = null
this.mapList = []
this.tileList = []
this.mapObjectList = []
this.tool = 'move' this.tool = 'move'
this.drawMode = 'tile' this.drawMode = 'tile'
this.selectedTile = '' this.selectedTile = ''
this.selectedMapObject = null this.selectedMapObject = null
this.isTileListModalShown = false
this.isMapObjectListModalShown = false
this.isSettingsModalShown = false
this.isMapListModalShown = false
this.isCreateMapModalShown = false
this.shouldClearTiles = false this.shouldClearTiles = false
} }
} }