diff --git a/src/components/gameMaster/mapEditor/Map.vue b/src/components/gameMaster/mapEditor/Map.vue index 9016b73..a376a13 100644 --- a/src/components/gameMaster/mapEditor/Map.vue +++ b/src/components/gameMaster/mapEditor/Map.vue @@ -10,7 +10,7 @@ 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' +import { onBeforeUnmount, onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue' const tileMap = shallowRef() const mapEditor = useMapEditorComposable() @@ -41,19 +41,32 @@ function handlePointerDown(pointer: Phaser.Input.Pointer) { } } +function handleKeyDown(event: KeyboardEvent) { + //CTRL+Y + if (event.key === 'y' && event.ctrlKey ) { + mapTiles.value!.redo() + } + + //CTRL+Z + if (event.key === 'z' && event.ctrlKey) { + mapTiles.value!.undo() + } +} + function handlePointerMove(pointer: Phaser.Input.Pointer) { - if (mapEditor.inputMode.value === 'hold') { + if (mapEditor.inputMode.value === 'hold' && pointer.isDown) { handlePointerDown(pointer) } } function handlePointerUp(pointer: Phaser.Input.Pointer) { - if (mapEditor.drawMode.value === 'tile') { + if (mapEditor.drawMode.value === 'tile' ) { mapTiles.value?.finalizeCommand() } } onMounted(() => { + addEventListener('keydown', handleKeyDown) scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown) scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove) scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp) @@ -65,4 +78,8 @@ onUnmounted(() => { scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp) mapEditor.reset() }) + +onBeforeUnmount(() => { + removeEventListener('keydown', handleKeyDown) +}) diff --git a/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue b/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue index 751c667..94185d6 100644 --- a/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue +++ b/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue @@ -9,7 +9,7 @@ import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composable import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { TileStorage } from '@/storage/storages' import { useScene } from 'phavuer' -import { onMounted, onUnmounted, shallowRef, watch } from 'vue' +import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue' import Tileset = Phaser.Tilemaps.Tileset @@ -22,27 +22,25 @@ const tileStorage = new TileStorage() const tileMap = shallowRef() const tileLayer = shallowRef() -defineExpose({ handlePointer, finalizeCommand }) +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 - -type EditorCommand = { - operation: 'draw' | 'erase' - tileName?: string, - affectedTiles: TileChangeSet -} - -//It must check if the position is in the set already. Otherwise it will duplicate positions -class TileChangeSet extends Set<{ x: number; y: number }> { - has(value: { x: number; y: number }): boolean { - for (const pos of this) { - if (pos.x === value.x && pos.y === value.y) return true - } - return false - } -} +let commandIndex = ref(0) +let originTiles: string[][] = [] function createTileMap() { const mapData = new Phaser.Tilemaps.MapData({ @@ -93,17 +91,7 @@ function pencil(pointer: Phaser.Input.Pointer) { // Place tile placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value) - if (!currentCommand) { - currentCommand = { - operation: 'draw', - tileName: mapEditor.selectedTile.value, - affectedTiles: new TileChangeSet() - } - } - - if (!currentCommand.affectedTiles.has({ x: tile.x, y: tile.y })) { - currentCommand.affectedTiles.add({ x: tile.x, y: tile.y }) - } + createCommandUpdate(tile.x, tile.y, mapEditor.selectedTile.value, 'draw') // Adjust mapEditorStore.map.tiles map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value @@ -122,17 +110,7 @@ function eraser(pointer: Phaser.Input.Pointer) { // Place tile placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile') - if (!currentCommand) { - currentCommand = { - operation: 'erase', - tileName: 'blank_tile', - affectedTiles: new TileChangeSet() - } - } - - if (!currentCommand.affectedTiles.has({ x: tile.x, y: tile.y })) { - currentCommand.affectedTiles.add({ x: tile.x, y: tile.y }) - } + createCommandUpdate(tile.x, tile.y, 'blank_tile', 'erase') // Adjust mapEditorStore.map.tiles map.tiles[tile.y][tile.x] = 'blank_tile' @@ -165,28 +143,11 @@ function tilePicker(pointer: Phaser.Input.Pointer) { mapEditor.setSelectedTile(map.tiles[tile.y][tile.x]) } -function finalizeCommand() { - commandStack.push(currentCommand!) - currentCommand = null -} - -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 - if (!pointer.isDown) return + 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 @@ -211,26 +172,106 @@ function handlePointer(pointer: Phaser.Input.Pointer) { } } +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() tileLayer.value = await createTileLayer(tileMap.value) - // 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') - - // Then overlay the map tiles, but only within the current map dimensions - const mapTiles = mapEditor.currentMap.value.tiles - for (let y = 0; y < mapEditor.currentMap.value.height; y++) { - for (let x = 0; x < mapEditor.currentMap.value.width; x++) { - if (mapTiles[y] && mapTiles[y][x] !== undefined) { - blankTiles[y][x] = mapTiles[y][x] - } - } - } - - setLayerTiles(tileMap.value, tileLayer.value, blankTiles) + setLayerTiles(tileMap.value, tileLayer.value, mapState.tiles) }) onUnmounted(() => { diff --git a/src/components/gameMaster/mapEditor/partials/Toolbar.vue b/src/components/gameMaster/mapEditor/partials/Toolbar.vue index 5993a75..999a55a 100644 --- a/src/components/gameMaster/mapEditor/partials/Toolbar.vue +++ b/src/components/gameMaster/mapEditor/partials/Toolbar.vue @@ -169,6 +169,8 @@ function initKeyShortcuts(event: KeyboardEvent) { // prevent if focused on composables if (document.activeElement?.tagName === 'INPUT') return + if (event.ctrlKey) return + const keyActions: { [key: string]: string } = { m: 'move', p: 'pencil',