1
0
forked from noxious/client

Teleport modal restored, and expanded undo/redo to include all placed and erase edits across each map element type (map object advanced actions WIP)

This commit is contained in:
Andrei 2025-02-08 15:07:21 -06:00
parent f258c65403
commit ca307d4de3
7 changed files with 316 additions and 126 deletions

View File

@ -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)

View File

@ -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)
}
@ -76,6 +128,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)
}

View File

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

View File

@ -4,29 +4,98 @@
</template>
<script setup lang="ts">
import type { MapObject, Map as MapT, PlacedMapObject as PlacedMapObjectT, UUID } from '@/application/types'
import type {
MapObject,
Map as MapT,
PlacedMapObject as PlacedMapObjectT,
UUID,
MapEventTile
} 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 { getTile } from '@/services/mapService'
import { cloneArray, 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)
const map = computed(() => mapEditor.currentMap.value!)
defineExpose({ handlePointer })
defineExpose({ handlePointer, finalizeCommand })
const emit = defineEmits(['createCommand'])
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
@ -47,6 +116,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
}
@ -55,6 +127,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)
}
@ -78,11 +152,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
}
@ -92,6 +170,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)
@ -104,6 +185,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
}

View File

@ -93,7 +93,7 @@ import { onBeforeUnmount, onMounted, ref } from 'vue'
const mapEditor = useMapEditorComposable()
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-tile-list', 'open-map-object-list', 'close-lists'])
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'open-teleport-settings', 'close-editor', 'open-tile-list', 'open-map-object-list', 'close-lists'])
// track when clicked outside of toolbar items
const toolbar = ref(null)
@ -115,6 +115,7 @@ function setDrawMode(value: string) {
emit('close-lists')
if (value === 'tile') emit('open-tile-list')
if (value === 'map_object') emit('open-map-object-list')
if (value === 'teleport') emit('open-teleport-settings')
}
mapEditor.setDrawMode(value)
@ -155,6 +156,7 @@ function handleClick(tool: string) {
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
}
function cycleToolMode(tool: 'pencil' | 'eraser') {

View File

@ -11,6 +11,7 @@
@clear="clear"
@open-maps="mapModal?.open"
@open-settings="mapSettingsModal?.open"
@open-teleport-settings="teleportModal?.open"
@close-editor="mapEditor.toggleActive"
@close-lists="tileList?.close"
@closeLists="objectList?.close"
@ -45,6 +46,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()
@ -54,6 +56,7 @@ const mapModal = useTemplateRef('mapModal')
const tileList = useTemplateRef('tileList')
const objectList = useTemplateRef('objectList')
const mapSettingsModal = useTemplateRef('mapSettingsModal')
const teleportSettings = useTemplateRef('teleportModal')
const isLoaded = ref(false)
@ -96,14 +99,8 @@ 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, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
}

View File

@ -147,3 +147,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))
}