import React, { useState } from "react"; import { Group } from "react-konva"; import MapControls from "./MapControls"; import MapInteraction from "./MapInteraction"; import MapToken from "./MapToken"; import MapDrawing from "./MapDrawing"; import MapFog from "./MapFog"; import MapGrid from "./MapGrid"; import MapMeasure from "./MapMeasure"; import NetworkedMapPointer from "../../network/NetworkedMapPointer"; import MapNotes from "./MapNotes"; import { useTokenData } from "../../contexts/TokenDataContext"; import { useSettings } from "../../contexts/SettingsContext"; import TokenMenu from "../token/TokenMenu"; import TokenDragOverlay from "../token/TokenDragOverlay"; import NoteMenu from "../note/NoteMenu"; import NoteDragOverlay from "../note/NoteDragOverlay"; import { AddShapeAction, CutShapeAction, EditShapeAction, RemoveShapeAction, } from "../../actions"; function Map({ map, mapState, mapActions, onMapTokenStateChange, onMapTokenStateRemove, onMapChange, onMapStateChange, onMapDraw, onMapDrawUndo, onMapDrawRedo, onFogDraw, onFogDrawUndo, onFogDrawRedo, onMapNoteChange, onMapNoteRemove, allowMapDrawing, allowFogDrawing, allowMapChange, allowNoteEditing, disabledTokens, session, }) { const { tokensById } = useTokenData(); const [selectedToolId, setSelectedToolId] = useState("pan"); const { settings, setSettings } = useSettings(); function handleToolSettingChange(tool, change) { setSettings((prevSettings) => ({ ...prevSettings, [tool]: { ...prevSettings[tool], ...change, }, })); } const drawShapes = Object.values(mapState?.drawShapes || {}); const fogShapes = Object.values(mapState?.fogShapes || {}); function handleToolAction(action) { if (action === "eraseAll") { onMapDraw(new RemoveShapeAction(drawShapes.map((s) => s.id))); } if (action === "mapUndo") { onMapDrawUndo(); } if (action === "mapRedo") { onMapDrawRedo(); } if (action === "fogUndo") { onFogDrawUndo(); } if (action === "fogRedo") { onFogDrawRedo(); } } function handleMapShapeAdd(shape) { onMapDraw(new AddShapeAction([shape])); } function handleMapShapesRemove(shapeIds) { onMapDraw(new RemoveShapeAction(shapeIds)); } function handleFogShapeAdd(shape) { onFogDraw(new AddShapeAction([shape])); } function handleFogShapeCut(shape) { onFogDraw(new CutShapeAction([shape])); } function handleFogShapesRemove(shapeIds) { onFogDraw(new RemoveShapeAction(shapeIds)); } function handleFogShapesEdit(shapes) { onFogDraw(new EditShapeAction(shapes)); } const disabledControls = []; if (!allowMapDrawing) { disabledControls.push("drawing"); } if (!map) { disabledControls.push("pan"); disabledControls.push("measure"); disabledControls.push("pointer"); } if (!allowFogDrawing) { disabledControls.push("fog"); } if (!allowMapChange) { disabledControls.push("map"); } if (!allowNoteEditing) { disabledControls.push("note"); } const disabledSettings = { fog: [], drawing: [] }; if (drawShapes.length === 0) { disabledSettings.drawing.push("erase"); } if (!mapState || mapActions.mapDrawActionIndex < 0) { disabledSettings.drawing.push("undo"); } if ( !mapState || mapActions.mapDrawActionIndex === mapActions.mapDrawActions.length - 1 ) { disabledSettings.drawing.push("redo"); } if (!mapState || mapActions.fogDrawActionIndex < 0) { disabledSettings.fog.push("undo"); } if ( !mapState || mapActions.fogDrawActionIndex === mapActions.fogDrawActions.length - 1 ) { disabledSettings.fog.push("redo"); } const mapControls = ( ); const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false); const [tokenMenuOptions, setTokenMenuOptions] = useState({}); const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); function handleTokenMenuOpen(tokenStateId, tokenImage) { setTokenMenuOptions({ tokenStateId, tokenImage }); setIsTokenMenuOpen(true); } function getMapTokenCategoryWeight(category) { switch (category) { case "character": return 0; case "vehicle": return 1; case "prop": return 2; default: return 0; } } // Sort so vehicles render below other tokens function sortMapTokenStates(a, b, tokenDraggingOptions) { const tokenA = tokensById[a.tokenId]; const tokenB = tokensById[b.tokenId]; if (tokenA && tokenB) { // If categories are different sort in order "prop", "vehicle", "character" if (tokenB.category !== tokenA.category) { const aWeight = getMapTokenCategoryWeight(tokenA.category); const bWeight = getMapTokenCategoryWeight(tokenB.category); return bWeight - aWeight; } else if ( tokenDraggingOptions && tokenDraggingOptions.dragging && tokenDraggingOptions.tokenState.id === a.id ) { // If dragging token a move above return 1; } else if ( tokenDraggingOptions && tokenDraggingOptions.dragging && tokenDraggingOptions.tokenState.id === b.id ) { // If dragging token b move above return -1; } else { // Else sort so last modified is on top return a.lastModified - b.lastModified; } } else if (tokenA) { return 1; } else if (tokenB) { return -1; } else { return 0; } } const mapTokens = map && mapState && ( {Object.values(mapState.tokens) .sort((a, b) => sortMapTokenStates(a, b, tokenDraggingOptions)) .map((tokenState) => ( setTokenDraggingOptions({ dragging: true, tokenState, tokenGroup: e.target, }) } onTokenDragEnd={() => setTokenDraggingOptions({ ...tokenDraggingOptions, dragging: false, }) } draggable={ selectedToolId === "pan" && !(tokenState.id in disabledTokens) && !tokenState.locked } mapState={mapState} fadeOnHover={selectedToolId === "drawing"} map={map} /> ))} ); const tokenMenu = ( setIsTokenMenuOpen(false)} onTokenStateChange={onMapTokenStateChange} tokenState={mapState && mapState.tokens[tokenMenuOptions.tokenStateId]} tokenImage={tokenMenuOptions.tokenImage} map={map} /> ); const tokenDragOverlay = tokenDraggingOptions && ( { onMapTokenStateRemove(state); setTokenDraggingOptions(null); }} onTokenStateChange={onMapTokenStateChange} tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState} tokenGroup={tokenDraggingOptions && tokenDraggingOptions.tokenGroup} dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)} token={tokensById[tokenDraggingOptions.tokenState.tokenId]} mapState={mapState} /> ); const mapDrawing = ( ); const mapFog = ( ); const mapGrid = map && map.showGrid && ; const mapMeasure = ( ); const mapPointer = ( ); const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); const [noteMenuOptions, setNoteMenuOptions] = useState({}); const [noteDraggingOptions, setNoteDraggingOptions] = useState(); function handleNoteMenuOpen(noteId, noteNode) { setNoteMenuOptions({ noteId, noteNode }); setIsNoteMenuOpen(true); } function sortNotes(a, b, noteDraggingOptions) { if ( noteDraggingOptions && noteDraggingOptions.dragging && noteDraggingOptions.noteId === a.id ) { // If dragging token `a` move above return 1; } else if ( noteDraggingOptions && noteDraggingOptions.dragging && noteDraggingOptions.noteId === b.id ) { // If dragging token `b` move above return -1; } else { // Else sort so last modified is on top return a.lastModified - b.lastModified; } } const mapNotes = ( sortNotes(a, b, noteDraggingOptions) ) : [] } onNoteMenuOpen={handleNoteMenuOpen} draggable={ allowNoteEditing && (selectedToolId === "note" || selectedToolId === "pan") } onNoteDragStart={(e, noteId) => setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) } onNoteDragEnd={() => setNoteDraggingOptions({ ...noteDraggingOptions, dragging: false }) } fadeOnHover={selectedToolId === "drawing"} /> ); const noteMenu = ( setIsNoteMenuOpen(false)} onNoteChange={onMapNoteChange} note={mapState && mapState.notes[noteMenuOptions.noteId]} noteNode={noteMenuOptions.noteNode} map={map} /> ); const noteDragOverlay = ( { onMapNoteRemove(noteId); setNoteDraggingOptions(null); }} /> ); return ( {mapControls} {tokenMenu} {noteMenu} {tokenDragOverlay} {noteDragOverlay} } selectedToolId={selectedToolId} onSelectedToolChange={setSelectedToolId} disabledControls={disabledControls} > {mapGrid} {mapDrawing} {mapNotes} {mapTokens} {mapFog} {mapPointer} {mapMeasure} ); } export default Map;