From af088d92e13b3597e202cb85a6ab52c6374efad6 Mon Sep 17 00:00:00 2001 From: gguio <109200692+gguio@users.noreply.github.com> Date: Thu, 28 Nov 2024 00:07:28 +0400 Subject: [PATCH] feat: minimap and full screen map (disabled by default) (#147) --- package.json | 1 + pnpm-lock.yaml | 35 +- prismarine-viewer/viewer/lib/mesher/models.ts | 21 +- prismarine-viewer/viewer/lib/mesher/shared.ts | 4 +- .../viewer/lib/mesher/test/tests.test.ts | 3 +- .../viewer/lib/mesher/worldConstants.ts | 1 + .../viewer/lib/worldrendererCommon.ts | 16 +- src/controls.ts | 11 +- src/globalState.ts | 2 + src/optionsGuiScheme.tsx | 13 + src/optionsStorage.ts | 2 + src/react/Fullmap.css | 13 + src/react/Fullmap.tsx | 512 +++++++++++++++ src/react/Input.tsx | 4 +- src/react/Minimap.stories.tsx | 74 +++ src/react/Minimap.tsx | 175 +++++ src/react/MinimapDrawer.ts | 324 ++++++++++ src/react/MinimapProvider.tsx | 610 ++++++++++++++++++ src/react/utilsApp.ts | 4 +- src/reactUi.tsx | 11 +- tsconfig.json | 2 +- 21 files changed, 1799 insertions(+), 39 deletions(-) create mode 100644 prismarine-viewer/viewer/lib/mesher/worldConstants.ts create mode 100644 src/react/Fullmap.css create mode 100644 src/react/Fullmap.tsx create mode 100644 src/react/Minimap.stories.tsx create mode 100644 src/react/Minimap.tsx create mode 100644 src/react/MinimapDrawer.ts create mode 100644 src/react/MinimapProvider.tsx diff --git a/package.json b/package.json index ee2126caf..1a7258250 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "react-dom": "^18.2.0", "react-select": "^5.8.0", "react-transition-group": "^4.4.5", + "react-zoom-pan-pinch": "3.4.4", "remark": "^15.0.1", "sanitize-filename": "^1.6.3", "skinview3d": "^3.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b0e4ae3a..ff67d9d06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,6 +193,9 @@ importers: react-transition-group: specifier: ^4.4.5 version: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-zoom-pan-pinch: + specifier: 3.4.4 + version: 3.4.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) remark: specifier: ^15.0.1 version: 15.0.1 @@ -7927,6 +7930,13 @@ packages: react: ^18.2.0 react-dom: ^16.8.0 || ^17.0.0 + react-zoom-pan-pinch@3.4.4: + resolution: {integrity: sha512-lGTu7D9lQpYEQ6sH+NSlLA7gicgKRW8j+D/4HO1AbSV2POvKRFzdWQ8eI0r3xmOsl4dYQcY+teV6MhULeg1xBw==} + engines: {node: '>=8', npm: '>=5'} + peerDependencies: + react: ^18.2.0 + react-dom: '*' + react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -9849,7 +9859,7 @@ snapshots: '@babel/core': 7.22.11 '@babel/helper-compilation-targets': 7.22.10 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.4 transitivePeerDependencies: @@ -10576,7 +10586,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.13 '@babel/types': 7.23.0 - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -13135,7 +13145,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.5.4) '@typescript-eslint/utils': 6.1.0(eslint@8.50.0)(typescript@5.5.4) - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.50.0 ts-api-utils: 1.0.3(typescript@5.5.4) optionalDependencies: @@ -13153,7 +13163,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.1.0 '@typescript-eslint/visitor-keys': 6.1.0 - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.0 @@ -13167,7 +13177,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.7.3 '@typescript-eslint/visitor-keys': 6.7.3 - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.0 @@ -13181,7 +13191,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.0.0 '@typescript-eslint/visitor-keys': 8.0.0 - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14908,7 +14918,7 @@ snapshots: detect-port@1.5.1: dependencies: address: 1.2.2 - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16561,7 +16571,7 @@ snapshots: https-proxy-agent@4.0.0: dependencies: agent-base: 5.1.1 - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16576,7 +16586,7 @@ snapshots: https-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -17650,7 +17660,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 @@ -19175,6 +19185,11 @@ snapshots: ts-easing: 0.2.0 tslib: 2.6.2 + react-zoom-pan-pinch@3.4.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react@18.2.0: dependencies: loose-envify: 1.4.0 diff --git a/prismarine-viewer/viewer/lib/mesher/models.ts b/prismarine-viewer/viewer/lib/mesher/models.ts index 91c0cf40f..8baec4dc7 100644 --- a/prismarine-viewer/viewer/lib/mesher/models.ts +++ b/prismarine-viewer/viewer/lib/mesher/models.ts @@ -4,7 +4,9 @@ import legacyJson from '../../../../src/preflatMap.json' import { BlockType } from '../../../examples/shared' import { World, BlockModelPartsResolved, WorldBlock as Block } from './world' import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon' -import { MesherGeometryOutput } from './shared' +import { INVISIBLE_BLOCKS } from './worldConstants' +import { MesherGeometryOutput, HighestBlockInfo } from './shared' + let blockProvider: WorldBlockProvider @@ -439,8 +441,6 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: } } -const invisibleBlocks = new Set(['air', 'cave_air', 'void_air', 'barrier']) - const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' let unknownBlockModel: BlockModelPartsResolved @@ -464,7 +464,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { // todo this can be removed here signs: {}, // isFull: true, - highestBlocks: {}, // todo migrate to map for 2% boost perf + highestBlocks: new Map([]), hadErrors: false, blocksCount: 0 } @@ -474,16 +474,13 @@ export function getSectionGeometry (sx, sy, sz, world: World) { for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) { for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) { let block = world.getBlock(cursor, blockProvider, attr)! - if (!invisibleBlocks.has(block.name)) { - const highest = attr.highestBlocks[`${cursor.x},${cursor.z}`] + if (!INVISIBLE_BLOCKS.has(block.name)) { + const highest = attr.highestBlocks.get(`${cursor.x},${cursor.z}`) if (!highest || highest.y < cursor.y) { - attr.highestBlocks[`${cursor.x},${cursor.z}`] = { - y: cursor.y, - name: block.name - } + attr.highestBlocks.set(`${cursor.x},${cursor.z}`, { y: cursor.y, stateId: block.stateId, biomeId: block.biome.id }) } } - if (invisibleBlocks.has(block.name)) continue + if (INVISIBLE_BLOCKS.has(block.name)) continue if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) { const key = `${cursor.x},${cursor.y},${cursor.z}` const props: any = block.getProperties() @@ -531,7 +528,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr) attr.blocksCount++ } - if (block.name !== 'water' && block.name !== 'lava' && !invisibleBlocks.has(block.name)) { + if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) { // cache let { models } = block diff --git a/prismarine-viewer/viewer/lib/mesher/shared.ts b/prismarine-viewer/viewer/lib/mesher/shared.ts index b08a2bd76..57d7d6a44 100644 --- a/prismarine-viewer/viewer/lib/mesher/shared.ts +++ b/prismarine-viewer/viewer/lib/mesher/shared.ts @@ -32,7 +32,9 @@ export type MesherGeometryOutput = { tiles: Record, signs: Record, // isFull: boolean - highestBlocks: Record + highestBlocks: Map hadErrors: boolean blocksCount: number } + +export type HighestBlockInfo = { y: number, stateId: number | undefined, biomeId: number | undefined } diff --git a/prismarine-viewer/viewer/lib/mesher/test/tests.test.ts b/prismarine-viewer/viewer/lib/mesher/test/tests.test.ts index e1d51f223..4e322bcf6 100644 --- a/prismarine-viewer/viewer/lib/mesher/test/tests.test.ts +++ b/prismarine-viewer/viewer/lib/mesher/test/tests.test.ts @@ -1,5 +1,6 @@ import { test, expect } from 'vitest' import supportedVersions from '../../../../../src/supportedVersions.mjs' +import { INVISIBLE_BLOCKS } from '../worldConstants' import { setup } from './mesherTester' const lastVersion = supportedVersions.at(-1) @@ -16,7 +17,7 @@ const addPositions = [ test('Known blocks are not rendered', () => { const { mesherWorld, getGeometry, pos, mcData } = setup(lastVersion, addPositions as any) - const ignoreAsExpected = new Set(['air', 'cave_air', 'void_air', 'barrier', 'water', 'lava', 'moving_piston', 'light']) + const ignoreAsExpected = new Set([...INVISIBLE_BLOCKS, 'water', 'lava', 'moving_piston', 'light']) let time = 0 let times = 0 diff --git a/prismarine-viewer/viewer/lib/mesher/worldConstants.ts b/prismarine-viewer/viewer/lib/mesher/worldConstants.ts new file mode 100644 index 000000000..75f08e89e --- /dev/null +++ b/prismarine-viewer/viewer/lib/mesher/worldConstants.ts @@ -0,0 +1 @@ +export const INVISIBLE_BLOCKS = new Set(['air', 'void_air', 'cave_air', 'barrier']) diff --git a/prismarine-viewer/viewer/lib/worldrendererCommon.ts b/prismarine-viewer/viewer/lib/worldrendererCommon.ts index 65546de4f..7beb0a924 100644 --- a/prismarine-viewer/viewer/lib/worldrendererCommon.ts +++ b/prismarine-viewer/viewer/lib/worldrendererCommon.ts @@ -14,7 +14,7 @@ import TypedEmitter from 'typed-emitter' import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs' import { toMajorVersion } from '../../../src/utils' import { buildCleanupDecorator } from './cleanupDecorator' -import { MesherGeometryOutput, defaultMesherConfig } from './mesher/shared' +import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput } from './mesher/shared' import { chunkPos } from './simpleUtils' import { HandItemBlock } from './holdingBlock' import { updateStatText } from './ui/newStats' @@ -62,6 +62,7 @@ export abstract class WorldRendererCommon dirty (pos: Vec3, value: boolean): void update (/* pos: Vec3, value: boolean */): void textureDownloaded (): void + chunkFinished (key: string): void }> customTexturesDataUrl = undefined as string | undefined @worldCleanup() @@ -81,7 +82,7 @@ export abstract class WorldRendererCommon handleResize = () => { } mesherConfig = defaultMesherConfig camera: THREE.PerspectiveCamera - highestBlocks: Record = {} + highestBlocks = new Map() blockstatesModels: any customBlockStates: Record | undefined customModels: Record | undefined @@ -134,10 +135,10 @@ export abstract class WorldRendererCommon this.handleWorkerMessage(data) if (data.type === 'geometry') { const geometry = data.geometry as MesherGeometryOutput - for (const key in geometry.highestBlocks) { - const highest = geometry.highestBlocks[key] - if (!this.highestBlocks[key] || this.highestBlocks[key].y < highest.y) { - this.highestBlocks[key] = highest + for (const [key, highest] of geometry.highestBlocks.entries()) { + const currHighest = this.highestBlocks.get(key) + if (!currHighest || currHighest.y < highest.y) { + this.highestBlocks.set(key, highest) } } const chunkCoords = data.key.split(',').map(Number) @@ -156,6 +157,7 @@ export abstract class WorldRendererCommon return x === chunkCoords[0] && z === chunkCoords[2] })) { this.finishedChunks[`${chunkCoords[0]},${chunkCoords[2]}`] = true + this.renderUpdateEmitter.emit(`chunkFinished`, `${chunkCoords[0] / 16},${chunkCoords[2] / 16}`) } } if (this.sectionsOutstanding.size === 0) { @@ -363,7 +365,7 @@ export abstract class WorldRendererCommon const endZ = Math.ceil((z + 1) / 16) * 16 for (let x = startX; x < endX; x += 16) { for (let z = startZ; z < endZ; z += 16) { - delete this.highestBlocks[`${x},${z}`] + this.highestBlocks.delete(`${x},${z}`) } } } diff --git a/src/controls.ts b/src/controls.ts index 602ea81d2..3b41c06f1 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -7,7 +7,7 @@ import { ControMax } from 'contro-max/build/controMax' import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types' import { stringStartsWith } from 'contro-max/build/stringUtils' import { UserOverrideCommand, UserOverridesConfig } from 'contro-max/build/types/store' -import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, loadedGameState } from './globalState' +import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, loadedGameState, hideModal } from './globalState' import { goFullscreen, pointerLock, reloadChunks } from './utils' import { options } from './optionsStorage' import { openPlayerInventory } from './inventoryWindows' @@ -54,6 +54,7 @@ export const contro = new ControMax({ ui: { toggleFullscreen: ['F11'], back: [null/* 'Escape' */, 'B'], + toggleMap: ['KeyM'], leftClick: [null, 'A'], rightClick: [null, 'Y'], speedupCursor: [null, 'Left Stick'], @@ -424,6 +425,14 @@ contro.on('trigger', ({ command }) => { if (command === 'ui.toggleFullscreen') { void goFullscreen(true) } + + if (command === 'ui.toggleMap') { + if (activeModalStack.at(-1)?.reactType === 'full-map') { + hideModal({ reactType: 'full-map' }) + } else { + showModal({ reactType: 'full-map' }) + } + } }) contro.on('release', ({ command }) => { diff --git a/src/globalState.ts b/src/globalState.ts index cefa78103..90e3e359c 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -1,6 +1,7 @@ //@ts-check import { proxy, ref, subscribe } from 'valtio' +import { WorldWarp } from 'flying-squid/dist/lib/modules/warps' import { pointerLock } from './utils' import type { OptionsGroupType } from './optionsGuiScheme' @@ -153,6 +154,7 @@ export const gameAdditionalState = proxy({ isFlying: false, isSprinting: false, isSneaking: false, + warps: [] as WorldWarp[] }) window.gameAdditionalState = gameAdditionalState diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 434ad4846..94d6cdf8a 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -250,6 +250,19 @@ export const guiOptionsScheme: { ], }, }, + { + custom () { + return Map + }, + showMinimap: { + text: 'Enable Minimap', + values: [ + 'always', + 'singleplayer', + 'never' + ], + }, + }, { custom () { return Experimental diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 53deb927f..618d99e88 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -88,6 +88,8 @@ const defaultOptions = { /** Wether to popup sign editor on server action */ autoSignEditor: true, wysiwygSignEditor: 'auto' as 'auto' | 'always' | 'never', + showMinimap: 'never' as 'always' | 'singleplayer' | 'never', + minimapOptimizations: true, displayBossBars: false, // boss bar overlay was removed for some reason, enable safely disabledUiParts: [] as string[], neighborChunkUpdates: true diff --git a/src/react/Fullmap.css b/src/react/Fullmap.css new file mode 100644 index 000000000..5b4b37ce8 --- /dev/null +++ b/src/react/Fullmap.css @@ -0,0 +1,13 @@ + +.map { + width: 70% !important; + height: 80% !important; + border: 1px solid black; +} + +@media (max-width: 500px) { + .map { + width: 100% !important; + height: 100% !important; + } +} diff --git a/src/react/Fullmap.tsx b/src/react/Fullmap.tsx new file mode 100644 index 000000000..69a2c5148 --- /dev/null +++ b/src/react/Fullmap.tsx @@ -0,0 +1,512 @@ +import { Vec3 } from 'vec3' +import { useRef, useEffect, useState, CSSProperties, Dispatch, SetStateAction } from 'react' +import { WorldWarp } from 'flying-squid/dist/lib/modules/warps' +import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch' +import { MinimapDrawer, DrawerAdapter, ChunkInfo } from './MinimapDrawer' +import Button from './Button' +import Input from './Input' +import './Fullmap.css' + + +type FullmapProps = { + toggleFullMap: () => void, + adapter: DrawerAdapter, + drawer: MinimapDrawer | null, + canvasRef: any +} + +export default ({ toggleFullMap, adapter }: FullmapProps) => { + const [grid, setGrid] = useState(() => new Set()) + const zoomRef = useRef(null) + const redrawCell = useRef(false) + const [lastWarpPos, setLastWarpPos] = useState({ x: 0, y: 0, z: 0 }) + const stateRef = useRef({ scale: 1, positionX: 0, positionY: 0 }) + const cells = useRef({ columns: 0, rows: 0 }) + const [isWarpInfoOpened, setIsWarpInfoOpened] = useState(false) + const [initWarp, setInitWarp] = useState(undefined) + const [warpPreview, setWarpPreview] = useState<{ name: string, x: number, z: number, clientX: number, clientY: number } | undefined>(undefined) + + const updateGrid = () => { + const wrapperRect = zoomRef.current?.instance.wrapperComponent?.getBoundingClientRect() + if (!wrapperRect) return + const cellSize = 64 + const columns = Math.ceil(wrapperRect.width / (cellSize * stateRef.current.scale)) + const rows = Math.ceil(wrapperRect.height / (cellSize * stateRef.current.scale)) + cells.current.rows = rows + cells.current.columns = columns + const leftBorder = - Math.floor(stateRef.current.positionX / (stateRef.current.scale * cellSize)) * cellSize + const topBorder = - Math.floor(stateRef.current.positionY / (stateRef.current.scale * cellSize)) * cellSize + const newGrid = new Set() + for (let row = -1; row < rows; row += 1) { + for (let col = -1; col < columns; col += 1) { + const x = leftBorder + col * cellSize + const y = topBorder + row * cellSize + newGrid.add(`${x},${y}`) + } + } + setGrid(newGrid) + } + + useEffect(() => { + adapter.full = true + console.log('[fullmap] set full property to true') + updateGrid() + }, []) + + return
+ {window.screen.width > 500 ?
+ :
+} + + +const MapChunk = ( + { x, y, scale, adapter, worldX, worldZ, setIsWarpInfoOpened, setLastWarpPos, redraw, setInitWarp, setWarpPreview }: + { + x: number, + y: number, + scale: number, + adapter: DrawerAdapter, + worldX: number, + worldZ: number, + setIsWarpInfoOpened: (x: boolean) => void, + setLastWarpPos: (obj: { x: number, y: number, z: number }) => void, + redraw?: boolean + setInitWarp?: (warp: WorldWarp | undefined) => void + setWarpPreview?: (warpInfo) => void + } +) => { + const containerRef = useRef(null) + const drawerRef = useRef(null) + const touchTimer = useRef | null>(null) + const canvasRef = useRef(null) + const [isCanvas, setIsCanvas] = useState(false) + + const longPress = (e) => { + touchTimer.current = setTimeout(() => { + touchTimer.current = null + handleClick(e) + }, 500) + } + + const cancel = () => { + if (touchTimer.current) clearTimeout(touchTimer.current) + } + + const handleClick = (e: MouseEvent | TouchEvent) => { + // console.log('click:', e) + if (!drawerRef.current) return + let clientX: number + let clientY: number + if ('buttons' in e && e.button === 2) { + clientX = e.clientX + clientY = e.clientY + } else if ('changedTouches' in e) { + clientX = (e).changedTouches[0].clientX + clientY = (e).changedTouches[0].clientY + } else { return } + const [x, z] = getXZ(clientX, clientY) + const mapX = Math.floor(x + worldX) + const mapZ = Math.floor(z + worldZ) + const y = adapter.getHighestBlockY(mapX, mapZ) + drawerRef.current.setWarpPosOnClick(new Vec3(mapX, y, mapZ)) + setLastWarpPos(drawerRef.current.lastWarpPos) + const { lastWarpPos } = drawerRef.current + const initWarp = adapter.warps.find(warp => Math.hypot(lastWarpPos.x - warp.x, lastWarpPos.z - warp.z) < 2) + setInitWarp?.(initWarp) + setIsWarpInfoOpened(true) + } + + const getXZ = (clientX: number, clientY: number) => { + const rect = canvasRef.current!.getBoundingClientRect() + const factor = scale * (drawerRef.current?.mapPixel ?? 1) + const x = (clientX - rect.left) / factor + const y = (clientY - rect.top) / factor + return [x, y] + } + + const handleMouseMove = (e: MouseEvent) => { + const [x, z] = getXZ(e.clientX, e.clientY) + const warp = adapter.warps.find(w => Math.hypot(w.x - x - worldX, w.z - z - worldZ) < 2) + setWarpPreview?.( + warp ? { name: warp.name, x: warp.x, z: warp.z, clientX: e.clientX, clientY: e.clientY } : undefined + ) + } + + const handleRedraw = (key?: string, chunk?: ChunkInfo) => { + if (key !== `${worldX / 16},${worldZ / 16}`) return + adapter.mapDrawer.canvas = canvasRef.current! + adapter.mapDrawer.full = true + // console.log('handle redraw:', key) + // if (chunk) { + // drawerRef.current?.chunksStore.set(key, chunk) + // } + if (!adapter.chunksStore.has(key)) { + adapter.chunksStore.set(key, 'requested') + void adapter.loadChunk(key) + return + } + const timeout = setTimeout(() => { + const center = new Vec3(worldX + 8, 0, worldZ + 8) + drawerRef.current!.lastBotPos = center + drawerRef.current?.drawChunk(key) + // drawerRef.current?.drawWarps(center) + // drawerRef.current?.drawPlayerPos(center.x, center.z) + clearTimeout(timeout) + }, 100) + } + + useEffect(() => { + // if (canvasRef.current && !drawerRef.current) { + // drawerRef.current = adapter.mapDrawer + // } else if (canvasRef.current && drawerRef.current) { + // } + if (canvasRef.current) void adapter.drawChunkOnCanvas(`${worldX / 16},${worldZ / 16}`, canvasRef.current) + }, [canvasRef.current]) + + useEffect(() => { + canvasRef.current?.addEventListener('contextmenu', handleClick) + canvasRef.current?.addEventListener('touchstart', longPress) + canvasRef.current?.addEventListener('touchend', cancel) + canvasRef.current?.addEventListener('touchmove', cancel) + canvasRef.current?.addEventListener('mousemove', handleMouseMove) + + return () => { + canvasRef.current?.removeEventListener('contextmenu', handleClick) + canvasRef.current?.removeEventListener('touchstart', longPress) + canvasRef.current?.removeEventListener('touchend', cancel) + canvasRef.current?.removeEventListener('touchmove', cancel) + canvasRef.current?.removeEventListener('mousemove', handleMouseMove) + } + }, [canvasRef.current, scale]) + + useEffect(() => { + // handleRedraw() + }, [drawerRef.current, redraw]) + + useEffect(() => { + const intersectionObserver = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + setIsCanvas(true) + } + } + }) + intersectionObserver.observe(containerRef.current!) + + // adapter.on('chunkReady', handleRedraw) + + return () => { + intersectionObserver.disconnect() + // adapter.off('chunkReady', handleRedraw) + } + }, []) + + return
+ +
+} + +const WarpInfo = ( + { adapter, warpPos, setIsWarpInfoOpened, afterWarpIsSet, initWarp, toggleFullMap }: + { + adapter: DrawerAdapter, + warpPos: { x: number, y: number, z: number }, + setIsWarpInfoOpened: Dispatch>, + afterWarpIsSet?: () => void + initWarp?: WorldWarp, + setInitWarp?: React.Dispatch>, + toggleFullMap?: ({ command }: { command: string }) => void + } +) => { + const [warp, setWarp] = useState(initWarp ?? { + name: '', + x: warpPos?.x ?? 100, + y: warpPos?.y ?? 100, + z: warpPos?.z ?? 100, + color: '', + disabled: false, + world: adapter.world + }) + + const posInputStyle: CSSProperties = { + flexGrow: '1', + } + const fieldCont: CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: '5px' + } + + const updateChunk = () => { + for (let i = -1; i < 2; i += 1) { + for (let j = -1; j < 2; j += 1) { + adapter.emit( + 'chunkReady', + `${Math.floor(warp.x / 16) + j},${Math.floor(warp.z / 16) + i}` + ) + } + } + } + + const tpNow = () => { + adapter.off('updateChunk', tpNow) + } + + const quickTp = () => { + toggleFullMap?.({ command: 'ui.toggleMap' }) + adapter.quickTp?.(warp.x, warp.z) + } + + return
+
500 ? '100%' : '50%', + minWidth: '100px', + maxWidth: '300px', + padding: '20px', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + border: '2px solid black' + }} + > +

