Renamed zone > map

This commit is contained in:
2025-01-02 17:31:31 +01:00
parent 736ddddc54
commit 40c87f0ee3
65 changed files with 762 additions and 762 deletions

View File

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

View File

@ -0,0 +1,118 @@
<template>
<Image v-for="tile in mapEditorStore.map?.mapEventTiles" v-bind="getImageProps(tile)" />
</template>
<script setup lang="ts">
import { MapEventTileType, type MapEventTile } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { Image, useScene } from 'phavuer'
import { onMounted, onUnmounted } from 'vue'
const scene = useScene()
const mapEditorStore = useMapEditorStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function getImageProps(tile: MapEventTile) {
return {
x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY),
texture: tile.type,
depth: 999
}
}
function pencil(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is blocking tile or teleport
if (mapEditorStore.drawMode !== 'blocking tile' && mapEditorStore.drawMode !== 'teleport') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return
// If teleport, check if there is a selected map
if (mapEditorStore.drawMode === 'teleport' && !mapEditorStore.teleportSettings.toMapId) 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'
? {
toMapId: mapEditorStore.teleportSettings.toMapId,
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>

View File

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

View File

@ -0,0 +1,248 @@
<template>
<SelectedMapObject v-if="selectedMapObject" :mapObject="selectedMapObject" :movingMapObject="movingMapObject" @move="moveMapObject" @rotate="rotateMapObject" @delete="deleteMapObject" />
<MapObject v-for="mapObject in mapEditorStore.map?.mapObjects" :tilemap="tilemap" :mapObject :selectedMapObject :movingMapObject @pointerup="clickMapObject(mapObject)" />
</template>
<script setup lang="ts">
import type { PlacedMapObject as MapObjectT } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import SelectedMapObject from '@/components/gameMaster/mapEditor/partials/SelectedMapObject.vue'
import MapObject from '@/components/gameMaster/mapEditor/mapPartials/MapObject.vue'
import { getTile } from '@/composables/mapComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, ref, watch } from 'vue'
const scene = useScene()
const mapEditorStore = useMapEditorStore()
const selectedMapObject = ref<MapObjectT | null>(null)
const movingMapObject = ref<MapObjectT | null>(null)
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function pencil(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is object
if (mapEditorStore.drawMode !== 'object') return
// Check if there is a selected object
if (!mapEditorStore.selectedObject) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = mapEditorStore.map?.mapObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (existingObject) return
const newObject = {
id: uuidv4(),
mapId: mapEditorStore.map.id,
map: mapEditorStore.map,
objectId: mapEditorStore.selectedObject.id,
object: mapEditorStore.selectedObject,
depth: 0,
isRotated: false,
positionX: tile.x,
positionY: tile.y
}
// Add new object to mapObjects
mapEditorStore.map.mapObjects = mapEditorStore.map.mapObjects.concat(newObject as MapObjectT)
}
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 object
if (mapEditorStore.eraserMode !== 'object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = mapEditorStore.map.mapObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (!existingObject) return
// Remove existing object
mapEditorStore.map.mapObjects = mapEditorStore.map.mapObjects.filter((object) => object.id !== existingObject.id)
}
function objectPicker(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is object
if (mapEditorStore.drawMode !== 'object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// If alt is not pressed, return
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = mapEditorStore.map.mapObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (!existingObject) return
// Select the object
mapEditorStore.setSelectedObject(existingObject)
}
function moveMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
movingMapObject.value = mapEditorStore.map.mapObjects.find((object) => object.id === id) as MapObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingMapObject.value) return
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
movingMapObject.value.positionX = tile.x
movingMapObject.value.positionY = tile.y
}
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
function handlePointerUp() {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
movingMapObject.value = null
}
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
}
function rotateMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
mapEditorStore.map.mapObjects = mapEditorStore.map.mapObjects.map((object) => {
if (object.id === id) {
return {
...object,
isRotated: !object.isRotated
}
}
return object
})
}
function deleteMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
mapEditorStore.map.mapObjects = mapEditorStore.map.mapObjects.filter((object) => object.id !== id)
selectedMapObject.value = null
}
function clickMapObject(mapObject: MapObjectT) {
selectedMapObject.value = mapObject
// If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) {
mapEditorStore.setSelectedObject(mapObject.object)
}
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
})
// watch mapEditorStore.objectList and update originX and originY of objects in mapObjects
watch(
() => mapEditorStore.objectList,
(newObjects) => {
if (!mapEditorStore.map) return
const updatedMapObjects = mapEditorStore.map.mapObjects.map((mapObject) => {
const updatedObject = newObjects.find((obj) => obj.id === mapObject.object.id)
if (updatedObject) {
return {
...mapObject,
object: {
...mapObject.object,
originX: updatedObject.originX,
originY: updatedObject.originY
}
}
}
return mapObject
})
// Update the map with the new mapObjects
mapEditorStore.setMap({
...mapEditorStore.map,
mapObjects: updatedMapObjects
})
// Update selectedObject if it's set
if (mapEditorStore.selectedObject) {
const updatedObject = newObjects.find((obj) => obj.id === mapEditorStore.selectedObject?.id)
if (updatedObject) {
mapEditorStore.setSelectedObject({
...mapEditorStore.selectedObject,
originX: updatedObject.originX,
originY: updatedObject.originY
})
}
}
},
{ deep: true }
)
</script>

