From 0c450b24ed1c36a9047a30cb6f45d063395c6a1f Mon Sep 17 00:00:00 2001 From: Andrei <amborn02@gmail.com> Date: Sat, 8 Feb 2025 15:07:21 -0600 Subject: [PATCH] Teleport modal restored, and expanded undo/redo to include all placed and erase edits across each map element type (map object advanced actions WIP) --- src/components/gameMaster/mapEditor/Map.vue | 115 ++++++++++++-- .../mapEditor/mapPartials/MapEventTiles.vue | 64 +++++++- .../mapEditor/mapPartials/MapTiles.vue | 145 ++++++------------ .../mapPartials/PlacedMapObjects.vue | 25 ++- .../gameMaster/mapEditor/partials/Toolbar.vue | 1 + src/components/screens/MapEditor.vue | 12 +- src/services/mapService.ts | 5 + 7 files changed, 244 insertions(+), 123 deletions(-) diff --git a/src/components/gameMaster/mapEditor/Map.vue b/src/components/gameMaster/mapEditor/Map.vue index 2a926ae..7acceb7 100644 --- a/src/components/gameMaster/mapEditor/Map.vue +++ b/src/components/gameMaster/mapEditor/Map.vue @@ -1,30 +1,109 @@ <template> - <MapTiles ref="mapTiles" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer /> - <PlacedMapObjects ref="mapObjects" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer /> - <MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap /> + <MapTiles ref="mapTiles" @createCommand="addCommand" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer /> + <PlacedMapObjects ref="mapObjects" @createCommand="addCommand" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer /> + <MapEventTiles ref="eventTiles" @createCommand="addCommand" v-if="tileMap" :tileMap /> </template> <script setup lang="ts"> + import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue' import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue' import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue' import { useMapEditorComposable } from '@/composables/useMapEditorComposable' -import { createTileLayer, createTileMap } from '@/services/mapService' +import { cloneArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService' import { TileStorage } from '@/storage/storages' import { useScene } from 'phavuer' -import { onBeforeUnmount, onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue' + +import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue' +import type { MapEventTile, PlacedMapObject as PlacedMapObjectT } from '@/application/types' const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() const mapEditor = useMapEditorComposable() - const scene = useScene() const mapTiles = useTemplateRef('mapTiles') const mapObjects = useTemplateRef('mapObjects') const eventTiles = useTemplateRef('eventTiles') +//Record of commands +let commandStack: EditorCommand[] = [] +let commandIndex = ref(0) + +let originTiles: string[][] = [] +let originEventTiles: MapEventTile[] = [] +let originObjects: PlacedMapObjectT[] = [] + +//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' +} + +function applyCommands(tiles: any[], ...commands: EditorCommand[]): any[] { + let tileVersion = cloneArray(tiles) + for (let command of commands) { + tileVersion = command.apply(tileVersion) + } + return tileVersion +} + +watch(() => commandIndex.value!, (val) => { + if (val !== undefined) { + update(commandStack.slice(0, val)) + } +}) + +function update(commands: EditorCommand[]) { + 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') + + let modifiedTiles = applyCommands(originTiles, ...tileCommands) + placeTiles(tileMap.value!, tileMapLayer.value!, modifiedTiles) + + mapEditor.currentMap.value.tiles = modifiedTiles + mapEditor.currentMap.value.mapEventTiles = applyCommands(originEventTiles, ...eventTileCommands) + mapEditor.currentMap.value.placedMapObjects = applyCommands(originObjects, ...objectCommands) +} + +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() { + if (commandIndex.value > 0) { + commandIndex.value-- + } +} + +function redo() { + if (commandIndex.value <= 9 && commandIndex.value <= commandStack.length) { + commandIndex.value++ + } +} + function handlePointerDown(pointer: Phaser.Input.Pointer) { if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return @@ -54,12 +133,12 @@ function handlePointerDown(pointer: Phaser.Input.Pointer) { function handleKeyDown(event: KeyboardEvent) { //CTRL+Y if (event.key === 'y' && event.ctrlKey) { - mapTiles.value!.redo() + redo() } //CTRL+Z if (event.key === 'z' && event.ctrlKey) { - mapTiles.value!.undo() + undo() } } @@ -70,8 +149,19 @@ function handlePointerMove(pointer: Phaser.Input.Pointer) { } function handlePointerUp(pointer: Phaser.Input.Pointer) { - if (mapEditor.drawMode.value === 'tile') { - mapTiles.value?.finalizeCommand() + switch(mapEditor.drawMode.value) { + case 'tile': + mapTiles.value!.finalizeCommand() + break + case 'map_object': + mapObjects.value!.finalizeCommand() + break + case 'teleport': + eventTiles.value!.finalizeCommand() + break + case 'blocking tile': + eventTiles.value!.finalizeCommand() + break } } @@ -79,6 +169,11 @@ onMounted(async () => { let mapValue = mapEditor.currentMap.value if (!mapValue) return + //Clone + originTiles = cloneArray(mapValue.tiles) + originObjects = cloneArray(mapValue.placedMapObjects) + originEventTiles = cloneArray(mapValue.mapEventTiles) + 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 40fdac3..4dc7812 100644 --- a/src/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue +++ b/src/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue @@ -3,22 +3,72 @@ </template> <script setup lang="ts"> + import { MapEventTileType, type MapEventTile, type Map as MapT, type UUID } from '@/application/types' import { uuidv4 } from '@/application/utilities' import { useMapEditorComposable } from '@/composables/useMapEditorComposable' -import { getTile, tileToWorldX, tileToWorldY } from '@/services/mapService' +import { cloneArray, getTile, tileToWorldX, tileToWorldY } from '@/services/mapService' import { Image } from 'phavuer' -import { shallowRef } from 'vue' +import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue' const mapEditor = useMapEditorComposable() -defineExpose({ handlePointer }) +defineExpose({ handlePointer, finalizeCommand }) + +const emit = defineEmits(['createCommand']) const props = defineProps<{ tileMap: Phaser.Tilemaps.Tilemap }>() -const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() + +// *** COMMAND STATE *** + +let currentCommand: EventTileCommand | null = null + +class EventTileCommand implements EditorCommand { + public operation: 'draw' | 'erase' = 'draw' + public type: 'event_tile' = 'event_tile' + public affectedTiles: MapEventTile[] = [] + + apply(elements: MapEventTile[]) { + let tileVersion = cloneArray(elements) as MapEventTile[] + if (this.operation === 'draw') { + tileVersion = tileVersion.concat(this.affectedTiles) + } + else if (this.operation === 'erase') { + tileVersion = tileVersion.filter((v) => !this.affectedTiles.includes(v)) + } + return tileVersion + } + + constructor(operation: 'draw' | 'erase') { + this.operation = operation + } +} + +function createCommandUpdate(tile: MapEventTile, operation: 'draw' | 'erase') { + if (!currentCommand) { + currentCommand = new EventTileCommand(operation) + } + + //If position is already in, do not proceed + for (const priorTile of currentCommand.affectedTiles) { + if (priorTile.positionX === tile.positionX && priorTile.positionY == tile.positionY) return + } + + currentCommand.affectedTiles.push(tile) +} + +function finalizeCommand() { + if (!currentCommand) return + emit('createCommand', currentCommand) + currentCommand = null +} + + +// *** HANDLERS *** + function getImageProps(tile: MapEventTile) { return { @@ -44,7 +94,7 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) { const newEventTile = { id: uuidv4() as UUID, mapId: map.id, - map: map.id, + map: map, type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT, positionX: tile.x, positionY: tile.y, @@ -59,6 +109,8 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) { : undefined } + createCommandUpdate(newEventTile, 'draw') + map.mapEventTiles.push(newEventTile) } @@ -77,6 +129,8 @@ function erase(pointer: Phaser.Input.Pointer, map: MapT) { else return } + createCommandUpdate(existingEventTile, 'erase') + // Remove existing event tile map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id) } diff --git a/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue b/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue index 755b3ef..671e8b0 100644 --- a/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue +++ b/src/components/gameMaster/mapEditor/mapPartials/MapTiles.vue @@ -5,57 +5,70 @@ <script setup lang="ts"> import Controls from '@/components/utilities/Controls.vue' import { useMapEditorComposable } from '@/composables/useMapEditorComposable' -import { createTileArray, getTile, placeTile, placeTiles } from '@/services/mapService' +import { cloneArray, createTileArray, getTile, placeTile, placeTiles } from '@/services/mapService' import { onMounted, ref, watch } from 'vue' +import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue' const mapEditor = useMapEditorComposable() -defineExpose({ handlePointer, finalizeCommand, undo, redo }) +defineExpose({ handlePointer, finalizeCommand }) + +const emit = defineEmits(['createCommand']) const props = defineProps<{ tileMap: Phaser.Tilemaps.Tilemap tileMapLayer: Phaser.Tilemaps.TilemapLayer }>() -class EditorCommand { + +// *** COMMAND STATE *** + +let currentCommand: TileCommand | null = null + +class TileCommand implements EditorCommand { public operation: 'draw' | 'erase' = 'draw' + public type: 'tile' = 'tile' public tileName: string = 'blank_tile' - public affectedTiles: number[][] + 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 + } + return tileVersion + } 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 -let commandIndex = ref(0) -let originTiles: string[][] = [] +function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase') { + if (!currentCommand) { + currentCommand = new TileCommand(operation, tileName) + } -function pencil(pointer: Phaser.Input.Pointer) { - let map = mapEditor.currentMap.value - if (!map) return + //If position is already in, do not proceed + for (const vec of currentCommand.affectedTiles) { + if (vec[0] === x && vec[1] === y) return + } - // Check if there is a selected tile - if (!mapEditor.selectedTile.value) return - - // Check if there is a tile - const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY) - if (!tile) return - - // Place tile - placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, mapEditor.selectedTile.value) - - createCommandUpdate(tile.x, tile.y, mapEditor.selectedTile.value, 'draw') - - // Adjust mapEditorStore.map.tiles - map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value + currentCommand.affectedTiles.push([x, y]) } -function eraser(pointer: Phaser.Input.Pointer) { +function finalizeCommand() { + if (!currentCommand) return + emit('createCommand', currentCommand) + currentCommand = null +} + + +// *** HANDLERS *** + + +function draw(pointer: Phaser.Input.Pointer, tileName: string) { let map = mapEditor.currentMap.value if (!map) return @@ -64,12 +77,12 @@ function eraser(pointer: Phaser.Input.Pointer) { if (!tile) return // Place tile - placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, 'blank_tile') + placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, tileName) - createCommandUpdate(tile.x, tile.y, 'blank_tile', 'erase') + createCommandUpdate(tile.x, tile.y, tileName, tileName === 'blank_tile' ? 'erase': 'draw') // Adjust mapEditorStore.map.tiles - map.tiles[tile.y][tile.x] = 'blank_tile' + map.tiles[tile.y][tile.x] = tileName } function paint(pointer: Phaser.Input.Pointer) { @@ -113,10 +126,10 @@ function handlePointer(pointer: Phaser.Input.Pointer) { // Check if draw mode is tile switch (mapEditor.tool.value) { case 'pencil': - pencil(pointer) + draw(pointer, mapEditor.selectedTile.value!) break case 'eraser': - eraser(pointer) + draw(pointer, 'blank_tile') break case 'paint': paint(pointer) @@ -124,70 +137,9 @@ 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 - } +// *** LIFECYCLE *** - 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 (!mapEditor.currentMap.value) return - - let indexedCommands = commandStack.slice(0, commandIndex.value) - let modifiedTiles = applyCommands(originTiles, ...indexedCommands) - - placeTiles(props.tileMap, props.tileMapLayer, 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.value, @@ -205,9 +157,6 @@ onMounted(async () => { if (!mapEditor.currentMap.value) return const mapState = mapEditor.currentMap.value - //Clone - originTiles = cloneArray(mapState.tiles) - placeTiles(props.tileMap, props.tileMapLayer, mapState.tiles) }) </script> diff --git a/src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue b/src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue index 515d79f..278a799 100644 --- a/src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue +++ b/src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue @@ -16,7 +16,7 @@ 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 { getTile } from '@/services/mapService' +import { cloneArray, getTile } from '@/services/mapService' import { useScene } from 'phavuer' import { computed, onMounted, onUnmounted, ref } from 'vue' @@ -27,7 +27,9 @@ const scene = useScene() const mapEditor = useMapEditorComposable() const map = computed(() => mapEditor.currentMap.value!) -defineExpose({ handlePointer }) +defineExpose({ handlePointer, finalizeCommand }) + +const emit = defineEmits(['createCommand']) const props = defineProps<{ tileMap: Tilemap @@ -71,6 +73,9 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) { // Add new object to mapObjects map.placedMapObjects.push(newPlacedMapObject) + + createCommandUpdate(newPlacedMapObject, 'place') + mapEditor.selectedPlacedObject.value = newPlacedMapObject } @@ -79,6 +84,8 @@ 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) } @@ -102,11 +109,15 @@ 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 } @@ -116,6 +127,9 @@ function moveMapObject(id: string, map: MapT) { function handlePointerUp() { 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() } scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp) @@ -128,6 +142,13 @@ function rotatePlacedMapObject(id: string, map: MapT) { 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) mapEditor.selectedPlacedObject.value = null } diff --git a/src/components/gameMaster/mapEditor/partials/Toolbar.vue b/src/components/gameMaster/mapEditor/partials/Toolbar.vue index 78c8e73..42a5843 100644 --- a/src/components/gameMaster/mapEditor/partials/Toolbar.vue +++ b/src/components/gameMaster/mapEditor/partials/Toolbar.vue @@ -150,6 +150,7 @@ function handleClick(tool: string) { mapEditor.setTool(tool) selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false + } function cycleToolMode(tool: 'pencil' | 'eraser') { diff --git a/src/components/screens/MapEditor.vue b/src/components/screens/MapEditor.vue index 0f5bbdf..71cef1e 100644 --- a/src/components/screens/MapEditor.vue +++ b/src/components/screens/MapEditor.vue @@ -34,6 +34,7 @@ import { MapStorage } from '@/storage/storages' import { useGameStore } from '@/stores/gameStore' import { Game, Scene } from 'phavuer' import { ref, useTemplateRef } from 'vue' +import teleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue' const mapStorage = new MapStorage() const mapEditor = useMapEditorComposable() @@ -41,6 +42,7 @@ const gameStore = useGameStore() const mapModal = useTemplateRef('mapModal') const mapSettingsModal = useTemplateRef('mapSettingsModal') +const teleportSettings = useTemplateRef('teleportModal') const isLoaded = ref(false) @@ -86,15 +88,9 @@ function save() { if (!currentMap) return const data = { + ...currentMap, mapId: currentMap.id, - name: currentMap.name, - width: currentMap.width, - height: currentMap.height, - tiles: currentMap.tiles, - pvp: currentMap.pvp, - mapEffects: currentMap.mapEffects, - mapEventTiles: currentMap.mapEventTiles, - placedMapObjects: currentMap.placedMapObjects.map(({ id, mapObject, isRotated, positionX, positionY }) => ({ id, mapObject, isRotated, positionX, positionY })) ?? [] + placedMapObjects: currentMap.placedMapObjects.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? [] } gameStore.connection?.emit('gm:map:update', data, (response: MapT) => { diff --git a/src/services/mapService.ts b/src/services/mapService.ts index c07f9ee..fd4d1bf 100644 --- a/src/services/mapService.ts +++ b/src/services/mapService.ts @@ -151,3 +151,8 @@ export function createTileLayer(tileMap: Phaser.Tilemaps.Tilemap, tilesArray: st return layer } + +//Recursive Array Clone +export function cloneArray(arr: any[]): any[] { + return arr.map((item) => (item instanceof Array ? cloneArray(item) : item)) +} \ No newline at end of file