From 94596394978589e023e85cc5422202de37b6e182 Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 10 Feb 2025 16:02:38 -0600 Subject: [PATCH] Best undo/redo function across all map editor elements --- src/components/gameMaster/mapEditor/Map.vue | 122 +++++++++++------ .../mapEditor/mapPartials/MapEventTiles.vue | 18 ++- .../mapEditor/mapPartials/MapTiles.vue | 40 +++--- .../mapPartials/PlacedMapObjects.vue | 125 +++++------------- src/components/screens/MapEditor.vue | 1 - src/composables/useMapEditorComposable.ts | 7 - 6 files changed, 147 insertions(+), 166 deletions(-) diff --git a/src/components/gameMaster/mapEditor/Map.vue b/src/components/gameMaster/mapEditor/Map.vue index 7acceb7..d55c0ba 100644 --- a/src/components/gameMaster/mapEditor/Map.vue +++ b/src/components/gameMaster/mapEditor/Map.vue @@ -1,6 +1,6 @@ @@ -10,12 +10,13 @@ import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEven import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue' import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue' import { useMapEditorComposable } from '@/composables/useMapEditorComposable' -import { cloneArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService' +import { cloneArray, createTileArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService' import { TileStorage } from '@/storage/storages' import { useScene } from 'phavuer' import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue' -import type { MapEventTile, PlacedMapObject as PlacedMapObjectT } from '@/application/types' +import type { PlacedMapObject as PlacedMapObjectT, Map as MapT, MapEventTile } from '@/application/types' +import { useManualRefHistory, useRefHistory } from '@vueuse/core' const tileMap = shallowRef() const tileMapLayer = shallowRef() @@ -28,18 +29,20 @@ const mapObjects = useTemplateRef('mapObjects') const eventTiles = useTemplateRef('eventTiles') //Record of commands -let commandStack: EditorCommand[] = [] +let commandStack: (EditorCommand | number) [] = [] let commandIndex = ref(0) let originTiles: string[][] = [] let originEventTiles: MapEventTile[] = [] -let originObjects: PlacedMapObjectT[] = [] +let originObjects = ref(mapEditor.currentMap.value.placedMapObjects) + +const {undo, redo, commit, reset, history, canUndo, canRedo} = useRefHistory(originObjects, {clone:true, deep:true, capacity:9}) //Command Pattern basic interface, extended to store what elements have been changed by each edit export interface EditorCommand { apply: (elements: any[]) => any[] type: 'tile' | 'map_object' | 'event_tile' - operation: 'draw' | 'erase' | 'place' | 'move' | 'delete' | 'rotate' + operation: 'draw' | 'erase' | 'clear' } function applyCommands(tiles: any[], ...commands: EditorCommand[]): any[] { @@ -50,57 +53,95 @@ function applyCommands(tiles: any[], ...commands: EditorCommand[]): any[] { return tileVersion } -watch(() => commandIndex.value!, (val) => { - if (val !== undefined) { - update(commandStack.slice(0, val)) +watch( + () => mapEditor.shouldClearTiles.value, + (shouldClear) => { + if (shouldClear && mapEditor.currentMap.value) { + mapTiles.value!.clearTiles() + eventTiles.value!.clearTiles() + mapEditor.currentMap.value.placedMapObjects = [] + updateAndCommit(mapEditor.currentMap.value) + mapEditor.resetClearTilesFlag() + } } -}) +) -function update(commands: EditorCommand[]) { +function update(commands: (EditorCommand | number)[]) { if (!mapEditor.currentMap.value) return - const tileCommands = commands.filter((command) => command.type === 'tile') - const eventTileCommands = commands.filter((command) => command.type === 'event_tile') - const objectCommands = commands.filter((command) => command.type === 'map_object') + if (commandStack.length >= 9) { + if (typeof commandStack[0] !== 'number') { + const base = commandStack.shift() as EditorCommand + if (base.operation !== 'clear') { + switch (base.type) { + case 'tile': + originTiles = base.apply(originTiles) as string[][] + break + case 'event_tile': + originEventTiles = base.apply(originEventTiles) as MapEventTile[] + break + } + } + else { + commandStack.shift() + } + } + else if (typeof commandStack[0] === 'number') { + commandStack.shift() + } + } + + let tileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'tile') as EditorCommand[] + let eventTileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'event_tile') as EditorCommand[] let modifiedTiles = applyCommands(originTiles, ...tileCommands) placeTiles(tileMap.value!, tileMapLayer.value!, modifiedTiles) + let eventTiles = applyCommands(originEventTiles, ...eventTileCommands) + mapEditor.currentMap.value.tiles = modifiedTiles - mapEditor.currentMap.value.mapEventTiles = applyCommands(originEventTiles, ...eventTileCommands) - mapEditor.currentMap.value.placedMapObjects = applyCommands(originObjects, ...objectCommands) + mapEditor.currentMap.value.mapEventTiles = eventTiles +} + +function updateMapObjects(map: MapT) { + originObjects.value = map.placedMapObjects +} + +function updateAndCommit(map?: MapT) { + commandStack = commandStack.slice(0, commandIndex.value) + if (map) updateMapObjects(map) + commit() + commandStack.push(0) + commandIndex.value = commandStack.length + + console.log(history.value) + console.log(commandStack) + console.log(commandIndex.value) } function addCommand(command: EditorCommand) { commandStack = commandStack.slice(0, commandIndex.value) commandStack.push(command) - - if (commandStack.length >= 9) { - switch (commandStack[0].type) { - case 'tile': - originTiles = commandStack.shift()?.apply(originTiles) as string[][] - break - case 'map_object': - originObjects = commandStack.shift()?.apply(originObjects) as PlacedMapObjectT[] - break - case 'event_tile': - originEventTiles = commandStack.shift()?.apply(originEventTiles) as MapEventTile[] - break - } - } - commandIndex.value = commandStack.length } -function undo() { +function undoEdit() { if (commandIndex.value > 0) { - commandIndex.value-- + if (typeof(commandStack[--commandIndex.value]) === 'number' && canUndo) { + undo() + mapEditor.currentMap.value.placedMapObjects = originObjects.value + } + update(commandStack.slice(0, commandIndex.value)) } } -function redo() { - if (commandIndex.value <= 9 && commandIndex.value <= commandStack.length) { - commandIndex.value++ +function redoEdit() { + if (commandIndex.value <= 9 && commandIndex.value < commandStack.length) { + if (typeof(commandStack[commandIndex.value++]) === 'number' && canRedo) { + redo() + mapEditor.currentMap.value.placedMapObjects = originObjects.value + } + update(commandStack.slice(0, commandIndex.value)) } } @@ -133,12 +174,12 @@ function handlePointerDown(pointer: Phaser.Input.Pointer) { function handleKeyDown(event: KeyboardEvent) { //CTRL+Y if (event.key === 'y' && event.ctrlKey) { - redo() + redoEdit() } //CTRL+Z if (event.key === 'z' && event.ctrlKey) { - undo() + undoEdit() } } @@ -154,7 +195,7 @@ function handlePointerUp(pointer: Phaser.Input.Pointer) { mapTiles.value!.finalizeCommand() break case 'map_object': - mapObjects.value!.finalizeCommand() + updateAndCommit() break case 'teleport': eventTiles.value!.finalizeCommand() @@ -171,9 +212,10 @@ onMounted(async () => { //Clone originTiles = cloneArray(mapValue.tiles) - originObjects = cloneArray(mapValue.placedMapObjects) originEventTiles = cloneArray(mapValue.mapEventTiles) + commit() + const tileStorage = new TileStorage() const allTiles = await tileStorage.getAll() const allTileIds = allTiles.map((tile) => tile.id) diff --git a/src/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue b/src/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue index 77db3a2..2a23ebb 100644 --- a/src/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue +++ b/src/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue @@ -13,7 +13,7 @@ import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue' const mapEditor = useMapEditorComposable() -defineExpose({ handlePointer, finalizeCommand }) +defineExpose({ handlePointer, finalizeCommand, clearTiles}) const emit = defineEmits(['createCommand']) @@ -27,7 +27,7 @@ const props = defineProps<{ let currentCommand: EventTileCommand | null = null class EventTileCommand implements EditorCommand { - public operation: 'draw' | 'erase' = 'draw' + public operation: 'draw' | 'erase' | 'clear' = 'draw' public type: 'event_tile' = 'event_tile' public affectedTiles: MapEventTile[] = [] @@ -39,15 +39,18 @@ class EventTileCommand implements EditorCommand { else if (this.operation === 'erase') { tileVersion = tileVersion.filter((v) => !this.affectedTiles.includes(v)) } + else if (this.operation === 'clear') { + tileVersion = [] + } return tileVersion } - constructor(operation: 'draw' | 'erase') { + constructor(operation: 'draw' | 'erase' | 'clear') { this.operation = operation } } -function createCommandUpdate(tile: MapEventTile, operation: 'draw' | 'erase') { +function createCommandUpdate(tile?: MapEventTile, operation: 'draw' | 'erase' | 'clear') { if (!currentCommand) { currentCommand = new EventTileCommand(operation) } @@ -149,4 +152,11 @@ function handlePointer(pointer: Phaser.Input.Pointer) { break } } + +function clearTiles() { + if (mapEditor.currentMap.value.mapEventTiles.length === 0) return + createCommandUpdate(null, 'clear') + finalizeCommand() +} + diff --git a/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue b/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue index 671e8b0..2d0e524 100644 --- a/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue +++ b/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue @@ -11,7 +11,7 @@ import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue' const mapEditor = useMapEditorComposable() -defineExpose({ handlePointer, finalizeCommand }) +defineExpose({ handlePointer, finalizeCommand, clearTiles }) const emit = defineEmits(['createCommand']) @@ -20,32 +20,37 @@ const props = defineProps<{ tileMapLayer: Phaser.Tilemaps.TilemapLayer }>() - // *** COMMAND STATE *** let currentCommand: TileCommand | null = null class TileCommand implements EditorCommand { - public operation: 'draw' | 'erase' = 'draw' + public operation: 'draw' | 'erase' | 'clear' = 'draw' public type: 'tile' = 'tile' public tileName: string = 'blank_tile' public affectedTiles: number[][] = [] apply(elements: string[][]) { - let tileVersion = cloneArray(elements) as string[][] - for (const position of this.affectedTiles) { - tileVersion[position[1]][position[0]] = this.tileName + let tileVersion + if (this.operation === 'clear') { + tileVersion = createTileArray(props.tileMapLayer.width, props.tileMapLayer.height, 'blank_tile') + } + else { + tileVersion = cloneArray(elements) as string[][] + for (const position of this.affectedTiles) { + tileVersion[position[1]][position[0]] = this.tileName + } } return tileVersion } - constructor(operation: 'draw' | 'erase', tileName: string) { + constructor(operation: 'draw' | 'erase' | 'clear', tileName: string) { this.operation = operation this.tileName = tileName } } -function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase') { +function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase' | 'clear') { if (!currentCommand) { currentCommand = new TileCommand(operation, tileName) } @@ -137,21 +142,14 @@ function handlePointer(pointer: Phaser.Input.Pointer) { } } - // *** LIFECYCLE *** - -watch( - () => mapEditor.shouldClearTiles.value, - (shouldClear) => { - if (shouldClear && mapEditor.currentMap.value) { - const blankTiles = createTileArray(props.tileMapLayer.width, props.tileMapLayer.height, 'blank_tile') - placeTiles(props.tileMap, props.tileMapLayer, blankTiles) - mapEditor.currentMap.value.tiles = blankTiles - mapEditor.resetClearTilesFlag() - } - } -) +function clearTiles() { + const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, 'blank_tile') + placeTiles(props.tileMap, props.tileMapLayer, tileArray) + createCommandUpdate(0,0,"blank_tile",'clear') + finalizeCommand() +} onMounted(async () => { if (!mapEditor.currentMap.value) return diff --git a/src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue b/src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue index 81aabfe..ba6a1ff 100644 --- a/src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue +++ b/src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue @@ -1,5 +1,5 @@ @@ -7,101 +7,38 @@ import type { MapObject, Map as MapT, - PlacedMapObject as PlacedMapObjectT, - UUID, - MapEventTile + PlacedMapObject as PlacedMapObjectT } from '@/application/types' import { uuidv4 } from '@/application/utilities' import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue' import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue' import { useMapEditorComposable } from '@/composables/useMapEditorComposable' -import { cloneArray, getTile } from '@/services/mapService' +import { getTile } from '@/services/mapService' import { useScene } from 'phavuer' import Tilemap = Phaser.Tilemaps.Tilemap import TilemapLayer = Phaser.Tilemaps.TilemapLayer -import { computed } from 'vue' -import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue' -import Vector2 = Phaser.Math.Vector2 -import Tile = Phaser.Tilemaps.Tile const scene = useScene() const mapEditor = useMapEditorComposable() -const map = computed(() => mapEditor.currentMap.value!) -defineExpose({ handlePointer, finalizeCommand }) +const emit = defineEmits<{(e: 'update', map: MapT): void, (e: 'updateAndCommit', map: MapT): void}>() -const emit = defineEmits(['createCommand']) +defineExpose({ handlePointer }) const props = defineProps<{ tileMap: Tilemap tileMapLayer: TilemapLayer }>() - -// *** COMMAND STATE *** - -let currentCommand: MapObjectCommand | null = null - -class MapObjectCommand implements EditorCommand { - public operation: 'place' | 'move' | 'delete' | 'rotate' = 'place' - public type: 'map_object' = 'map_object' - public affectedTiles: PlacedMapObjectT[] = [] - public targetPosition?: Phaser.Math.Vector2 - - apply(elements: PlacedMapObjectT[]) { - let tileVersion = cloneArray(elements) as PlacedMapObjectT[] - if (this.operation === 'place') { - tileVersion = tileVersion.concat(this.affectedTiles) - } - else if (this.operation === 'delete') { - tileVersion = tileVersion.filter((v) => !this.affectedTiles.includes(v)) - } - else if (this.operation === 'move') { - const targetObject = tileVersion.find((v) => this.affectedTiles[0].id === v.id) - if (targetObject) { - targetObject.positionX = this.targetPosition!.x - targetObject.positionY = this.targetPosition!.y - } - } - - return tileVersion - } - - constructor(operation: 'place' | 'move' | 'delete' | 'rotate') { - this.operation = operation - } -} - -function createCommandUpdate(object: PlacedMapObjectT, operation: 'place' | 'move' | 'delete' | 'rotate', targetPosition?: Phaser.Math.Vector2) { - if (!currentCommand) { - currentCommand = new MapObjectCommand(operation) - } - else { - if (targetPosition) { - currentCommand.targetPosition = targetPosition - } - } - - currentCommand.affectedTiles.push(object) -} - -function finalizeCommand() { - if (!currentCommand) return - emit('createCommand', currentCommand) - currentCommand = null -} - - // *** HANDLERS *** - function pencil(pointer: Phaser.Input.Pointer, map: MapT) { const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY) if (!tile) return // Check if object already exists on position - const existingPlacedMapObject = findObjectByPointer(pointer, map) + const existingPlacedMapObject = findObjectByPointer(pointer, mapEditor.currentMap.value!) if (existingPlacedMapObject) return if (!mapEditor.selectedMapObject.value) return @@ -115,11 +52,10 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) { } // Add new object to mapObjects + mapEditor.selectedPlacedObject.value = newPlacedMapObject map.placedMapObjects.push(newPlacedMapObject) - createCommandUpdate(newPlacedMapObject, 'place') - - mapEditor.selectedPlacedObject.value = newPlacedMapObject + emit('update', map) } function eraser(pointer: Phaser.Input.Pointer, map: MapT) { @@ -127,10 +63,10 @@ function eraser(pointer: Phaser.Input.Pointer, map: MapT) { const existingPlacedMapObject = findObjectByPointer(pointer, map) if (!existingPlacedMapObject) return - createCommandUpdate(existingPlacedMapObject, 'delete') - // Remove existing object map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id) + + emit('update', map) } function findObjectByPointer(pointer: Phaser.Input.Pointer, map: MapT): PlacedMapObjectT | undefined { @@ -152,48 +88,51 @@ function objectPicker(pointer: Phaser.Input.Pointer, map: MapT) { function moveMapObject(id: string, map: MapT) { mapEditor.movingPlacedObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT - let t: Tile - function handlePointerMove(pointer: Phaser.Input.Pointer) { if (!mapEditor.movingPlacedObject.value) return const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY) if (!tile) return - t = tile - mapEditor.movingPlacedObject.value.positionX = tile.x mapEditor.movingPlacedObject.value.positionY = tile.y } scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove) - function handlePointerUp() { + function handlePointerUp(pointer: Phaser.Input.Pointer) { scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove) - mapEditor.movingPlacedObject.value = null - createCommandUpdate(mapEditor.movingPlacedObject.value!, 'move', new Vector2(t.x, t.y)) - finalizeCommand() + const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY) + if (!tile) return + + map.placedMapObjects.map((placed) => { + if (placed.id === id) { + placed.positionX = tile.x + placed.positionY = tile.y + }}) + + mapEditor.movingPlacedObject.value = null } + emit('updateAndCommit', map) scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp) } function rotatePlacedMapObject(id: string, map: MapT) { - const matchingObject = map.placedMapObjects.find((placedMapObject) => placedMapObject.id === id) - matchingObject!.isRotated = !matchingObject!.isRotated + + map.placedMapObjects.map((placed) => { + if (placed.id === id) { + console.log(placed.id) + placed.isRotated = !placed.isRotated + }}) + + emit('updateAndCommit', map) } function deletePlacedMapObject(id: string, map: MapT) { - let mapE = mapEditor.currentMap.value! - - const foundObject = mapE.placedMapObjects.find((obj) => obj.id === id) - if (!foundObject) return - - createCommandUpdate(foundObject, 'delete') - finalizeCommand() - - mapE.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id) + map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id) mapEditor.selectedPlacedObject.value = null + emit('updateAndCommit', map) } function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) { diff --git a/src/components/screens/MapEditor.vue b/src/components/screens/MapEditor.vue index df868ea..0175fe5 100644 --- a/src/components/screens/MapEditor.vue +++ b/src/components/screens/MapEditor.vue @@ -113,7 +113,6 @@ function clear() { if (!mapEditor.currentMap.value) return // Clear placed objects, event tiles and tiles - mapEditor.clearMap() mapEditor.triggerClearTiles() } diff --git a/src/composables/useMapEditorComposable.ts b/src/composables/useMapEditorComposable.ts index 4b095ae..73ede23 100644 --- a/src/composables/useMapEditorComposable.ts +++ b/src/composables/useMapEditorComposable.ts @@ -36,12 +36,6 @@ export function useMapEditorComposable() { } } - const clearMap = () => { - if (!currentMap.value) return - currentMap.value.placedMapObjects = [] - currentMap.value.mapEventTiles = [] - } - const toggleActive = () => { if (active.value) reset() active.value = !active.value @@ -105,7 +99,6 @@ export function useMapEditorComposable() { // Methods loadMap, updateProperty, - clearMap, toggleActive, setTool, setDrawMode,