View File

@ -0,0 +1,227 @@
<template>
<Controls :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { AssetDataT } from '@/application/types'
import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, watch } from 'vue'
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const tileMap = createTileMap()
const tileLayer = createTileLayer()
/**
* A Tilemap is a container for Tilemap data.
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/
function createTileMap() {
const mapData = new Phaser.Tilemaps.MapData({
width: mapEditorStore.map?.width,
height: mapEditorStore.map?.height,
tileWidth: config.tile_size.x,
tileHeight: config.tile_size.y,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapData)
emit('tileMap:create', newTileMap)
return newTileMap
}
/**
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
*/
function createTileLayer() {
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => {
return tileMap.addTilesetImage(tile.key, tile.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any
// Add blank tile
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
function pencil(pointer: Phaser.Input.Pointer) {
// Check if 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
if (!mapEditorStore.selectedTile) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, mapEditorStore.selectedTile)
// Adjust mapEditorStore.map.tiles
mapEditorStore.map.tiles[tile.y][tile.x] = mapEditorStore.selectedTile
}
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 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
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
// Adjust mapEditorStore.map.tiles
mapEditorStore.map.tiles[tile.y][tile.x] = 'blank_tile'
}
function paint(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'paint') return
// Check if there is a selected tile
if (!mapEditorStore.selectedTile) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Set new tileArray with selected tile
setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, mapEditorStore.selectedTile))
// Adjust mapEditorStore.map.tiles
mapEditorStore.map.tiles = createTileArray(tileMap.width, tileMap.height, mapEditorStore.selectedTile)
}
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(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 tile
if (mapEditorStore.drawMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
mapEditorStore.setSelectedTile(mapEditorStore.map.tiles[tile.y][tile.x])
}
watch(
() => mapEditorStore.shouldClearTiles,
(shouldClear) => {
if (shouldClear && mapEditorStore.map) {
const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile')
setLayerTiles(tileMap, tileLayer, blankTiles)
mapEditorStore.map.tiles = blankTiles
mapEditorStore.resetClearTilesFlag()
}
}
)
onMounted(() => {
if (!mapEditorStore.map?.tiles) {
return
}
// First fill the entire map with blank tiles using current map dimensions
const blankTiles = createTileArray(mapEditorStore.map.width, mapEditorStore.map.height, 'blank_tile')
// Then overlay the map tiles, but only within the current map dimensions
const mapTiles = mapEditorStore.map.tiles
for (let y = 0; y < mapEditorStore.map.height; y++) {
for (let x = 0; x < mapEditorStore.map.width; x++) {
// Only copy if the source tiles array has this position
if (mapTiles[y] && mapTiles[y][x] !== undefined) {
blankTiles[y][x] = mapTiles[y][x]
}
}
}
setLayerTiles(tileMap, tileLayer, blankTiles)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
tileMap.destroyLayer('tiles')
tileMap.removeAllLayers()
tileMap.destroy()
})
</script>

View File

@ -0,0 +1,58 @@
<template>
<Modal :isModalOpen="true" @modal:close="() => mapEditorStore.toggleCreateMapModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="submit" class="inline">
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-full">
<label for="name">Name</label>
<input class="input-field max-w-64" v-model="name" name="name" id="name" />
</div>
<div class="form-field-half">
<label for="name">Width</label>
<input class="input-field max-w-64" v-model="width" name="name" id="name" type="number" />
</div>
<div class="form-field-half">
<label for="name">Height</label>
<input class="input-field max-w-64" v-model="height" name="name" id="name" type="number" />
</div>
<div class="form-field-full">
<label for="name">PVP enabled</label>
<select class="input-field" name="pvp" id="pvp">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { ref } from 'vue'
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const name = ref('')
const width = ref(0)
const height = ref(0)
function submit() {
gameStore.connection.emit('gm:map_editor:map:create', { name: name.value, width: width.value, height: height.value }, (response: Map[]) => {
mapEditorStore.setMapList(response)
})
mapEditorStore.toggleCreateMapModal()
}
</script>

View File

@ -0,0 +1,61 @@
<template>
<CreateMap v-if="mapEditorStore.isCreateMapModalShown" />
<Modal :is-modal-open="mapEditorStore.isMapListModalShown" @modal:close="() => mapEditorStore.toggleMapListModal()" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'">
<template #modalHeader>
<h3 class="text-lg text-white">Maps</h3>
</template>
<template #modalBody>
<div class="my-4 mx-auto">
<div class="text-center mb-4 px-2 flex gap-2.5">
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button>
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => mapEditorStore.toggleCreateMapModal()">New</button>
</div>
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapEditorStore.mapList" :key="map.id">
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
<span>{{ map.name }}</span>
<span class="ml-auto gap-1 flex">
<button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteMap(map.id)">x</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import type { Map } from '@/application/types'
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onMounted } from 'vue'
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
onMounted(async () => {
fetchMaps()
})
function fetchMaps() {
gameStore.connection?.emit('gm:map_editor:map:list', {}, (response: Map[]) => {
mapEditorStore.setMapList(response)
})
}
function loadMap(id: number) {
gameStore.connection?.emit('gm:map_editor:map:request', { mapId: id }, (response: Map) => {
mapEditorStore.setMap(response)
})
mapEditorStore.toggleMapListModal()
}
function deleteMap(id: number) {
gameStore.connection?.emit('gm:map_editor:map:delete', { mapId: id }, () => {
fetchMaps()
})
}
</script>

