Undo and redo cycling through map edit history

This commit is contained in:
Andrei 2025-01-29 19:48:09 -06:00
parent e530f69311
commit fb6e2aa742
3 changed files with 135 additions and 75 deletions

View File

@ -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<Phaser.Tilemaps.Tilemap>()
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)
})
</script>

View File

@ -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<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
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(() => {

View File

@ -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',