Point on the map

+
+
+ Name: +
+ { + if (!e.target) return + setWarp(prev => { return { ...prev, name: e.target.value } }) + }} + autoFocus + /> +
+
+
+ X: +
+ { + if (!e.target) return + setWarp(prev => { return { ...prev, x: Number(e.target.value) } }) + }} + /> +
+ Z: +
+ { + if (!e.target) return + setWarp(prev => { return { ...prev, z: Number(e.target.value) } }) + }} + /> +
+
+
Color:
+ { + if (!e.target) return + setWarp(prev => { return { ...prev, color: e.target.value } }) + }} + rootStyles={{ width: '30px', }} + style={{ left: '0px' }} + /> +
+
+ + { + if (!e.target) return + setWarp(prev => { return { ...prev, disabled: e.target.checked } }) + }} + /> +
+ +
+ + + {initWarp && } +
+
+
+} diff --git a/src/react/Input.tsx b/src/react/Input.tsx index 41dbc7ba8..1da36cc3f 100644 --- a/src/react/Input.tsx +++ b/src/react/Input.tsx @@ -9,10 +9,10 @@ interface Props extends React.ComponentProps<'input'> { validateInput?: (value: string) => CSSProperties | undefined } -export default ({ autoFocus, rootStyles, inputRef, validateInput, ...inputProps }: Props) => { +export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, ...inputProps }: Props) => { const ref = useRef(null!) const [validationStyle, setValidationStyle] = useState({}) - const [value, setValue] = useState(inputProps.value ?? '') + const [value, setValue] = useState(defaultValue ?? '') useEffect(() => { setValue(inputProps.value === '' || inputProps.value ? inputProps.value : value) diff --git a/src/react/Minimap.stories.tsx b/src/react/Minimap.stories.tsx new file mode 100644 index 000000000..5b28a5b99 --- /dev/null +++ b/src/react/Minimap.stories.tsx @@ -0,0 +1,74 @@ +import { Vec3 } from 'vec3' +import type { Meta, StoryObj } from '@storybook/react' +import { WorldWarp } from 'flying-squid/dist/lib/modules/warps' +import { TypedEventEmitter } from 'contro-max/build/typedEventEmitter' +import { useEffect } from 'react' + +import Minimap from './Minimap' +import { DrawerAdapter, MapUpdates } from './MinimapDrawer' + +const meta: Meta = { + component: Minimap, + decorators: [ + (Story, context) => { + + useEffect(() => { + console.log('map updated') + adapter.emit('updateMap') + + }, [context.args['fullMap']]) + + return
+ } + ] +} + +export default meta +type Story = StoryObj + + +class DrawerAdapterImpl extends TypedEventEmitter { + playerPosition: Vec3 + yaw: number + warps: WorldWarp[] + chunksStore: any = {} + full: boolean + + constructor (pos?: Vec3, warps?: WorldWarp[]) { + super() + this.playerPosition = pos ?? new Vec3(0, 0, 0) + this.warps = warps ?? [] as WorldWarp[] + } + + async getHighestBlockColor (x: number, z: number) { + console.log('got color') + return 'green' + } + + getHighestBlockY (x: number, z: number) { + return 0 + } + + setWarp (warp: WorldWarp, remove?: boolean): void { + const index = this.warps.findIndex(w => w.name === warp.name) + if (index === -1) { + this.warps.push(warp) + } else { + this.warps[index] = warp + } + this.emit('updateWarps') + } + + clearChunksStore (x: number, z: number) { } + + async loadChunk (key: string) {} +} + +const adapter = new DrawerAdapterImpl() as any + +export const Primary: Story = { + args: { + adapter, + fullMap: false + }, +} diff --git a/src/react/Minimap.tsx b/src/react/Minimap.tsx new file mode 100644 index 000000000..1dae0344d --- /dev/null +++ b/src/react/Minimap.tsx @@ -0,0 +1,175 @@ +import { useRef, useEffect, useState } from 'react' +import { MinimapDrawer, DrawerAdapter, ChunkInfo } from './MinimapDrawer' +import Fullmap from './Fullmap' + + +export type DisplayMode = 'fullmapOnly' | 'minimapOnly' + +export default ( + { adapter, showMinimap, showFullmap, singleplayer, fullMap, toggleFullMap, displayMode }: + { + adapter: DrawerAdapter, + showMinimap: string, + showFullmap: string, + singleplayer: boolean, + fullMap?: boolean, + toggleFullMap?: ({ command }: { command: string }) => void + displayMode?: DisplayMode + } +) => { + const full = useRef(false) + const canvasTick = useRef(0) + const canvasRef = useRef(null) + const warpsAndPartsCanvasRef = useRef(null) + const playerPosCanvasRef = useRef(null) + const warpsDrawerRef = useRef(null) + const drawerRef = useRef(null) + const playerPosDrawerRef = useRef(null) + const [position, setPosition] = useState({ x: 0, y: 0, z: 0 }) + + const updateMap = () => { + setPosition({ x: adapter.playerPosition.x, y: adapter.playerPosition.y, z: adapter.playerPosition.z }) + if (drawerRef.current) { + if (!full.current) { + rotateMap() + drawerRef.current.draw(adapter.playerPosition) + drawerRef.current.drawPlayerPos() + drawerRef.current.drawWarps() + } + if (canvasTick.current % 300 === 0 && !fullMap) { + if ('requestIdleCallback' in window) { + requestIdleCallback(() => { + drawerRef.current?.clearChunksStore() + }) + } else { + drawerRef.current.clearChunksStore() + } + canvasTick.current = 0 + } + } + canvasTick.current += 1 + } + + const updateWarps = () => { } + + const rotateMap = () => { + if (!drawerRef.current) return + drawerRef.current.canvas.style.transform = `rotate(${adapter.yaw}rad)` + if (!warpsDrawerRef.current) return + warpsDrawerRef.current.canvas.style.transform = `rotate(${adapter.yaw}rad)` + } + + const updateChunkOnMap = (key: string, chunk: ChunkInfo) => { + adapter.chunksStore.set(key, chunk) + } + + useEffect(() => { + if (canvasRef.current && !drawerRef.current) { + drawerRef.current = adapter.mapDrawer + drawerRef.current.canvas = canvasRef.current + // drawerRef.current.adapter.on('chunkReady', updateChunkOnMap) + } else if (canvasRef.current && drawerRef.current) { + drawerRef.current.canvas = canvasRef.current + } + + }, [canvasRef.current]) + + // useEffect(() => { + // if (warpsAndPartsCanvasRef.current && !warpsDrawerRef.current) { + // warpsDrawerRef.current = new MinimapDrawer(warpsAndPartsCanvasRef.current, adapter) + // } else if (warpsAndPartsCanvasRef.current && warpsDrawerRef.current) { + // warpsDrawerRef.current.canvas = warpsAndPartsCanvasRef.current + // } + // }, [warpsAndPartsCanvasRef.current]) + + // useEffect(() => { + // if (playerPosCanvasRef.current && !playerPosDrawerRef.current) { + // playerPosDrawerRef.current = new MinimapDrawer(playerPosCanvasRef.current, adapter) + // } else if (playerPosCanvasRef.current && playerPosDrawerRef.current) { + // playerPosDrawerRef.current.canvas = playerPosCanvasRef.current + // } + // }, [playerPosCanvasRef.current]) + + useEffect(() => { + adapter.on('updateMap', updateMap) + adapter.on('updateWaprs', updateWarps) + + return () => { + adapter.off('updateMap', updateMap) + adapter.off('updateWaprs', updateWarps) + } + }, [adapter]) + + useEffect(() => { + return () => { + // if (drawerRef.current) drawerRef.current.adapter.off('chunkReady', updateChunkOnMap) + } + }, []) + + const displayFullmap = fullMap && displayMode !== 'minimapOnly' && (showFullmap === 'singleplayer' && singleplayer || showFullmap === 'always') + const displayMini = displayMode !== 'fullmapOnly' && (showMinimap === 'singleplayer' && singleplayer || showMinimap === 'always') + return displayFullmap + ? { + toggleFullMap?.({ command: 'ui.toggleMap' }) + }} + adapter={adapter} + drawer={drawerRef.current} + canvasRef={canvasRef} + /> + : displayMini + ?
{ + toggleFullMap?.({ command: 'ui.toggleMap' }) + }} + > + + + +
+ {position.x.toFixed(2)} {position.y.toFixed(2)} {position.z.toFixed(2)} +
+
: null +} diff --git a/src/react/MinimapDrawer.ts b/src/react/MinimapDrawer.ts new file mode 100644 index 000000000..935bdd4bb --- /dev/null +++ b/src/react/MinimapDrawer.ts @@ -0,0 +1,324 @@ +import { Vec3 } from 'vec3' +import { TypedEventEmitter } from 'contro-max/build/typedEventEmitter' +import { WorldWarp } from 'flying-squid/dist/lib/modules/warps' +import { Chunk } from 'prismarine-world/types/world' + +export type MapUpdates = { + updateBlockColor: (pos: Vec3) => void + updatePlayerPosition: () => void + updateWarps: () => void +} + +export interface DrawerAdapter extends TypedEventEmitter { + getHighestBlockY: (x: number, z: number, chunk?: Chunk) => number + clearChunksStore: (x: number, z: number) => void + chunksStore: Map + playerPosition: Vec3 + warps: WorldWarp[] + loadingChunksQueue: Set + mapDrawer: MinimapDrawer + yaw: number + full: boolean + world: string + setWarp: (warp: WorldWarp, remove?: boolean) => void + quickTp?: (x: number, z: number) => void + loadChunk: (key: string) => Promise + drawChunkOnCanvas: (key: string, canvas: HTMLCanvasElement) => Promise +} + +export type ChunkInfo = { + heightmap: Uint8Array, + colors: string[], +} + +export class MinimapDrawer { + canvasWidthCenterX: number + canvasWidthCenterY: number + _mapSize: number + radius: number + ctx: CanvasRenderingContext2D + _canvas: HTMLCanvasElement + chunksInView = new Set() + lastBotPos: Vec3 + lastWarpPos: Vec3 + mapPixel: number + yaw: number + chunksStore = new Map() + loadingChunksQueue: undefined | Set + warps: WorldWarp[] + loadChunk: undefined | ((key: string) => Promise) + _full = false + + setMapPixel () { + if (this.full) { + this.radius = Math.floor(Math.min(this.canvas.width, this.canvas.height) / 2) + this._mapSize = 16 + } else { + this.radius = Math.floor(Math.min(this.canvas.width, this.canvas.height) / 2.2) + this._mapSize = this.radius * 2 + } + this.mapPixel = Math.floor(this.radius * 2 / this.mapSize) + } + + get full () { + return this._full + } + + set full (full: boolean) { + this._full = full + this.setMapPixel() + } + + get canvas () { + return this._canvas + } + + set canvas (canvas: HTMLCanvasElement) { + this.ctx = canvas.getContext('2d', { willReadFrequently: true })! + this.ctx.imageSmoothingEnabled = false + this.canvasWidthCenterX = canvas.width / 2 + this.canvasWidthCenterY = canvas.height / 2 + this._canvas = canvas + this.setMapPixel() + } + + get mapSize () { + return this._mapSize + } + + set mapSize (mapSize: number) { + this._mapSize = mapSize + this.mapPixel = Math.floor(this.radius * 2 / this.mapSize) + this.draw(this.lastBotPos) + } + + draw (botPos: Vec3,) { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) + + this.lastBotPos = botPos + this.updateChunksInView() + for (const key of this.chunksInView) { + if (!this.chunksStore.has(key) && !this.loadingChunksQueue?.has(key)) { + void this.loadChunk?.(key) + } + this.drawChunk(key) + } + if (!this.full) this.drawPartsOfWorld() + } + + updateChunksInView (viewX?: number, viewZ?: number) { + const worldCenterX = viewX ?? this.lastBotPos.x + const worldCenterZ = viewZ ?? this.lastBotPos.z + + const radius = this.mapSize / 2 + const leftViewBorder = Math.floor((worldCenterX - radius) / 16) - 1 + const rightViewBorder = Math.ceil((worldCenterX + radius) / 16) + const topViewBorder = Math.floor((worldCenterZ - radius) / 16) - 1 + const bottomViewBorder = Math.ceil((worldCenterZ + radius) / 16) + + this.chunksInView.clear() + for (let i = topViewBorder; i <= bottomViewBorder; i += 1) { + for (let j = leftViewBorder; j <= rightViewBorder; j += 1) { + this.chunksInView.add(`${j},${i}`) + } + } + } + + drawChunk (key: string, chunkInfo?: ChunkInfo | null) { + const [chunkX, chunkZ] = key.split(',').map(Number) + const chunkWorldX = chunkX * 16 + const chunkWorldZ = chunkZ * 16 + const chunkCanvasX = Math.floor((chunkWorldX - this.lastBotPos.x) * this.mapPixel + this.canvasWidthCenterX) + const chunkCanvasY = Math.floor((chunkWorldZ - this.lastBotPos.z) * this.mapPixel + this.canvasWidthCenterY) + const chunk = chunkInfo ?? this.chunksStore.get(key) + if (typeof chunk !== 'object') { + const chunkSize = this.mapPixel * 16 + this.ctx.fillStyle = chunk === 'requested' ? 'rgb(200, 200, 200)' : 'rgba(0, 0, 0, 0.5)' + this.ctx.fillRect(chunkCanvasX, chunkCanvasY, chunkSize, chunkSize) + return + } + for (let row = 0; row < 16; row += 1) { + for (let col = 0; col < 16; col += 1) { + const index = row * 16 + col + const color = chunk?.colors[index] ?? 'rgb(255, 0, 0)' + const pixelX = chunkCanvasX + this.mapPixel * col + const pixelY = chunkCanvasY + this.mapPixel * row + this.drawPixel(pixelX, pixelY, color) + } + } + } + + drawPixel (pixelX: number, pixelY: number, color: string) { + // if (!this.full && Math.hypot(pixelX - this.canvasWidthCenterX, pixelY - this.canvasWidthCenterY) > this.radius) { + // this.ctx.clearRect(pixelX, pixelY, this.mapPixel, this.mapPixel) + // return + // } + this.ctx.fillStyle = color + this.ctx.fillRect( + pixelX, + pixelY, + this.mapPixel, + this.mapPixel + ) + } + + clearChunksStore () { + for (const key of this.chunksStore.keys()) { + const [x, z] = key.split(',').map(x => Number(x) * 16) + if (Math.hypot((this.lastBotPos.x - x), (this.lastBotPos.z - z)) > this.radius * 5) { + this.chunksStore.delete(key) + } + } + } + + setWarpPosOnClick (mousePos: Vec3) { + this.lastWarpPos = new Vec3(mousePos.x, mousePos.y, mousePos.z) + } + + drawWarps (centerPos?: Vec3) { + for (const warp of this.warps) { + // if (!full) { + // const distance = this.getDistance( + // centerPos?.x ?? this.adapter.playerPosition.x, + // centerPos?.z ?? this.adapter.playerPosition.z, + // warp.x, + // warp.z + // ) + // if (distance > this.mapSize) continue + // } + const offset = this.full ? 0 : this.radius * 0.1 + const z = Math.floor( + (this.mapSize / 2 - (centerPos?.z ?? this.lastBotPos.z) + warp.z) * this.mapPixel + ) + offset + const x = Math.floor( + (this.mapSize / 2 - (centerPos?.x ?? this.lastBotPos.x) + warp.x) * this.mapPixel + ) + offset + const dz = z - this.canvasWidthCenterX + const dx = x - this.canvasWidthCenterY + const circleDist = Math.hypot(dx, dz) + + const angle = Math.atan2(dz, dx) + const circleZ = circleDist > this.mapSize / 2 && !this.full ? + this.canvasWidthCenterX + this.mapSize / 2 * Math.sin(angle) + : z + const circleX = circleDist > this.mapSize / 2 && !this.full ? + this.canvasWidthCenterY + this.mapSize / 2 * Math.cos(angle) + : x + this.ctx.beginPath() + this.ctx.arc( + circleX, + circleZ, + circleDist > this.mapSize / 2 && !this.full + ? this.mapPixel * 1.5 + : this.full ? this.mapPixel : this.mapPixel * 2, + 0, + Math.PI * 2, + false + ) + this.ctx.strokeStyle = 'black' + this.ctx.lineWidth = this.mapPixel + this.ctx.stroke() + this.ctx.fillStyle = warp.disabled ? 'rgba(255, 255, 255, 0.4)' : warp.color ?? '#d3d3d3' + this.ctx.fill() + this.ctx.closePath() + } + } + + drawPartsOfWorld () { + this.ctx.fillStyle = 'white' + this.ctx.shadowOffsetX = 1 + this.ctx.shadowOffsetY = 1 + this.ctx.shadowColor = 'black' + this.ctx.font = `${this.radius / 4}px serif` + this.ctx.textAlign = 'center' + this.ctx.textBaseline = 'middle' + this.ctx.strokeStyle = 'black' + this.ctx.lineWidth = 1 + + const angle = - Math.PI / 2 + const angleS = angle + Math.PI + const angleW = angle + Math.PI * 3 / 2 + const angleE = angle + Math.PI / 2 + + this.ctx.strokeText( + 'N', + this.canvasWidthCenterX + this.radius * Math.cos(angle), + this.canvasWidthCenterY + this.radius * Math.sin(angle) + ) + this.ctx.strokeText( + 'S', + this.canvasWidthCenterX + this.radius * Math.cos(angleS), + this.canvasWidthCenterY + this.radius * Math.sin(angleS) + ) + this.ctx.strokeText( + 'W', + this.canvasWidthCenterX + this.radius * Math.cos(angleW), + this.canvasWidthCenterY + this.radius * Math.sin(angleW) + ) + this.ctx.strokeText( + 'E', + this.canvasWidthCenterX + this.radius * Math.cos(angleE), + this.canvasWidthCenterY + this.radius * Math.sin(angleE) + ) + this.ctx.fillText( + 'N', + this.canvasWidthCenterX + this.radius * Math.cos(angle), + this.canvasWidthCenterY + this.radius * Math.sin(angle) + ) + this.ctx.fillText( + 'S', + this.canvasWidthCenterX + this.radius * Math.cos(angleS), + this.canvasWidthCenterY + this.radius * Math.sin(angleS) + ) + this.ctx.fillText( + 'W', + this.canvasWidthCenterX + this.radius * Math.cos(angleW), + this.canvasWidthCenterY + this.radius * Math.sin(angleW) + ) + this.ctx.fillText( + 'E', + this.canvasWidthCenterX + this.radius * Math.cos(angleE), + this.canvasWidthCenterY + this.radius * Math.sin(angleE) + ) + + this.ctx.shadowOffsetX = 0 + this.ctx.shadowOffsetY = 0 + } + + drawPlayerPos (canvasWorldCenterX?: number, canvasWorldCenterZ?: number, disableTurn?: boolean) { + this.ctx.setTransform(1, 0, 0, 1, 0, 0) + + const x = (this.lastBotPos.x - (canvasWorldCenterX ?? this.lastBotPos.x)) * this.mapPixel + const z = (this.lastBotPos.z - (canvasWorldCenterZ ?? this.lastBotPos.z)) * this.mapPixel + const center = this.mapSize / 2 * this.mapPixel + (this.full ? 0 : this.radius * 0.1) + this.ctx.translate(center + x, center + z) + if (!disableTurn) this.ctx.rotate(-this.yaw) + + const size = 3 + const factor = this.full ? 2 : 1 + const width = size * factor + const height = size * factor + + this.ctx.beginPath() + this.ctx.moveTo(0, -height) + this.ctx.lineTo(-width, height) + this.ctx.lineTo(width, height) + this.ctx.closePath() + + this.ctx.strokeStyle = '#000000' + this.ctx.lineWidth = this.full ? 2 : 1 + this.ctx.stroke() + this.ctx.fillStyle = '#FFFFFF' + this.ctx.fill() + + // Reset transformations + this.ctx.setTransform(1, 0, 0, 1, 0, 0) + } + + rotateMap (angle: number) { + this.ctx.setTransform(1, 0, 0, 1, 0, 0) + this.ctx.translate(this.canvasWidthCenterX, this.canvasWidthCenterY) + this.ctx.rotate(angle) + this.ctx.translate(-this.canvasWidthCenterX, -this.canvasWidthCenterY) + } +} diff --git a/src/react/MinimapProvider.tsx b/src/react/MinimapProvider.tsx new file mode 100644 index 000000000..4b877732b --- /dev/null +++ b/src/react/MinimapProvider.tsx @@ -0,0 +1,610 @@ +import { useEffect, useState } from 'react' +import { versions } from 'minecraft-data' +import { simplify } from 'prismarine-nbt' +import RegionFile from 'prismarine-provider-anvil/src/region' +import { Vec3 } from 'vec3' +import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' +import { WorldWarp } from 'flying-squid/dist/lib/modules/warps' +import { TypedEventEmitter } from 'contro-max/build/typedEventEmitter' +import { PCChunk } from 'prismarine-chunk' +import { Chunk } from 'prismarine-world/types/world' +import { Block } from 'prismarine-block' +import { INVISIBLE_BLOCKS } from 'prismarine-viewer/viewer/lib/mesher/worldConstants' +import { getRenamedData } from 'flying-squid/dist/blockRenames' +import { useSnapshot } from 'valtio' +import BlockData from '../../prismarine-viewer/viewer/lib/moreBlockDataGenerated.json' +import preflatMap from '../preflatMap.json' +import { contro } from '../controls' +import { gameAdditionalState, showModal, hideModal, miscUiState, loadedGameState, activeModalStack } from '../globalState' +import { options } from '../optionsStorage' +import Minimap, { DisplayMode } from './Minimap' +import { ChunkInfo, DrawerAdapter, MapUpdates, MinimapDrawer } from './MinimapDrawer' +import { useIsModalActive } from './utilsApp' + +const getBlockKey = (x: number, z: number) => { + return `${x},${z}` +} + +const findHeightMap = (obj: PCChunk): number[] | undefined => { + function search (obj: any): any | undefined { + for (const key in obj) { + if (['heightmap', 'heightmaps'].includes(key.toLowerCase())) { + return obj[key] + } else if (typeof obj[key] === 'object' && obj[key] !== null) { + const result = search(obj[key]) + return result + } + } + } + return search(obj) +} + +export class DrawerAdapterImpl extends TypedEventEmitter implements DrawerAdapter { + playerPosition: Vec3 + yaw: number + mapDrawer = new MinimapDrawer() + warps: WorldWarp[] + world: string + chunksStore = new Map() + loadingChunksQueue = new Set() + currChunk: PCChunk | undefined + currChunkPos: { x: number, z: number } = { x: 0, z: 0 } + isOldVersion: boolean + blockData: any + heightMap: Record = {} + regions = new Map() + chunksHeightmaps: Record = {} + loadChunk: (key: string) => Promise + loadChunkFullmap: ((key: string) => Promise) | undefined + _full: boolean + isBuiltinHeightmapAvailable = false + + constructor (pos?: Vec3) { + super() + this.full = false + this.playerPosition = pos ?? new Vec3(0, 0, 0) + this.warps = gameAdditionalState.warps + this.mapDrawer.warps = this.warps + this.mapDrawer.loadChunk = this.loadChunk + this.mapDrawer.loadingChunksQueue = this.loadingChunksQueue + this.mapDrawer.chunksStore = this.chunksStore + + // check if should use heightmap + if (localServer) { + const chunkX = Math.floor(this.playerPosition.x / 16) + const chunkZ = Math.floor(this.playerPosition.z / 16) + const regionX = Math.floor(chunkX / 32) + const regionZ = Math.floor(chunkZ / 32) + const regionKey = `${regionX},${regionZ}` + const worldFolder = this.getSingleplayerRootPath() + if (worldFolder && options.minimapOptimizations) { + const path = `${worldFolder}/region/r.${regionX}.${regionZ}.mca` + const region = new RegionFile(path) + void region.initialize() + this.regions.set(regionKey, region) + const readX = chunkX % 32 + const readZ = chunkZ % 32 + void this.regions.get(regionKey)!.read(readX, readZ).then((rawChunk) => { + const chunk = simplify(rawChunk as any) + const heightmap = findHeightMap(chunk) + if (heightmap) { + this.isBuiltinHeightmapAvailable = true + this.loadChunkFullmap = this.loadChunkFromRegion + console.log('using heightmap') + } else { + this.isBuiltinHeightmapAvailable = false + this.loadChunkFullmap = this.loadChunkNoRegion + console.log('[minimap] not using heightmap') + } + }).catch(err => { + console.error(err) + this.isBuiltinHeightmapAvailable = false + this.loadChunkFullmap = this.loadChunkFromViewer + }) + } else { + this.isBuiltinHeightmapAvailable = false + this.loadChunkFullmap = this.loadChunkFromViewer + } + } else { + this.isBuiltinHeightmapAvailable = false + this.loadChunkFullmap = this.loadChunkFromViewer + } + // if (localServer) { + // this.overwriteWarps(localServer.warps) + // this.on('cellReady', (key: string) => { + // if (this.loadingChunksQueue.size === 0) return + // const [x, z] = this.loadingChunksQueue.values().next().value.split(',').map(Number) + // this.loadChunk(x, z) + // this.loadingChunksQueue.delete(`${x},${z}`) + // }) + // } else { + // const storageWarps = localStorage.getItem(`warps: ${loadedGameState.username} ${loadedGameState.serverIp ?? ''}`) + // this.overwriteWarps(JSON.parse(storageWarps ?? '[]')) + // } + this.isOldVersion = versionToNumber(bot.version) < versionToNumber('1.13') + this.blockData = {} + for (const blockKey of Object.keys(BlockData.colors)) { + const renamedKey = getRenamedData('blocks', blockKey, '1.20.2', bot.version) + this.blockData[renamedKey as string] = BlockData.colors[blockKey] + } + + viewer.world?.renderUpdateEmitter.on('chunkFinished', (key) => { + if (!this.loadingChunksQueue.has(key)) return + void this.loadChunk(key) + this.loadingChunksQueue.delete(key) + }) + } + + get full () { + return this._full + } + + set full (full: boolean) { + console.log('this is minimap') + this.loadChunk = this.loadChunkMinimap + this.mapDrawer.loadChunk = this.loadChunk + this._full = full + } + + overwriteWarps (newWarps: WorldWarp[]) { + this.warps.splice(0, this.warps.length) + for (const warp of newWarps) { + this.warps.push({ ...warp }) + } + } + + setWarp (warp: WorldWarp, remove?: boolean): void { + this.world = bot.game.dimension + const index = this.warps.findIndex(w => w.name === warp.name) + if (index === -1) { + this.warps.push(warp) + } else if (remove && index !== -1) { + this.warps.splice(index, 1) + } else { + this.warps[index] = warp + } + if (localServer) { + // type suppressed until server is updated. It works fine + void (localServer as any).setWarp(warp, remove) + } else if (remove) { + localStorage.removeItem(`warps: ${loadedGameState.username} ${loadedGameState.serverIp}`) + } else { + localStorage.setItem(`warps: ${loadedGameState.username} ${loadedGameState.serverIp}`, JSON.stringify(this.warps)) + } + this.emit('updateWarps') + } + + getHighestBlockY (x: number, z: number, chunk?: Chunk) { + const chunkX = Math.floor(x / 16) * 16 + const chunkZ = Math.floor(z / 16) * 16 + if (this.chunksHeightmaps[`${chunkX},${chunkZ}`]) { + return this.chunksHeightmaps[`${chunkX},${chunkZ}`][x - chunkX + (z - chunkZ) * 16] - 1 + } + const source = chunk ?? bot.world + const { height, minY } = (bot.game as any) + for (let i = height; i > 0; i -= 1) { + const block = source.getBlock(new Vec3(x & 15, minY + i, z & 15)) + if (block && !INVISIBLE_BLOCKS.has(block.name)) { + return minY + i + } + } + return minY + } + + async getChunkSingleplayer (chunkX: number, chunkZ: number) { + // absolute coords + const region = (localServer!.overworld.storageProvider as any).getRegion(chunkX * 16, chunkZ * 16) + if (!region) return 'unavailable' + const chunk = await localServer!.players[0]!.world.getColumn(chunkX, chunkZ) + return chunk + } + + async loadChunkMinimap (key: string) { + const [chunkX, chunkZ] = key.split(',').map(Number) + const chunkWorldX = chunkX * 16 + const chunkWorldZ = chunkZ * 16 + if (viewer.world.finishedChunks[`${chunkWorldX},${chunkWorldZ}`]) { + const heightmap = new Uint8Array(256) + const colors = Array.from({ length: 256 }).fill('') as string[] + for (let z = 0; z < 16; z += 1) { + for (let x = 0; x < 16; x += 1) { + const blockX = chunkWorldX + x + const blockZ = chunkWorldZ + z + const hBlock = viewer.world.highestBlocks.get(`${blockX},${blockZ}`) + const block = bot.world.getBlock(new Vec3(blockX, hBlock?.y ?? 0, blockZ)) + // const block = Block.fromStateId(hBlock?.stateId ?? -1, hBlock?.biomeId ?? -1) + const index = z * 16 + x + if (!block || !hBlock) { + console.warn(`[loadChunk] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`) + heightmap[index] = 0 + colors[index] = 'rgba(0, 0, 0, 0.5)' + continue + } + heightmap[index] = hBlock.y + let color: string + if (this.isOldVersion) { + color = BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] + ?? 'rgb(0, 0, 255)' + } else { + color = this.blockData[block.name] ?? 'rgb(0, 255, 0)' + } + colors[index] = color + } + } + const chunk = { heightmap, colors } + this.applyShadows(chunk) + this.chunksStore.set(key, chunk) + this.emit(`chunkReady`, `${chunkX},${chunkZ}`) + } else { + this.loadingChunksQueue.add(`${chunkX},${chunkZ}`) + this.chunksStore.set(key, 'requested') + } + } + + async loadChunkNoRegion (key: string) { + const [chunkX, chunkZ] = key.split(',').map(Number) + const chunkWorldX = chunkX * 16 + const chunkWorldZ = chunkZ * 16 + const chunkInfo = await this.getChunkSingleplayer(chunkX, chunkZ) + if (chunkInfo === 'unavailable') return null + const heightmap = new Uint8Array(256) + const colors = Array.from({ length: 256 }).fill('') as string[] + for (let z = 0; z < 16; z += 1) { + for (let x = 0; x < 16; x += 1) { + const blockX = chunkWorldX + x + const blockZ = chunkWorldZ + z + const blockY = this.getHighestBlockY(blockX, blockZ, chunkInfo) + const block = chunkInfo.getBlock(new Vec3(blockX & 15, blockY, blockZ & 15)) + if (!block) { + console.warn(`[cannot get the block] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`) + return null + } + const index = z * 16 + x + heightmap[index] = blockY + const color = this.isOldVersion ? BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] ?? 'rgb(0, 0, 255)' : this.blockData[block.name] ?? 'rgb(0, 255, 0)' + colors[index] = color + } + } + const chunk: ChunkInfo = { heightmap, colors } + this.applyShadows(chunk) + return chunk + } + + async loadChunkFromRegion (key: string): Promise { + const [chunkX, chunkZ] = key.split(',').map(Number) + const chunkWorldX = chunkX * 16 + const chunkWorldZ = chunkZ * 16 + const heightmap = await this.getChunkHeightMapFromRegion(chunkX, chunkZ) as unknown as Uint8Array + if (!heightmap) return null + const chunkInfo = await this.getChunkSingleplayer(chunkX, chunkZ) + if (chunkInfo === 'unavailable') return null + const colors = Array.from({ length: 256 }).fill('') as string[] + for (let z = 0; z < 16; z += 1) { + for (let x = 0; x < 16; x += 1) { + const blockX = chunkWorldX + x + const blockZ = chunkWorldZ + z + const index = z * 16 + x + heightmap[index] -= 1 + if (heightmap[index] < 0) heightmap[index] = 0 + const blockY = heightmap[index] + const block = chunkInfo.getBlock(new Vec3(blockX & 15, blockY, blockZ & 15)) + if (!block) { + console.warn(`[cannot get the block] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`) + return null + } + const color = this.isOldVersion ? BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] ?? 'rgb(0, 0, 255)' : this.blockData[block.name] ?? 'rgb(0, 255, 0)' + colors[index] = color + } + } + const chunk: ChunkInfo = { heightmap, colors } + this.applyShadows(chunk) + return chunk + } + + getSingleplayerRootPath (): string | undefined { + return localServer!.options.worldFolder + } + + async getChunkHeightMapFromRegion (chunkX: number, chunkZ: number, cb?: (hm: number[]) => void) { + const regionX = Math.floor(chunkX / 32) + const regionZ = Math.floor(chunkZ / 32) + const regionKey = `${regionX},${regionZ}` + if (!this.regions.has(regionKey)) { + const worldFolder = this.getSingleplayerRootPath() + if (!worldFolder) return + const path = `${worldFolder}/region/r.${regionX}.${regionZ}.mca` + const region = new RegionFile(path) + await region.initialize() + this.regions.set(regionKey, region) + } + const rawChunk = await this.regions.get(regionKey)!.read(chunkX % 32, chunkZ % 32) + const chunk = simplify(rawChunk as any) + console.log(`chunk ${chunkX}, ${chunkZ}:`, chunk) + const heightmap = findHeightMap(chunk) + console.log(`heightmap ${chunkX}, ${chunkZ}:`, heightmap) + cb?.(heightmap!) + return heightmap + // this.chunksHeightmaps[`${chunkX},${chunkZ}`] = heightmap + } + + async loadChunkFromViewer (key: string) { + const [chunkX, chunkZ] = key.split(',').map(Number) + const chunkWorldX = chunkX * 16 + const chunkWorldZ = chunkZ * 16 + if (viewer.world.finishedChunks[`${chunkWorldX},${chunkWorldZ}`]) { + const heightmap = new Uint8Array(256) + const colors = Array.from({ length: 256 }).fill('') as string[] + for (let z = 0; z < 16; z += 1) { + for (let x = 0; x < 16; x += 1) { + const blockX = chunkWorldX + x + const blockZ = chunkWorldZ + z + const hBlock = viewer.world.highestBlocks.get(`${blockX},${blockZ}`) + const block = bot.world.getBlock(new Vec3(blockX, hBlock?.y ?? 0, blockZ)) + // const block = Block.fromStateId(hBlock?.stateId ?? -1, hBlock?.biomeId ?? -1) + const index = z * 16 + x + if (!block || !hBlock) { + console.warn(`[loadChunk] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`) + heightmap[index] = 0 + colors[index] = 'rgba(0, 0, 0, 0.5)' + continue + } + heightmap[index] = hBlock.y + const color = this.isOldVersion ? BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] ?? 'rgb(0, 0, 255)' : this.blockData[block.name] ?? 'rgb(0, 255, 0)' + colors[index] = color + } + } + const chunk = { heightmap, colors } + this.applyShadows(chunk) + return chunk + } else { + return null + } + } + + applyShadows (chunk: ChunkInfo) { + for (let j = 0; j < 16; j += 1) { + for (let i = 0; i < 16; i += 1) { + const index = j * 16 + i + const color = chunk.colors[index] + // if (i === 0 || j === 0 || i === 15 || j === 16) { + // const r = Math.floor(Math.random() * 2) + // chunk.colors[index] = r===0 ? this.makeDarker(color) : this.makeLighter(color) + // continue + // } + + const h = chunk.heightmap[index] + let isLighterOrDarker = 0 + + const r = chunk.heightmap[index + 1] ?? 0 + const u = chunk.heightmap[index - 16] ?? 0 + const ur = chunk.heightmap[index - 15] ?? 0 + if (r > h || u > h || ur > h) { + chunk.colors[index] = this.makeDarker(color) + isLighterOrDarker -= 1 + } + + const l = chunk.heightmap[index - 1] ?? 0 + const d = chunk.heightmap[index + 16] ?? 0 + const dl = chunk.heightmap[index + 15] ?? 0 + if (l > h || d > h || dl > h) { + chunk.colors[index] = this.makeLighter(color) + isLighterOrDarker += 1 + } + + let linkedIndex: number | undefined + if (i === 1) { + linkedIndex = index - 1 + } else if (i === 14) { + linkedIndex = index + 1 + } else if (j === 1) { + linkedIndex = index - 16 + } else if (j === 14) { + linkedIndex = index + 16 + } + if (linkedIndex !== undefined) { + const linkedColor = chunk.colors[linkedIndex] + switch (isLighterOrDarker) { + case 1: + chunk.colors[linkedIndex] = this.makeLighter(linkedColor) + break + case -1: + chunk.colors[linkedIndex] = this.makeDarker(linkedColor) + break + default: + break + } + } + } + } + } + + makeDarker (color: string) { + let rgbArray = color.match(/\d+/g)?.map(Number) ?? [] + if (rgbArray.length !== 3) return color + rgbArray = rgbArray.map(element => { + let newColor = element - 20 + if (newColor < 0) newColor = 0 + return newColor + }) + return `rgb(${rgbArray.join(',')})` + } + + makeLighter (color: string) { + let rgbArray = color.match(/\d+/g)?.map(Number) ?? [] + if (rgbArray.length !== 3) return color + rgbArray = rgbArray.map(element => { + let newColor = element + 20 + if (newColor > 255) newColor = 255 + return newColor + }) + return `rgb(${rgbArray.join(',')})` + } + + clearChunksStore (x: number, z: number) { + for (const key of Object.keys(this.chunksStore)) { + const [chunkX, chunkZ] = key.split(',').map(Number) + if (Math.hypot((chunkX - x), (chunkZ - z)) > 300) { + delete this.chunksStore[key] + delete this.chunksHeightmaps[key] + for (let i = 0; i < 16; i += 1) { + for (let j = 0; j < 16; j += 1) { + delete this.heightMap[`${chunkX + i},${chunkZ + j}`] + } + } + } + } + } + + quickTp (x: number, z: number) { + const y = this.getHighestBlockY(x, z) + bot.chat(`/tp ${x} ${y + 20} ${z}`) + const timeout = setTimeout(() => { + const y = this.getHighestBlockY(x, z) + bot.chat(`/tp ${x} ${y + 20} ${z}`) + clearTimeout(timeout) + }, 500) + } + + async drawChunkOnCanvas (key: string, canvas: HTMLCanvasElement) { + // console.log('chunk', key, 'on canvas') + if (!this.loadChunkFullmap) { + // wait for it to be available + await new Promise(resolve => { + const interval = setInterval(() => { + if (this.loadChunkFullmap) { + clearInterval(interval) + resolve(undefined) + } + }, 100) + setTimeout(() => { + clearInterval(interval) + resolve(undefined) + }, 10_000) + }) + if (!this.loadChunkFullmap) { + throw new Error('loadChunkFullmap not available') + } + } + const chunk = await this.loadChunkFullmap(key) + const [worldX, worldZ] = key.split(',').map(x => Number(x) * 16) + const center = new Vec3(worldX + 8, 0, worldZ + 8) + this.mapDrawer.lastBotPos = center + this.mapDrawer.canvas = canvas + this.mapDrawer.full = true + this.mapDrawer.drawChunk(key, chunk) + } +} + +const Inner = ( + { adapter, displayMode, toggleFullMap }: + { + adapter: DrawerAdapterImpl + displayMode?: DisplayMode, + toggleFullMap?: ({ command }: { command?: string }) => void + } +) => { + + const updateWarps = (newWarps: WorldWarp[] | Error) => { + if (newWarps instanceof Error) { + console.error('An error occurred:', newWarps.message) + return + } + + adapter.overwriteWarps(newWarps) + } + + const updateMap = () => { + if (!adapter) return + adapter.playerPosition = bot.entity.position + adapter.yaw = bot.entity.yaw + adapter.emit('updateMap') + } + + useEffect(() => { + bot.on('move', updateMap) + localServer?.on('warpsUpdated' as keyof ServerEvents, updateWarps) + + return () => { + bot?.off('move', updateMap) + localServer?.off('warpsUpdated' as keyof ServerEvents, updateWarps) + } + }, []) + + return
+ +
+} + +export default ({ displayMode }: { displayMode?: DisplayMode }) => { + const [adapter] = useState(() => new DrawerAdapterImpl(bot.entity.position)) + + const { showMinimap } = useSnapshot(options) + const fullMapOpened = useIsModalActive('full-map') + + + const readChunksHeightMaps = async () => { + const { worldFolder } = localServer!.options + const path = `${worldFolder}/region/r.0.0.mca` + const region = new RegionFile(path) + await region.initialize() + const chunks: Record = {} + console.log('Reading chunks...') + console.log(chunks) + let versionDetected = false + for (const [i, _] of Array.from({ length: 32 }).entries()) { + for (const [k, _] of Array.from({ length: 32 }).entries()) { + // todo, may use faster reading, but features is not commonly used + // eslint-disable-next-line no-await-in-loop + const nbt = await region.read(i, k) + chunks[`${i},${k}`] = nbt + if (nbt && !versionDetected) { + const simplified = simplify(nbt) + const version = versions.pc.find(x => x['dataVersion'] === simplified.DataVersion)?.minecraftVersion + console.log('Detected version', version ?? 'unknown') + versionDetected = true + } + } + } + Object.defineProperty(chunks, 'simplified', { + get () { + const mapped = {} + for (const [i, _] of Array.from({ length: 32 }).entries()) { + for (const [k, _] of Array.from({ length: 32 }).entries()) { + const key = `${i},${k}` + const chunk = chunks[key] + if (!chunk) continue + mapped[key] = simplify(chunk) + } + } + return mapped + }, + }) + console.log('Done!', chunks) + } + + if ( + displayMode === 'minimapOnly' + ? showMinimap === 'never' || (showMinimap === 'singleplayer' && !miscUiState.singleplayer) + : !fullMapOpened + ) { + return null + } + + const toggleFullMap = () => { + if (activeModalStack.at(-1)?.reactType === 'full-map') { + hideModal({ reactType: 'full-map' }) + } else { + showModal({ reactType: 'full-map' }) + } + } + + return +} diff --git a/src/react/utilsApp.ts b/src/react/utilsApp.ts index 53df4cda0..445007ec7 100644 --- a/src/react/utilsApp.ts +++ b/src/react/utilsApp.ts @@ -6,7 +6,8 @@ import { activeModalStack, miscUiState } from '../globalState' export const watchedModalsFromHooks = new Set() // todo should not be there export const hardcodedKnownModals = [ - 'player_win:' + 'player_win:', + 'full-map' // todo ] export const useUsingTouch = () => { @@ -17,6 +18,7 @@ export const useIsModalActive = (modal: string, useIncludes = false) => { watchedModalsFromHooks.add(modal) }, []) useEffect(() => { + // watchedModalsFromHooks.add(modal) return () => { watchedModalsFromHooks.delete(modal) } diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 2fb63c197..1a3af31e1 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -19,6 +19,7 @@ import ScoreboardProvider from './react/ScoreboardProvider' import SignEditorProvider from './react/SignEditorProvider' import IndicatorEffectsProvider from './react/IndicatorEffectsProvider' import PlayerListOverlayProvider from './react/PlayerListOverlayProvider' +import MinimapProvider from './react/MinimapProvider' import HudBarsProvider from './react/HudBarsProvider' import XPBarProvider from './react/XPBarProvider' import DebugOverlay from './react/DebugOverlay' @@ -27,7 +28,7 @@ import PauseScreen from './react/PauseScreen' import SoundMuffler from './react/SoundMuffler' import TouchControls from './react/TouchControls' import widgets from './react/widgets' -import { useIsWidgetActive } from './react/utilsApp' +import { useIsModalActive, useIsWidgetActive } from './react/utilsApp' import GlobalSearchInput from './react/GlobalSearchInput' import TouchAreasControlsProvider from './react/TouchAreasControlsProvider' import NotificationProvider, { showNotification } from './react/NotificationProvider' @@ -101,9 +102,11 @@ const InGameComponent = ({ children }) => { const InGameUi = () => { const { gameLoaded, showUI: showUIRaw } = useSnapshot(miscUiState) - const { disabledUiParts, displayBossBars } = useSnapshot(options) - const hasModals = useSnapshot(activeModalStack).length > 0 + const { disabledUiParts, displayBossBars, showMinimap } = useSnapshot(options) + const modalsSnapshot = useSnapshot(activeModalStack) + const hasModals = modalsSnapshot.length > 0 const showUI = showUIRaw || hasModals + const displayFullmap = modalsSnapshot.some(modal => modal.reactType === 'full-map') if (!gameLoaded || !bot || disabledUiParts.includes('*')) return return <> @@ -116,6 +119,7 @@ const InGameUi = () => { {!disabledUiParts.includes('players-list') && } {!disabledUiParts.includes('chat') && } + {showMinimap !== 'never' && } {!disabledUiParts.includes('title') && } {!disabledUiParts.includes('scoreboard') && } {!disabledUiParts.includes('effects-indicators') && } @@ -137,6 +141,7 @@ const InGameUi = () => { + {displayFullmap && } {/* because of z-index */} {showUI && } diff --git a/tsconfig.json b/tsconfig.json index 3c20075c1..addc64f19 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,6 @@ "prismarine-viewer/examples" ], "exclude": [ - "node_modules", + "node_modules" ] }