View File

@ -0,0 +1,106 @@
<template>
<Modal :is-modal-open="mapEditorStore.isSettingsModalShown" @modal:close="() => mapEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="430" :bg-style="'none'">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
</template>
<template #modalBody>
<div class="m-4">
<div class="space-x-2">
<button class="btn-cyan py-1.5 px-4" type="button" @click.prevent="screen = 'settings'">Settings</button>
<button class="btn-cyan py-1.5 px-4" type="button" @click.prevent="screen = 'effects'">Effects</button>
</div>
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'settings'">
<div class="gap-2.5 flex flex-wrap mt-4">
<div class="form-field-full">
<label for="name">Name</label>
<input class="input-field" v-model="name" name="name" id="name" />
</div>
<div class="form-field-half">
<label for="width">Width</label>
<input class="input-field" v-model="width" name="width" id="width" type="number" />
</div>
<div class="form-field-half">
<label for="height">Height</label>
<input class="input-field" v-model="height" name="height" id="height" type="number" />
</div>
<div class="form-field-full">
<label for="pvp">PVP enabled</label>
<select v-model="pvp" class="input-field" name="pvp" id="pvp">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
</div>
</form>
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'effects'">
<div v-for="(effect, index) in mapEffects" :key="effect.id" class="mb-2 flex items-center space-x-2 mt-4">
<input class="input-field flex-grow" v-model="effect.effect" placeholder="Effect name" />
<input class="input-field w-20" v-model.number="effect.strength" type="number" placeholder="Strength" />
<button class="btn-red py-1 px-2" type="button" @click="removeEffect(index)">Delete</button>
</div>
<button class="btn-green py-1 px-2 mt-2" type="button" @click="addEffect">Add Effect</button>
</form>
</div>
</template>
</Modal>
</template>
<script setup>
import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { ref, watch } from 'vue'
const mapEditorStore = useMapEditorStore()
const screen = ref('settings')
mapEditorStore.setMapName(mapEditorStore.map?.name)
mapEditorStore.setMapWidth(mapEditorStore.map?.width)
mapEditorStore.setMapHeight(mapEditorStore.map?.height)
mapEditorStore.setMapPvp(mapEditorStore.map?.pvp)
mapEditorStore.setMapEffects(mapEditorStore.map?.mapEffects)
const name = ref(mapEditorStore.mapSettings?.name)
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) => {
mapEditorStore.setMapName(value)
})
watch(width, (value) => {
mapEditorStore.setMapWidth(value)
})
watch(height, (value) => {
mapEditorStore.setMapHeight(value)
})
watch(pvp, (value) => {
mapEditorStore.setMapPvp(value)
})
watch(
mapEffects,
(value) => {
mapEditorStore.setMapEffects(value)
},
{ deep: true }
)
const addEffect = () => {
mapEffects.value.push({
id: Date.now().toString(), // Simple unique id generation
mapId: mapEditorStore.map?.id,
map: mapEditorStore.map,
effect: '',
strength: 1
})
}
const removeEffect = (index) => {
mapEffects.value.splice(index, 1)
}
</script>

