diff --git a/src/actions/Action.js b/src/actions/Action.ts similarity index 67% rename from src/actions/Action.js rename to src/actions/Action.ts index a147f2b..a6f12fe 100644 --- a/src/actions/Action.js +++ b/src/actions/Action.ts @@ -1,40 +1,31 @@ -// Load Diff for auto complete -// eslint-disable-next-line no-unused-vars import { Diff } from "deep-diff"; import { diff, revertChanges } from "../helpers/diff"; import cloneDeep from "lodash.clonedeep"; -/** - * @callback ActionUpdate - * @param {any} state - */ - /** * Implementation of the Command Pattern * Wraps an update function with internal state to support undo */ -class Action { +class Action { /** * The update function called with the current state and should return the updated state * This is implemented in the child class - * - * @type {ActionUpdate} */ - update; + update(state: State): State { + return state; + } /** * The changes caused by the last state update - * @type {Diff} */ - changes; + changes: Diff[] | undefined; /** * Executes the action update on the state - * @param {any} state The current state to update - * @returns {any} The updated state + * @param {State} state The current state to update */ - execute(state) { + execute(state: State): State { if (state && this.update) { let newState = this.update(cloneDeep(state)); this.changes = diff(state, newState); @@ -45,10 +36,10 @@ class Action { /** * Reverts the changes caused by the last call of `execute` - * @param {any} state The current state to perform the undo on - * @returns {any} The state with the last changes reverted + * @param {State} state The current state to perform the undo on + * @returns {State} The state with the last changes reverted */ - undo(state) { + undo(state: State): State { if (state && this.changes) { let revertedState = cloneDeep(state); revertChanges(revertedState, this.changes); diff --git a/src/actions/AddShapeAction.js b/src/actions/AddShapeAction.js deleted file mode 100644 index 5147d05..0000000 --- a/src/actions/AddShapeAction.js +++ /dev/null @@ -1,15 +0,0 @@ -import Action from "./Action"; - -class AddShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - for (let shape of shapes) { - shapesById[shape.id] = shape; - } - return shapesById; - }; - } -} - -export default AddShapeAction; diff --git a/src/actions/AddStatesAction.ts b/src/actions/AddStatesAction.ts new file mode 100644 index 0000000..14d9053 --- /dev/null +++ b/src/actions/AddStatesAction.ts @@ -0,0 +1,21 @@ +import Action from "./Action"; + +import { ID } from "../types/Action"; + +class AddStatesAction extends Action> { + states: State[]; + + constructor(states: State[]) { + super(); + this.states = states; + } + + update(statesById: Record) { + for (let state of this.states) { + statesById[state.id] = state; + } + return statesById; + } +} + +export default AddStatesAction; diff --git a/src/actions/CutFogAction.ts b/src/actions/CutFogAction.ts new file mode 100644 index 0000000..779a921 --- /dev/null +++ b/src/actions/CutFogAction.ts @@ -0,0 +1,41 @@ +import polygonClipping from "polygon-clipping"; + +import Action from "./Action"; +import { + addPolygonDifferenceToFog, + addPolygonIntersectionToFog, + fogToGeometry, +} from "../helpers/actions"; + +import { Fog, FogState } from "../types/Fog"; + +class CutFogAction extends Action { + fogs: Fog[]; + + constructor(fog: Fog[]) { + super(); + this.fogs = fog; + } + + update(fogsById: FogState): FogState { + let actionGeom = this.fogs.map(fogToGeometry); + let cutFogs: FogState = {}; + for (let fog of Object.values(fogsById)) { + const fogGeom = fogToGeometry(fog); + try { + const difference = polygonClipping.difference(fogGeom, ...actionGeom); + const intersection = polygonClipping.intersection( + fogGeom, + ...actionGeom + ); + addPolygonDifferenceToFog(fog, difference, cutFogs); + addPolygonIntersectionToFog(fog, intersection, cutFogs); + } catch { + console.error("Unable to find intersection for fogs"); + } + } + return cutFogs; + } +} + +export default CutFogAction; diff --git a/src/actions/CutShapeAction.js b/src/actions/CutShapeAction.js deleted file mode 100644 index 59688e6..0000000 --- a/src/actions/CutShapeAction.js +++ /dev/null @@ -1,38 +0,0 @@ -import polygonClipping from "polygon-clipping"; - -import Action from "./Action"; -import { - addPolygonDifferenceToShapes, - addPolygonIntersectionToShapes, - shapeToGeometry, -} from "../helpers/actions"; - -class CutShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - let actionGeom = shapes.map(shapeToGeometry); - let cutShapes = {}; - for (let shape of Object.values(shapesById)) { - const shapeGeom = shapeToGeometry(shape); - try { - const difference = polygonClipping.difference( - shapeGeom, - ...actionGeom - ); - const intersection = polygonClipping.intersection( - shapeGeom, - ...actionGeom - ); - addPolygonDifferenceToShapes(shape, difference, cutShapes); - addPolygonIntersectionToShapes(shape, intersection, cutShapes); - } catch { - console.error("Unable to find intersection for shapes"); - } - } - return cutShapes; - }; - } -} - -export default CutShapeAction; diff --git a/src/actions/EditShapeAction.js b/src/actions/EditShapeAction.js deleted file mode 100644 index e531df5..0000000 --- a/src/actions/EditShapeAction.js +++ /dev/null @@ -1,17 +0,0 @@ -import Action from "./Action"; - -class EditShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - for (let edit of shapes) { - if (edit.id in shapesById) { - shapesById[edit.id] = { ...shapesById[edit.id], ...edit }; - } - } - return shapesById; - }; - } -} - -export default EditShapeAction; diff --git a/src/actions/EditStatesAction.ts b/src/actions/EditStatesAction.ts new file mode 100644 index 0000000..247828c --- /dev/null +++ b/src/actions/EditStatesAction.ts @@ -0,0 +1,23 @@ +import Action from "./Action"; + +import { ID } from "../types/Action"; + +class EditStatesAction extends Action> { + edits: Partial[]; + + constructor(edits: Partial[]) { + super(); + this.edits = edits; + } + + update(statesById: Record) { + for (let edit of this.edits) { + if (edit.id !== undefined && edit.id in statesById) { + statesById[edit.id] = { ...statesById[edit.id], ...edit }; + } + } + return statesById; + } +} + +export default EditStatesAction; diff --git a/src/actions/RemoveShapeAction.js b/src/actions/RemoveShapeAction.js deleted file mode 100644 index baa2df0..0000000 --- a/src/actions/RemoveShapeAction.js +++ /dev/null @@ -1,13 +0,0 @@ -import Action from "./Action"; -import { omit } from "../helpers/shared"; - -class RemoveShapeAction extends Action { - constructor(shapeIds) { - super(); - this.update = (shapesById) => { - return omit(shapesById, shapeIds); - }; - } -} - -export default RemoveShapeAction; diff --git a/src/actions/RemoveStatesAction.ts b/src/actions/RemoveStatesAction.ts new file mode 100644 index 0000000..aa422ad --- /dev/null +++ b/src/actions/RemoveStatesAction.ts @@ -0,0 +1,21 @@ +import Action from "./Action"; +import { omit } from "../helpers/shared"; + +import { ID } from "../types/Action"; + +class RemoveStatesAction extends Action< + Record +> { + stateIds: string[]; + + constructor(stateIds: string[]) { + super(); + this.stateIds = stateIds; + } + + update(statesById: Record) { + return omit(statesById, this.stateIds); + } +} + +export default RemoveStatesAction; diff --git a/src/actions/SubtractFogAction.ts b/src/actions/SubtractFogAction.ts new file mode 100644 index 0000000..9fa6ff7 --- /dev/null +++ b/src/actions/SubtractFogAction.ts @@ -0,0 +1,32 @@ +import polygonClipping from "polygon-clipping"; + +import Action from "./Action"; +import { addPolygonDifferenceToFog, fogToGeometry } from "../helpers/actions"; + +import { Fog, FogState } from "../types/Fog"; + +class SubtractFogAction extends Action { + fogs: Fog[]; + + constructor(fogs: Fog[]) { + super(); + this.fogs = fogs; + } + + update(fogsById: FogState): FogState { + const actionGeom = this.fogs.map(fogToGeometry); + let subtractedFogs: FogState = {}; + for (let fog of Object.values(fogsById)) { + const fogGeom = fogToGeometry(fog); + try { + const difference = polygonClipping.difference(fogGeom, ...actionGeom); + addPolygonDifferenceToFog(fog, difference, subtractedFogs); + } catch { + console.error("Unable to find difference for fogs"); + } + } + return subtractedFogs; + } +} + +export default SubtractFogAction; diff --git a/src/actions/SubtractShapeAction.js b/src/actions/SubtractShapeAction.js deleted file mode 100644 index 13f915a..0000000 --- a/src/actions/SubtractShapeAction.js +++ /dev/null @@ -1,32 +0,0 @@ -import polygonClipping from "polygon-clipping"; - -import Action from "./Action"; -import { - addPolygonDifferenceToShapes, - shapeToGeometry, -} from "../helpers/actions"; - -class SubtractShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - const actionGeom = shapes.map(shapeToGeometry); - let subtractedShapes = {}; - for (let shape of Object.values(shapesById)) { - const shapeGeom = shapeToGeometry(shape); - try { - const difference = polygonClipping.difference( - shapeGeom, - ...actionGeom - ); - addPolygonDifferenceToShapes(shape, difference, subtractedShapes); - } catch { - console.error("Unable to find difference for shapes"); - } - } - return subtractedShapes; - }; - } -} - -export default SubtractShapeAction; diff --git a/src/actions/index.js b/src/actions/index.js deleted file mode 100644 index 7822cc1..0000000 --- a/src/actions/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import AddShapeAction from "./AddShapeAction"; -import CutShapeAction from "./CutShapeAction"; -import EditShapeAction from "./EditShapeAction"; -import RemoveShapeAction from "./RemoveShapeAction"; -import SubtractShapeAction from "./SubtractShapeAction"; - -export { - AddShapeAction, - CutShapeAction, - EditShapeAction, - RemoveShapeAction, - SubtractShapeAction, -}; diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 0000000..4b37526 --- /dev/null +++ b/src/actions/index.ts @@ -0,0 +1,13 @@ +import AddStatesAction from "./AddStatesAction"; +import CutFogAction from "./CutFogAction"; +import EditStatesAction from "./EditStatesAction"; +import RemoveStatesAction from "./RemoveStatesAction"; +import SubtractFogAction from "./SubtractFogAction"; + +export { + AddStatesAction, + CutFogAction, + EditStatesAction, + RemoveStatesAction, + SubtractFogAction, +}; diff --git a/src/components/Slider.tsx b/src/components/Slider.tsx index 96785c7..9a28e28 100644 --- a/src/components/Slider.tsx +++ b/src/components/Slider.tsx @@ -78,7 +78,7 @@ Slider.defaultProps = { value: 0, ml: 0, mr: 0, - labelFunc: (value: any) => value, + labelFunc: (value: number) => value, }; export default Slider; diff --git a/src/components/banner/Banner.tsx b/src/components/banner/Banner.tsx index 5e8eb08..66278da 100644 --- a/src/components/banner/Banner.tsx +++ b/src/components/banner/Banner.tsx @@ -1,5 +1,7 @@ import Modal from "react-modal"; import { useThemeUI, Close } from "theme-ui"; +import { RequestCloseEventHandler } from "../../types/Events"; +import CSS from "csstype"; function Banner({ isOpen, @@ -8,11 +10,11 @@ function Banner({ allowClose, backgroundColor, }: { - isOpen: boolean, - onRequestClose: any, - children: any, - allowClose: boolean, - backgroundColor?: any + isOpen: boolean; + onRequestClose: RequestCloseEventHandler; + children: React.ReactNode; + allowClose: boolean; + backgroundColor?: CSS.Property.Color; }) { const { theme } = useThemeUI(); @@ -23,7 +25,8 @@ function Banner({ style={{ overlay: { bottom: "0", top: "initial", zIndex: 2000 }, content: { - backgroundColor: backgroundColor || theme.colors?.highlight, + backgroundColor: + backgroundColor || (theme.colors?.highlight as CSS.Property.Color), color: "hsl(210, 50%, 96%)", top: "initial", left: "50%", diff --git a/src/components/banner/ErrorBanner.tsx b/src/components/banner/ErrorBanner.tsx index cdfac99..1dbedff 100644 --- a/src/components/banner/ErrorBanner.tsx +++ b/src/components/banner/ErrorBanner.tsx @@ -2,7 +2,13 @@ import { Box, Text } from "theme-ui"; import Banner from "./Banner"; -function ErrorBanner({ error, onRequestClose }: { error: Error | undefined, onRequestClose: any }) { +function ErrorBanner({ + error, + onRequestClose, +}: { + error: Error | undefined; + onRequestClose; +}) { return ( diff --git a/src/components/dice/DiceInteraction.tsx b/src/components/dice/DiceInteraction.tsx index 8fd9d50..7b7fb68 100644 --- a/src/components/dice/DiceInteraction.tsx +++ b/src/components/dice/DiceInteraction.tsx @@ -35,7 +35,7 @@ type DiceInteractionProps = { canvas: HTMLCanvasElement | WebGLRenderingContext; }) => void; onPointerDown: () => void; - onPointerUp: () => any; + onPointerUp: () => void; }; function DiceInteraction({ diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index 2f6618b..e9c84e7 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -21,12 +21,21 @@ import NoteMenu from "../note/NoteMenu"; import NoteDragOverlay from "../note/NoteDragOverlay"; import { - AddShapeAction, - CutShapeAction, - EditShapeAction, - RemoveShapeAction, + AddStatesAction, + CutFogAction, + EditStatesAction, + RemoveStatesAction, } from "../../actions"; import Session from "../../network/Session"; +import { Drawing } from "../../types/Drawing"; +import { Fog } from "../../types/Fog"; +import { Map, MapToolId } from "../../types/Map"; +import { MapState } from "../../types/MapState"; +import { Settings } from "../../types/Settings"; +import { + MapChangeEventHandler, + MapResetEventHandler, +} from "../../types/Events"; function Map({ map, @@ -51,43 +60,39 @@ function Map({ disabledTokens, session, }: { - map: any; + map: Map; mapState: MapState; - mapActions: any; - onMapTokenStateChange: any; - onMapTokenStateRemove: any; - onMapChange: any; - onMapReset: any; - onMapDraw: any; - onMapDrawUndo: any; - onMapDrawRedo: any; - onFogDraw: any; - onFogDrawUndo: any; - onFogDrawRedo: any; - onMapNoteChange: any; - onMapNoteRemove: any; + mapActions: ; + onMapTokenStateChange: ; + onMapTokenStateRemove: ; + onMapChange: MapChangeEventHandler; + onMapReset: MapResetEventHandler; + onMapDraw: ; + onMapDrawUndo: ; + onMapDrawRedo: ; + onFogDraw: ; + onFogDrawUndo: ; + onFogDrawRedo: ; + onMapNoteChange: ; + onMapNoteRemove: ; allowMapDrawing: boolean; allowFogDrawing: boolean; allowMapChange: boolean; allowNoteEditing: boolean; - disabledTokens: any; + disabledTokens: ; session: Session; }) { const { addToast } = useToasts(); const { tokensById } = useTokenData(); - const [selectedToolId, setSelectedToolId] = useState("move"); - const { settings, setSettings }: { settings: any; setSettings: any } = - useSettings(); + const [selectedToolId, setSelectedToolId] = useState("move"); + const { settings, setSettings } = useSettings(); - function handleToolSettingChange(tool: any, change: any) { - setSettings((prevSettings: any) => ({ + function handleToolSettingChange(change: Partial) { + setSettings((prevSettings) => ({ ...prevSettings, - [tool]: { - ...prevSettings[tool], - ...change, - }, + ...change, })); } @@ -96,7 +101,7 @@ function Map({ function handleToolAction(action: string) { if (action === "eraseAll") { - onMapDraw(new RemoveShapeAction(drawShapes.map((s) => s.id))); + onMapDraw(new RemoveStatesAction(drawShapes.map((s) => s.id))); } if (action === "mapUndo") { onMapDrawUndo(); @@ -112,28 +117,28 @@ function Map({ } } - function handleMapShapeAdd(shape: Shape) { - onMapDraw(new AddShapeAction([shape])); + function handleMapShapeAdd(shape: Drawing) { + onMapDraw(new AddStatesAction([shape])); } function handleMapShapesRemove(shapeIds: string[]) { - onMapDraw(new RemoveShapeAction(shapeIds)); + onMapDraw(new RemoveStatesAction(shapeIds)); } - function handleFogShapesAdd(shapes: Shape[]) { - onFogDraw(new AddShapeAction(shapes)); + function handleFogShapesAdd(shapes: Fog[]) { + onFogDraw(new AddStatesAction(shapes)); } - function handleFogShapesCut(shapes: Shape[]) { - onFogDraw(new CutShapeAction(shapes)); + function handleFogShapesCut(shapes: Fog[]) { + onFogDraw(new CutFogAction(shapes)); } function handleFogShapesRemove(shapeIds: string[]) { - onFogDraw(new RemoveShapeAction(shapeIds)); + onFogDraw(new RemoveStatesAction(shapeIds)); } - function handleFogShapesEdit(shapes: Shape[]) { - onFogDraw(new EditShapeAction(shapes)); + function handleFogShapesEdit(shapes: Partial[]) { + onFogDraw(new EditStatesAction(shapes)); } const disabledControls = []; @@ -155,7 +160,10 @@ function Map({ disabledControls.push("note"); } - const disabledSettings: { fog: any[]; drawing: any[] } = { + const disabledSettings: { + fog: string[]; + drawing: string[]; + } = { fog: [], drawing: [], }; @@ -197,19 +205,10 @@ function Map({ /> ); - const [isTokenMenuOpen, setIsTokenMenuOpen]: [ - isTokenMenuOpen: boolean, - setIsTokenMenuOpen: React.Dispatch> - ] = useState(false); - const [tokenMenuOptions, setTokenMenuOptions]: [ - tokenMenuOptions: any, - setTokenMenuOptions: any - ] = useState({}); - const [tokenDraggingOptions, setTokenDraggingOptions]: [ - tokenDraggingOptions: any, - setTokenDragginOptions: any - ] = useState(); - function handleTokenMenuOpen(tokenStateId: string, tokenImage: any) { + const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false); + const [tokenMenuOptions, setTokenMenuOptions] = useState({}); + const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); + function handleTokenMenuOpen(tokenStateId: string, tokenImage) { setTokenMenuOptions({ tokenStateId, tokenImage }); setIsTokenMenuOpen(true); } @@ -240,7 +239,7 @@ function Map({ const tokenDragOverlay = tokenDraggingOptions && ( { + onTokenStateRemove={(state) => { onMapTokenStateRemove(state); setTokenDraggingOptions(null); }} @@ -292,14 +291,14 @@ function Map({ ); const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); - const [noteMenuOptions, setNoteMenuOptions] = useState({}); - const [noteDraggingOptions, setNoteDraggingOptions] = useState(); - function handleNoteMenuOpen(noteId: string, noteNode: any) { + const [noteMenuOptions, setNoteMenuOptions] = useState({}); + const [noteDraggingOptions, setNoteDraggingOptions] = useState(); + function handleNoteMenuOpen(noteId: string, noteNode) { setNoteMenuOptions({ noteId, noteNode }); setIsNoteMenuOpen(true); } - function sortNotes(a: any, b: any, noteDraggingOptions: any) { + function sortNotes(a, b, noteDraggingOptions) { if ( noteDraggingOptions && noteDraggingOptions.dragging && @@ -338,7 +337,7 @@ function Map({ allowNoteEditing && (selectedToolId === "note" || selectedToolId === "move") } - onNoteDragStart={(e: any, noteId: any) => + onNoteDragStart={(e, noteId) => setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) } onNoteDragEnd={() => @@ -364,7 +363,7 @@ function Map({ dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)} noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup} noteId={noteDraggingOptions && noteDraggingOptions.noteId} - onNoteRemove={(noteId: any) => { + onNoteRemove={(noteId) => { onMapNoteRemove(noteId); setNoteDraggingOptions(null); }} diff --git a/src/components/map/MapControls.tsx b/src/components/map/MapControls.tsx index 92a32a8..88e87ff 100644 --- a/src/components/map/MapControls.tsx +++ b/src/components/map/MapControls.tsx @@ -1,4 +1,4 @@ -import React, { useState, Fragment } from "react"; +import { useState, Fragment } from "react"; import { IconButton, Flex, Box } from "theme-ui"; import RadioIconButton from "../RadioIconButton"; @@ -21,21 +21,26 @@ import FullScreenExitIcon from "../../icons/FullScreenExitIcon"; import NoteToolIcon from "../../icons/NoteToolIcon"; import useSetting from "../../hooks/useSetting"; -import { Map } from "../../types/Map"; +import { Map, MapTool, MapToolId } from "../../types/Map"; import { MapState } from "../../types/MapState"; +import { + MapChangeEventHandler, + MapResetEventHandler, +} from "../../types/Events"; +import { Settings } from "../../types/Settings"; type MapControlsProps = { - onMapChange: () => void; - onMapReset: () => void; + onMapChange: MapChangeEventHandler; + onMapReset: MapResetEventHandler; currentMap?: Map; currentMapState?: MapState; - selectedToolId: string; - onSelectedToolChange: () => void; - toolSettings: any; - onToolSettingChange: () => void; - onToolAction: () => void; + selectedToolId: MapToolId; + onSelectedToolChange: (toolId: MapToolId) => void; + toolSettings: Settings; + onToolSettingChange: (change: Partial) => void; + onToolAction: (actionId: string) => void; disabledControls: string[]; - disabledSettings: string[]; + disabledSettings: Partial>; }; function MapContols({ @@ -54,7 +59,7 @@ function MapContols({ const [isExpanded, setIsExpanded] = useState(true); const [fullScreen, setFullScreen] = useSetting("map.fullScreen"); - const toolsById = { + const toolsById: Record = { move: { id: "move", icon: , @@ -89,7 +94,14 @@ function MapContols({ title: "Note Tool (N)", }, }; - const tools = ["move", "fog", "drawing", "measure", "pointer", "note"]; + const tools: MapToolId[] = [ + "move", + "fog", + "drawing", + "measure", + "pointer", + "note", + ]; const sections = [ { @@ -174,32 +186,41 @@ function MapContols({ function getToolSettings() { const Settings = toolsById[selectedToolId].SettingsComponent; - if (Settings) { - return ( - - - onToolSettingChange(selectedToolId, change) - } - onToolAction={onToolAction} - disabledActions={disabledSettings[selectedToolId]} - /> - - ); - } else { + if ( + !Settings || + selectedToolId === "move" || + selectedToolId === "measure" || + selectedToolId === "note" + ) { return null; } + return ( + + + onToolSettingChange({ + [selectedToolId]: { + ...toolSettings[selectedToolId], + ...change, + }, + }) + } + onToolAction={onToolAction} + disabledActions={disabledSettings[selectedToolId]} + /> + + ); } return ( diff --git a/src/components/map/MapEditor.js b/src/components/map/MapEditor.tsx similarity index 85% rename from src/components/map/MapEditor.js rename to src/components/map/MapEditor.tsx index 1040468..de4efdf 100644 --- a/src/components/map/MapEditor.js +++ b/src/components/map/MapEditor.tsx @@ -21,8 +21,17 @@ import GridOffIcon from "../../icons/GridOffIcon"; import MapGrid from "./MapGrid"; import MapGridEditor from "./MapGridEditor"; +import { Map } from "../../types/Map"; +import { GridInset } from "../../types/Grid"; -function MapEditor({ map, onSettingsChange }) { +type MapSettingsChangeEventHandler = (change: Partial) => void; + +type MapEditorProps = { + map: Map; + onSettingsChange: MapSettingsChangeEventHandler; +}; + +function MapEditor({ map, onSettingsChange }: MapEditorProps) { const [mapImage] = useMapImage(map); const [stageWidth, setStageWidth] = useState(1); @@ -36,12 +45,17 @@ function MapEditor({ map, onSettingsChange }) { const mapLayerRef = useRef(); const [preventMapInteraction, setPreventMapInteraction] = useState(false); - function handleResize(width, height) { - setStageWidth(width); - setStageHeight(height); + function handleResize(width?: number, height?: number): void { + if (width) { + setStageWidth(width); + } + + if (height) { + setStageHeight(height); + } } - const containerRef = useRef(); + const containerRef = useRef(null); usePreventOverscroll(containerRef); const [mapWidth, mapHeight] = useImageCenter( @@ -67,17 +81,21 @@ function MapEditor({ map, onSettingsChange }) { preventMapInteraction ); - function handleGridChange(inset) { - onSettingsChange("grid", { - ...map.grid, - inset, + function handleGridChange(inset: GridInset) { + onSettingsChange({ + grid: { + ...map.grid, + inset, + }, }); } function handleMapReset() { - onSettingsChange("grid", { - ...map.grid, - inset: defaultInset, + onSettingsChange({ + grid: { + ...map.grid, + inset: defaultInset, + }, }); } @@ -120,8 +138,9 @@ function MapEditor({ map, onSettingsChange }) { > ( + stageRender={(children: React.ReactNode) => ( void; +type FogCutEventHandler = (fog: Fog[]) => void; +type FogRemoveEventHandler = (fogId: string[]) => void; +type FogEditEventHandler = (edit: Partial[]) => void; +type FogErrorEventHandler = (message: string) => void; + +type MapFogProps = { + map: Map; + shapes: Fog[]; + onShapesAdd: FogAddEventHandler; + onShapesCut: FogCutEventHandler; + onShapesRemove: FogRemoveEventHandler; + onShapesEdit: FogEditEventHandler; + onShapeError: FogErrorEventHandler; + active: boolean; + toolSettings: FogToolSettings; + editable: boolean; +}; + function MapFog({ map, shapes, @@ -58,7 +81,7 @@ function MapFog({ active, toolSettings, editable, -}) { +}: MapFogProps) { const stageScale = useDebouncedStageScale(); const mapWidth = useMapWidth(); const mapHeight = useMapHeight(); @@ -76,7 +99,7 @@ function MapFog({ const [editOpacity] = useSetting("fog.editOpacity"); const mapStageRef = useMapStage(); - const [drawingShape, setDrawingShape] = useState(null); + const [drawingShape, setDrawingShape] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); const [editingShapes, setEditingShapes] = useState([]); @@ -84,7 +107,7 @@ function MapFog({ const [fogShapes, setFogShapes] = useState(shapes); // Bounding boxes for guides const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState([]); - const [guides, setGuides] = useState([]); + const [guides, setGuides] = useState([]); const shouldHover = active && @@ -108,8 +131,14 @@ function MapFog({ const mapStage = mapStageRef.current; function getBrushPosition(snapping = true) { + if (!mapStage) { + return; + } const mapImage = mapStage.findOne("#mapImage"); let position = getRelativePointerPosition(mapImage); + if (!position) { + return; + } if (shouldUseGuides && snapping) { for (let guide of guides) { if (guide.orientation === "vertical") { @@ -129,6 +158,9 @@ function MapFog({ function handleBrushDown() { if (toolSettings.type === "brush") { const brushPosition = getBrushPosition(); + if (!brushPosition) { + return; + } setDrawingShape({ type: "fog", data: { @@ -143,6 +175,9 @@ function MapFog({ } if (toolSettings.type === "rectangle") { const brushPosition = getBrushPosition(); + if (!brushPosition) { + return; + } setDrawingShape({ type: "fog", data: { @@ -166,7 +201,13 @@ function MapFog({ function handleBrushMove() { if (toolSettings.type === "brush" && isBrushDown && drawingShape) { const brushPosition = getBrushPosition(); + if (!brushPosition) { + return; + } setDrawingShape((prevShape) => { + if (!prevShape) { + return prevShape; + } const prevPoints = prevShape.data.points; if ( Vector2.compare( @@ -193,7 +234,13 @@ function MapFog({ if (toolSettings.type === "rectangle" && isBrushDown && drawingShape) { const prevPoints = drawingShape.data.points; const brushPosition = getBrushPosition(); + if (!brushPosition) { + return; + } setDrawingShape((prevShape) => { + if (!prevShape) { + return prevShape; + } return { ...prevShape, data: { @@ -223,7 +270,7 @@ function MapFog({ const shapesToSubtract = shapes.filter((shape) => cut ? !shape.visible : shape.visible ); - const subtractAction = new SubtractShapeAction(shapesToSubtract); + const subtractAction = new SubtractFogAction(shapesToSubtract); const state = subtractAction.execute({ [drawingShape.id]: drawingShape, }); @@ -235,7 +282,7 @@ function MapFog({ if (drawingShapes.length > 0) { if (cut) { // Run a pre-emptive cut action to check whether we've cut anything - const cutAction = new CutShapeAction(drawingShapes); + const cutAction = new CutFogAction(drawingShapes); const state = cutAction.execute(keyBy(shapes, "id")); if (Object.keys(state).length === shapes.length) { @@ -300,7 +347,7 @@ function MapFog({ function handlePointerMove() { if (shouldUseGuides) { - let guides = []; + let guides: Guide[] = []; const brushPosition = getBrushPosition(false); const absoluteBrushPosition = Vector2.multiply(brushPosition, { x: mapWidth, @@ -393,7 +440,7 @@ function MapFog({ const shapesToSubtract = shapes.filter((shape) => cut ? !shape.visible : shape.visible ); - const subtractAction = new SubtractShapeAction(shapesToSubtract); + const subtractAction = new SubtractFogAction(shapesToSubtract); const state = subtractAction.execute({ [polygonShape.id]: polygonShape, }); @@ -405,7 +452,7 @@ function MapFog({ if (polygonShapes.length > 0) { if (cut) { // Run a pre-emptive cut action to check whether we've cut anything - const cutAction = new CutShapeAction(polygonShapes); + const cutAction = new CutFogAction(polygonShapes); const state = cutAction.execute(keyBy(shapes, "id")); if (Object.keys(state).length === shapes.length) { diff --git a/src/components/map/MapGrid.js b/src/components/map/MapGrid.tsx similarity index 74% rename from src/components/map/MapGrid.js rename to src/components/map/MapGrid.tsx index be5e9ba..6f04e02 100644 --- a/src/components/map/MapGrid.js +++ b/src/components/map/MapGrid.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import useImage from "use-image"; import { useDataURL } from "../../contexts/AssetsContext"; @@ -8,8 +8,9 @@ import { mapSources as defaultMapSources } from "../../maps"; import { getImageLightness } from "../../helpers/image"; import Grid from "../Grid"; +import { Map } from "../../types/Map"; -function MapGrid({ map }) { +function MapGrid({ map }: { map: Map }) { let mapSourceMap = map; const mapURL = useDataURL( mapSourceMap, @@ -17,13 +18,14 @@ function MapGrid({ map }) { undefined, map.type === "file" ); - const [mapImage, mapLoadingStatus] = useImage(mapURL); + + const [mapImage, mapLoadingStatus] = useImage(mapURL || ""); const [isImageLight, setIsImageLight] = useState(true); // When the map changes find the average lightness of its pixels useEffect(() => { - if (mapLoadingStatus === "loaded") { + if (mapLoadingStatus === "loaded" && mapImage) { setIsImageLight(getImageLightness(mapImage)); } }, [mapImage, mapLoadingStatus]); diff --git a/src/components/map/MapGridEditor.js b/src/components/map/MapGridEditor.tsx similarity index 89% rename from src/components/map/MapGridEditor.js rename to src/components/map/MapGridEditor.tsx index a059180..75254e9 100644 --- a/src/components/map/MapGridEditor.js +++ b/src/components/map/MapGridEditor.tsx @@ -1,5 +1,6 @@ -import React, { useRef } from "react"; +import { useRef } from "react"; import { Group, Circle, Rect } from "react-konva"; +import { KonvaEventObject, Node } from "konva/types/Node"; import { useDebouncedStageScale, @@ -12,8 +13,15 @@ import { useKeyboard } from "../../contexts/KeyboardContext"; import Vector2 from "../../helpers/Vector2"; import shortcuts from "../../shortcuts"; +import { Map } from "../../types/Map"; +import { GridInset } from "../../types/Grid"; -function MapGridEditor({ map, onGridChange }) { +type MapGridEditorProps = { + map: Map; + onGridChange: (inset: GridInset) => void; +}; + +function MapGridEditor({ map, onGridChange }: MapGridEditorProps) { const stageScale = useDebouncedStageScale(); const mapWidth = useMapWidth(); const mapHeight = useMapHeight(); @@ -39,21 +47,21 @@ function MapGridEditor({ map, onGridChange }) { } const handlePositions = getHandlePositions(); - const handlePreviousPositionRef = useRef(); + const handlePreviousPositionRef = useRef(); - function handleScaleCircleDragStart(event) { + function handleScaleCircleDragStart(event: KonvaEventObject) { const handle = event.target; const position = getHandleNormalizedPosition(handle); handlePreviousPositionRef.current = position; } - function handleScaleCircleDragMove(event) { + function handleScaleCircleDragMove(event: KonvaEventObject) { const handle = event.target; onGridChange(getHandleInset(handle)); handlePreviousPositionRef.current = getHandleNormalizedPosition(handle); } - function handleScaleCircleDragEnd(event) { + function handleScaleCircleDragEnd(event: KonvaEventObject) { onGridChange(getHandleInset(event.target)); setPreventMapInteraction(false); } @@ -66,11 +74,14 @@ function MapGridEditor({ map, onGridChange }) { setPreventMapInteraction(false); } - function getHandleInset(handle) { + function getHandleInset(handle: Node): GridInset { const name = handle.name(); // Find distance and direction of dragging const previousPosition = handlePreviousPositionRef.current; + if (!previousPosition) { + return map.grid.inset; + } const position = getHandleNormalizedPosition(handle); const distance = Vector2.distance(previousPosition, position); const direction = Vector2.normalize( @@ -154,7 +165,7 @@ function MapGridEditor({ map, onGridChange }) { } } - function nudgeGrid(direction, scale) { + function nudgeGrid(direction: Vector2, scale: number) { const inset = map.grid.inset; const gridSizeNormalized = Vector2.divide( Vector2.subtract(inset.bottomRight, inset.topLeft), @@ -170,7 +181,7 @@ function MapGridEditor({ map, onGridChange }) { }); } - function handleKeyDown(event) { + function handleKeyDown(event: KeyboardEvent) { const nudgeAmount = event.shiftKey ? 2 : 0.5; if (shortcuts.gridNudgeUp(event)) { // Stop arrow up/down scrolling if overflowing @@ -191,7 +202,7 @@ function MapGridEditor({ map, onGridChange }) { useKeyboard(handleKeyDown); - function getHandleNormalizedPosition(handle) { + function getHandleNormalizedPosition(handle: Node) { return Vector2.divide({ x: handle.x(), y: handle.y() }, mapSize); } diff --git a/src/components/map/SelectMapButton.tsx b/src/components/map/SelectMapButton.tsx index 18c1654..aae8164 100644 --- a/src/components/map/SelectMapButton.tsx +++ b/src/components/map/SelectMapButton.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { IconButton } from "theme-ui"; import SelectMapModal from "../../modals/SelectMapModal"; @@ -6,6 +6,20 @@ import SelectMapIcon from "../../icons/SelectMapIcon"; import { useMapData } from "../../contexts/MapDataContext"; import { useUserId } from "../../contexts/UserIdContext"; +import { + MapChangeEventHandler, + MapResetEventHandler, +} from "../../types/Events"; +import { Map } from "../../types/Map"; +import { MapState } from "../../types/MapState"; + +type SelectMapButtonProps = { + onMapChange: MapChangeEventHandler; + onMapReset: MapResetEventHandler; + currentMap?: Map; + currentMapState?: MapState; + disabled: boolean; +}; function SelectMapButton({ onMapChange, @@ -13,7 +27,7 @@ function SelectMapButton({ currentMap, currentMapState, disabled, -}) { +}: SelectMapButtonProps) { const [isModalOpen, setIsModalOpen] = useState(false); const { updateMapState } = useMapData(); diff --git a/src/components/party/ChangeNicknameButton.tsx b/src/components/party/ChangeNicknameButton.tsx index be599e3..a3a478f 100644 --- a/src/components/party/ChangeNicknameButton.tsx +++ b/src/components/party/ChangeNicknameButton.tsx @@ -4,7 +4,13 @@ import { IconButton } from "theme-ui"; import ChangeNicknameModal from "../../modals/ChangeNicknameModal"; import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon"; -function ChangeNicknameButton({ nickname, onChange }: { nickname: string, onChange: any}) { +function ChangeNicknameButton({ + nickname, + onChange, +}: { + nickname: string; + onChange; +}) { const [isChangeModalOpen, setIsChangeModalOpen] = useState(false); function openModal() { setIsChangeModalOpen(true); diff --git a/src/components/party/DiceRoll.tsx b/src/components/party/DiceRoll.tsx index e770205..f6ccaee 100644 --- a/src/components/party/DiceRoll.tsx +++ b/src/components/party/DiceRoll.tsx @@ -1,12 +1,20 @@ import { Flex, Box, Text } from "theme-ui"; -function DiceRoll({ rolls, type, children }: { rolls: any, type: string, children: any}) { +function DiceRoll({ + rolls, + type, + children, +}: { + rolls; + type: string; + children; +}) { return ( {children} {rolls - .filter((d: any) => d.type === type && d.roll !== "unknown") - .map((dice: any, index: string | number) => ( + .filter((d) => d.type === type && d.roll !== "unknown") + .map((dice, index: string | number) => ( {dice.roll} diff --git a/src/components/party/DiceRolls.tsx b/src/components/party/DiceRolls.tsx index 3798595..99eb8c6 100644 --- a/src/components/party/DiceRolls.tsx +++ b/src/components/party/DiceRolls.tsx @@ -24,14 +24,14 @@ const diceIcons = [ { type: "d100", Icon: D100Icon }, ]; -function DiceRolls({ rolls }: { rolls: any }) { +function DiceRolls({ rolls }: { rolls }) { const total = getDiceRollTotal(rolls); const [expanded, setExpanded] = useState(false); let expandedRolls = []; for (let icon of diceIcons) { - if (rolls.some((roll: any) => roll.type === icon.type)) { + if (rolls.some((roll) => roll.type === icon.type)) { expandedRolls.push( @@ -45,29 +45,29 @@ function DiceRolls({ rolls }: { rolls: any }) { } return ( - - - setExpanded(!expanded)} - > - - - - {total} - - - {expanded && ( - - {expandedRolls} - - )} + + + setExpanded(!expanded)} + > + + + + {total} + + {expanded && ( + + {expandedRolls} + + )} + ); } diff --git a/src/components/party/DiceTrayButton.tsx b/src/components/party/DiceTrayButton.tsx index 1a00263..51ba962 100644 --- a/src/components/party/DiceTrayButton.tsx +++ b/src/components/party/DiceTrayButton.tsx @@ -16,7 +16,12 @@ function DiceTrayButton({ onShareDiceChange, diceRolls, onDiceRollsChange, -}: { shareDice: boolean, onShareDiceChange: any, diceRolls: [], onDiceRollsChange: any}) { +}: { + shareDice: boolean; + onShareDiceChange; + diceRolls: []; + onDiceRollsChange; +}) { const [isExpanded, setIsExpanded] = useState(false); const [fullScreen] = useSetting("map.fullScreen"); diff --git a/src/components/party/Nickname.tsx b/src/components/party/Nickname.tsx index 00b316a..2510d81 100644 --- a/src/components/party/Nickname.tsx +++ b/src/components/party/Nickname.tsx @@ -4,7 +4,15 @@ import Stream from "./Stream"; import DiceRolls from "./DiceRolls"; // TODO: check if stream is a required or optional param -function Nickname({ nickname, stream, diceRolls }: { nickname: string, stream?: any, diceRolls: any}) { +function Nickname({ + nickname, + stream, + diceRolls, +}: { + nickname: string; + stream?; + diceRolls; +}) { return ( ({ ...prevState, timer: newTimer })); + setPlayerState((prevState) => ({ ...prevState, timer: newTimer })); } function handleTimerStop() { - setPlayerState((prevState: any) => ({ ...prevState, timer: null })); + setPlayerState((prevState) => ({ ...prevState, timer: null })); } useEffect(() => { let prevTime = performance.now(); let request = requestAnimationFrame(update); let counter = 0; - function update(time: any) { + function update(time) { request = requestAnimationFrame(update); const deltaTime = time - prevTime; prevTime = time; @@ -51,9 +68,9 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }: { g current: playerState.timer.current - counter, }; if (newTimer.current < 0) { - setPlayerState((prevState: any) => ({ ...prevState, timer: null })); + setPlayerState((prevState) => ({ ...prevState, timer: null })); } else { - setPlayerState((prevState: any) => ({ ...prevState, timer: newTimer })); + setPlayerState((prevState) => ({ ...prevState, timer: newTimer })); } counter = 0; } @@ -65,7 +82,7 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }: { g }, [playerState.timer, setPlayerState]); function handleNicknameChange(newNickname: string) { - setPlayerState((prevState: any) => ({ ...prevState, nickname: newNickname })); + setPlayerState((prevState) => ({ ...prevState, nickname: newNickname })); } function handleDiceRollsChange(newDiceRolls: number[]) { diff --git a/src/components/party/StartStreamButton.tsx b/src/components/party/StartStreamButton.tsx index 23e8de0..14cc877 100644 --- a/src/components/party/StartStreamButton.tsx +++ b/src/components/party/StartStreamButton.tsx @@ -6,7 +6,15 @@ import Link from "../Link"; import StartStreamModal from "../../modals/StartStreamModal"; -function StartStreamButton({ onStreamStart, onStreamEnd, stream }: { onStreamStart: any, onStreamEnd: any, stream: any}) { +function StartStreamButton({ + onStreamStart, + onStreamEnd, + stream, +}: { + onStreamStart; + onStreamEnd; + stream; +}) { const [isStreamModalOpoen, setIsStreamModalOpen] = useState(false); function openModal() { setIsStreamModalOpen(true); @@ -45,7 +53,7 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }: { onStreamSta function handleStreamStart() { // Must be defined this way in typescript due to open issue - https://github.com/microsoft/TypeScript/issues/33232 - const mediaDevices = navigator.mediaDevices as any; + const mediaDevices = navigator.mediaDevices; mediaDevices .getDisplayMedia({ video: true, @@ -55,10 +63,12 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }: { onStreamSta echoCancellation: false, }, }) - .then((localStream: { getTracks: () => any; }) => { + .then((localStream: { getTracks }) => { const tracks = localStream.getTracks(); - const hasAudio = tracks.some((track: { kind: string; }) => track.kind === "audio"); + const hasAudio = tracks.some( + (track: { kind: string }) => track.kind === "audio" + ); setNoAudioTrack(!hasAudio); // Ensure an audio track is present diff --git a/src/components/party/StartTimerButton.tsx b/src/components/party/StartTimerButton.tsx index 3c413fc..8072226 100644 --- a/src/components/party/StartTimerButton.tsx +++ b/src/components/party/StartTimerButton.tsx @@ -4,7 +4,15 @@ import { IconButton } from "theme-ui"; import StartTimerModal from "../../modals/StartTimerModal"; import StartTimerIcon from "../../icons/StartTimerIcon"; -function StartTimerButton({ onTimerStart, onTimerStop, timer }: { onTimerStart: any, onTimerStop: any, timer: any }) { +function StartTimerButton({ + onTimerStart, + onTimerStop, + timer, +}: { + onTimerStart; + onTimerStop; + timer; +}) { const [isTimerModalOpen, setIsTimerModalOpen] = useState(false); function openModal() { diff --git a/src/components/party/Stream.tsx b/src/components/party/Stream.tsx index 6dc5eec..58f7844 100644 --- a/src/components/party/Stream.tsx +++ b/src/components/party/Stream.tsx @@ -6,13 +6,18 @@ import StreamMuteIcon from "../../icons/StreamMuteIcon"; import Banner from "../banner/Banner"; import Slider from "../Slider"; -function Stream({ stream, nickname }: { stream: MediaStream, nickname: string }) { +function Stream({ + stream, + nickname, +}: { + stream: MediaStream; + nickname: string; +}) { const [streamVolume, setStreamVolume] = useState(1); - const [showStreamInteractBanner, setShowStreamInteractBanner] = useState( - false - ); + const [showStreamInteractBanner, setShowStreamInteractBanner] = + useState(false); const [streamMuted, setStreamMuted] = useState(false); - const audioRef = useRef(); + const audioRef = useRef(); useEffect(() => { if (audioRef.current) { @@ -51,9 +56,8 @@ function Stream({ stream, nickname }: { stream: MediaStream, nickname: string }) // Platforms like iOS don't allow you to control audio volume // Detect this by trying to change the audio volume - const [isVolumeControlAvailable, setIsVolumeControlAvailable] = useState( - true - ); + const [isVolumeControlAvailable, setIsVolumeControlAvailable] = + useState(true); useEffect(() => { let audio = audioRef.current; function checkVolumeControlAvailable() { @@ -75,7 +79,7 @@ function Stream({ stream, nickname }: { stream: MediaStream, nickname: string }) }, []); // Use an audio context gain node to control volume to go past 100% - const audioGainRef = useRef(); + const audioGainRef = useRef(); useEffect(() => { let audioContext: AudioContext; if (stream && !streamMuted && isVolumeControlAvailable && audioGainRef) { diff --git a/src/components/party/Timer.tsx b/src/components/party/Timer.tsx index 3f208ce..9762868 100644 --- a/src/components/party/Timer.tsx +++ b/src/components/party/Timer.tsx @@ -4,8 +4,8 @@ import { Box, Progress } from "theme-ui"; import usePortal from "../../hooks/usePortal"; -function Timer({ timer, index }: { timer: any, index: number}) { - const progressBarRef = useRef(); +function Timer({ timer, index }: { timer; index: number }) { + const progressBarRef = useRef(); useEffect(() => { if (progressBarRef.current && timer) { @@ -16,7 +16,7 @@ function Timer({ timer, index }: { timer: any, index: number}) { useEffect(() => { let request = requestAnimationFrame(animate); let previousTime = performance.now(); - function animate(time: any) { + function animate(time) { request = requestAnimationFrame(animate); const deltaTime = time - previousTime; previousTime = time; diff --git a/src/contexts/AssetsContext.tsx b/src/contexts/AssetsContext.tsx index 9d64c9c..8f2266c 100644 --- a/src/contexts/AssetsContext.tsx +++ b/src/contexts/AssetsContext.tsx @@ -10,10 +10,16 @@ import useDebounce from "../hooks/useDebounce"; import { omit } from "../helpers/shared"; import { Asset } from "../types/Asset"; +export type GetAssetEventHanlder = ( + assetId: string +) => Promise; +export type AddAssetsEventHandler = (assets: Asset[]) => Promise; +export type PutAssetEventsHandler = (asset: Asset) => Promise; + type AssetsContext = { - getAsset: (assetId: string) => Promise; - addAssets: (assets: Asset[]) => void; - putAsset: (asset: Asset) => void; + getAsset: GetAssetEventHanlder; + addAssets: AddAssetsEventHandler; + putAsset: PutAssetEventsHandler; }; const AssetsContext = React.createContext(undefined); @@ -30,7 +36,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) { } }, [worker, databaseStatus]); - const getAsset = useCallback( + const getAsset = useCallback( async (assetId) => { if (database) { return await database.table("assets").get(assetId); @@ -39,7 +45,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) { [database] ); - const addAssets = useCallback( + const addAssets = useCallback( async (assets) => { if (database) { await database.table("assets").bulkAdd(assets); @@ -48,7 +54,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) { [database] ); - const putAsset = useCallback( + const putAsset = useCallback( async (asset) => { if (database) { // Check for broadcast channel and attempt to use worker to put map to avoid UI lockup diff --git a/src/contexts/DatabaseContext.tsx b/src/contexts/DatabaseContext.tsx index 167ef6b..ec68448 100644 --- a/src/contexts/DatabaseContext.tsx +++ b/src/contexts/DatabaseContext.tsx @@ -8,24 +8,28 @@ import { getDatabase } from "../database"; //@ts-ignore import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax +import { DatabaseWorkerService } from "../workers/DatabaseWorker"; + +export type DatabaseStatus = "loading" | "disabled" | "upgrading" | "loaded"; type DatabaseContext = { database: Dexie | undefined; - databaseStatus: any; + databaseStatus: DatabaseStatus; databaseError: Error | undefined; - worker: Comlink.Remote; + worker: Comlink.Remote; }; -// TODO: check what default we want here const DatabaseContext = React.createContext(undefined); -const worker = Comlink.wrap(new DatabaseWorker()); +const worker: Comlink.Remote = Comlink.wrap( + new DatabaseWorker() +); export function DatabaseProvider({ children }: { children: React.ReactNode }) { const [database, setDatabase] = useState(); const [databaseStatus, setDatabaseStatus] = - useState<"loading" | "disabled" | "upgrading" | "loaded">("loading"); + useState("loading"); const [databaseError, setDatabaseError] = useState(); useEffect(() => { diff --git a/src/contexts/DiceLoadingContext.tsx b/src/contexts/DiceLoadingContext.tsx index 5f6fc43..97b6f57 100644 --- a/src/contexts/DiceLoadingContext.tsx +++ b/src/contexts/DiceLoadingContext.tsx @@ -1,12 +1,16 @@ import React, { useState, useContext, ReactChild } from "react"; -type DiceLoadingContext = { - assetLoadStart: any, - assetLoadFinish: any, - isLoading: boolean, -} +export type AssetLoadStartEventHandler = () => void; +export type AssetLoadFinishEventHandler = () => void; -const DiceLoadingContext = React.createContext(undefined); +type DiceLoadingContext = { + assetLoadStart: AssetLoadStartEventHandler; + assetLoadFinish: AssetLoadFinishEventHandler; + isLoading: boolean; +}; + +const DiceLoadingContext = + React.createContext(undefined); export function DiceLoadingProvider({ children }: { children: ReactChild }) { const [loadingAssetCount, setLoadingAssetCount] = useState(0); diff --git a/src/contexts/GridContext.tsx b/src/contexts/GridContext.tsx index f7390d1..81f1f7d 100644 --- a/src/contexts/GridContext.tsx +++ b/src/contexts/GridContext.tsx @@ -2,8 +2,8 @@ import React, { useContext, useState, useEffect } from "react"; import Vector2 from "../helpers/Vector2"; import Size from "../helpers/Size"; -// eslint-disable-next-line no-unused-vars -import { getGridPixelSize, getCellPixelSize, Grid } from "../helpers/grid"; +import { getGridPixelSize, getCellPixelSize } from "../helpers/grid"; +import { Grid } from "../types/Grid"; /** * @typedef GridContextValue @@ -16,14 +16,14 @@ import { getGridPixelSize, getCellPixelSize, Grid } from "../helpers/grid"; * @property {Vector2} gridCellPixelOffset Offset of the grid cells to convert the center position of hex cells to the top left */ type GridContextValue = { - grid: Grid, - gridPixelSize: Size, - gridCellPixelSize: Size, - gridCellNormalizedSize: Size, - gridOffset: Vector2, - gridStrokeWidth: number, - gridCellPixelOffset: Vector2 -} + grid: Grid; + gridPixelSize: Size; + gridCellPixelSize: Size; + gridCellNormalizedSize: Size; + gridOffset: Vector2; + gridStrokeWidth: number; + gridCellPixelOffset: Vector2; +}; /** * @type {GridContextValue} @@ -66,11 +66,21 @@ export const GridCellPixelOffsetContext = React.createContext( const defaultStrokeWidth = 1 / 10; -export function GridProvider({ grid: inputGrid, width, height, children }: { grid: Required, width: number, height: number, children: any }) { +export function GridProvider({ + grid: inputGrid, + width, + height, + children, +}: { + grid: Grid; + width: number; + height: number; + children: React.ReactNode; +}) { let grid = inputGrid; if (!grid.size.x || !grid.size.y) { - grid = defaultValue.grid as Required; + grid = defaultValue.grid; } const [gridPixelSize, setGridPixelSize] = useState( diff --git a/src/contexts/GroupContext.tsx b/src/contexts/GroupContext.tsx index f8dd6bc..1916268 100644 --- a/src/contexts/GroupContext.tsx +++ b/src/contexts/GroupContext.tsx @@ -9,26 +9,41 @@ import { getGroupItems, groupsFromIds } from "../helpers/group"; import shortcuts from "../shortcuts"; import { Group, GroupContainer, GroupItem } from "../types/Group"; +export type GroupSelectMode = "single" | "multiple" | "range"; +export type GroupSelectModeChangeEventHandler = ( + selectMode: GroupSelectMode +) => void; +export type GroupOpenEventHandler = (groupId: string) => void; +export type GroupCloseEventHandler = () => void; +export type GroupsChangeEventHandler = (newGroups: Group[]) => void; +export type SubgroupsChangeEventHandler = ( + items: GroupItem[], + groupId: string +) => void; +export type GroupSelectEventHandler = (groupId: string) => void; +export type GroupsSelectEventHandler = (groupIds: string[]) => void; +export type GroupClearSelectionEventHandler = () => void; +export type GroupFilterChangeEventHandler = (filter: string) => void; +export type GroupClearFilterEventHandler = () => void; + type GroupContext = { groups: Group[]; - activeGroups: Group[]; + activeGroups: Group[] | GroupItem[]; openGroupId: string | undefined; - openGroupItems: Group[]; + openGroupItems: GroupItem[]; filter: string | undefined; filteredGroupItems: GroupItem[]; selectedGroupIds: string[]; - selectMode: any; - onSelectModeChange: React.Dispatch< - React.SetStateAction<"single" | "multiple" | "range"> - >; - onGroupOpen: (groupId: string) => void; - onGroupClose: () => void; - onGroupsChange: ( - newGroups: Group[] | GroupItem[], - groupId: string | undefined - ) => void; - onGroupSelect: (groupId: string | undefined) => void; - onFilterChange: React.Dispatch>; + selectMode: GroupSelectMode; + onSelectModeChange: GroupSelectModeChangeEventHandler; + onGroupOpen: GroupOpenEventHandler; + onGroupClose: GroupCloseEventHandler; + onGroupsChange: GroupsChangeEventHandler; + onSubgroupChange: SubgroupsChangeEventHandler; + onGroupSelect: GroupSelectEventHandler; + onClearSelection: GroupClearSelectionEventHandler; + onFilterChange: GroupFilterChangeEventHandler; + onFilterClear: GroupClearFilterEventHandler; }; const GroupContext = React.createContext(undefined); @@ -36,8 +51,8 @@ const GroupContext = React.createContext(undefined); type GroupProviderProps = { groups: Group[]; itemNames: Record; - onGroupsChange: (groups: Group[]) => void; - onGroupsSelect: (groupIds: string[]) => void; + onGroupsChange: GroupsChangeEventHandler; + onGroupsSelect: GroupsSelectEventHandler; disabled: boolean; children: React.ReactNode; }; @@ -51,15 +66,13 @@ export function GroupProvider({ children, }: GroupProviderProps) { const [selectedGroupIds, setSelectedGroupIds] = useState([]); - // Either single, multiple or range - const [selectMode, setSelectMode] = - useState<"single" | "multiple" | "range">("single"); + const [selectMode, setSelectMode] = useState("single"); /** * Group Open */ const [openGroupId, setOpenGroupId] = useState(); - const [openGroupItems, setOpenGroupItems] = useState([]); + const [openGroupItems, setOpenGroupItems] = useState([]); useEffect(() => { if (openGroupId) { const openGroups = groupsFromIds([openGroupId], groups); @@ -128,81 +141,78 @@ export function GroupProvider({ ? filteredGroupItems : groups; - /** - * @param {Group[] | GroupItem[]} newGroups - * @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object - */ - function handleGroupsChange( - newGroups: Group[] | GroupItem[], - groupId: string | undefined - ) { - if (groupId) { - // If a group is specidifed then update that group with the new items - const groupIndex = groups.findIndex((group) => group.id === groupId); - let updatedGroups = cloneDeep(groups); - const group = updatedGroups[groupIndex]; + function handleGroupsChange(newGroups: Group[]) { + onGroupsChange(newGroups); + } + + function handleSubgroupChange(items: GroupItem[], groupId: string) { + const groupIndex = groups.findIndex((group) => group.id === groupId); + let updatedGroups = cloneDeep(groups); + const group = updatedGroups[groupIndex]; + if (group.type === "group") { updatedGroups[groupIndex] = { ...group, - items: newGroups, - } as GroupContainer; + items, + }; onGroupsChange(updatedGroups); } else { - onGroupsChange(newGroups); + throw new Error(`Group ${group} not a subgroup`); } } - function handleGroupSelect(groupId: string | undefined) { + function handleGroupSelect(groupId: string) { let groupIds: string[] = []; - if (groupId) { - switch (selectMode) { - case "single": - groupIds = [groupId]; - break; - case "multiple": - if (selectedGroupIds.includes(groupId)) { - groupIds = selectedGroupIds.filter((id) => id !== groupId); - } else { - groupIds = [...selectedGroupIds, groupId]; - } - break; - case "range": - if (selectedGroupIds.length > 0) { - const currentIndex = activeGroups.findIndex( - (g) => g.id === groupId - ); - const lastIndex = activeGroups.findIndex( - (g) => g.id === selectedGroupIds[selectedGroupIds.length - 1] - ); - let idsToAdd: string[] = []; - let idsToRemove: string[] = []; - const direction = currentIndex > lastIndex ? 1 : -1; - for ( - let i = lastIndex + direction; - direction < 0 ? i >= currentIndex : i <= currentIndex; - i += direction - ) { - const id = activeGroups[i].id; - if (selectedGroupIds.includes(id)) { - idsToRemove.push(id); - } else { - idsToAdd.push(id); - } + switch (selectMode) { + case "single": + groupIds = [groupId]; + break; + case "multiple": + if (selectedGroupIds.includes(groupId)) { + groupIds = selectedGroupIds.filter((id) => id !== groupId); + } else { + groupIds = [...selectedGroupIds, groupId]; + } + break; + case "range": + if (selectedGroupIds.length > 0) { + const currentIndex = activeGroups.findIndex((g) => g.id === groupId); + const lastIndex = activeGroups.findIndex( + (g) => g.id === selectedGroupIds[selectedGroupIds.length - 1] + ); + let idsToAdd: string[] = []; + let idsToRemove: string[] = []; + const direction = currentIndex > lastIndex ? 1 : -1; + for ( + let i = lastIndex + direction; + direction < 0 ? i >= currentIndex : i <= currentIndex; + i += direction + ) { + const id = activeGroups[i].id; + if (selectedGroupIds.includes(id)) { + idsToRemove.push(id); + } else { + idsToAdd.push(id); } - groupIds = [...selectedGroupIds, ...idsToAdd].filter( - (id) => !idsToRemove.includes(id) - ); - } else { - groupIds = [groupId]; } - break; - default: - groupIds = []; - } + groupIds = [...selectedGroupIds, ...idsToAdd].filter( + (id) => !idsToRemove.includes(id) + ); + } else { + groupIds = [groupId]; + } + break; + default: + groupIds = []; } setSelectedGroupIds(groupIds); onGroupsSelect(groupIds); } + function handleClearSelection() { + setSelectedGroupIds([]); + onGroupsSelect([]); + } + /** * Shortcuts */ @@ -239,7 +249,7 @@ export function GroupProvider({ useBlur(handleBlur); - const value = { + const value: GroupContext = { groups, activeGroups, openGroupId, @@ -252,8 +262,11 @@ export function GroupProvider({ onGroupOpen: handleGroupOpen, onGroupClose: handleGroupClose, onGroupsChange: handleGroupsChange, + onSubgroupChange: handleSubgroupChange, onGroupSelect: handleGroupSelect, + onClearSelection: handleClearSelection, onFilterChange: setFilter, + onFilterClear: () => setFilter(undefined), }; return ( diff --git a/src/contexts/ImageSourceContext.tsx b/src/contexts/ImageSourceContext.tsx deleted file mode 100644 index c566300..0000000 --- a/src/contexts/ImageSourceContext.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { useContext, useState, useEffect, ReactChild } from "react"; -import { ImageFile } from "../helpers/image"; - -import { omit } from "../helpers/shared"; - -export const ImageSourcesStateContext = React.createContext(undefined) as any; -export const ImageSourcesUpdaterContext = React.createContext(() => {}) as any; - -/** - * Helper to manage sharing of custom image sources between uses of useImageSource - */ -export function ImageSourcesProvider({ children }: { children: ReactChild }) { - const [imageSources, setImageSources] = useState({}); - - // Revoke url when no more references - useEffect(() => { - let sourcesToCleanup: any = []; - for (let source of Object.values(imageSources) as any) { - if (source.references <= 0) { - URL.revokeObjectURL(source.url); - sourcesToCleanup.push(source.id); - } - } - if (sourcesToCleanup.length > 0) { - setImageSources((prevSources: any) => omit(prevSources, sourcesToCleanup)); - } - }, [imageSources]); - - return ( - - - {children} - - - ); -} - -/** - * Get id from image data - */ -function getImageFileId(data: any, thumbnail: ImageFile) { - if (thumbnail) { - return `${data.id}-thumbnail`; - } - if (data.resolutions) { - // Check is a resolution is specified - if (data.quality && data.resolutions[data.quality]) { - return `${data.id}-${data.quality}`; - } else if (!data.file) { - // Fallback to the highest resolution - const resolutionArray = Object.keys(data.resolutions); - const resolution: any = resolutionArray[resolutionArray.length - 1]; - return `${data.id}-${resolution.id}`; - } - } - return data.id; -} - -/** - * Helper function to load either file or default image into a URL - */ -export function useImageSource(data: any, defaultSources: string, unknownSource: string, thumbnail: ImageFile) { - const imageSources: any = useContext(ImageSourcesStateContext); - if (imageSources === undefined) { - throw new Error( - "useImageSource must be used within a ImageSourcesProvider" - ); - } - const setImageSources: any = useContext(ImageSourcesUpdaterContext); - if (setImageSources === undefined) { - throw new Error( - "useImageSource must be used within a ImageSourcesProvider" - ); - } - - useEffect(() => { - if (!data || data.type !== "file") { - return; - } - const id = getImageFileId(data, thumbnail); - - function updateImageSource(file: File) { - if (file) { - setImageSources((prevSources: any) => { - if (id in prevSources) { - // Check if the image source is already added - return { - ...prevSources, - [id]: { - ...prevSources[id], - // Increase references - references: prevSources[id].references + 1, - }, - }; - } else { - const url = URL.createObjectURL(new Blob([file])); - return { - ...prevSources, - [id]: { url, id, references: 1 }, - }; - } - }); - } - } - - if (thumbnail) { - updateImageSource(data.thumbnail.file); - } else if (data.resolutions) { - // Check is a resolution is specified - if (data.quality && data.resolutions[data.quality]) { - updateImageSource(data.resolutions[data.quality].file); - } - // If no file available fallback to the highest resolution - else if (!data.file) { - const resolutionArray = Object.keys(data.resolutions); - updateImageSource( - data.resolutions[resolutionArray[resolutionArray.length - 1]].file - ); - } else { - updateImageSource(data.file); - } - } else { - updateImageSource(data.file); - } - - return () => { - // Decrease references - setImageSources((prevSources: any) => { - if (id in prevSources) { - return { - ...prevSources, - [id]: { - ...prevSources[id], - references: prevSources[id].references - 1, - }, - }; - } else { - return prevSources; - } - }); - }; - }, [data, unknownSource, thumbnail, setImageSources]); - - if (!data) { - return unknownSource; - } - - if (data.type === "default") { - return defaultSources[data.key]; - } - - if (data.type === "file") { - const id = getImageFileId(data, thumbnail); - return imageSources[id]?.url; - } - - return unknownSource; -} diff --git a/src/contexts/MapDataContext.tsx b/src/contexts/MapDataContext.tsx index fd12cf9..5cd76ef 100644 --- a/src/contexts/MapDataContext.tsx +++ b/src/contexts/MapDataContext.tsx @@ -53,10 +53,7 @@ type MapDataContext = { const MapDataContext = React.createContext(undefined); -const defaultMapState: Pick< - MapState, - "tokens" | "drawShapes" | "fogShapes" | "editFlags" | "notes" -> = { +const defaultMapState: Omit = { tokens: {}, drawShapes: {}, fogShapes: {}, diff --git a/src/contexts/PartyContext.tsx b/src/contexts/PartyContext.tsx index 716076f..12e7406 100644 --- a/src/contexts/PartyContext.tsx +++ b/src/contexts/PartyContext.tsx @@ -1,10 +1,16 @@ import React, { useState, useEffect, useContext } from "react"; -import { PartyState } from "../components/party/PartyState"; import Session from "../network/Session"; +import { PartyState } from "../types/PartyState"; + const PartyContext = React.createContext(undefined); -export function PartyProvider({ session, children }: { session: Session, children: any}) { +type PartyProviderProps = { + session: Session; + children: React.ReactNode; +}; + +export function PartyProvider({ session, children }: PartyProviderProps) { const [partyState, setPartyState] = useState({}); useEffect(() => { diff --git a/src/contexts/PlayerContext.tsx b/src/contexts/PlayerContext.tsx index 4f0f4eb..e7d7003 100644 --- a/src/contexts/PlayerContext.tsx +++ b/src/contexts/PlayerContext.tsx @@ -5,29 +5,32 @@ import { useUserId } from "./UserIdContext"; import { getRandomMonster } from "../helpers/monsters"; -import useNetworkedState from "../hooks/useNetworkedState"; +import useNetworkedState, { + SetNetworkedState, +} from "../hooks/useNetworkedState"; import Session from "../network/Session"; -import { PlayerInfo } from "../components/party/PartyState"; +import { PlayerState } from "../types/PlayerState"; -export const PlayerStateContext = React.createContext(undefined); -export const PlayerUpdaterContext = React.createContext(() => {}); +export const PlayerStateContext = + React.createContext(undefined); +export const PlayerUpdaterContext = + React.createContext | undefined>(undefined); -export function PlayerProvider({ - session, - children, -}: { +type PlayerProviderProps = { session: Session; children: React.ReactNode; -}) { +}; + +export function PlayerProvider({ session, children }: PlayerProviderProps) { const userId = useUserId(); const { database, databaseStatus } = useDatabase(); - const [playerState, setPlayerState] = useNetworkedState( + const [playerState, setPlayerState] = useNetworkedState( { nickname: "", - timer: null, + timer: undefined, dice: { share: false, rolls: [] }, - sessionId: null, + sessionId: undefined, userId, }, session, @@ -43,13 +46,13 @@ export function PlayerProvider({ async function loadNickname() { const storedNickname = await database?.table("user").get("nickname"); if (storedNickname !== undefined) { - setPlayerState((prevState: PlayerInfo) => ({ + setPlayerState((prevState) => ({ ...prevState, nickname: storedNickname.value, })); } else { const name = getRandomMonster(); - setPlayerState((prevState: any) => ({ ...prevState, nickname: name })); + setPlayerState((prevState) => ({ ...prevState, nickname: name })); database?.table("user").add({ key: "nickname", value: name }); } } @@ -71,7 +74,7 @@ export function PlayerProvider({ useEffect(() => { if (userId) { - setPlayerState((prevState: PlayerInfo) => { + setPlayerState((prevState) => { if (prevState) { return { ...prevState, @@ -85,8 +88,7 @@ export function PlayerProvider({ useEffect(() => { function updateSessionId() { - setPlayerState((prevState: PlayerInfo) => { - // TODO: check useNetworkState requirements here + setPlayerState((prevState) => { if (prevState) { return { ...prevState, diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx index 097c005..40ba49a 100644 --- a/src/contexts/SettingsContext.tsx +++ b/src/contexts/SettingsContext.tsx @@ -14,7 +14,7 @@ const SettingsContext = const settingsProvider = getSettings(); -export function SettingsProvider({ children }: { children: any }) { +export function SettingsProvider({ children }: { children: React.ReactNode }) { const [settings, setSettings] = useState(settingsProvider.getAll()); useEffect(() => { diff --git a/src/contexts/TileDragContext.tsx b/src/contexts/TileDragContext.tsx index 2a3844c..e344833 100644 --- a/src/contexts/TileDragContext.tsx +++ b/src/contexts/TileDragContext.tsx @@ -19,6 +19,7 @@ import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group"; import Vector2 from "../helpers/Vector2"; import usePreventSelect from "../hooks/usePreventSelect"; +import { GroupItem } from "../types/Group"; const TileDragIdContext = React.createContext(undefined); @@ -72,7 +73,9 @@ export function TileDragProvider({ openGroupId, selectedGroupIds, onGroupsChange, + onSubgroupChange, onGroupSelect, + onClearSelection, filter, } = useGroup(); @@ -145,24 +148,28 @@ export function TileDragProvider({ selectedIndices = selectedIndices.sort((a, b) => a - b); if (over.id.startsWith(GROUP_ID_PREFIX)) { - onGroupSelect(undefined); + onClearSelection(); // Handle tile group const overId = over.id.slice(9); if (overId !== active.id) { const overGroupIndex = activeGroups.findIndex( (group) => group.id === overId ); - onGroupsChange( - moveGroupsInto(activeGroups, overGroupIndex, selectedIndices), - openGroupId + const newGroups = moveGroupsInto( + activeGroups, + overGroupIndex, + selectedIndices ); + if (!openGroupId) { + onGroupsChange(newGroups); + } } } else if (over.id === UNGROUP_ID) { if (openGroupId) { - onGroupSelect(undefined); + onClearSelection(); // Handle tile ungroup const newGroups = ungroup(groups, openGroupId, selectedIndices); - onGroupsChange(newGroups, undefined); + onGroupsChange(newGroups); } } else if (over.id === ADD_TO_MAP_ID) { onDragAdd && @@ -173,10 +180,16 @@ export function TileDragProvider({ const overGroupIndex = activeGroups.findIndex( (group) => group.id === over.id ); - onGroupsChange( - moveGroups(activeGroups, overGroupIndex, selectedIndices), - openGroupId + const newGroups = moveGroups( + activeGroups, + overGroupIndex, + selectedIndices ); + if (openGroupId) { + onSubgroupChange(newGroups as GroupItem[], openGroupId); + } else { + onGroupsChange(newGroups); + } } } diff --git a/src/contexts/TokenDataContext.tsx b/src/contexts/TokenDataContext.tsx index fadc712..19f5d6a 100644 --- a/src/contexts/TokenDataContext.tsx +++ b/src/contexts/TokenDataContext.tsx @@ -23,7 +23,7 @@ export type UpdateTokenEventHandler = ( export type GetTokenEventHandler = ( tokenId: string ) => Promise; -export type UpdateTokenGroupsEventHandler = (groups: any[]) => Promise; +export type UpdateTokenGroupsEventHandler = (groups: Group[]) => Promise; export type UpdateTokensHiddenEventHandler = ( ids: string[], hideInSidebar: boolean diff --git a/src/database.ts b/src/database.ts index 1874ec6..0c565da 100644 --- a/src/database.ts +++ b/src/database.ts @@ -2,7 +2,7 @@ import Dexie, { DexieOptions } from "dexie"; import { v4 as uuid } from "uuid"; -import { loadVersions } from "./upgrade"; +import { loadVersions, UpgradeEventHandler } from "./upgrade"; import { getDefaultMaps } from "./maps"; import { getDefaultTokens } from "./tokens"; @@ -10,7 +10,7 @@ import { getDefaultTokens } from "./tokens"; * Populate DB with initial data * @param {Dexie} db */ -function populate(db) { +function populate(db: Dexie) { db.on("populate", () => { const userId = uuid(); db.table("user").add({ key: "userId", value: userId }); @@ -35,16 +35,16 @@ function populate(db) { * @param {string=} name * @param {number=} versionNumber * @param {boolean=} populateData - * @param {import("./upgrade").OnUpgrade=} onUpgrade + * @param {UpgradeEventHandler=} onUpgrade * @returns {Dexie} */ export function getDatabase( options: DexieOptions, - name = "OwlbearRodeoDB", - versionNumber = undefined, - populateData = true, - onUpgrade = undefined -) { + name: string | undefined = "OwlbearRodeoDB", + versionNumber: number | undefined = undefined, + populateData: boolean | undefined = true, + onUpgrade: UpgradeEventHandler | undefined = undefined +): Dexie { let db = new Dexie(name, options); loadVersions(db, versionNumber, onUpgrade); if (populateData) { diff --git a/src/dice/Dice.ts b/src/dice/Dice.ts index 275680b..0264711 100644 --- a/src/dice/Dice.ts +++ b/src/dice/Dice.ts @@ -16,15 +16,13 @@ import d100Source from "./shared/d100.glb"; import { lerp } from "../helpers/shared"; import { importTextureAsync } from "../helpers/babylon"; +import { InstancedMesh, Material, Mesh, Scene } from "@babylonjs/core"; import { - BaseTexture, - InstancedMesh, - Material, - Mesh, - Scene, - Texture, -} from "@babylonjs/core"; -import { DiceType } from "../types/Dice"; + DiceType, + BaseDiceTextureSources, + isDiceMeshes, + DiceMeshes, +} from "../types/Dice"; const minDiceRollSpeed = 600; const maxDiceRollSpeed = 800; @@ -35,13 +33,11 @@ class Dice { static async loadMeshes( material: Material, scene: Scene, - sourceOverrides?: any - ): Promise> { - let meshes: any = {}; - const addToMeshes = async (type: string | number, defaultSource: any) => { - let source: string = sourceOverrides - ? sourceOverrides[type] - : defaultSource; + sourceOverrides?: Record + ): Promise { + let meshes: Partial = {}; + const addToMeshes = async (type: DiceType, defaultSource: string) => { + let source = sourceOverrides ? sourceOverrides[type] : defaultSource; const mesh = await this.loadMesh(source, material, scene); meshes[type] = mesh; }; @@ -54,12 +50,16 @@ class Dice { addToMeshes("d20", d20Source), addToMeshes("d100", d100Source), ]); - return meshes; + if (isDiceMeshes(meshes)) { + return meshes; + } else { + throw new Error("Dice meshes failed to load, missing mesh source"); + } } static async loadMesh(source: string, material: Material, scene: Scene) { let mesh = (await SceneLoader.ImportMeshAsync("", source, "", scene)) - .meshes[1]; + .meshes[1] as Mesh; mesh.setParent(null); mesh.material = material; @@ -69,19 +69,18 @@ class Dice { return mesh; } - static async loadMaterial(materialName: string, textures: any, scene: Scene) { + static async loadMaterial( + materialName: string, + textures: BaseDiceTextureSources, + scene: Scene + ) { let pbr = new PBRMaterial(materialName, scene); - let [albedo, normal, metalRoughness]: [ - albedo: BaseTexture, - normal: Texture, - metalRoughness: Texture - ] = await Promise.all([ + let [albedo, normal, metalRoughness] = await Promise.all([ importTextureAsync(textures.albedo), importTextureAsync(textures.normal), importTextureAsync(textures.metalRoughness), ]); pbr.albedoTexture = albedo; - // pbr.normalTexture = normal; pbr.bumpTexture = normal; pbr.metallicTexture = metalRoughness; pbr.useRoughnessFromMetallicTextureAlpha = false; @@ -98,12 +97,10 @@ class Dice { ) { let instance = mesh.createInstance(name); instance.position = mesh.position; - for (let child of mesh.getChildTransformNodes()) { - // TODO: type correctly another time -> should not be any - const locator: any = child.clone(child.name, instance); - // TODO: handle possible null value + for (let child of mesh.getChildMeshes()) { + const locator = child.clone(child.name, instance); if (!locator) { - throw Error; + throw new Error("Unable to clone dice locator"); } locator.setAbsolutePosition(child.getAbsolutePosition()); locator.name = child.name; @@ -120,7 +117,7 @@ class Dice { return instance; } - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { switch (diceType) { case "d4": return { mass: 4, friction: 4 }; @@ -133,7 +130,7 @@ class Dice { return { mass: 7, friction: 4 }; case "d12": return { mass: 8, friction: 4 }; - case "20": + case "d20": return { mass: 10, friction: 4 }; default: return { mass: 10, friction: 4 }; @@ -145,12 +142,14 @@ class Dice { instance.physicsImpostor?.setAngularVelocity(Vector3.Zero()); const scene = instance.getScene(); - // TODO: remove any typing in this function -> this is just to get it working - const diceTraySingle: any = scene.getNodeByID("dice_tray_single"); - const diceTrayDouble = scene.getNodeByID("dice_tray_double"); - const visibleDiceTray: any = diceTraySingle?.isVisible + const diceTraySingle = scene.getMeshByID("dice_tray_single"); + const diceTrayDouble = scene.getMeshByID("dice_tray_double"); + const visibleDiceTray = diceTraySingle?.isVisible ? diceTraySingle : diceTrayDouble; + if (!visibleDiceTray) { + throw new Error("No dice tray to roll in"); + } const trayBounds = visibleDiceTray?.getBoundingInfo().boundingBox; const position = new Vector3( diff --git a/src/dice/diceTray/DiceTray.ts b/src/dice/diceTray/DiceTray.ts index e2237bd..78ffd86 100644 --- a/src/dice/diceTray/DiceTray.ts +++ b/src/dice/diceTray/DiceTray.ts @@ -2,10 +2,9 @@ import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader"; import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { PhysicsImpostor } from "@babylonjs/core/Physics/physicsImpostor"; import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { AbstractMesh, Scene, ShadowGenerator } from "@babylonjs/core"; -//@ts-ignore import singleMeshSource from "./single.glb"; -//@ts-ignore import doubleMeshSource from "./double.glb"; import singleAlbedo from "./singleAlbedo.jpg"; @@ -17,7 +16,6 @@ import doubleMetalRoughness from "./doubleMetalRoughness.jpg"; import doubleNormal from "./doubleNormal.jpg"; import { importTextureAsync } from "../../helpers/babylon"; -import { Scene, ShadowGenerator, Texture } from "@babylonjs/core"; class DiceTray { _size; @@ -30,12 +28,12 @@ class DiceTray { this._size = newSize; const wallOffsetWidth = this.collisionSize / 2 + this.width / 2 - 0.5; const wallOffsetHeight = this.collisionSize / 2 + this.height / 2 - 0.5; - this.wallTop.position.z = -wallOffsetHeight; - this.wallRight.position.x = -wallOffsetWidth; - this.wallBottom.position.z = wallOffsetHeight; - this.wallLeft.position.x = wallOffsetWidth; - this.singleMesh.isVisible = newSize === "single"; - this.doubleMesh.isVisible = newSize === "double"; + if (this.wallTop) this.wallTop.position.z = -wallOffsetHeight; + if (this.wallRight) this.wallRight.position.x = -wallOffsetWidth; + if (this.wallBottom) this.wallBottom.position.z = wallOffsetHeight; + if (this.wallLeft) this.wallLeft.position.x = wallOffsetWidth; + if (this.singleMesh) this.singleMesh.isVisible = newSize === "single"; + if (this.doubleMesh) this.doubleMesh.isVisible = newSize === "double"; } scene; @@ -44,17 +42,21 @@ class DiceTray { get width() { return this.size === "single" ? 10 : 20; } - + height = 20; collisionSize = 50; - wallTop: any; - wallRight: any; - wallBottom: any; - wallLeft: any; - singleMesh: any; - doubleMesh: any; + wallTop?: Mesh; + wallRight?: Mesh; + wallBottom?: Mesh; + wallLeft?: Mesh; + singleMesh?: AbstractMesh; + doubleMesh?: AbstractMesh; - constructor(initialSize: string, scene: Scene, shadowGenerator: ShadowGenerator) { + constructor( + initialSize: string, + scene: Scene, + shadowGenerator: ShadowGenerator + ) { this._size = initialSize; this.scene = scene; this.shadowGenerator = shadowGenerator; @@ -65,7 +67,13 @@ class DiceTray { await this.loadMeshes(); } - createCollision(name: string, x: number, y: number, z: number, friction: number) { + createCollision( + name: string, + x: number, + y: number, + z: number, + friction: number + ): Mesh { let collision = Mesh.CreateBox( name, this.collisionSize, @@ -134,15 +142,6 @@ class DiceTray { doubleAlbedoTexture, doubleNormalTexture, doubleMetalRoughnessTexture, - ]: [ - singleMeshes: any, - doubleMeshes: any, - singleAlbedoTexture: Texture, - singleNormalTexture: Texture, - singleMetalRoughnessTexture: Texture, - doubleAlbedoTexture: Texture, - doubleNormalTexture: Texture, - doubleMetalRoughnessTexture: Texture ] = await Promise.all([ SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene), SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene), @@ -159,8 +158,6 @@ class DiceTray { this.singleMesh.name = "dice_tray"; let singleMaterial = new PBRMaterial("dice_tray_mat_single", this.scene); singleMaterial.albedoTexture = singleAlbedoTexture; - // TODO: ask Mitch about texture - // singleMaterial.normalTexture = singleNormalTexture; singleMaterial.bumpTexture = singleNormalTexture; singleMaterial.metallicTexture = singleMetalRoughnessTexture; singleMaterial.useRoughnessFromMetallicTextureAlpha = false; @@ -177,8 +174,6 @@ class DiceTray { this.doubleMesh.name = "dice_tray"; let doubleMaterial = new PBRMaterial("dice_tray_mat_double", this.scene); doubleMaterial.albedoTexture = doubleAlbedoTexture; - // TODO: ask Mitch about texture - //doubleMaterial.normalTexture = doubleNormalTexture; doubleMaterial.bumpTexture = doubleNormalTexture; doubleMaterial.metallicTexture = doubleMetalRoughnessTexture; doubleMaterial.useRoughnessFromMetallicTextureAlpha = false; diff --git a/src/dice/galaxy/GalaxyDice.ts b/src/dice/galaxy/GalaxyDice.ts index 9947f27..c05de32 100644 --- a/src/dice/galaxy/GalaxyDice.ts +++ b/src/dice/galaxy/GalaxyDice.ts @@ -1,12 +1,14 @@ -import { InstancedMesh, Material, Mesh, Scene } from "@babylonjs/core"; +import { InstancedMesh, Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class GalaxyDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; static async load(scene: Scene) { @@ -22,8 +24,7 @@ class GalaxyDice extends Dice { } } - // TODO: check static -> rename function? - static createInstance(diceType: string, scene: Scene): InstancedMesh { + static createInstance(diceType: DiceType, scene: Scene): InstancedMesh { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/gemstone/GemstoneDice.ts b/src/dice/gemstone/GemstoneDice.ts index b6a2043..bc4653b 100644 --- a/src/dice/gemstone/GemstoneDice.ts +++ b/src/dice/gemstone/GemstoneDice.ts @@ -1,5 +1,6 @@ import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { Color3 } from "@babylonjs/core/Maths/math"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; @@ -8,18 +9,22 @@ import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; import { importTextureAsync } from "../../helpers/babylon"; -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { BaseDiceTextureSources, DiceMeshes, DiceType } from "../../types/Dice"; class GemstoneDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 1.5, friction: properties.friction }; } - static async loadMaterial(materialName: string, textures: any, scene: Scene) { + static async loadMaterial( + materialName: string, + textures: BaseDiceTextureSources, + scene: Scene + ) { let pbr = new PBRMaterial(materialName, scene); let [albedo, normal, metalRoughness] = await Promise.all([ importTextureAsync(textures.albedo), @@ -27,7 +32,6 @@ class GemstoneDice extends Dice { importTextureAsync(textures.metalRoughness), ]); pbr.albedoTexture = albedo; - // TODO: ask Mitch about texture pbr.bumpTexture = normal; pbr.metallicTexture = metalRoughness; pbr.useRoughnessFromMetallicTextureAlpha = false; @@ -56,7 +60,7 @@ class GemstoneDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/glass/GlassDice.ts b/src/dice/glass/GlassDice.ts index a745487..a631048 100644 --- a/src/dice/glass/GlassDice.ts +++ b/src/dice/glass/GlassDice.ts @@ -1,5 +1,6 @@ import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { Color3 } from "@babylonjs/core/Maths/math"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; @@ -8,18 +9,28 @@ import mask from "./mask.png"; import normal from "./normal.jpg"; import { importTextureAsync } from "../../helpers/babylon"; -import { Material, Mesh, Scene } from "@babylonjs/core"; + +import { BaseDiceTextureSources, DiceMeshes, DiceType } from "../../types/Dice"; + +type GlassDiceTextureSources = Pick< + BaseDiceTextureSources, + "albedo" | "normal" +> & { mask: string }; class GlassDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 1.5, friction: properties.friction }; } - static async loadMaterial(materialName: string, textures: any, scene: Scene) { + static async loadGlassMaterial( + materialName: string, + textures: GlassDiceTextureSources, + scene: Scene + ) { let pbr = new PBRMaterial(materialName, scene); let [albedo, normal, mask] = await Promise.all([ importTextureAsync(textures.albedo), @@ -27,7 +38,6 @@ class GlassDice extends Dice { importTextureAsync(textures.mask), ]); pbr.albedoTexture = albedo; - // pbr.normalTexture = normal; pbr.bumpTexture = normal; pbr.roughness = 0.25; pbr.metallic = 0; @@ -47,7 +57,7 @@ class GlassDice extends Dice { static async load(scene: Scene) { if (!this.material) { - this.material = await this.loadMaterial( + this.material = await this.loadGlassMaterial( "glass_pbr", { albedo, mask, normal }, scene @@ -58,7 +68,7 @@ class GlassDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/iron/IronDice.ts b/src/dice/iron/IronDice.ts index 2e9c5b7..c9a7be8 100644 --- a/src/dice/iron/IronDice.ts +++ b/src/dice/iron/IronDice.ts @@ -1,15 +1,17 @@ -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class IronDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 2, friction: properties.friction }; } @@ -27,7 +29,7 @@ class IronDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/nebula/NebulaDice.ts b/src/dice/nebula/NebulaDice.ts index eb08d26..662b569 100644 --- a/src/dice/nebula/NebulaDice.ts +++ b/src/dice/nebula/NebulaDice.ts @@ -1,12 +1,14 @@ -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class NebulaDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; static async load(scene: Scene) { @@ -22,7 +24,7 @@ class NebulaDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/sunrise/SunriseDice.ts b/src/dice/sunrise/SunriseDice.ts index 233886c..2d1df72 100644 --- a/src/dice/sunrise/SunriseDice.ts +++ b/src/dice/sunrise/SunriseDice.ts @@ -1,12 +1,14 @@ -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class SunriseDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; static async load(scene: Scene) { @@ -22,7 +24,7 @@ class SunriseDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/sunset/SunsetDice.ts b/src/dice/sunset/SunsetDice.ts index 7e66a20..bfa107f 100644 --- a/src/dice/sunset/SunsetDice.ts +++ b/src/dice/sunset/SunsetDice.ts @@ -1,12 +1,14 @@ -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class SunsetDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; static async load(scene: Scene) { @@ -22,7 +24,7 @@ class SunsetDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/walnut/WalnutDice.ts b/src/dice/walnut/WalnutDice.ts index 9813f11..c489723 100644 --- a/src/dice/walnut/WalnutDice.ts +++ b/src/dice/walnut/WalnutDice.ts @@ -1,3 +1,4 @@ +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; @@ -11,8 +12,7 @@ import d10Source from "./d10.glb"; import d12Source from "./d12.glb"; import d20Source from "./d20.glb"; import d100Source from "./d100.glb"; -import { Material, Mesh, Scene } from "@babylonjs/core"; -import { DiceType } from "../../types/Dice"; +import { DiceMeshes, DiceType } from "../../types/Dice"; const sourceOverrides = { d4: d4Source, @@ -25,10 +25,10 @@ const sourceOverrides = { }; class WalnutDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 1.4, friction: properties.friction }; } diff --git a/src/global.d.ts b/src/global.d.ts index 8e26e5a..3d962a4 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -2,8 +2,20 @@ declare module "pepjs"; declare module "socket.io-msgpack-parser"; declare module "fake-indexeddb"; declare module "fake-indexeddb/lib/FDBKeyRange"; -declare module "*.glb"; -declare module "*.png"; -declare module "*.mp4"; -declare module "*.bin"; +declare module "*.glb" { + const source: string; + export default source; +} +declare module "*.png" { + const source: string; + export default source; +} +declare module "*.mp4" { + const source: string; + export default source; +} +declare module "*.bin" { + const source: string; + export default source; +} declare module "react-router-hash-link"; diff --git a/src/helpers/FakeStorage.ts b/src/helpers/FakeStorage.ts index 48fcd7f..b4af379 100644 --- a/src/helpers/FakeStorage.ts +++ b/src/helpers/FakeStorage.ts @@ -2,11 +2,11 @@ * A faked local or session storage used when the user has disabled storage */ class FakeStorage { - data: { [keyName: string ]: any} = {}; + data: { [keyName: string]: any } = {}; key(index: number) { return Object.keys(this.data)[index] || null; } - getItem(keyName: string ) { + getItem(keyName: string) { return this.data[keyName] || null; } setItem(keyName: string, keyValue: any) { diff --git a/src/helpers/Vector2.ts b/src/helpers/Vector2.ts index 8e727db..f3f6e37 100644 --- a/src/helpers/Vector2.ts +++ b/src/helpers/Vector2.ts @@ -153,36 +153,51 @@ class Vector2 { } /** - * Returns the min of `value` and `minimum`, if `minimum` is undefined component wise min is returned instead + * Returns the min of `a` and `b` * @param {Vector2} a - * @param {(Vector2 | number)} [minimum] Value to compare - * @returns {(Vector2 | number)} + * @param {Vector2 | number} b Value to compare + * @returns {Vector2} */ - static min(a: Vector2, minimum?: Vector2 | number): Vector2 | number { - if (minimum === undefined) { - return a.x < a.y ? a.x : a.y; - } else if (typeof minimum === "number") { - return { x: Math.min(a.x, minimum), y: Math.min(a.y, minimum) }; + static min(a: Vector2, b: Vector2 | number): Vector2 { + if (typeof b === "number") { + return { x: Math.min(a.x, b), y: Math.min(a.y, b) }; } else { - return { x: Math.min(a.x, minimum.x), y: Math.min(a.y, minimum.y) }; + return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y) }; } } + /** - * Returns the max of `a` and `maximum`, if `maximum` is undefined component wise max is returned instead + * Returns the component wise minimum of `a` * @param {Vector2} a - * @param {(Vector2 | number)} [maximum] Value to compare - * @returns {(Vector2 | number)} + * @returns {number} */ - static max(a: Vector2, maximum?: Vector2 | number): Vector2 | number { - if (maximum === undefined) { - return a.x > a.y ? a.x : a.y; - } else if (typeof maximum === "number") { - return { x: Math.max(a.x, maximum), y: Math.max(a.y, maximum) }; + static componentMin(a: Vector2): number { + return a.x < a.y ? a.x : a.y; + } + + /** + * Returns the max of `a` and `b` + * @param {Vector2} a + * @param {Vector2 | number} b Value to compare + * @returns {Vector2} + */ + static max(a: Vector2, b: Vector2 | number): Vector2 { + if (typeof b === "number") { + return { x: Math.max(a.x, b), y: Math.max(a.y, b) }; } else { - return { x: Math.max(a.x, maximum.x), y: Math.max(a.y, maximum.y) }; + return { x: Math.max(a.x, b.x), y: Math.max(a.y, b.y) }; } } + /** + * Returns the component wise maximum of `a` + * @param {Vector2} a + * @returns {number)} + */ + static componentMax(a: Vector2): number { + return a.x > a.y ? a.x : a.y; + } + /** * Rounds `p` to the nearest value of `to` * @param {Vector2} p diff --git a/src/helpers/actions.ts b/src/helpers/actions.ts index a1870b8..a50b77f 100644 --- a/src/helpers/actions.ts +++ b/src/helpers/actions.ts @@ -1,20 +1,31 @@ +import { MultiPolygon, Ring, Polygon, Geom } from "polygon-clipping"; import shortid from "shortid"; +import { Fog, FogState } from "../types/Fog"; -export function addPolygonDifferenceToShapes(shape: any, difference: any, shapes: any) { +export function addPolygonDifferenceToFog( + fog: Fog, + difference: MultiPolygon, + shapes: FogState +) { for (let i = 0; i < difference.length; i++) { let newId = shortid.generate(); // Holes detected let holes = []; if (difference[i].length > 1) { for (let j = 1; j < difference[i].length; j++) { - holes.push(difference[i][j].map(([x, y]: [ x: number, y: number ]) => ({ x, y }))); + holes.push( + difference[i][j].map(([x, y]: [x: number, y: number]) => ({ x, y })) + ); } } - const points = difference[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y })); + const points = difference[i][0].map(([x, y]: [x: number, y: number]) => ({ + x, + y, + })); shapes[newId] = { - ...shape, + ...fog, id: newId, data: { points, @@ -24,11 +35,18 @@ export function addPolygonDifferenceToShapes(shape: any, difference: any, shapes } } -export function addPolygonIntersectionToShapes(shape: any, intersection: any, shapes: any) { +export function addPolygonIntersectionToFog( + shape: Fog, + intersection: MultiPolygon, + shapes: FogState +) { for (let i = 0; i < intersection.length; i++) { let newId = shortid.generate(); - const points = intersection[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y })); + const points = intersection[i][0].map(([x, y]: [x: number, y: number]) => ({ + x, + y, + })); shapes[newId] = { ...shape, @@ -43,9 +61,9 @@ export function addPolygonIntersectionToShapes(shape: any, intersection: any, sh } } -export function shapeToGeometry(shape) { - const shapePoints = shape.data.points.map(({ x, y }) => [x, y]); - const shapeHoles = shape.data.holes.map((hole) => +export function fogToGeometry(fog: Fog): Geom { + const shapePoints: Ring = fog.data.points.map(({ x, y }) => [x, y]); + const shapeHoles: Polygon = fog.data.holes.map((hole) => hole.map(({ x, y }) => [x, y]) ); return [[shapePoints, ...shapeHoles]]; diff --git a/src/helpers/blobToBuffer.ts b/src/helpers/blobToBuffer.ts index 905f5d5..f97396a 100644 --- a/src/helpers/blobToBuffer.ts +++ b/src/helpers/blobToBuffer.ts @@ -7,16 +7,12 @@ async function blobToBuffer(blob: Blob): Promise { const arrayBuffer = await blob.arrayBuffer(); return new Uint8Array(arrayBuffer); } else { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const reader = new FileReader(); - function onLoadEnd(event: any) { + function onLoadEnd() { reader.removeEventListener("loadend", onLoadEnd, false); - if (event.error) { - reject(event.error); - } else { - resolve(Buffer.from(reader.result as ArrayBuffer)); - } + resolve(Buffer.from(reader.result as ArrayBuffer)); } reader.addEventListener("loadend", onLoadEnd, false); diff --git a/src/helpers/dexie.js b/src/helpers/dexie.js deleted file mode 100644 index 9c82927..0000000 --- a/src/helpers/dexie.js +++ /dev/null @@ -1,48 +0,0 @@ -import set from "lodash.set"; -import unset from "lodash.unset"; -import cloneDeep from "lodash.clonedeep"; - -/** - * Remove all empty values from an object recursively - * @param {Object} obj - */ -function trimArraysInObject(obj) { - for (let key in obj) { - const value = obj[key]; - if (Array.isArray(value)) { - let arr = []; - for (let i = 0; i < value.length; i++) { - const el = value[i]; - if (typeof el === "object") { - arr.push(trimArraysInObject(el)); - } else if (el !== undefined) { - arr.push(el); - } - } - obj[key] = arr; - } else if (typeof obj[key] === "object") { - obj[key] = trimArraysInObject(obj[key]); - } - } - return obj; -} - -export function applyObservableChange(change) { - // Custom application of dexie change to fix issue with array indices being wrong - // https://github.com/dfahlander/Dexie.js/issues/1176 - // TODO: Fix dexie observable source - let obj = cloneDeep(change.oldObj); - const changes = Object.entries(change.mods).reverse(); - for (let [key, value] of changes) { - if (value === null) { - unset(obj, key); - } else { - obj = set(obj, key, value); - } - } - - // Trim empty values from calling unset on arrays - obj = trimArraysInObject(obj); - - return obj; -} diff --git a/src/helpers/dice.ts b/src/helpers/dice.ts index 8cb086d..1251506 100644 --- a/src/helpers/dice.ts +++ b/src/helpers/dice.ts @@ -1,13 +1,15 @@ +import { InstancedMesh, TransformNode } from "@babylonjs/core"; import { Vector3 } from "@babylonjs/core/Maths/math"; -import { DiceRoll } from "../types/Dice"; + +import { DiceMesh, DiceRoll } from "../types/Dice"; /** * Find the number facing up on a mesh instance of a dice * @param {Object} instance The dice instance */ -export function getDiceInstanceRoll(instance: any) { +export function getDiceInstanceRoll(instance: InstancedMesh) { let highestDot = -1; - let highestLocator; + let highestLocator: TransformNode | undefined = undefined; for (let locator of instance.getChildTransformNodes()) { let dif = locator .getAbsolutePosition() @@ -19,17 +21,19 @@ export function getDiceInstanceRoll(instance: any) { highestLocator = locator; } } + if (!highestLocator) { + return 0; + } return parseInt(highestLocator.name.slice(12)); } /** * Find the number facing up on a dice object - * @param {Object} dice The Dice object */ -export function getDiceRoll(dice: any) { +export function getDiceRoll(dice: DiceMesh) { let number = getDiceInstanceRoll(dice.instance); // If the dice is a d100 add the d10 - if (dice.type === "d100") { + if (dice.d10Instance) { const d10Number = getDiceInstanceRoll(dice.d10Instance); // Both zero set to 100 if (d10Number === 0 && number === 0) { @@ -44,7 +48,7 @@ export function getDiceRoll(dice: any) { } export function getDiceRollTotal(diceRolls: DiceRoll[]) { - return diceRolls.reduce((accumulator: number, dice: any) => { + return diceRolls.reduce((accumulator: number, dice) => { if (dice.roll === "unknown") { return accumulator; } else { diff --git a/src/helpers/diff.ts b/src/helpers/diff.ts index 784478e..9eee365 100644 --- a/src/helpers/diff.ts +++ b/src/helpers/diff.ts @@ -1,7 +1,7 @@ -import { applyChange, Diff, revertChange, diff as deepDiff }from "deep-diff"; +import { applyChange, Diff, revertChange, diff as deepDiff } from "deep-diff"; import get from "lodash.get"; -export function applyChanges(target: LHS, changes: Diff[]) { +export function applyChanges(target: LHS, changes: Diff[]) { for (let change of changes) { if (change.path && (change.kind === "E" || change.kind === "A")) { // If editing an object or array ensure that the value exists @@ -15,7 +15,7 @@ export function applyChanges(target: LHS, changes: Diff[]) { } } -export function revertChanges(target: LHS, changes: Diff[]) { +export function revertChanges(target: LHS, changes: Diff[]) { for (let change of changes) { revertChange(target, true, change); } diff --git a/src/helpers/drawing.ts b/src/helpers/drawing.ts index 92b04db..a4d2dee 100644 --- a/src/helpers/drawing.ts +++ b/src/helpers/drawing.ts @@ -238,17 +238,6 @@ export function getFogShapesBoundingBoxes( return boxes; } -/** - * @typedef Edge - * @property {Vector2} start - * @property {Vector2} end - */ - -// type Edge = { -// start: Vector2, -// end: Vector2 -// } - /** * @typedef Guide * @property {Vector2} start @@ -257,7 +246,7 @@ export function getFogShapesBoundingBoxes( * @property {number} distance */ -type Guide = { +export type Guide = { start: Vector2; end: Vector2; orientation: "horizontal" | "vertical"; diff --git a/src/helpers/grid.ts b/src/helpers/grid.ts index 52b906b..7c329f6 100644 --- a/src/helpers/grid.ts +++ b/src/helpers/grid.ts @@ -4,6 +4,7 @@ import Vector2 from "./Vector2"; import Size from "./Size"; import { logError } from "./logging"; +import { Grid, GridInset, GridScale } from "../types/Grid"; const SQRT3 = 1.73205; const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented"); @@ -180,7 +181,10 @@ export function getCellCorners( * @param {number} gridWidth Width of the grid in pixels after inset * @returns {number} */ -function getGridHeightFromWidth(grid: Grid, gridWidth: number): number { +function getGridHeightFromWidth( + grid: Pick, + gridWidth: number +): number { switch (grid.type) { case "square": return (grid.size.y * gridWidth) / grid.size.x; @@ -203,7 +207,7 @@ function getGridHeightFromWidth(grid: Grid, gridWidth: number): number { * @returns {GridInset} */ export function getGridDefaultInset( - grid: Grid, + grid: Pick, mapWidth: number, mapHeight: number ): GridInset { @@ -220,7 +224,7 @@ export function getGridDefaultInset( * @returns {GridInset} */ export function getGridUpdatedInset( - grid: Required, + grid: Grid, mapWidth: number, mapHeight: number ): GridInset { @@ -303,7 +307,7 @@ export function hexOffsetToCube( * @param {Size} cellSize */ export function gridDistance( - grid: Required, + grid: Grid, a: Vector2, b: Vector2, cellSize: Size @@ -313,12 +317,14 @@ export function gridDistance( const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize); if (grid.type === "square") { if (grid.measurement.type === "chebyshev") { - return Vector2.max(Vector2.abs(Vector2.subtract(aCoord, bCoord))); + return Vector2.componentMax( + Vector2.abs(Vector2.subtract(aCoord, bCoord)) + ); } else if (grid.measurement.type === "alternating") { // Alternating diagonal distance like D&D 3.5 and Pathfinder const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord)); - const max: any = Vector2.max(delta); - const min: any = Vector2.min(delta); + const max = Vector2.componentMax(delta); + const min = Vector2.componentMin(delta); return max - min + Math.floor(1.5 * min); } else if (grid.measurement.type === "euclidean") { return Vector2.magnitude( @@ -434,17 +440,16 @@ export function gridSizeVaild(x: number, y: number): boolean { /** * Finds a grid size for an image by finding the closest size to the average grid size - * @param {Image} image + * @param {HTMLImageElement} image * @param {number[]} candidates * @returns {Vector2 | null} */ function gridSizeHeuristic( - image: CanvasImageSource, + image: HTMLImageElement, candidates: number[] ): Vector2 | null { - // TODO: check type for Image and CanvasSourceImage - const width: any = image.width; - const height: any = image.height; + const width = image.width; + const height = image.height; // Find the best candidate by comparing the absolute z-scores of each axis let bestX = 1; let bestY = 1; @@ -470,17 +475,17 @@ function gridSizeHeuristic( /** * Finds the grid size of an image by running the image through a machine learning model - * @param {Image} image + * @param {HTMLImageElement} image * @param {number[]} candidates * @returns {Vector2 | null} */ async function gridSizeML( - image: CanvasImageSource, + image: HTMLImageElement, candidates: number[] ): Promise { // TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match - const width: any = image.width; - const height: any = image.height; + const width = image.width; + const height = image.height; const ratio = width / height; let canvas = document.createElement("canvas"); let context = canvas.getContext("2d"); @@ -545,12 +550,12 @@ async function gridSizeML( /** * Finds the grid size of an image by either using a ML model or falling back to a heuristic - * @param {Image} image + * @param {HTMLImageElement} image * @returns {Vector2} */ -export async function getGridSizeFromImage(image: CanvasImageSource) { - const width: any = image.width; - const height: any = image.height; +export async function getGridSizeFromImage(image: HTMLImageElement) { + const width = image.width; + const height = image.height; const candidates = dividers(width, height); let prediction; diff --git a/src/helpers/group.ts b/src/helpers/group.ts index 17103c3..ccbd0d3 100644 --- a/src/helpers/group.ts +++ b/src/helpers/group.ts @@ -197,10 +197,12 @@ export function findGroup(groups: Group[], groupId: string): Group | undefined { /** * Transform and item array to a record of item ids to item names */ -export function getItemNames(items: any[], itemKey: string = "id") { +export function getItemNames( + items: Item[] +) { let names: Record = {}; for (let item of items) { - names[item[itemKey]] = item.name; + names[item.id] = item.name; } return names; } diff --git a/src/helpers/konva.tsx b/src/helpers/konva.tsx index 7eb8edc..776d4a6 100644 --- a/src/helpers/konva.tsx +++ b/src/helpers/konva.tsx @@ -1,14 +1,23 @@ import React, { useState, useEffect, useRef } from "react"; -import { Line, Group, Path, Circle } from "react-konva"; -// eslint-disable-next-line no-unused-vars import Konva from "konva"; +import { Line, Group, Path, Circle } from "react-konva"; +import { LineConfig } from "konva/types/shapes/Line"; import Color from "color"; + import Vector2 from "./Vector2"; +type HoleyLineProps = { + holes: number[][]; +} & LineConfig; + // Holes should be wound in the opposite direction as the containing points array -export function HoleyLine({ holes, ...props }: { holes: any; props: [] }) { +export function HoleyLine({ holes, ...props }: HoleyLineProps) { // Converted from https://github.com/rfestag/konva/blob/master/src/shapes/Line.ts - function drawLine(points: number[], context: any, shape: any) { + function drawLine( + points: number[], + context: Konva.Context, + shape: Konva.Line + ) { const length = points.length; const tension = shape.tension(); const closed = shape.closed(); @@ -76,7 +85,7 @@ export function HoleyLine({ holes, ...props }: { holes: any; props: [] }) { } // Draw points and holes - function sceneFunc(context: any, shape: any) { + function sceneFunc(context: Konva.Context, shape: Konva.Line) { const points = shape.points(); const closed = shape.closed(); @@ -106,22 +115,18 @@ export function HoleyLine({ holes, ...props }: { holes: any; props: [] }) { } } - return ; + return ; } -export function Tick({ - x, - y, - scale, - onClick, - cross, -}: { - x: any; - y: any; - scale: any; - onClick: any; - cross: any; -}) { +type TickProps = { + x: number; + y: number; + scale: number; + onClick: (evt: Konva.KonvaEventObject) => void; + cross: boolean; +}; + +export function Tick({ x, y, scale, onClick, cross }: TickProps) { const [fill, setFill] = useState("white"); function handleEnter() { setFill("hsl(260, 100%, 80%)"); @@ -160,19 +165,21 @@ interface TrailPoint extends Vector2 { lifetime: number; } +type TrailProps = { + position: Vector2; + size: number; + duration: number; + segments: number; + color: string; +}; + export function Trail({ position, size, duration, segments, color, -}: { - position: Vector2; - size: any; - duration: number; - segments: any; - color: string; -}) { +}: TrailProps) { const trailRef: React.MutableRefObject = useRef(); const pointsRef: React.MutableRefObject = useRef([]); const prevPositionRef = useRef(position); @@ -206,7 +213,7 @@ export function Trail({ useEffect(() => { let prevTime = performance.now(); let request = requestAnimationFrame(animate); - function animate(time: any) { + function animate(time: number) { request = requestAnimationFrame(animate); const deltaTime = time - prevTime; prevTime = time; @@ -243,14 +250,13 @@ export function Trail({ }, []); // Custom scene function for drawing a trail from a line - function sceneFunc(context: any) { + function sceneFunc(context: CanvasRenderingContext2D) { // Resample points to ensure a smooth trail const resampledPoints = Vector2.resample(pointsRef.current, segments); if (resampledPoints.length === 0) { return; } // Draws a line offset in the direction perpendicular to its travel direction - // TODO: check alpha type const drawOffsetLine = (from: Vector2, to: Vector2, alpha: number) => { const forward = Vector2.normalize(Vector2.subtract(from, to)); // Rotate the forward vector 90 degrees based off of the direction @@ -328,7 +334,7 @@ Trail.defaultProps = { */ export function getRelativePointerPosition( node: Konva.Node -): { x: number; y: number } | undefined { +): Vector2 | undefined { let transform = node.getAbsoluteTransform().copy(); transform.invert(); let position = node.getStage()?.getPointerPosition(); @@ -340,10 +346,9 @@ export function getRelativePointerPosition( export function getRelativePointerPositionNormalized( node: Konva.Node -): { x: number; y: number } | undefined { +): Vector2 | undefined { const relativePosition = getRelativePointerPosition(node); if (!relativePosition) { - // TODO: handle possible null value return; } return { @@ -357,8 +362,8 @@ export function getRelativePointerPositionNormalized( * @param {number[]} points points in an x, y alternating array * @returns {Vector2[]} a `Vector2` array */ -export function convertPointArray(points: number[]) { - return points.reduce((acc: any[], _, i, arr) => { +export function convertPointArray(points: number[]): Vector2[] { + return points.reduce((acc: Vector2[], _, i, arr) => { if (i % 2 === 0) { acc.push({ x: arr[i], y: arr[i + 1] }); } diff --git a/src/helpers/logging.ts b/src/helpers/logging.ts index bb547fe..b7c9a75 100644 --- a/src/helpers/logging.ts +++ b/src/helpers/logging.ts @@ -1,6 +1,6 @@ import { captureException } from "@sentry/react"; -export function logError(error: any): void { +export function logError(error: Error): void { console.error(error); if (process.env.REACT_APP_LOGGING === "true") { captureException(error); diff --git a/src/helpers/map.ts b/src/helpers/map.ts index 17241a7..13382a2 100644 --- a/src/helpers/map.ts +++ b/src/helpers/map.ts @@ -32,8 +32,6 @@ const mapResolutions: Resolution[] = [ /** * Get the asset id of the preview file to send for a map - * @param {any} map - * @returns {undefined|string} */ export function getMapPreviewAsset(map: Map): string | undefined { if (map.type === "file") { @@ -126,7 +124,7 @@ export async function createMapFromFile( ) { const resized = await resizeImage( image, - Vector2.max(resolutionPixelSize) as number, + Vector2.componentMax(resolutionPixelSize), file.type, resolution.quality ); diff --git a/src/helpers/select.tsx b/src/helpers/select.tsx deleted file mode 100644 index 8184add..0000000 --- a/src/helpers/select.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useEffect, useState } from "react"; -import Fuse from "fuse.js"; - -import { groupBy } from "./shared"; - -/** - * Helpers for the SelectMapModal and SelectTokenModal - */ - -// Helper for generating search results for items -export function useSearch(items: any[], search: string) { - // TODO: add types to search items -> don't like the never type - const [filteredItems, setFilteredItems]: [ - filteredItems: any, - setFilteredItems: any - ] = useState([]); - const [filteredItemScores, setFilteredItemScores]: [ - filteredItemScores: {}, - setFilteredItemScores: React.Dispatch> - ] = useState({}); - const [fuse, setFuse] = useState(); - - // Update search index when items change - useEffect(() => { - setFuse(new Fuse(items, { keys: ["name", "group"], includeScore: true })); - }, [items]); - - // Perform search when search changes - useEffect(() => { - if (search) { - const query = fuse?.search(search); - setFilteredItems(query?.map((result: any) => result.item)); - let reduceResult: {} | undefined = query?.reduce( - (acc: {}, value: any) => ({ ...acc, [value.item.id]: value.score }), - {} - ); - if (reduceResult) { - setFilteredItemScores(reduceResult); - } - } - }, [search, items, fuse]); - - return [filteredItems, filteredItemScores]; -} - -// Helper for grouping items -export function useGroup( - items: any[], - filteredItems: any[], - useFiltered: boolean, - filteredScores: any[] -) { - const itemsByGroup = groupBy(useFiltered ? filteredItems : items, "group"); - // Get the groups of the items sorting by the average score if we're filtering or the alphabetical order - // with "" at the start and "default" at the end if not - let itemGroups = Object.keys(itemsByGroup); - if (useFiltered) { - itemGroups.sort((a, b) => { - const aScore = itemsByGroup[a].reduce( - (acc: any, item: any) => (acc + filteredScores[item.id]) / 2 - ); - const bScore = itemsByGroup[b].reduce( - (acc: any, item: any) => (acc + filteredScores[item.id]) / 2 - ); - return aScore - bScore; - }); - } else { - itemGroups.sort((a, b) => { - if (a === "" || b === "default") { - return -1; - } - if (b === "" || a === "default") { - return 1; - } - return a.localeCompare(b); - }); - } - return [itemsByGroup, itemGroups]; -} - -// Helper for handling selecting items -export function handleItemSelect( - item: any, - selectMode: any, - selectedIds: string[], - setSelectedIds: any, - itemsByGroup: any, - itemGroups: any -) { - if (!item) { - setSelectedIds([]); - return; - } - switch (selectMode) { - case "single": - setSelectedIds([item.id]); - break; - case "multiple": - setSelectedIds((prev: any[]) => { - if (prev.includes(item.id)) { - return prev.filter((id: number) => id !== item.id); - } else { - return [...prev, item.id]; - } - }); - break; - case "range": - // Create items array - let items = itemGroups.reduce( - (acc: [], group: any) => [...acc, ...itemsByGroup[group]], - [] - ); - - // Add all items inbetween the previous selected item and the current selected - if (selectedIds.length > 0) { - const mapIndex = items.findIndex((m: any) => m.id === item.id); - const lastIndex = items.findIndex( - (m: any) => m.id === selectedIds[selectedIds.length - 1] - ); - let idsToAdd: string[] = []; - let idsToRemove: string[] = []; - const direction = mapIndex > lastIndex ? 1 : -1; - for ( - let i = lastIndex + direction; - direction < 0 ? i >= mapIndex : i <= mapIndex; - i += direction - ) { - const itemId: string = items[i].id; - if (selectedIds.includes(itemId)) { - idsToRemove.push(itemId); - } else { - idsToAdd.push(itemId); - } - } - setSelectedIds((prev: any[]) => { - let ids = [...prev, ...idsToAdd]; - return ids.filter((id) => !idsToRemove.includes(id)); - }); - } else { - setSelectedIds([item.id]); - } - break; - default: - setSelectedIds([]); - } -} diff --git a/src/helpers/shared.ts b/src/helpers/shared.ts index 438d166..d1a0773 100644 --- a/src/helpers/shared.ts +++ b/src/helpers/shared.ts @@ -23,8 +23,9 @@ export function fromEntries(iterable: Iterable<[string | number, any]>) { } // Check to see if all tracks are muted -export function isStreamStopped(stream: MediaStream) { - return stream.getTracks().reduce((a: any, b: any) => a && b, { mute: true }); +export function isStreamStopped(stream: MediaStream): boolean { + // TODO: Check what this thing actually does + return stream.getTracks().reduce((a, b) => a && b, { muted: true }).muted; } export function roundTo(x: number, to: number): number { @@ -62,9 +63,12 @@ export function isEmpty(obj: Object): boolean { return Object.keys(obj).length === 0 && obj.constructor === Object; } -export function keyBy(array: Type[], key: string): Record { +export function keyBy>( + array: Type[], + key: string +): Record { return array.reduce( - (prev: any, current: any) => ({ + (prev, current) => ({ ...prev, [key ? current[key] : current]: current, }), diff --git a/src/helpers/timer.ts b/src/helpers/timer.ts index 9c014d1..757bf8d 100644 --- a/src/helpers/timer.ts +++ b/src/helpers/timer.ts @@ -1,24 +1,14 @@ +import { Duration } from "../types/Timer"; + const MILLISECONDS_IN_HOUR = 3600000; const MILLISECONDS_IN_MINUTE = 60000; const MILLISECONDS_IN_SECOND = 1000; -/** - * @typedef Time - * @property {number} hour - * @property {number} minute - * @property {number} second - */ -type Time = { - hour: number, - minute: number, - second: number -} - /** * Returns a timers duration in milliseconds * @param {Time} t The object with an hour, minute and second property */ -export function getHMSDuration(t: Time) { +export function getHMSDuration(t: Duration): number { if (!t) { return 0; } @@ -33,7 +23,7 @@ export function getHMSDuration(t: Time) { * Returns an object with an hour, minute and second property * @param {number} duration The duration in milliseconds */ -export function getDurationHMS(duration: number) { +export function getDurationHMS(duration: number): Duration { let workingDuration = duration; const hour = Math.floor(workingDuration / MILLISECONDS_IN_HOUR); workingDuration -= hour * MILLISECONDS_IN_HOUR; diff --git a/src/hooks/useGridSnapping.tsx b/src/hooks/useGridSnapping.tsx index 3fc68a5..52676f2 100644 --- a/src/hooks/useGridSnapping.tsx +++ b/src/hooks/useGridSnapping.tsx @@ -73,7 +73,7 @@ function useGridSnapping( const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint); if ( distanceToSnapPoint < - (Vector2.min(gridCellPixelSize) as number) * gridSnappingSensitivity + Vector2.componentMin(gridCellPixelSize) * gridSnappingSensitivity ) { // Reverse grid offset let offsetSnapPoint = Vector2.add( diff --git a/src/hooks/useImageCenter.js b/src/hooks/useImageCenter.tsx similarity index 88% rename from src/hooks/useImageCenter.js rename to src/hooks/useImageCenter.tsx index 3ffa355..e8a86f6 100644 --- a/src/hooks/useImageCenter.js +++ b/src/hooks/useImageCenter.tsx @@ -1,5 +1,17 @@ import { useEffect, useRef } from "react"; +type useImageCenterProps = { + data: + stageRef: + stageWidth: number; + stageHeight: number; + stageTranslateRef: + setStageScale: + imageLayerRef: + containerRef: + responsive?: boolean +} + function useImageCenter( data, stageRef, @@ -14,8 +26,8 @@ function useImageCenter( const stageRatio = stageWidth / stageHeight; const imageRatio = data ? data.width / data.height : 1; - let imageWidth; - let imageHeight; + let imageWidth: number; + let imageHeight: number; if (stageRatio > imageRatio) { imageWidth = data ? stageHeight / (data.height / data.width) : stageWidth; imageHeight = stageHeight; diff --git a/src/hooks/useMapImage.js b/src/hooks/useMapImage.tsx similarity index 100% rename from src/hooks/useMapImage.js rename to src/hooks/useMapImage.tsx diff --git a/src/hooks/useNetworkedState.tsx b/src/hooks/useNetworkedState.tsx index fe7274c..294430e 100644 --- a/src/hooks/useNetworkedState.tsx +++ b/src/hooks/useNetworkedState.tsx @@ -1,39 +1,45 @@ import { useEffect, useState, useRef, useCallback } from "react"; import cloneDeep from "lodash.clonedeep"; +import { Diff } from "deep-diff"; import useDebounce from "./useDebounce"; import { diff, applyChanges } from "../helpers/diff"; import Session from "../network/Session"; /** - * @callback setNetworkedState - * @param {any} update The updated state or a state function passed into setState - * @param {boolean} sync Whether to sync the update with the session - * @param {boolean} force Whether to force a full update, usefull when partialUpdates is enabled + * @param update The updated state or a state function passed into setState + * @param sync Whether to sync the update with the session + * @param force Whether to force a full update, usefull when partialUpdates is enabled */ -// TODO: check parameter requirements here -type setNetworkedState = (update: any, sync?: boolean, force?: boolean) => void +export type SetNetworkedState = ( + update: React.SetStateAction, + sync?: boolean, + force?: boolean +) => void; + +type Update = { + id: string; + changes: Diff[]; +}; /** * Helper to sync a react state to a `Session` * - * @param {any} initialState + * @param {S} initialState * @param {Session} session `Session` instance * @param {string} eventName Name of the event to send to the session * @param {number} debounceRate Amount to debounce before sending to the session (ms) * @param {boolean} partialUpdates Allow sending of partial updates to the session * @param {string} partialUpdatesKey Key to lookup in the state to identify a partial update - * - * @returns {[any, setNetworkedState]} */ -function useNetworkedState( - initialState: any, +function useNetworkedState( + initialState: S, session: Session, eventName: string, debounceRate: number = 500, partialUpdates: boolean = true, partialUpdatesKey: string = "id" -): [any, setNetworkedState] { +): [S, SetNetworkedState] { const [state, _setState] = useState(initialState); // Used to control whether the state needs to be sent to the socket const dirtyRef = useRef(false); @@ -42,9 +48,9 @@ function useNetworkedState( const forceUpdateRef = useRef(false); // Update dirty at the same time as state - const setState = useCallback((update, sync = true, force = false) => { - dirtyRef.current = sync; - forceUpdateRef.current = force; + const setState = useCallback>((update, sync, force) => { + dirtyRef.current = sync || false; + forceUpdateRef.current = force || false; _setState(update); }, []); @@ -54,7 +60,7 @@ function useNetworkedState( }, [eventName]); const debouncedState = useDebounce(state, debounceRate); - const lastSyncedStateRef = useRef(); + const lastSyncedStateRef = useRef(); useEffect(() => { if (session.socket && dirtyRef.current) { // If partial updates enabled, send just the changes to the socket @@ -88,13 +94,13 @@ function useNetworkedState( ]); useEffect(() => { - function handleSocketEvent(data: any) { + function handleSocketEvent(data: S) { _setState(data); lastSyncedStateRef.current = data; } - function handleSocketUpdateEvent(update: any) { - _setState((prevState: any) => { + function handleSocketUpdateEvent(update: Update) { + _setState((prevState) => { if (prevState && prevState[partialUpdatesKey] === update.id) { let newState = { ...prevState }; applyChanges(newState, update.changes); diff --git a/src/ml/gridSize/GridSizeModel.ts b/src/ml/gridSize/GridSizeModel.ts index 25bde7e..565cf68 100644 --- a/src/ml/gridSize/GridSizeModel.ts +++ b/src/ml/gridSize/GridSizeModel.ts @@ -10,8 +10,7 @@ class GridSizeModel extends Model { static model: LayersModel; // Load tensorflow dynamically - // TODO: find type for tf - static tf: any; + static tf; constructor() { super(config as ModelJSON, { "group1-shard1of1.bin": weights }); } @@ -27,8 +26,7 @@ class GridSizeModel extends Model { } const model = GridSizeModel.model; - // TODO: check this mess -> changing type on prediction causes issues - const prediction: any = tf.tidy(() => { + const prediction = tf.tidy(() => { const image = tf.browser.fromPixels(imageData, 1).toFloat(); const normalized = image.div(tf.scalar(255.0)); const batched = tf.expandDims(normalized); diff --git a/src/modals/AddPartyMemberModal.tsx b/src/modals/AddPartyMemberModal.tsx index ece2f7f..e4ed209 100644 --- a/src/modals/AddPartyMemberModal.tsx +++ b/src/modals/AddPartyMemberModal.tsx @@ -8,7 +8,7 @@ function AddPartyMemberModal({ gameId, }: { isOpen: boolean; - onRequestClose: any; + onRequestClose; gameId: string; }) { return ( diff --git a/src/modals/ChangeNicknameModal.tsx b/src/modals/ChangeNicknameModal.tsx index b196dec..151c1bb 100644 --- a/src/modals/ChangeNicknameModal.tsx +++ b/src/modals/ChangeNicknameModal.tsx @@ -5,10 +5,10 @@ import Modal from "../components/Modal"; type ChangeNicknameModalProps = { isOpen: boolean; - onRequestClose: () => void; - onChangeSubmit: any; + onRequestClose; + onChangeSubmit; nickname: string; - onChange: any; + onChange; }; function ChangeNicknameModal({ diff --git a/src/modals/EditMapModal.tsx b/src/modals/EditMapModal.tsx index 8cc662b..a444eae 100644 --- a/src/modals/EditMapModal.tsx +++ b/src/modals/EditMapModal.tsx @@ -12,14 +12,18 @@ import { getGridDefaultInset } from "../helpers/grid"; import useResponsiveLayout from "../hooks/useResponsiveLayout"; import { Map } from "../types/Map"; import { MapState } from "../types/MapState"; +import { + UpdateMapEventHanlder, + UpdateMapStateEventHandler, +} from "../contexts/MapDataContext"; type EditMapProps = { isOpen: boolean; onDone: () => void; map: Map; mapState: MapState; - onUpdateMap: (id: string, update: Partial) => void; - onUpdateMapState: (id: string, update: Partial) => void; + onUpdateMap: UpdateMapEventHanlder; + onUpdateMapState: UpdateMapStateEventHandler; }; function EditMapModal({ @@ -48,52 +52,45 @@ function EditMapModal({ */ // Local cache of map setting changes // Applied when done is clicked or map selection is changed - const [mapSettingChanges, setMapSettingChanges] = useState({}); - const [mapStateSettingChanges, setMapStateSettingChanges] = useState({}); + const [mapSettingChanges, setMapSettingChanges] = useState>({}); + const [mapStateSettingChanges, setMapStateSettingChanges] = useState< + Partial + >({}); - function handleMapSettingsChange(key: string, value: string) { - setMapSettingChanges((prevChanges: any) => ({ + function handleMapSettingsChange(change: Partial) { + setMapSettingChanges((prevChanges) => ({ ...prevChanges, - [key]: value, - lastModified: Date.now(), + ...change, })); } - function handleMapStateSettingsChange(key: string, value: string) { - setMapStateSettingChanges((prevChanges: any) => ({ + function handleMapStateSettingsChange(change: Partial) { + setMapStateSettingChanges((prevChanges) => ({ ...prevChanges, - [key]: value, + ...change, })); } async function applyMapChanges() { if (!isEmpty(mapSettingChanges) || !isEmpty(mapStateSettingChanges)) { // Ensure grid values are positive - let verifiedChanges = { ...mapSettingChanges }; - if ("grid" in verifiedChanges && "size" in verifiedChanges.grid) { + let verifiedChanges: Partial = { ...mapSettingChanges }; + if (verifiedChanges.grid) { verifiedChanges.grid.size.x = verifiedChanges.grid.size.x || 1; verifiedChanges.grid.size.y = verifiedChanges.grid.size.y || 1; } // Ensure inset isn't flipped - if ("grid" in verifiedChanges && "inset" in verifiedChanges.grid) { + if (verifiedChanges.grid) { const inset = verifiedChanges.grid.inset; if ( inset.topLeft.x > inset.bottomRight.x || inset.topLeft.y > inset.bottomRight.y ) { - if ("size" in verifiedChanges.grid) { - verifiedChanges.grid.inset = getGridDefaultInset( - { size: verifiedChanges.grid.size, type: map.grid.type }, - map.width, - map.height - ); - } else { - verifiedChanges.grid.inset = getGridDefaultInset( - map.grid, - map.width, - map.height - ); - } + verifiedChanges.grid.inset = getGridDefaultInset( + { size: verifiedChanges.grid.size, type: map.grid.type }, + map.width, + map.height + ); } } await onUpdateMap(map.id, mapSettingChanges); diff --git a/src/modals/EditTokenModal.tsx b/src/modals/EditTokenModal.tsx index 655c7fe..cef1c27 100644 --- a/src/modals/EditTokenModal.tsx +++ b/src/modals/EditTokenModal.tsx @@ -43,8 +43,9 @@ function EditTokenModal({ Partial >({}); + // TODO: CHANGE MAP BACK? OR CHANGE THIS TO PARTIAL function handleTokenSettingsChange(key: string, value: Pick) { - setTokenSettingChanges((prevChanges: any) => ({ + setTokenSettingChanges((prevChanges) => ({ ...prevChanges, [key]: value, })); diff --git a/src/modals/ImportExportModal.tsx b/src/modals/ImportExportModal.tsx index 06d8b90..56ffa14 100644 --- a/src/modals/ImportExportModal.tsx +++ b/src/modals/ImportExportModal.tsx @@ -46,7 +46,7 @@ function ImportExportModal({ const [error, setError] = useState(); const backgroundTaskRunningRef = useRef(false); - const fileInputRef = useRef(); + const fileInputRef = useRef(); const [showImportSelector, setShowImportSelector] = useState(false); const [showExportSelector, setShowExportSelector] = useState(false); @@ -124,7 +124,7 @@ function ImportExportModal({ } useEffect(() => { - function handleBeforeUnload(event: any) { + function handleBeforeUnload(event) { if (backgroundTaskRunningRef.current) { event.returnValue = "Database is still processing, are you sure you want to leave?"; @@ -204,7 +204,7 @@ function ImportExportModal({ let newMaps: Map[] = []; let newStates: MapState[] = []; if (checkedMaps.length > 0) { - const mapIds = checkedMaps.map((map: any) => map.id); + const mapIds = checkedMaps.map((map) => map.id); const mapsToAdd = await importDB.table("maps").bulkGet(mapIds); for (let map of mapsToAdd) { let state: MapState = await importDB.table("states").get(map.id); @@ -257,7 +257,7 @@ function ImportExportModal({ const assetsToAdd = await importDB .table("assets") .bulkGet(Object.keys(newAssetIds)); - let newAssets: any[] = []; + let newAssets = []; for (let asset of assetsToAdd) { if (asset) { newAssets.push({ @@ -271,7 +271,7 @@ function ImportExportModal({ } // Add map groups with new ids - let newMapGroups: any[] = []; + let newMapGroups = []; if (checkedMapGroups.length > 0) { for (let group of checkedMapGroups) { if (group.type === "item") { @@ -290,7 +290,7 @@ function ImportExportModal({ } // Add token groups with new ids - let newTokenGroups: any[] = []; + let newTokenGroups = []; if (checkedTokenGroups.length > 0) { for (let group of checkedTokenGroups) { if (group.type === "item") { @@ -299,7 +299,7 @@ function ImportExportModal({ newTokenGroups.push({ ...group, id: uuid(), - items: group.items.map((item: any) => ({ + items: group.items.map((item) => ({ ...item, id: newTokenIds[item.id], })), diff --git a/src/modals/SelectMapModal.tsx b/src/modals/SelectMapModal.tsx index b21a4f3..4769a4b 100644 --- a/src/modals/SelectMapModal.tsx +++ b/src/modals/SelectMapModal.tsx @@ -81,7 +81,7 @@ function SelectMapModal({ * Image Upload */ - const fileInputRef = useRef(); + const fileInputRef = useRef(); const [isLoading, setIsLoading] = useState(false); const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = diff --git a/src/modals/SelectTokensModal.tsx b/src/modals/SelectTokensModal.tsx index 94bdb53..b170948 100644 --- a/src/modals/SelectTokensModal.tsx +++ b/src/modals/SelectTokensModal.tsx @@ -76,7 +76,7 @@ function SelectTokensModal({ * Image Upload */ - const fileInputRef = useRef(); + const fileInputRef = useRef(); const [isLoading, setIsLoading] = useState(false); const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = diff --git a/src/modals/StartModal.tsx b/src/modals/StartModal.tsx index e747810..1886dd4 100644 --- a/src/modals/StartModal.tsx +++ b/src/modals/StartModal.tsx @@ -38,7 +38,7 @@ function StartModal({ history.push(`/game/${shortid.generate()}`); } - const inputRef = useRef(); + const inputRef = useRef(); function focusInput() { inputRef.current && inputRef.current.focus(); } diff --git a/src/modals/StartTimerModal.tsx b/src/modals/StartTimerModal.tsx index 78c09b7..c70a280 100644 --- a/src/modals/StartTimerModal.tsx +++ b/src/modals/StartTimerModal.tsx @@ -28,7 +28,7 @@ function StartTimerModal({ onTimerStop, timer, }: StartTimerProps) { - const inputRef = useRef(); + const inputRef = useRef(); function focusInput() { inputRef.current && inputRef.current.focus(); } diff --git a/src/network/Connection.ts b/src/network/Connection.ts index 5fc6814..995ad9d 100644 --- a/src/network/Connection.ts +++ b/src/network/Connection.ts @@ -9,26 +9,26 @@ import blobToBuffer from "../helpers/blobToBuffer"; const MAX_BUFFER_SIZE = 16000; class Connection extends SimplePeer { - currentChunks: any; - dataChannels: any; + currentChunks; + dataChannels; - constructor(props: any) { + constructor(props) { super(props); - this.currentChunks = {} as Blob; + this.currentChunks = {}; this.dataChannels = {}; this.on("data", this.handleData); this.on("datachannel", this.handleDataChannel); } // Intercept the data event with decoding and chunking support - handleData(packed: any) { - const unpacked: any = decode(packed); + handleData(packed) { + const unpacked = decode(packed); // If the special property __chunked is set and true // The data is a partial chunk of the a larger file // So wait until all chunks are collected and assembled // before emitting the dataComplete event if (unpacked.__chunked) { - let chunk: any = this.currentChunks[unpacked.id] || { + let chunk = this.currentChunks[unpacked.id] || { data: [], count: 0, total: unpacked.total, @@ -65,7 +65,7 @@ class Connection extends SimplePeer { * @param {string=} channel * @param {string=} chunkId Optional ID to use for chunking */ - sendObject(object: any, channel?: string, chunkId?: string) { + sendObject(object, channel?: string, chunkId?: string) { try { const packedData = encode(object); const chunks = this.chunk(packedData, chunkId); @@ -83,7 +83,7 @@ class Connection extends SimplePeer { // Override the create data channel function to store our own named reference to it // and to use our custom data handler - createDataChannel(channelName: string, channelConfig: any, opts: any) { + createDataChannel(channelName: string, channelConfig, opts) { // TODO: resolve createDataChannel // @ts-ignore const channel = super.createDataChannel(channelName, channelConfig, opts); @@ -91,11 +91,11 @@ class Connection extends SimplePeer { return channel; } - handleDataChannel(channel: any) { + handleDataChannel(channel) { const channelName = channel.channelName; this.dataChannels[channelName] = channel; channel.on("data", this.handleData.bind(this)); - channel.on("error", (error: any) => { + channel.on("error", (error) => { this.emit("error", error); }); } diff --git a/src/network/NetworkedMapAndTokens.tsx b/src/network/NetworkedMapAndTokens.tsx index 5ce0f22..1474bd9 100644 --- a/src/network/NetworkedMapAndTokens.tsx +++ b/src/network/NetworkedMapAndTokens.tsx @@ -18,16 +18,40 @@ import Session from "./Session"; import Action from "../actions/Action"; -import Map, { - MapState, - Map as MapType, - TokenState, -} from "../components/map/Map"; +import Map from "../components/map/Map"; import TokenBar from "../components/token/TokenBar"; import GlobalImageDrop from "../components/image/GlobalImageDrop"; -const defaultMapActions = { +import { Map as MapType } from "../types/Map"; +import { MapState } from "../types/MapState"; +import { + Asset, + AssetManifest, + AssetManifestAsset, + AssetManifestAssets, +} from "../types/Asset"; +import { TokenState } from "../types/TokenState"; +import { Drawing, DrawingState } from "../types/Drawing"; +import { Fog, FogState } from "../types/Fog"; + +type MapActions = { + mapDrawActions: Action[]; + mapDrawActionIndex: number; + fogDrawActions: Action[]; + fogDrawActionIndex: number; +}; + +type MapActionsKey = keyof Pick< + MapActions, + "mapDrawActions" | "fogDrawActions" +>; +type MapActionsIndexKey = keyof Pick< + MapActions, + "mapDrawActionIndex" | "fogDrawActionIndex" +>; + +const defaultMapActions: MapActions = { mapDrawActions: [], mapDrawActionIndex: -1, fogDrawActions: [], @@ -51,26 +75,32 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { const { updateMapState } = useMapData(); const { getAsset, putAsset } = useAssets(); - const [currentMap, setCurrentMap] = useState(null); - const [currentMapState, setCurrentMapState]: [ - currentMapState: MapState, - setCurrentMapState: any - ] = useNetworkedState(null, session, "map_state", 500, true, "mapId"); - const [assetManifest, setAssetManifest] = useNetworkedState( - null, - session, - "manifest", - 500, - true, - "mapId" - ); + const [currentMap, setCurrentMap] = useState(null); + const [currentMapState, setCurrentMapState] = + useNetworkedState( + null, + session, + "map_state", + 500, + true, + "mapId" + ); + const [assetManifest, setAssetManifest] = + useNetworkedState( + null, + session, + "manifest", + 500, + true, + "mapId" + ); async function loadAssetManifestFromMap(map: MapType, mapState: MapState) { - const assets = {}; + const assets: AssetManifestAssets = {}; const { owner } = map; let processedTokens = new Set(); for (let tokenState of Object.values(mapState.tokens)) { - if (tokenState.file && !processedTokens.has(tokenState.file)) { + if (tokenState.type === "file" && !processedTokens.has(tokenState.file)) { processedTokens.add(tokenState.file); assets[tokenState.file] = { id: tokenState.file, @@ -80,9 +110,11 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } if (map.type === "file") { assets[map.thumbnail] = { id: map.thumbnail, owner }; - const qualityId = map.resolutions[map.quality]; - if (qualityId) { - assets[qualityId] = { id: qualityId, owner }; + if (map.quality !== "original") { + const qualityId = map.resolutions[map.quality]; + if (qualityId) { + assets[qualityId] = { id: qualityId, owner }; + } } else { assets[map.file] = { id: map.file, owner }; } @@ -90,8 +122,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { setAssetManifest({ mapId: map.id, assets }, true, true); } - function addAssetsIfNeeded(assets: any[]) { - setAssetManifest((prevManifest: any) => { + function addAssetsIfNeeded(assets: AssetManifestAsset[]) { + setAssetManifest((prevManifest) => { if (prevManifest?.assets) { let newAssets = { ...prevManifest.assets }; for (let asset of assets) { @@ -116,7 +148,10 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } async function requestAssetsIfNeeded() { - for (let asset of Object.values(assetManifest.assets) as any) { + if (!assetManifest) { + return; + } + for (let asset of Object.values(assetManifest.assets)) { if ( asset.owner === userId || requestingAssetsRef.current.has(asset.id) @@ -144,7 +179,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { if (cachedAsset) { requestingAssetsRef.current.delete(asset.id); - } else { + } else if (owner.sessionId) { assetLoadStart(asset.id); session.sendTo(owner.sessionId, "assetRequest", asset); } @@ -181,7 +216,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } }, [currentMap, debouncedMapState, userId, database, updateMapState]); - async function handleMapChange(newMap: any, newMapState: any) { + async function handleMapChange(newMap, newMapState) { // Clear map before sending new one setCurrentMap(null); session.socket?.emit("map", null); @@ -199,20 +234,20 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { await loadAssetManifestFromMap(newMap, newMapState); } - function handleMapReset(newMapState: any) { + function handleMapReset(newMapState) { setCurrentMapState(newMapState, true, true); setMapActions(defaultMapActions); } - const [mapActions, setMapActions] = useState(defaultMapActions); + const [mapActions, setMapActions] = useState(defaultMapActions); function addMapActions( - actions: Action[], - indexKey: string, - actionsKey: any, - shapesKey: any + actions: Action[], + indexKey: MapActionsIndexKey, + actionsKey: MapActionsKey, + shapesKey: "drawShapes" | "fogShapes" ) { - setMapActions((prevMapActions: any) => { + setMapActions((prevMapActions) => { const newActions = [ ...prevMapActions[actionsKey].slice(0, prevMapActions[indexKey] + 1), ...actions, @@ -225,39 +260,40 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { }; }); // Update map state by performing the actions on it - setCurrentMapState((prevMapState: any) => { - if (prevMapState) { - let shapes = prevMapState[shapesKey]; - for (let action of actions) { - shapes = action.execute(shapes); - } - return { - ...prevMapState, - [shapesKey]: shapes, - }; + setCurrentMapState((prevMapState) => { + if (!prevMapState) { + return prevMapState; } + let shapes = prevMapState[shapesKey]; + for (let action of actions) { + shapes = action.execute(shapes); + } + return { + ...prevMapState, + [shapesKey]: shapes, + }; }); } function updateActionIndex( - change: any, - indexKey: any, - actionsKey: any, - shapesKey: any + change, + indexKey: MapActionsIndexKey, + actionsKey: MapActionsKey, + shapesKey: "drawShapes" | "fogShapes" ) { - const prevIndex: any = mapActions[indexKey]; + const prevIndex = mapActions[indexKey]; const newIndex = Math.min( Math.max(mapActions[indexKey] + change, -1), mapActions[actionsKey].length - 1 ); - setMapActions((prevMapActions: Action[]) => ({ + setMapActions((prevMapActions) => ({ ...prevMapActions, [indexKey]: newIndex, })); // Update map state by either performing the actions or undoing them - setCurrentMapState((prevMapState: any) => { + setCurrentMapState((prevMapState) => { if (prevMapState) { let shapes = prevMapState[shapesKey]; if (prevIndex < newIndex) { @@ -283,7 +319,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { return newIndex; } - function handleMapDraw(action: Action) { + function handleMapDraw(action: Action) { addMapActions( [action], "mapDrawActionIndex", @@ -300,7 +336,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { updateActionIndex(1, "mapDrawActionIndex", "mapDrawActions", "drawShapes"); } - function handleFogDraw(action: Action) { + function handleFogDraw(action: Action) { addMapActions( [action], "fogDrawActionIndex", @@ -318,7 +354,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } // If map changes clear map actions - const previousMapIdRef = useRef(); + const previousMapIdRef = useRef(); useEffect(() => { if (currentMap && currentMap?.id !== previousMapIdRef.current) { setMapActions(defaultMapActions); @@ -326,8 +362,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } }, [currentMap]); - function handleNoteChange(note: any) { - setCurrentMapState((prevMapState: any) => ({ + function handleNoteChange(note) { + setCurrentMapState((prevMapState) => ({ ...prevMapState, notes: { ...prevMapState.notes, @@ -337,7 +373,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } function handleNoteRemove(noteId: string) { - setCurrentMapState((prevMapState: any) => ({ + setCurrentMapState((prevMapState) => ({ ...prevMapState, notes: omit(prevMapState.notes, [noteId]), })); @@ -352,7 +388,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { return; } - let assets = []; + let assets: AssetManifestAsset[] = []; for (let tokenState of tokenStates) { if (tokenState.type === "file") { assets.push({ id: tokenState.file, owner: tokenState.owner }); @@ -371,11 +407,11 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { }); } - function handleMapTokenStateChange(change: any) { + function handleMapTokenStateChange(change) { if (!currentMapState) { return; } - setCurrentMapState((prevMapState: any) => { + setCurrentMapState((prevMapState) => { let tokens = { ...prevMapState.tokens }; for (let id in change) { if (id in tokens) { @@ -390,8 +426,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { }); } - function handleMapTokenStateRemove(tokenState: any) { - setCurrentMapState((prevMapState: any) => { + function handleMapTokenStateRemove(tokenState) { + setCurrentMapState((prevMapState) => { const { [tokenState.id]: old, ...rest } = prevMapState.tokens; return { ...prevMapState, tokens: rest }; }); @@ -404,8 +440,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { reply, }: { id: string; - data: any; - reply: any; + data; + reply; }) { if (id === "assetRequest") { const asset = await getAsset(data.id); @@ -440,7 +476,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { assetProgressUpdate({ id, total, count }); } - async function handleSocketMap(map: any) { + async function handleSocketMap(map) { if (map) { setCurrentMap(map); } else { @@ -461,7 +497,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { const canChangeMap = !isLoading; - const canEditMapDrawing: any = + const canEditMapDrawing = currentMap && currentMapState && (currentMapState.editFlags.includes("drawing") || @@ -478,7 +514,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { (currentMapState.editFlags.includes("notes") || currentMap?.owner === userId); - const disabledMapTokens: { [key: string]: any } = {}; + const disabledMapTokens = {}; // If we have a map and state and have the token permission disabled // and are not the map owner if ( diff --git a/src/network/NetworkedMapPointer.tsx b/src/network/NetworkedMapPointer.tsx index 2d81a78..b28ab12 100644 --- a/src/network/NetworkedMapPointer.tsx +++ b/src/network/NetworkedMapPointer.tsx @@ -46,13 +46,13 @@ function NetworkedMapPointer({ // We use requestAnimationFrame as setInterval was being blocked during // re-renders on Chrome with Windows const ownPointerUpdateRef: React.MutableRefObject< - { position: any; visible: boolean; id: any; color: any } | undefined | null + { position; visible: boolean; id; color } | undefined | null > = useRef(); useEffect(() => { let prevTime = performance.now(); let request = requestAnimationFrame(update); let counter = 0; - function update(time: any) { + function update(time) { request = requestAnimationFrame(update); const deltaTime = time - prevTime; counter += deltaTime; @@ -79,7 +79,7 @@ function NetworkedMapPointer({ }; }, []); - function updateOwnPointerState(position: any, visible: boolean) { + function updateOwnPointerState(position, visible: boolean) { setLocalPointerState((prev) => ({ ...prev, [userId]: { position, visible, id: userId, color: pointerColor }, @@ -92,24 +92,24 @@ function NetworkedMapPointer({ }; } - function handleOwnPointerDown(position: any) { + function handleOwnPointerDown(position) { updateOwnPointerState(position, true); } - function handleOwnPointerMove(position: any) { + function handleOwnPointerMove(position) { updateOwnPointerState(position, true); } - function handleOwnPointerUp(position: any) { + function handleOwnPointerUp(position) { updateOwnPointerState(position, false); } // Handle pointer data receive - const interpolationsRef: React.MutableRefObject = useRef({}); + const interpolationsRef: React.MutableRefObject = useRef({}); useEffect(() => { // TODO: Handle player disconnect while pointer visible - function handleSocketPlayerPointer(pointer: any) { - const interpolations: any = interpolationsRef.current; + function handleSocketPlayerPointer(pointer) { + const interpolations = interpolationsRef.current; const id = pointer.id; if (!(id in interpolations)) { interpolations[id] = { @@ -154,8 +154,8 @@ function NetworkedMapPointer({ function animate() { request = requestAnimationFrame(animate); const time = performance.now(); - let interpolatedPointerState: any = {}; - for (let interp of Object.values(interpolationsRef.current) as any) { + let interpolatedPointerState = {}; + for (let interp of Object.values(interpolationsRef.current)) { if (!interp.from || !interp.to) { continue; } @@ -200,7 +200,7 @@ function NetworkedMapPointer({ return ( - {Object.values(localPointerState).map((pointer: any) => ( + {Object.values(localPointerState).map((pointer) => ( { + const handleSignal = (signal) => { this.socket.emit("signal", JSON.stringify({ to: peer.id, signal })); }; @@ -269,9 +275,9 @@ class Session extends EventEmitter { * @property {peerReply} reply */ this.emit("peerConnect", { peer, reply }); - } + }; - const handleDataComplete = (data: any) => { + const handleDataComplete = (data) => { /** * Peer Data Event - Data received by a peer * @@ -285,7 +291,7 @@ class Session extends EventEmitter { let peerDataEvent: { peer: SessionPeer; id: string; - data: any; + data; reply: peerReply; } = { peer, @@ -293,7 +299,7 @@ class Session extends EventEmitter { data: data.data, reply: reply, }; - console.log(`Data: ${JSON.stringify(data)}`) + console.log(`Data: ${JSON.stringify(data)}`); this.emit("peerData", peerDataEvent); }; @@ -444,7 +450,7 @@ class Session extends EventEmitter { } } - _handleSignal(data: any) { + _handleSignal(data) { const { from, signal } = data; if (!(from in this.peers)) { if (!this._addPeer(from, false)) { diff --git a/src/routes/Donate.tsx b/src/routes/Donate.tsx index 94d3c78..718a1a0 100644 --- a/src/routes/Donate.tsx +++ b/src/routes/Donate.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { ChangeEvent, FormEvent, useEffect, useState } from "react"; import { Box, Flex, @@ -18,7 +18,7 @@ import LoadingOverlay from "../components/LoadingOverlay"; import { logError } from "../helpers/logging"; import { Stripe } from "@stripe/stripe-js"; -type Price = { price?: string, name: string, value: number } +type Price = { price?: string; name: string; value: number }; const prices: Price[] = [ { price: "$5.00", name: "Small", value: 5 }, @@ -32,11 +32,9 @@ function Donate() { const hasDonated = query.has("success"); const [loading, setLoading] = useState(true); - // TODO: check with Mitch about changes here from useState(null) - // TODO: typing with error a little messy - const [error, setError]= useState(); + const [error, setError] = useState(undefined); - const [stripe, setStripe]: [ stripe: Stripe | undefined, setStripe: React.Dispatch] = useState(); + const [stripe, setStripe] = useState(); useEffect(() => { import("@stripe/stripe-js").then(({ loadStripe }) => { loadStripe(process.env.REACT_APP_STRIPE_API_KEY as string) @@ -55,7 +53,7 @@ function Donate() { }); }, []); - async function handleSubmit(event: any) { + async function handleSubmit(event: FormEvent) { event.preventDefault(); if (loading) { return; @@ -76,7 +74,8 @@ function Donate() { const result = await stripe?.redirectToCheckout({ sessionId: session.id }); if (result?.error) { - setError(result.error.message); + const stripeError = new Error(result.error.message); + setError(stripeError); } } @@ -87,7 +86,7 @@ function Donate() { setValue(price.value); setSelectedPrice(price.name); } - + return ( setValue(e.target.value)} + onChange={(e: ChangeEvent) => + setValue(parseInt(e.target.value)) + } /> )} @@ -169,7 +170,7 @@ function Donate() {