Undo and redo cycling through map edit history
This commit is contained in:
parent
e530f69311
commit
fb6e2aa742
@ -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>
|
||||
|
@ -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(() => {
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user