View File

@ -0,0 +1,84 @@
<template>
<Modal :isModalOpen="mapEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (mapEditorStore.isObjectListModalShown = false)" :bg-style="'none'">
<template #modalHeader>
<h3 class="text-lg text-white">Objects</h3>
</template>
<template #modalBody>
<div class="flex pt-4 pl-4">
<div class="w-full flex gap-1.5 flex-row">
<div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div>
</div>
</div>
<div class="flex flex-col h-full p-4">
<div class="mb-4 flex flex-wrap gap-2">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
{{ tag }}
</button>
</div>
<div class="h-full overflow-auto">
<div class="flex justify-between flex-wrap gap-2.5 items-center">
<div v-for="(object, index) in filteredObjects" :key="index" class="max-w-1/4 inline-block">
<img
class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/assets/objects/${object.id}.png`"
alt="Object"
@click="mapEditorStore.setSelectedObject(object)"
:class="{
'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg scale-105': mapEditorStore.selectedObject?.id === object.id,
'border-transparent hover:border-gray-300': mapEditorStore.selectedObject?.id !== object.id
}"
/>
</div>
</div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { Object, PlacedMapObject } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const isModalOpen = ref(false)
const mapEditorStore = useMapEditorStore()
const searchQuery = ref('')
const selectedTags = ref<string[]>([])
const uniqueTags = computed(() => {
const allTags = mapEditorStore.objectList.flatMap((obj) => obj.tags || [])
return Array.from(new Set(allTags))
})
const filteredObjects = computed(() => {
return mapEditorStore.objectList.filter((object) => {
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)))
return matchesSearch && matchesTags
})
})
const toggleTag = (tag: string) => {
if (selectedTags.value.includes(tag)) {
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
} else {
selectedTags.value.push(tag)
}
}
onMounted(async () => {
isModalOpen.value = true
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
mapEditorStore.setObjectList(response)
})
})
</script>

View File

@ -0,0 +1,33 @@
<template>
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0">
<div class="self-end mt-2 flex gap-2">
<button @mousedown.stop @click="handleDelete" class="btn-red py-1.5 px-4">
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
</button>
<button @mousedown.stop @click="handleRotate" class="btn-cyan py-1.5 px-4">Rotate</button>
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { PlacedMapObject } from '@/application/types'
const props = defineProps<{
mapObject: PlacedMapObject
}>()
const emit = defineEmits(['move', 'rotate', 'delete'])
const handleMove = () => {
emit('move', props.mapObject.id)
}
const handleRotate = () => {
emit('rotate', props.mapObject.id)
}
const handleDelete = () => {
emit('delete', props.mapObject.id)
}
</script>

View File

@ -0,0 +1,82 @@
<template>
<Modal :is-modal-open="showTeleportModal" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="" class="inline">
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-half">
<label for="positionX">Position X</label>
<input class="input-field" v-model="toPositionX" name="positionX" id="positionX" type="number" />
</div>
<div class="form-field-half">
<label for="positionY">Position Y</label>
<input class="input-field" v-model="toPositionY" name="positionY" id="positionY" type="number" />
</div>
<div class="form-field-full">
<label for="rotation">Rotation</label>
<select v-model="toRotation" class="input-field" name="rotation" id="rotation">
<option :value="0">North</option>
<option :value="2">East</option>
<option :value="4">South</option>
<option :value="6">West</option>
</select>
</div>
<div class="form-field-full">
<label for="toMapId">Map to teleport to</label>
<select v-model="toMapId" class="input-field" name="toMapId" id="toMapId">
<option :value="0">Select map</option>
<option v-for="map in mapEditorStore.mapList" :key="map.id" :value="map.id">{{ map.name }}</option>
</select>
</div>
</div>
</form>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref, watch } from 'vue'
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
const mapEditorStore = useMapEditorStore()
const gameStore = useGameStore()
onMounted(fetchMaps)
function fetchMaps() {
gameStore.connection?.emit('gm:map_editor:map:list', {}, (response: Map[]) => {
mapEditorStore.setMapList(response)
})
}
const { toPositionX, toPositionY, toRotation, toMapId } = useRefTeleportSettings()
function useRefTeleportSettings() {
const settings = mapEditorStore.teleportSettings
return {
toPositionX: ref(settings.toPositionX),
toPositionY: ref(settings.toPositionY),
toRotation: ref(settings.toRotation),
toMapId: ref(settings.toMapId)
}
}
watch([toPositionX, toPositionY, toRotation, toMapId], updateTeleportSettings)
function updateTeleportSettings() {
mapEditorStore.setTeleportSettings({
toPositionX: toPositionX.value,
toPositionY: toPositionY.value,
toRotation: toRotation.value,
toMapId: toMapId.value
})
}
</script>

View File

@ -0,0 +1,236 @@
<template>
<Modal :isModalOpen="mapEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (mapEditorStore.isTileListModalShown = false)" :bg-style="'none'">
<template #modalHeader>
<h3 class="text-lg text-white">Tiles</h3>
</template>
<template #modalBody>
<div class="h-full overflow-auto" v-if="!selectedGroup">
<div class="flex pt-4 pl-4">
<div class="w-full flex gap-1.5 flex-row">
<div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div>
</div>
</div>
<div class="flex flex-col h-full p-4">
<div class="mb-4 flex flex-wrap gap-2">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
{{ tag }}
</button>
</div>
<div class="h-[calc(100%_-_60px)] flex-grow overflow-y-auto">
<div class="grid grid-cols-8 gap-2 justify-items-center">
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
<img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/assets/tiles/${group.parent.id}.png`"
:alt="group.parent.name"
@click="openGroup(group)"
@load="() => processTile(group.parent)"
:class="{
'border-cyan shadow-lg scale-105': isActiveTile(group.parent),
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
{{ group.children.length + 1 }}
</span>
</div>
</div>
</div>
</div>
</div>
<div v-else class="h-full overflow-auto">
<div class="p-4">
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
<div class="grid grid-cols-8 gap-2 justify-items-center">
<div class="flex flex-col items-center justify-center">
<img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
:alt="selectedGroup.parent.name"
@click="selectTile(selectedGroup.parent.id)"
:class="{
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span>
</div>
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
<img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
:alt="childTile.name"
@click="selectTile(childTile.id)"
:class="{
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span>
</div>
</div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { Tile } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const isModalOpen = ref(false)
const mapEditorStore = useMapEditorStore()
const searchQuery = ref('')
const selectedTags = ref<string[]>([])
const tileCategories = ref<Map<string, string>>(new Map())
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
const uniqueTags = computed(() => {
const allTags = mapEditorStore.tileList.flatMap((tile) => tile.tags || [])
return Array.from(new Set(allTags))
})
const groupedTiles = computed(() => {
const groups: { parent: Tile; children: Tile[] }[] = []
const filteredTiles = mapEditorStore.tileList.filter((tile) => {
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)))
return matchesSearch && matchesTags
})
filteredTiles.forEach((tile) => {
const parentGroup = groups.find((group) => areTilesRelated(group.parent, tile))
if (parentGroup && parentGroup.parent.id !== tile.id) {
parentGroup.children.push(tile)
} else {
groups.push({ parent: tile, children: [] })
}
})
return groups
})
const tileColorData = ref<Map<string, { r: number; g: number; b: number }>>(new Map())
const tileEdgeData = ref<Map<string, number>>(new Map())
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
const colorSimilarityThreshold = 30 // Adjust this value as needed
const edgeComplexitySimilarityThreshold = 20 // Adjust this value as needed
const color1 = tileColorData.value.get(tile1.id)
const color2 = tileColorData.value.get(tile2.id)
const edge1 = tileEdgeData.value.get(tile1.id)
const edge2 = tileEdgeData.value.get(tile2.id)
if (!color1 || !color2 || edge1 === undefined || edge2 === undefined) {
return false
}
const colorDifference = Math.sqrt(Math.pow(color1.r - color2.r, 2) + Math.pow(color1.g - color2.g, 2) + Math.pow(color1.b - color2.b, 2))
const edgeComplexityDifference = Math.abs(edge1 - edge2)
const namePrefix1 = tile1.name.split('_')[0]
const namePrefix2 = tile2.name.split('_')[0]
return colorDifference <= colorSimilarityThreshold && edgeComplexityDifference <= edgeComplexitySimilarityThreshold && namePrefix1 === namePrefix2
}
const toggleTag = (tag: string) => {
if (selectedTags.value.includes(tag)) {
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
} else {
selectedTags.value.push(tag)
}
}
function processTile(tile: Tile) {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx!.drawImage(img, 0, 0, img.width, img.height)
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height)
tileColorData.value.set(tile.id, getDominantColor(imageData))
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
}
img.src = `${config.server_endpoint}/assets/tiles/${tile.id}.png`
}
function getDominantColor(imageData: ImageData) {
let r = 0,
g = 0,
b = 0,
total = 0
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] > 0) {
// Only consider non-transparent pixels
r += imageData.data[i]
g += imageData.data[i + 1]
b += imageData.data[i + 2]
total++
}
}
return {
r: Math.round(r / total),
g: Math.round(g / total),
b: Math.round(b / total)
}
}
function getEdgeComplexity(imageData: ImageData) {
let edgePixels = 0
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const i = (y * imageData.width + x) * 4
if (imageData.data[i + 3] > 0 && (x === 0 || y === 0 || x === imageData.width - 1 || y === imageData.height - 1 || imageData.data[i - 1] === 0 || imageData.data[i + 7] === 0)) {
edgePixels++
}
}
}
return edgePixels
}
function getTileCategory(tile: Tile): string {
return tileCategories.value.get(tile.id) || ''
}
function openGroup(group: { parent: Tile; children: Tile[] }) {
selectedGroup.value = group
}
function closeGroup() {
selectedGroup.value = null
}
function selectTile(tile: string) {
mapEditorStore.setSelectedTile(tile)
}
function isActiveTile(tile: Tile): boolean {
return mapEditorStore.selectedTile === tile.id
}
onMounted(async () => {
isModalOpen.value = true
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
mapEditorStore.setTileList(response)
response.forEach((tile) => processTile(tile))
})
})
</script>

