forked from noxious/client
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 PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { useScene } from 'phavuer'
|
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 tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
@ -41,8 +41,20 @@ 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) {
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
if (mapEditor.inputMode.value === 'hold') {
|
if (mapEditor.inputMode.value === 'hold' && pointer.isDown) {
|
||||||
handlePointerDown(pointer)
|
handlePointerDown(pointer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,6 +66,7 @@ function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
addEventListener('keydown', handleKeyDown)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
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_MOVE, handlePointerMove)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
@ -65,4 +78,8 @@ onUnmounted(() => {
|
|||||||
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
mapEditor.reset()
|
mapEditor.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
removeEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -9,7 +9,7 @@ import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composable
|
|||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { TileStorage } from '@/storage/storages'
|
import { TileStorage } from '@/storage/storages'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
|
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
import Tileset = Phaser.Tilemaps.Tileset
|
import Tileset = Phaser.Tilemaps.Tileset
|
||||||
|
|
||||||
@ -22,27 +22,25 @@ const tileStorage = new TileStorage()
|
|||||||
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
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
|
//Record of commands
|
||||||
let commandStack: EditorCommand[] = []
|
let commandStack: EditorCommand[] = []
|
||||||
let currentCommand: EditorCommand | null = null
|
let currentCommand: EditorCommand | null = null
|
||||||
|
let commandIndex = ref(0)
|
||||||
type EditorCommand = {
|
let originTiles: string[][] = []
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTileMap() {
|
function createTileMap() {
|
||||||
const mapData = new Phaser.Tilemaps.MapData({
|
const mapData = new Phaser.Tilemaps.MapData({
|
||||||
@ -93,17 +91,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
|||||||
// Place tile
|
// Place tile
|
||||||
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value)
|
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value)
|
||||||
|
|
||||||
if (!currentCommand) {
|
createCommandUpdate(tile.x, tile.y, mapEditor.selectedTile.value, 'draw')
|
||||||
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust mapEditorStore.map.tiles
|
// Adjust mapEditorStore.map.tiles
|
||||||
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value
|
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value
|
||||||
@ -122,17 +110,7 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
// Place tile
|
// Place tile
|
||||||
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
|
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
|
||||||
|
|
||||||
if (!currentCommand) {
|
createCommandUpdate(tile.x, tile.y, 'blank_tile', 'erase')
|
||||||
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust mapEditorStore.map.tiles
|
// Adjust mapEditorStore.map.tiles
|
||||||
map.tiles[tile.y][tile.x] = 'blank_tile'
|
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])
|
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) {
|
function handlePointer(pointer: Phaser.Input.Pointer) {
|
||||||
if (!tileMap.value || !tileLayer.value) return
|
if (!tileMap.value || !tileLayer.value) return
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
// 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
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey) return
|
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 () => {
|
onMounted(async () => {
|
||||||
if (!mapEditor.currentMap.value) return
|
if (!mapEditor.currentMap.value) return
|
||||||
|
const mapState = mapEditor.currentMap.value
|
||||||
|
|
||||||
|
//Clone
|
||||||
|
originTiles = cloneArray(mapEditor.currentMap.value.tiles)
|
||||||
|
|
||||||
tileMap.value = createTileMap()
|
tileMap.value = createTileMap()
|
||||||
tileLayer.value = await createTileLayer(tileMap.value)
|
tileLayer.value = await createTileLayer(tileMap.value)
|
||||||
|
|
||||||
// First fill the entire map with blank tiles using current map dimensions
|
setLayerTiles(tileMap.value, tileLayer.value, mapState.tiles)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
@ -169,6 +169,8 @@ function initKeyShortcuts(event: KeyboardEvent) {
|
|||||||
// prevent if focused on composables
|
// prevent if focused on composables
|
||||||
if (document.activeElement?.tagName === 'INPUT') return
|
if (document.activeElement?.tagName === 'INPUT') return
|
||||||
|
|
||||||
|
if (event.ctrlKey) return
|
||||||
|
|
||||||
const keyActions: { [key: string]: string } = {
|
const keyActions: { [key: string]: string } = {
|
||||||
m: 'move',
|
m: 'move',
|
||||||
p: 'pencil',
|
p: 'pencil',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user