<template> <Controls v-if="tileLayer" :layer="tileLayer" :depth="0" /> </template> <script setup lang="ts"> import config from '@/application/config' import Controls from '@/components/utilities/Controls.vue' import { createTileMap, createTileLayer, createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { TileStorage } from '@/storage/storages' import { useScene } from 'phavuer' import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue' const emit = defineEmits(['tileMap:create']) const scene = useScene() const mapEditor = useMapEditorComposable() const tileStorage = new TileStorage() const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() defineExpose({ handlePointer, finalizeCommand, undo, redo }) class EditorCommand { public operation: 'draw' | 'erase' = 'draw' public tileName: string = "blank_tile" public affectedTiles: number[][] constructor(operation: 'draw' | 'erase', tileName: string) { this.operation = operation this.tileName = tileName this.affectedTiles = [] } } //Record of commands let commandStack: EditorCommand[] = [] let currentCommand: EditorCommand | null = null let commandIndex = ref(0) let originTiles: string[][] = [] function pencil(pointer: Phaser.Input.Pointer) { let map = mapEditor.currentMap.value if (!map) return // Check if there is a selected tile if (!mapEditor.selectedTile.value) return if (!tileMap.value || !tileLayer.value) return // Check if there is a tile const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) if (!tile) return // Place tile placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value) createCommandUpdate(tile.x, tile.y, mapEditor.selectedTile.value, 'draw') // Adjust mapEditorStore.map.tiles map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value } function eraser(pointer: Phaser.Input.Pointer) { let map = mapEditor.currentMap.value if (!map) return if (!tileMap.value || !tileLayer.value) return // Check if there is a tile const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) if (!tile) return // Place tile placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile') createCommandUpdate(tile.x, tile.y, 'blank_tile', 'erase') // Adjust mapEditorStore.map.tiles map.tiles[tile.y][tile.x] = 'blank_tile' } function paint(pointer: Phaser.Input.Pointer) { if (!tileMap.value || !tileLayer.value) return // Set new tileArray with selected tile const tileArray = createTileArray(tileMap.value.width, tileMap.value.height, mapEditor.selectedTile.value) setLayerTiles(tileMap.value, tileLayer.value, tileArray) // Adjust mapEditorStore.map.tiles if (mapEditor.currentMap.value) { mapEditor.currentMap.value.tiles = tileArray } } // When alt is pressed, and the pointer is down, select the tile that the pointer is over function tilePicker(pointer: Phaser.Input.Pointer) { let map = mapEditor.currentMap.value if (!map) return if (!tileMap.value || !tileLayer.value) return // Check if there is a tile const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) if (!tile) return // Select the tile mapEditor.setSelectedTile(map.tiles[tile.y][tile.x]) } function handlePointer(pointer: Phaser.Input.Pointer) { if (!tileMap.value || !tileLayer.value) return // Check if left mouse button is pressed if (!pointer.isDown && pointer.button === 0) return // Check if shift is not pressed, this means we are moving the camera if (pointer.event.shiftKey) return // Check if alt is pressed if (pointer.event.altKey) { tilePicker(pointer) return } // Check if draw mode is tile switch (mapEditor.tool.value) { case 'pencil': pencil(pointer) break case 'eraser': eraser(pointer) break case 'paint': paint(pointer) break } } function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase') { if (!currentCommand) { currentCommand = new EditorCommand(operation, tileName) } //If position is already in, do not proceed for (const vec of currentCommand.affectedTiles) { if (vec[0] === x && vec[1] === y) return } currentCommand.affectedTiles.push([x,y]) } function finalizeCommand() { if (!currentCommand) return //Cut the stack so the current edit is the last commandStack = commandStack.slice(0, commandIndex.value) commandStack.push(currentCommand) if (commandStack.length >= 9) { originTiles = applyCommands(originTiles, commandStack.shift()!) } commandIndex.value = commandStack.length currentCommand = null } function undo() { if (commandIndex.value > 0) { commandIndex.value-- updateMapTiles() } } function redo() { if (commandIndex.value <= 9 && commandIndex.value <= commandStack.length) { commandIndex.value++ updateMapTiles() } } function applyCommands(tiles: string[][], ...commands: EditorCommand[]): string[][] { let tileVersion = cloneArray(tiles) for (let command of commands) { for (const position of command.affectedTiles) { tileVersion[position[1]][position[0]] = command.tileName } } return tileVersion } function updateMapTiles() { if (!tileMap.value || !tileLayer.value || !mapEditor.currentMap.value) return let indexedCommands = commandStack.slice(0, commandIndex.value) let modifiedTiles = applyCommands(originTiles, ...indexedCommands) //replaceTiles(mapEditor.currentMap.value.tiles, layer, tileMap.value.width, tileMap.value.height) setLayerTiles(tileMap.value, tileLayer.value, modifiedTiles) mapEditor.currentMap.value.tiles = modifiedTiles } //Recursive Array Clone function cloneArray(arr: any[]) : any[] { return arr.map((item) => item instanceof Array ? cloneArray(item) : item) } 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) replaceTiles(mapEditor.currentMap.value.tiles, blankTiles, tileLayer.value.width, tileLayer.value.height) mapEditor.resetClearTilesFlag() } } ) // Then overlay the map tiles, but only within the current map dimensions function replaceTiles(originalTiles: string[][], mapTiles: string[][], width: number, height: number) { for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { if (mapTiles[y] && mapTiles[y][x] !== undefined) { originalTiles[y][x] = mapTiles[y][x] } } } } onMounted(async () => { if (!mapEditor.currentMap.value) return const mapState = mapEditor.currentMap.value //Clone originTiles = cloneArray(mapEditor.currentMap.value.tiles) tileMap.value = createTileMap(scene, mapEditor.currentMap.value) emit('tileMap:create', tileMap.value) tileLayer.value = createTileLayer(tileMap.value, mapEditor.currentMap.value) setLayerTiles(tileMap.value, tileLayer.value, mapState.tiles) }) onUnmounted(() => { if (tileMap.value) { tileMap.value.destroyLayer('tiles') tileMap.value.removeAllLayers() tileMap.value.destroy() } }) </script>