View File

@ -0,0 +1,178 @@
<template>
<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 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': 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': mapEditorStore.tool !== 'move' }">(M)</span>
</button>
<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': 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': mapEditorStore.tool !== 'pencil' }">(P)</span>
<div class="select" v-if="mapEditorStore.tool === 'pencil'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
{{ mapEditorStore.drawMode }}
<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 class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditorStore.tool === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('tile')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('object')">
Object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')">
Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('blocking tile')">Blocking tile</span>
</div>
</div>
</button>
<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': 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': mapEditorStore.tool !== 'eraser' }">(E)</span>
<div class="select" v-if="mapEditorStore.tool === 'eraser'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
{{ mapEditorStore.eraserMode }}
<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 class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('object')">
Object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')">
Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('blocking tile')">Blocking tile</span>
</div>
</div>
</button>
<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': 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': mapEditorStore.tool !== 'paint' }">(B)</span>
</button>
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="mapEditorStore.map"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
</div>
<div 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="() => mapEditorStore.toggleMapListModal()">Load</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="mapEditorStore.map">Clear</button>
<button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleActive()">Exit</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onClickOutside } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref } from 'vue'
const mapEditorStore = useMapEditorStore()
const emit = defineEmits(['save', 'clear'])
// track when clicked outside of toolbar items
const toolbar = ref(null)
// track select state
let selectPencilOpen = ref(false)
let selectEraserOpen = ref(false)
// drawMode
function setDrawMode(value: string) {
mapEditorStore.isTileListModalShown = value === 'tile'
mapEditorStore.isObjectListModalShown = value === 'object'
mapEditorStore.setDrawMode(value)
selectPencilOpen.value = false
}
// drawMode
function setEraserMode(value: string) {
mapEditorStore.setEraserMode(value)
selectEraserOpen.value = false
}
function handleClick(tool: string) {
if (tool === 'settings') {
mapEditorStore.toggleSettingsModal()
} else {
mapEditorStore.setTool(tool)
}
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
}
function cycleToolMode(tool: 'pencil' | 'eraser') {
const modes = ['tile', 'object', 'teleport', 'blocking tile']
const currentMode = tool === 'pencil' ? mapEditorStore.drawMode : mapEditorStore.eraserMode
const currentIndex = modes.indexOf(currentMode)
const nextIndex = (currentIndex + 1) % modes.length
const nextMode = modes[nextIndex]
if (tool === 'pencil') {
setDrawMode(nextMode)
} else {
setEraserMode(nextMode)
}
}
function initKeyShortcuts(event: KeyboardEvent) {
// Check if map is set
if (!mapEditorStore.map) return
// prevent if focused on composables
if (document.activeElement?.tagName === 'INPUT') return
const keyActions: { [key: string]: string } = {
m: 'move',
p: 'pencil',
e: 'eraser',
b: 'paint',
z: 'settings'
}
if (keyActions.hasOwnProperty(event.key)) {
const tool = keyActions[event.key]
if ((tool === 'pencil' || tool === 'eraser') && mapEditorStore.tool === tool) {
cycleToolMode(tool)
} else {
handleClick(tool)
}
}
}
function handleClickOutside() {
selectPencilOpen.value = false
selectEraserOpen.value = false
}
onClickOutside(toolbar, handleClickOutside)
onMounted(() => {
addEventListener('keydown', initKeyShortcuts)
})
onBeforeUnmount(() => {
removeEventListener('keydown', initKeyShortcuts)
})
</script>