typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-16 14:55:33 +10:00
parent 68c1c6db0c
commit d80bfa2f1e
103 changed files with 1402 additions and 1336 deletions

View File

@ -1,40 +1,31 @@
// Load Diff for auto complete
// eslint-disable-next-line no-unused-vars
import { Diff } from "deep-diff"; import { Diff } from "deep-diff";
import { diff, revertChanges } from "../helpers/diff"; import { diff, revertChanges } from "../helpers/diff";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
/**
* @callback ActionUpdate
* @param {any} state
*/
/** /**
* Implementation of the Command Pattern * Implementation of the Command Pattern
* Wraps an update function with internal state to support undo * Wraps an update function with internal state to support undo
*/ */
class Action { class Action<State> {
/** /**
* The update function called with the current state and should return the updated state * The update function called with the current state and should return the updated state
* This is implemented in the child class * This is implemented in the child class
*
* @type {ActionUpdate}
*/ */
update; update(state: State): State {
return state;
}
/** /**
* The changes caused by the last state update * The changes caused by the last state update
* @type {Diff}
*/ */
changes; changes: Diff<State, State>[] | undefined;
/** /**
* Executes the action update on the state * Executes the action update on the state
* @param {any} state The current state to update * @param {State} state The current state to update
* @returns {any} The updated state
*/ */
execute(state) { execute(state: State): State {
if (state && this.update) { if (state && this.update) {
let newState = this.update(cloneDeep(state)); let newState = this.update(cloneDeep(state));
this.changes = diff(state, newState); this.changes = diff(state, newState);
@ -45,10 +36,10 @@ class Action {
/** /**
* Reverts the changes caused by the last call of `execute` * Reverts the changes caused by the last call of `execute`
* @param {any} state The current state to perform the undo on * @param {State} state The current state to perform the undo on
* @returns {any} The state with the last changes reverted * @returns {State} The state with the last changes reverted
*/ */
undo(state) { undo(state: State): State {
if (state && this.changes) { if (state && this.changes) {
let revertedState = cloneDeep(state); let revertedState = cloneDeep(state);
revertChanges(revertedState, this.changes); revertChanges(revertedState, this.changes);

View File

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

View File

@ -0,0 +1,21 @@
import Action from "./Action";
import { ID } from "../types/Action";
class AddStatesAction<State extends ID> extends Action<Record<string, State>> {
states: State[];
constructor(states: State[]) {
super();
this.states = states;
}
update(statesById: Record<string, State>) {
for (let state of this.states) {
statesById[state.id] = state;
}
return statesById;
}
}
export default AddStatesAction;

View File

@ -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<FogState> {
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;

View File

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

View File

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

View File

@ -0,0 +1,23 @@
import Action from "./Action";
import { ID } from "../types/Action";
class EditStatesAction<State extends ID> extends Action<Record<string, State>> {
edits: Partial<State>[];
constructor(edits: Partial<State>[]) {
super();
this.edits = edits;
}
update(statesById: Record<string, State>) {
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;

View File

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

View File

@ -0,0 +1,21 @@
import Action from "./Action";
import { omit } from "../helpers/shared";
import { ID } from "../types/Action";
class RemoveStatesAction<State extends ID> extends Action<
Record<string, State>
> {
stateIds: string[];
constructor(stateIds: string[]) {
super();
this.stateIds = stateIds;
}
update(statesById: Record<string, State>) {
return omit(statesById, this.stateIds);
}
}
export default RemoveStatesAction;

View File

@ -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<FogState> {
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;

View File

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

View File

@ -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,
};

13
src/actions/index.ts Normal file
View File

@ -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,
};

View File

@ -78,7 +78,7 @@ Slider.defaultProps = {
value: 0, value: 0,
ml: 0, ml: 0,
mr: 0, mr: 0,
labelFunc: (value: any) => value, labelFunc: (value: number) => value,
}; };
export default Slider; export default Slider;

View File

@ -1,5 +1,7 @@
import Modal from "react-modal"; import Modal from "react-modal";
import { useThemeUI, Close } from "theme-ui"; import { useThemeUI, Close } from "theme-ui";
import { RequestCloseEventHandler } from "../../types/Events";
import CSS from "csstype";
function Banner({ function Banner({
isOpen, isOpen,
@ -8,11 +10,11 @@ function Banner({
allowClose, allowClose,
backgroundColor, backgroundColor,
}: { }: {
isOpen: boolean, isOpen: boolean;
onRequestClose: any, onRequestClose: RequestCloseEventHandler;
children: any, children: React.ReactNode;
allowClose: boolean, allowClose: boolean;
backgroundColor?: any backgroundColor?: CSS.Property.Color;
}) { }) {
const { theme } = useThemeUI(); const { theme } = useThemeUI();
@ -23,7 +25,8 @@ function Banner({
style={{ style={{
overlay: { bottom: "0", top: "initial", zIndex: 2000 }, overlay: { bottom: "0", top: "initial", zIndex: 2000 },
content: { content: {
backgroundColor: backgroundColor || theme.colors?.highlight, backgroundColor:
backgroundColor || (theme.colors?.highlight as CSS.Property.Color),
color: "hsl(210, 50%, 96%)", color: "hsl(210, 50%, 96%)",
top: "initial", top: "initial",
left: "50%", left: "50%",

View File

@ -2,7 +2,13 @@ import { Box, Text } from "theme-ui";
import Banner from "./Banner"; import Banner from "./Banner";
function ErrorBanner({ error, onRequestClose }: { error: Error | undefined, onRequestClose: any }) { function ErrorBanner({
error,
onRequestClose,
}: {
error: Error | undefined;
onRequestClose;
}) {
return ( return (
<Banner isOpen={!!error} onRequestClose={onRequestClose}> <Banner isOpen={!!error} onRequestClose={onRequestClose}>
<Box p={1}> <Box p={1}>

View File

@ -35,7 +35,7 @@ type DiceInteractionProps = {
canvas: HTMLCanvasElement | WebGLRenderingContext; canvas: HTMLCanvasElement | WebGLRenderingContext;
}) => void; }) => void;
onPointerDown: () => void; onPointerDown: () => void;
onPointerUp: () => any; onPointerUp: () => void;
}; };
function DiceInteraction({ function DiceInteraction({

View File

@ -21,12 +21,21 @@ import NoteMenu from "../note/NoteMenu";
import NoteDragOverlay from "../note/NoteDragOverlay"; import NoteDragOverlay from "../note/NoteDragOverlay";
import { import {
AddShapeAction, AddStatesAction,
CutShapeAction, CutFogAction,
EditShapeAction, EditStatesAction,
RemoveShapeAction, RemoveStatesAction,
} from "../../actions"; } from "../../actions";
import Session from "../../network/Session"; 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({ function Map({
map, map,
@ -51,43 +60,39 @@ function Map({
disabledTokens, disabledTokens,
session, session,
}: { }: {
map: any; map: Map;
mapState: MapState; mapState: MapState;
mapActions: any; mapActions: ;
onMapTokenStateChange: any; onMapTokenStateChange: ;
onMapTokenStateRemove: any; onMapTokenStateRemove: ;
onMapChange: any; onMapChange: MapChangeEventHandler;
onMapReset: any; onMapReset: MapResetEventHandler;
onMapDraw: any; onMapDraw: ;
onMapDrawUndo: any; onMapDrawUndo: ;
onMapDrawRedo: any; onMapDrawRedo: ;
onFogDraw: any; onFogDraw: ;
onFogDrawUndo: any; onFogDrawUndo: ;
onFogDrawRedo: any; onFogDrawRedo: ;
onMapNoteChange: any; onMapNoteChange: ;
onMapNoteRemove: any; onMapNoteRemove: ;
allowMapDrawing: boolean; allowMapDrawing: boolean;
allowFogDrawing: boolean; allowFogDrawing: boolean;
allowMapChange: boolean; allowMapChange: boolean;
allowNoteEditing: boolean; allowNoteEditing: boolean;
disabledTokens: any; disabledTokens: ;
session: Session; session: Session;
}) { }) {
const { addToast } = useToasts(); const { addToast } = useToasts();
const { tokensById } = useTokenData(); const { tokensById } = useTokenData();
const [selectedToolId, setSelectedToolId] = useState("move"); const [selectedToolId, setSelectedToolId] = useState<MapToolId>("move");
const { settings, setSettings }: { settings: any; setSettings: any } = const { settings, setSettings } = useSettings();
useSettings();
function handleToolSettingChange(tool: any, change: any) { function handleToolSettingChange(change: Partial<Settings>) {
setSettings((prevSettings: any) => ({ setSettings((prevSettings) => ({
...prevSettings, ...prevSettings,
[tool]: { ...change,
...prevSettings[tool],
...change,
},
})); }));
} }
@ -96,7 +101,7 @@ function Map({
function handleToolAction(action: string) { function handleToolAction(action: string) {
if (action === "eraseAll") { if (action === "eraseAll") {
onMapDraw(new RemoveShapeAction(drawShapes.map((s) => s.id))); onMapDraw(new RemoveStatesAction(drawShapes.map((s) => s.id)));
} }
if (action === "mapUndo") { if (action === "mapUndo") {
onMapDrawUndo(); onMapDrawUndo();
@ -112,28 +117,28 @@ function Map({
} }
} }
function handleMapShapeAdd(shape: Shape) { function handleMapShapeAdd(shape: Drawing) {
onMapDraw(new AddShapeAction([shape])); onMapDraw(new AddStatesAction([shape]));
} }
function handleMapShapesRemove(shapeIds: string[]) { function handleMapShapesRemove(shapeIds: string[]) {
onMapDraw(new RemoveShapeAction(shapeIds)); onMapDraw(new RemoveStatesAction(shapeIds));
} }
function handleFogShapesAdd(shapes: Shape[]) { function handleFogShapesAdd(shapes: Fog[]) {
onFogDraw(new AddShapeAction(shapes)); onFogDraw(new AddStatesAction(shapes));
} }
function handleFogShapesCut(shapes: Shape[]) { function handleFogShapesCut(shapes: Fog[]) {
onFogDraw(new CutShapeAction(shapes)); onFogDraw(new CutFogAction(shapes));
} }
function handleFogShapesRemove(shapeIds: string[]) { function handleFogShapesRemove(shapeIds: string[]) {
onFogDraw(new RemoveShapeAction(shapeIds)); onFogDraw(new RemoveStatesAction(shapeIds));
} }
function handleFogShapesEdit(shapes: Shape[]) { function handleFogShapesEdit(shapes: Partial<Fog>[]) {
onFogDraw(new EditShapeAction(shapes)); onFogDraw(new EditStatesAction(shapes));
} }
const disabledControls = []; const disabledControls = [];
@ -155,7 +160,10 @@ function Map({
disabledControls.push("note"); disabledControls.push("note");
} }
const disabledSettings: { fog: any[]; drawing: any[] } = { const disabledSettings: {
fog: string[];
drawing: string[];
} = {
fog: [], fog: [],
drawing: [], drawing: [],
}; };
@ -197,19 +205,10 @@ function Map({
/> />
); );
const [isTokenMenuOpen, setIsTokenMenuOpen]: [ const [isTokenMenuOpen, setIsTokenMenuOpen] = useState<boolean>(false);
isTokenMenuOpen: boolean, const [tokenMenuOptions, setTokenMenuOptions] = useState({});
setIsTokenMenuOpen: React.Dispatch<React.SetStateAction<boolean>> const [tokenDraggingOptions, setTokenDraggingOptions] = useState();
] = useState<boolean>(false); function handleTokenMenuOpen(tokenStateId: string, tokenImage) {
const [tokenMenuOptions, setTokenMenuOptions]: [
tokenMenuOptions: any,
setTokenMenuOptions: any
] = useState({});
const [tokenDraggingOptions, setTokenDraggingOptions]: [
tokenDraggingOptions: any,
setTokenDragginOptions: any
] = useState();
function handleTokenMenuOpen(tokenStateId: string, tokenImage: any) {
setTokenMenuOptions({ tokenStateId, tokenImage }); setTokenMenuOptions({ tokenStateId, tokenImage });
setIsTokenMenuOpen(true); setIsTokenMenuOpen(true);
} }
@ -240,7 +239,7 @@ function Map({
const tokenDragOverlay = tokenDraggingOptions && ( const tokenDragOverlay = tokenDraggingOptions && (
<TokenDragOverlay <TokenDragOverlay
onTokenStateRemove={(state: any) => { onTokenStateRemove={(state) => {
onMapTokenStateRemove(state); onMapTokenStateRemove(state);
setTokenDraggingOptions(null); setTokenDraggingOptions(null);
}} }}
@ -292,14 +291,14 @@ function Map({
); );
const [isNoteMenuOpen, setIsNoteMenuOpen] = useState<boolean>(false); const [isNoteMenuOpen, setIsNoteMenuOpen] = useState<boolean>(false);
const [noteMenuOptions, setNoteMenuOptions] = useState<any>({}); const [noteMenuOptions, setNoteMenuOptions] = useState({});
const [noteDraggingOptions, setNoteDraggingOptions] = useState<any>(); const [noteDraggingOptions, setNoteDraggingOptions] = useState();
function handleNoteMenuOpen(noteId: string, noteNode: any) { function handleNoteMenuOpen(noteId: string, noteNode) {
setNoteMenuOptions({ noteId, noteNode }); setNoteMenuOptions({ noteId, noteNode });
setIsNoteMenuOpen(true); setIsNoteMenuOpen(true);
} }
function sortNotes(a: any, b: any, noteDraggingOptions: any) { function sortNotes(a, b, noteDraggingOptions) {
if ( if (
noteDraggingOptions && noteDraggingOptions &&
noteDraggingOptions.dragging && noteDraggingOptions.dragging &&
@ -338,7 +337,7 @@ function Map({
allowNoteEditing && allowNoteEditing &&
(selectedToolId === "note" || selectedToolId === "move") (selectedToolId === "note" || selectedToolId === "move")
} }
onNoteDragStart={(e: any, noteId: any) => onNoteDragStart={(e, noteId) =>
setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target })
} }
onNoteDragEnd={() => onNoteDragEnd={() =>
@ -364,7 +363,7 @@ function Map({
dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)} dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)}
noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup} noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup}
noteId={noteDraggingOptions && noteDraggingOptions.noteId} noteId={noteDraggingOptions && noteDraggingOptions.noteId}
onNoteRemove={(noteId: any) => { onNoteRemove={(noteId) => {
onMapNoteRemove(noteId); onMapNoteRemove(noteId);
setNoteDraggingOptions(null); setNoteDraggingOptions(null);
}} }}

View File

@ -1,4 +1,4 @@
import React, { useState, Fragment } from "react"; import { useState, Fragment } from "react";
import { IconButton, Flex, Box } from "theme-ui"; import { IconButton, Flex, Box } from "theme-ui";
import RadioIconButton from "../RadioIconButton"; import RadioIconButton from "../RadioIconButton";
@ -21,21 +21,26 @@ import FullScreenExitIcon from "../../icons/FullScreenExitIcon";
import NoteToolIcon from "../../icons/NoteToolIcon"; import NoteToolIcon from "../../icons/NoteToolIcon";
import useSetting from "../../hooks/useSetting"; import useSetting from "../../hooks/useSetting";
import { Map } from "../../types/Map"; import { Map, MapTool, MapToolId } from "../../types/Map";
import { MapState } from "../../types/MapState"; import { MapState } from "../../types/MapState";
import {
MapChangeEventHandler,
MapResetEventHandler,
} from "../../types/Events";
import { Settings } from "../../types/Settings";
type MapControlsProps = { type MapControlsProps = {
onMapChange: () => void; onMapChange: MapChangeEventHandler;
onMapReset: () => void; onMapReset: MapResetEventHandler;
currentMap?: Map; currentMap?: Map;
currentMapState?: MapState; currentMapState?: MapState;
selectedToolId: string; selectedToolId: MapToolId;
onSelectedToolChange: () => void; onSelectedToolChange: (toolId: MapToolId) => void;
toolSettings: any; toolSettings: Settings;
onToolSettingChange: () => void; onToolSettingChange: (change: Partial<Settings>) => void;
onToolAction: () => void; onToolAction: (actionId: string) => void;
disabledControls: string[]; disabledControls: string[];
disabledSettings: string[]; disabledSettings: Partial<Record<keyof Settings, string[]>>;
}; };
function MapContols({ function MapContols({
@ -54,7 +59,7 @@ function MapContols({
const [isExpanded, setIsExpanded] = useState(true); const [isExpanded, setIsExpanded] = useState(true);
const [fullScreen, setFullScreen] = useSetting("map.fullScreen"); const [fullScreen, setFullScreen] = useSetting("map.fullScreen");
const toolsById = { const toolsById: Record<string, MapTool> = {
move: { move: {
id: "move", id: "move",
icon: <MoveToolIcon />, icon: <MoveToolIcon />,
@ -89,7 +94,14 @@ function MapContols({
title: "Note Tool (N)", title: "Note Tool (N)",
}, },
}; };
const tools = ["move", "fog", "drawing", "measure", "pointer", "note"]; const tools: MapToolId[] = [
"move",
"fog",
"drawing",
"measure",
"pointer",
"note",
];
const sections = [ const sections = [
{ {
@ -174,32 +186,41 @@ function MapContols({
function getToolSettings() { function getToolSettings() {
const Settings = toolsById[selectedToolId].SettingsComponent; const Settings = toolsById[selectedToolId].SettingsComponent;
if (Settings) { if (
return ( !Settings ||
<Box selectedToolId === "move" ||
sx={{ selectedToolId === "measure" ||
position: "absolute", selectedToolId === "note"
top: "4px", ) {
left: "50%",
transform: "translateX(-50%)",
backgroundColor: "overlay",
borderRadius: "4px",
}}
p={1}
>
<Settings
settings={toolSettings[selectedToolId]}
onSettingChange={(change) =>
onToolSettingChange(selectedToolId, change)
}
onToolAction={onToolAction}
disabledActions={disabledSettings[selectedToolId]}
/>
</Box>
);
} else {
return null; return null;
} }
return (
<Box
sx={{
position: "absolute",
top: "4px",
left: "50%",
transform: "translateX(-50%)",
backgroundColor: "overlay",
borderRadius: "4px",
}}
p={1}
>
<Settings
settings={toolSettings[selectedToolId]}
onSettingChange={(change) =>
onToolSettingChange({
[selectedToolId]: {
...toolSettings[selectedToolId],
...change,
},
})
}
onToolAction={onToolAction}
disabledActions={disabledSettings[selectedToolId]}
/>
</Box>
);
} }
return ( return (

View File

@ -21,8 +21,17 @@ import GridOffIcon from "../../icons/GridOffIcon";
import MapGrid from "./MapGrid"; import MapGrid from "./MapGrid";
import MapGridEditor from "./MapGridEditor"; import MapGridEditor from "./MapGridEditor";
import { Map } from "../../types/Map";
import { GridInset } from "../../types/Grid";
function MapEditor({ map, onSettingsChange }) { type MapSettingsChangeEventHandler = (change: Partial<Map>) => void;
type MapEditorProps = {
map: Map;
onSettingsChange: MapSettingsChangeEventHandler;
};
function MapEditor({ map, onSettingsChange }: MapEditorProps) {
const [mapImage] = useMapImage(map); const [mapImage] = useMapImage(map);
const [stageWidth, setStageWidth] = useState(1); const [stageWidth, setStageWidth] = useState(1);
@ -36,12 +45,17 @@ function MapEditor({ map, onSettingsChange }) {
const mapLayerRef = useRef(); const mapLayerRef = useRef();
const [preventMapInteraction, setPreventMapInteraction] = useState(false); const [preventMapInteraction, setPreventMapInteraction] = useState(false);
function handleResize(width, height) { function handleResize(width?: number, height?: number): void {
setStageWidth(width); if (width) {
setStageHeight(height); setStageWidth(width);
}
if (height) {
setStageHeight(height);
}
} }
const containerRef = useRef(); const containerRef = useRef(null);
usePreventOverscroll(containerRef); usePreventOverscroll(containerRef);
const [mapWidth, mapHeight] = useImageCenter( const [mapWidth, mapHeight] = useImageCenter(
@ -67,17 +81,21 @@ function MapEditor({ map, onSettingsChange }) {
preventMapInteraction preventMapInteraction
); );
function handleGridChange(inset) { function handleGridChange(inset: GridInset) {
onSettingsChange("grid", { onSettingsChange({
...map.grid, grid: {
inset, ...map.grid,
inset,
},
}); });
} }
function handleMapReset() { function handleMapReset() {
onSettingsChange("grid", { onSettingsChange({
...map.grid, grid: {
inset: defaultInset, ...map.grid,
inset: defaultInset,
},
}); });
} }
@ -120,8 +138,9 @@ function MapEditor({ map, onSettingsChange }) {
> >
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}> <ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
<KonvaBridge <KonvaBridge
stageRender={(children) => ( stageRender={(children: React.ReactNode) => (
<Stage <Stage
// @ts-ignore https://github.com/konvajs/react-konva/issues/342
width={stageWidth} width={stageWidth}
height={stageHeight} height={stageHeight}
scale={{ x: stageScale, y: stageScale }} scale={{ x: stageScale, y: stageScale }}

View File

@ -31,6 +31,7 @@ import {
getGuidesFromBoundingBoxes, getGuidesFromBoundingBoxes,
getGuidesFromGridCell, getGuidesFromGridCell,
findBestGuides, findBestGuides,
Guide,
} from "../../helpers/drawing"; } from "../../helpers/drawing";
import colors from "../../helpers/colors"; import colors from "../../helpers/colors";
import { import {
@ -40,13 +41,35 @@ import {
} from "../../helpers/konva"; } from "../../helpers/konva";
import { keyBy } from "../../helpers/shared"; import { keyBy } from "../../helpers/shared";
import SubtractShapeAction from "../../actions/SubtractShapeAction"; import SubtractFogAction from "../../actions/SubtractFogAction";
import CutShapeAction from "../../actions/CutShapeAction"; import CutFogAction from "../../actions/CutFogAction";
import useSetting from "../../hooks/useSetting"; import useSetting from "../../hooks/useSetting";
import shortcuts from "../../shortcuts"; import shortcuts from "../../shortcuts";
import { Map } from "../../types/Map";
import { Fog, FogToolSettings } from "../../types/Fog";
type FogAddEventHandler = (fog: Fog[]) => void;
type FogCutEventHandler = (fog: Fog[]) => void;
type FogRemoveEventHandler = (fogId: string[]) => void;
type FogEditEventHandler = (edit: Partial<Fog>[]) => 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({ function MapFog({
map, map,
shapes, shapes,
@ -58,7 +81,7 @@ function MapFog({
active, active,
toolSettings, toolSettings,
editable, editable,
}) { }: MapFogProps) {
const stageScale = useDebouncedStageScale(); const stageScale = useDebouncedStageScale();
const mapWidth = useMapWidth(); const mapWidth = useMapWidth();
const mapHeight = useMapHeight(); const mapHeight = useMapHeight();
@ -76,7 +99,7 @@ function MapFog({
const [editOpacity] = useSetting("fog.editOpacity"); const [editOpacity] = useSetting("fog.editOpacity");
const mapStageRef = useMapStage(); const mapStageRef = useMapStage();
const [drawingShape, setDrawingShape] = useState(null); const [drawingShape, setDrawingShape] = useState<Fog | null>(null);
const [isBrushDown, setIsBrushDown] = useState(false); const [isBrushDown, setIsBrushDown] = useState(false);
const [editingShapes, setEditingShapes] = useState([]); const [editingShapes, setEditingShapes] = useState([]);
@ -84,7 +107,7 @@ function MapFog({
const [fogShapes, setFogShapes] = useState(shapes); const [fogShapes, setFogShapes] = useState(shapes);
// Bounding boxes for guides // Bounding boxes for guides
const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState([]); const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState([]);
const [guides, setGuides] = useState([]); const [guides, setGuides] = useState<Guide[]>([]);
const shouldHover = const shouldHover =
active && active &&
@ -108,8 +131,14 @@ function MapFog({
const mapStage = mapStageRef.current; const mapStage = mapStageRef.current;
function getBrushPosition(snapping = true) { function getBrushPosition(snapping = true) {
if (!mapStage) {
return;
}
const mapImage = mapStage.findOne("#mapImage"); const mapImage = mapStage.findOne("#mapImage");
let position = getRelativePointerPosition(mapImage); let position = getRelativePointerPosition(mapImage);
if (!position) {
return;
}
if (shouldUseGuides && snapping) { if (shouldUseGuides && snapping) {
for (let guide of guides) { for (let guide of guides) {
if (guide.orientation === "vertical") { if (guide.orientation === "vertical") {
@ -129,6 +158,9 @@ function MapFog({
function handleBrushDown() { function handleBrushDown() {
if (toolSettings.type === "brush") { if (toolSettings.type === "brush") {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
}
setDrawingShape({ setDrawingShape({
type: "fog", type: "fog",
data: { data: {
@ -143,6 +175,9 @@ function MapFog({
} }
if (toolSettings.type === "rectangle") { if (toolSettings.type === "rectangle") {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
}
setDrawingShape({ setDrawingShape({
type: "fog", type: "fog",
data: { data: {
@ -166,7 +201,13 @@ function MapFog({
function handleBrushMove() { function handleBrushMove() {
if (toolSettings.type === "brush" && isBrushDown && drawingShape) { if (toolSettings.type === "brush" && isBrushDown && drawingShape) {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
}
setDrawingShape((prevShape) => { setDrawingShape((prevShape) => {
if (!prevShape) {
return prevShape;
}
const prevPoints = prevShape.data.points; const prevPoints = prevShape.data.points;
if ( if (
Vector2.compare( Vector2.compare(
@ -193,7 +234,13 @@ function MapFog({
if (toolSettings.type === "rectangle" && isBrushDown && drawingShape) { if (toolSettings.type === "rectangle" && isBrushDown && drawingShape) {
const prevPoints = drawingShape.data.points; const prevPoints = drawingShape.data.points;
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
}
setDrawingShape((prevShape) => { setDrawingShape((prevShape) => {
if (!prevShape) {
return prevShape;
}
return { return {
...prevShape, ...prevShape,
data: { data: {
@ -223,7 +270,7 @@ function MapFog({
const shapesToSubtract = shapes.filter((shape) => const shapesToSubtract = shapes.filter((shape) =>
cut ? !shape.visible : shape.visible cut ? !shape.visible : shape.visible
); );
const subtractAction = new SubtractShapeAction(shapesToSubtract); const subtractAction = new SubtractFogAction(shapesToSubtract);
const state = subtractAction.execute({ const state = subtractAction.execute({
[drawingShape.id]: drawingShape, [drawingShape.id]: drawingShape,
}); });
@ -235,7 +282,7 @@ function MapFog({
if (drawingShapes.length > 0) { if (drawingShapes.length > 0) {
if (cut) { if (cut) {
// Run a pre-emptive cut action to check whether we've cut anything // 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")); const state = cutAction.execute(keyBy(shapes, "id"));
if (Object.keys(state).length === shapes.length) { if (Object.keys(state).length === shapes.length) {
@ -300,7 +347,7 @@ function MapFog({
function handlePointerMove() { function handlePointerMove() {
if (shouldUseGuides) { if (shouldUseGuides) {
let guides = []; let guides: Guide[] = [];
const brushPosition = getBrushPosition(false); const brushPosition = getBrushPosition(false);
const absoluteBrushPosition = Vector2.multiply(brushPosition, { const absoluteBrushPosition = Vector2.multiply(brushPosition, {
x: mapWidth, x: mapWidth,
@ -393,7 +440,7 @@ function MapFog({
const shapesToSubtract = shapes.filter((shape) => const shapesToSubtract = shapes.filter((shape) =>
cut ? !shape.visible : shape.visible cut ? !shape.visible : shape.visible
); );
const subtractAction = new SubtractShapeAction(shapesToSubtract); const subtractAction = new SubtractFogAction(shapesToSubtract);
const state = subtractAction.execute({ const state = subtractAction.execute({
[polygonShape.id]: polygonShape, [polygonShape.id]: polygonShape,
}); });
@ -405,7 +452,7 @@ function MapFog({
if (polygonShapes.length > 0) { if (polygonShapes.length > 0) {
if (cut) { if (cut) {
// Run a pre-emptive cut action to check whether we've cut anything // 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")); const state = cutAction.execute(keyBy(shapes, "id"));
if (Object.keys(state).length === shapes.length) { if (Object.keys(state).length === shapes.length) {

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useImage from "use-image"; import useImage from "use-image";
import { useDataURL } from "../../contexts/AssetsContext"; import { useDataURL } from "../../contexts/AssetsContext";
@ -8,8 +8,9 @@ import { mapSources as defaultMapSources } from "../../maps";
import { getImageLightness } from "../../helpers/image"; import { getImageLightness } from "../../helpers/image";
import Grid from "../Grid"; import Grid from "../Grid";
import { Map } from "../../types/Map";
function MapGrid({ map }) { function MapGrid({ map }: { map: Map }) {
let mapSourceMap = map; let mapSourceMap = map;
const mapURL = useDataURL( const mapURL = useDataURL(
mapSourceMap, mapSourceMap,
@ -17,13 +18,14 @@ function MapGrid({ map }) {
undefined, undefined,
map.type === "file" map.type === "file"
); );
const [mapImage, mapLoadingStatus] = useImage(mapURL);
const [mapImage, mapLoadingStatus] = useImage(mapURL || "");
const [isImageLight, setIsImageLight] = useState(true); const [isImageLight, setIsImageLight] = useState(true);
// When the map changes find the average lightness of its pixels // When the map changes find the average lightness of its pixels
useEffect(() => { useEffect(() => {
if (mapLoadingStatus === "loaded") { if (mapLoadingStatus === "loaded" && mapImage) {
setIsImageLight(getImageLightness(mapImage)); setIsImageLight(getImageLightness(mapImage));
} }
}, [mapImage, mapLoadingStatus]); }, [mapImage, mapLoadingStatus]);

View File

@ -1,5 +1,6 @@
import React, { useRef } from "react"; import { useRef } from "react";
import { Group, Circle, Rect } from "react-konva"; import { Group, Circle, Rect } from "react-konva";
import { KonvaEventObject, Node } from "konva/types/Node";
import { import {
useDebouncedStageScale, useDebouncedStageScale,
@ -12,8 +13,15 @@ import { useKeyboard } from "../../contexts/KeyboardContext";
import Vector2 from "../../helpers/Vector2"; import Vector2 from "../../helpers/Vector2";
import shortcuts from "../../shortcuts"; 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 stageScale = useDebouncedStageScale();
const mapWidth = useMapWidth(); const mapWidth = useMapWidth();
const mapHeight = useMapHeight(); const mapHeight = useMapHeight();
@ -39,21 +47,21 @@ function MapGridEditor({ map, onGridChange }) {
} }
const handlePositions = getHandlePositions(); const handlePositions = getHandlePositions();
const handlePreviousPositionRef = useRef(); const handlePreviousPositionRef = useRef<Vector2>();
function handleScaleCircleDragStart(event) { function handleScaleCircleDragStart(event: KonvaEventObject<MouseEvent>) {
const handle = event.target; const handle = event.target;
const position = getHandleNormalizedPosition(handle); const position = getHandleNormalizedPosition(handle);
handlePreviousPositionRef.current = position; handlePreviousPositionRef.current = position;
} }
function handleScaleCircleDragMove(event) { function handleScaleCircleDragMove(event: KonvaEventObject<MouseEvent>) {
const handle = event.target; const handle = event.target;
onGridChange(getHandleInset(handle)); onGridChange(getHandleInset(handle));
handlePreviousPositionRef.current = getHandleNormalizedPosition(handle); handlePreviousPositionRef.current = getHandleNormalizedPosition(handle);
} }
function handleScaleCircleDragEnd(event) { function handleScaleCircleDragEnd(event: KonvaEventObject<MouseEvent>) {
onGridChange(getHandleInset(event.target)); onGridChange(getHandleInset(event.target));
setPreventMapInteraction(false); setPreventMapInteraction(false);
} }
@ -66,11 +74,14 @@ function MapGridEditor({ map, onGridChange }) {
setPreventMapInteraction(false); setPreventMapInteraction(false);
} }
function getHandleInset(handle) { function getHandleInset(handle: Node): GridInset {
const name = handle.name(); const name = handle.name();
// Find distance and direction of dragging // Find distance and direction of dragging
const previousPosition = handlePreviousPositionRef.current; const previousPosition = handlePreviousPositionRef.current;
if (!previousPosition) {
return map.grid.inset;
}
const position = getHandleNormalizedPosition(handle); const position = getHandleNormalizedPosition(handle);
const distance = Vector2.distance(previousPosition, position); const distance = Vector2.distance(previousPosition, position);
const direction = Vector2.normalize( 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 inset = map.grid.inset;
const gridSizeNormalized = Vector2.divide( const gridSizeNormalized = Vector2.divide(
Vector2.subtract(inset.bottomRight, inset.topLeft), 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; const nudgeAmount = event.shiftKey ? 2 : 0.5;
if (shortcuts.gridNudgeUp(event)) { if (shortcuts.gridNudgeUp(event)) {
// Stop arrow up/down scrolling if overflowing // Stop arrow up/down scrolling if overflowing
@ -191,7 +202,7 @@ function MapGridEditor({ map, onGridChange }) {
useKeyboard(handleKeyDown); useKeyboard(handleKeyDown);
function getHandleNormalizedPosition(handle) { function getHandleNormalizedPosition(handle: Node) {
return Vector2.divide({ x: handle.x(), y: handle.y() }, mapSize); return Vector2.divide({ x: handle.x(), y: handle.y() }, mapSize);
} }

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import { useState } from "react";
import { IconButton } from "theme-ui"; import { IconButton } from "theme-ui";
import SelectMapModal from "../../modals/SelectMapModal"; import SelectMapModal from "../../modals/SelectMapModal";
@ -6,6 +6,20 @@ import SelectMapIcon from "../../icons/SelectMapIcon";
import { useMapData } from "../../contexts/MapDataContext"; import { useMapData } from "../../contexts/MapDataContext";
import { useUserId } from "../../contexts/UserIdContext"; 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({ function SelectMapButton({
onMapChange, onMapChange,
@ -13,7 +27,7 @@ function SelectMapButton({
currentMap, currentMap,
currentMapState, currentMapState,
disabled, disabled,
}) { }: SelectMapButtonProps) {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { updateMapState } = useMapData(); const { updateMapState } = useMapData();

View File

@ -4,7 +4,13 @@ import { IconButton } from "theme-ui";
import ChangeNicknameModal from "../../modals/ChangeNicknameModal"; import ChangeNicknameModal from "../../modals/ChangeNicknameModal";
import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon"; 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); const [isChangeModalOpen, setIsChangeModalOpen] = useState(false);
function openModal() { function openModal() {
setIsChangeModalOpen(true); setIsChangeModalOpen(true);

View File

@ -1,12 +1,20 @@
import { Flex, Box, Text } from "theme-ui"; 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 ( return (
<Flex sx={{ flexWrap: "wrap" }}> <Flex sx={{ flexWrap: "wrap" }}>
<Box sx={{ transform: "scale(0.8)" }}>{children}</Box> <Box sx={{ transform: "scale(0.8)" }}>{children}</Box>
{rolls {rolls
.filter((d: any) => d.type === type && d.roll !== "unknown") .filter((d) => d.type === type && d.roll !== "unknown")
.map((dice: any, index: string | number) => ( .map((dice, index: string | number) => (
<Text as="p" my={1} variant="caption" mx={1} key={index}> <Text as="p" my={1} variant="caption" mx={1} key={index}>
{dice.roll} {dice.roll}
</Text> </Text>

View File

@ -24,14 +24,14 @@ const diceIcons = [
{ type: "d100", Icon: D100Icon }, { type: "d100", Icon: D100Icon },
]; ];
function DiceRolls({ rolls }: { rolls: any }) { function DiceRolls({ rolls }: { rolls }) {
const total = getDiceRollTotal(rolls); const total = getDiceRollTotal(rolls);
const [expanded, setExpanded] = useState<boolean>(false); const [expanded, setExpanded] = useState<boolean>(false);
let expandedRolls = []; let expandedRolls = [];
for (let icon of diceIcons) { for (let icon of diceIcons) {
if (rolls.some((roll: any) => roll.type === icon.type)) { if (rolls.some((roll) => roll.type === icon.type)) {
expandedRolls.push( expandedRolls.push(
<DiceRoll rolls={rolls} type={icon.type} key={icon.type}> <DiceRoll rolls={rolls} type={icon.type} key={icon.type}>
<icon.Icon /> <icon.Icon />
@ -45,29 +45,29 @@ function DiceRolls({ rolls }: { rolls: any }) {
} }
return ( return (
<Flex sx={{ flexDirection: "column" }}> <Flex sx={{ flexDirection: "column" }}>
<Flex sx={{ alignItems: "center" }}> <Flex sx={{ alignItems: "center" }}>
<IconButton <IconButton
title={expanded ? "Hide Rolls" : "Show Rolls"} title={expanded ? "Hide Rolls" : "Show Rolls"}
aria-label={expanded ? "Hide Rolls" : "Show Rolls"} aria-label={expanded ? "Hide Rolls" : "Show Rolls"}
onClick={() => setExpanded(!expanded)} onClick={() => setExpanded(!expanded)}
> >
<DiceRollsIcon /> <DiceRollsIcon />
</IconButton> </IconButton>
<Text px={1} as="p" my={1} variant="body2" sx={{ width: "100%" }}> <Text px={1} as="p" my={1} variant="body2" sx={{ width: "100%" }}>
{total} {total}
</Text> </Text>
</Flex>
{expanded && (
<Flex
sx={{
flexDirection: "column",
}}
>
{expandedRolls}
</Flex>
)}
</Flex> </Flex>
{expanded && (
<Flex
sx={{
flexDirection: "column",
}}
>
{expandedRolls}
</Flex>
)}
</Flex>
); );
} }

View File

@ -16,7 +16,12 @@ function DiceTrayButton({
onShareDiceChange, onShareDiceChange,
diceRolls, diceRolls,
onDiceRollsChange, onDiceRollsChange,
}: { shareDice: boolean, onShareDiceChange: any, diceRolls: [], onDiceRollsChange: any}) { }: {
shareDice: boolean;
onShareDiceChange;
diceRolls: [];
onDiceRollsChange;
}) {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [fullScreen] = useSetting("map.fullScreen"); const [fullScreen] = useSetting("map.fullScreen");

View File

@ -4,7 +4,15 @@ import Stream from "./Stream";
import DiceRolls from "./DiceRolls"; import DiceRolls from "./DiceRolls";
// TODO: check if stream is a required or optional param // 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 ( return (
<Flex sx={{ flexDirection: "column" }}> <Flex sx={{ flexDirection: "column" }}>
<Text <Text

View File

@ -10,14 +10,31 @@ import SettingsButton from "../SettingsButton";
import StartTimerButton from "./StartTimerButton"; import StartTimerButton from "./StartTimerButton";
import Timer from "./Timer"; import Timer from "./Timer";
import DiceTrayButton from "./DiceTrayButton"; import DiceTrayButton from "./DiceTrayButton";
import { PartyState, PlayerDice, PlayerInfo, Timer as PartyTimer } from "./PartyState" import {
PartyState,
PlayerDice,
PlayerInfo,
Timer as PartyTimer,
} from "./PartyState";
import useSetting from "../../hooks/useSetting"; import useSetting from "../../hooks/useSetting";
import { useParty } from "../../contexts/PartyContext"; import { useParty } from "../../contexts/PartyContext";
import { usePlayerState, usePlayerUpdater } from "../../contexts/PlayerContext"; import { usePlayerState, usePlayerUpdater } from "../../contexts/PlayerContext";
function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }: { gameId: string, stream: any, partyStreams: any, onStreamStart: any, onStreamEnd: any}) { function Party({
gameId,
stream,
partyStreams,
onStreamStart,
onStreamEnd,
}: {
gameId: string;
stream;
partyStreams;
onStreamStart;
onStreamEnd;
}) {
const setPlayerState = usePlayerUpdater(); const setPlayerState = usePlayerUpdater();
const playerState: PlayerInfo = usePlayerState(); const playerState: PlayerInfo = usePlayerState();
const partyState: PartyState = useParty(); const partyState: PartyState = useParty();
@ -26,18 +43,18 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }: { g
const [shareDice, setShareDice] = useSetting("dice.shareDice"); const [shareDice, setShareDice] = useSetting("dice.shareDice");
function handleTimerStart(newTimer: PartyTimer) { function handleTimerStart(newTimer: PartyTimer) {
setPlayerState((prevState: any) => ({ ...prevState, timer: newTimer })); setPlayerState((prevState) => ({ ...prevState, timer: newTimer }));
} }
function handleTimerStop() { function handleTimerStop() {
setPlayerState((prevState: any) => ({ ...prevState, timer: null })); setPlayerState((prevState) => ({ ...prevState, timer: null }));
} }
useEffect(() => { useEffect(() => {
let prevTime = performance.now(); let prevTime = performance.now();
let request = requestAnimationFrame(update); let request = requestAnimationFrame(update);
let counter = 0; let counter = 0;
function update(time: any) { function update(time) {
request = requestAnimationFrame(update); request = requestAnimationFrame(update);
const deltaTime = time - prevTime; const deltaTime = time - prevTime;
prevTime = time; prevTime = time;
@ -51,9 +68,9 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }: { g
current: playerState.timer.current - counter, current: playerState.timer.current - counter,
}; };
if (newTimer.current < 0) { if (newTimer.current < 0) {
setPlayerState((prevState: any) => ({ ...prevState, timer: null })); setPlayerState((prevState) => ({ ...prevState, timer: null }));
} else { } else {
setPlayerState((prevState: any) => ({ ...prevState, timer: newTimer })); setPlayerState((prevState) => ({ ...prevState, timer: newTimer }));
} }
counter = 0; counter = 0;
} }
@ -65,7 +82,7 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }: { g
}, [playerState.timer, setPlayerState]); }, [playerState.timer, setPlayerState]);
function handleNicknameChange(newNickname: string) { function handleNicknameChange(newNickname: string) {
setPlayerState((prevState: any) => ({ ...prevState, nickname: newNickname })); setPlayerState((prevState) => ({ ...prevState, nickname: newNickname }));
} }
function handleDiceRollsChange(newDiceRolls: number[]) { function handleDiceRollsChange(newDiceRolls: number[]) {

View File

@ -6,7 +6,15 @@ import Link from "../Link";
import StartStreamModal from "../../modals/StartStreamModal"; 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); const [isStreamModalOpoen, setIsStreamModalOpen] = useState(false);
function openModal() { function openModal() {
setIsStreamModalOpen(true); setIsStreamModalOpen(true);
@ -45,7 +53,7 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }: { onStreamSta
function handleStreamStart() { function handleStreamStart() {
// Must be defined this way in typescript due to open issue - https://github.com/microsoft/TypeScript/issues/33232 // 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 mediaDevices
.getDisplayMedia({ .getDisplayMedia({
video: true, video: true,
@ -55,10 +63,12 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }: { onStreamSta
echoCancellation: false, echoCancellation: false,
}, },
}) })
.then((localStream: { getTracks: () => any; }) => { .then((localStream: { getTracks }) => {
const tracks = 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); setNoAudioTrack(!hasAudio);
// Ensure an audio track is present // Ensure an audio track is present

View File

@ -4,7 +4,15 @@ import { IconButton } from "theme-ui";
import StartTimerModal from "../../modals/StartTimerModal"; import StartTimerModal from "../../modals/StartTimerModal";
import StartTimerIcon from "../../icons/StartTimerIcon"; 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); const [isTimerModalOpen, setIsTimerModalOpen] = useState(false);
function openModal() { function openModal() {

View File

@ -6,13 +6,18 @@ import StreamMuteIcon from "../../icons/StreamMuteIcon";
import Banner from "../banner/Banner"; import Banner from "../banner/Banner";
import Slider from "../Slider"; 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 [streamVolume, setStreamVolume] = useState(1);
const [showStreamInteractBanner, setShowStreamInteractBanner] = useState( const [showStreamInteractBanner, setShowStreamInteractBanner] =
false useState(false);
);
const [streamMuted, setStreamMuted] = useState(false); const [streamMuted, setStreamMuted] = useState(false);
const audioRef = useRef<any>(); const audioRef = useRef();
useEffect(() => { useEffect(() => {
if (audioRef.current) { 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 // Platforms like iOS don't allow you to control audio volume
// Detect this by trying to change the audio volume // Detect this by trying to change the audio volume
const [isVolumeControlAvailable, setIsVolumeControlAvailable] = useState( const [isVolumeControlAvailable, setIsVolumeControlAvailable] =
true useState(true);
);
useEffect(() => { useEffect(() => {
let audio = audioRef.current; let audio = audioRef.current;
function checkVolumeControlAvailable() { 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% // Use an audio context gain node to control volume to go past 100%
const audioGainRef = useRef<any>(); const audioGainRef = useRef();
useEffect(() => { useEffect(() => {
let audioContext: AudioContext; let audioContext: AudioContext;
if (stream && !streamMuted && isVolumeControlAvailable && audioGainRef) { if (stream && !streamMuted && isVolumeControlAvailable && audioGainRef) {

View File

@ -4,8 +4,8 @@ import { Box, Progress } from "theme-ui";
import usePortal from "../../hooks/usePortal"; import usePortal from "../../hooks/usePortal";
function Timer({ timer, index }: { timer: any, index: number}) { function Timer({ timer, index }: { timer; index: number }) {
const progressBarRef = useRef<any>(); const progressBarRef = useRef();
useEffect(() => { useEffect(() => {
if (progressBarRef.current && timer) { if (progressBarRef.current && timer) {
@ -16,7 +16,7 @@ function Timer({ timer, index }: { timer: any, index: number}) {
useEffect(() => { useEffect(() => {
let request = requestAnimationFrame(animate); let request = requestAnimationFrame(animate);
let previousTime = performance.now(); let previousTime = performance.now();
function animate(time: any) { function animate(time) {
request = requestAnimationFrame(animate); request = requestAnimationFrame(animate);
const deltaTime = time - previousTime; const deltaTime = time - previousTime;
previousTime = time; previousTime = time;

View File

@ -10,10 +10,16 @@ import useDebounce from "../hooks/useDebounce";
import { omit } from "../helpers/shared"; import { omit } from "../helpers/shared";
import { Asset } from "../types/Asset"; import { Asset } from "../types/Asset";
export type GetAssetEventHanlder = (
assetId: string
) => Promise<Asset | undefined>;
export type AddAssetsEventHandler = (assets: Asset[]) => Promise<void>;
export type PutAssetEventsHandler = (asset: Asset) => Promise<void>;
type AssetsContext = { type AssetsContext = {
getAsset: (assetId: string) => Promise<Asset | undefined>; getAsset: GetAssetEventHanlder;
addAssets: (assets: Asset[]) => void; addAssets: AddAssetsEventHandler;
putAsset: (asset: Asset) => void; putAsset: PutAssetEventsHandler;
}; };
const AssetsContext = React.createContext<AssetsContext | undefined>(undefined); const AssetsContext = React.createContext<AssetsContext | undefined>(undefined);
@ -30,7 +36,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) {
} }
}, [worker, databaseStatus]); }, [worker, databaseStatus]);
const getAsset = useCallback( const getAsset = useCallback<GetAssetEventHanlder>(
async (assetId) => { async (assetId) => {
if (database) { if (database) {
return await database.table("assets").get(assetId); return await database.table("assets").get(assetId);
@ -39,7 +45,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) {
[database] [database]
); );
const addAssets = useCallback( const addAssets = useCallback<AddAssetsEventHandler>(
async (assets) => { async (assets) => {
if (database) { if (database) {
await database.table("assets").bulkAdd(assets); await database.table("assets").bulkAdd(assets);
@ -48,7 +54,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) {
[database] [database]
); );
const putAsset = useCallback( const putAsset = useCallback<PutAssetEventsHandler>(
async (asset) => { async (asset) => {
if (database) { if (database) {
// Check for broadcast channel and attempt to use worker to put map to avoid UI lockup // Check for broadcast channel and attempt to use worker to put map to avoid UI lockup

View File

@ -8,24 +8,28 @@ import { getDatabase } from "../database";
//@ts-ignore //@ts-ignore
import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax 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 = { type DatabaseContext = {
database: Dexie | undefined; database: Dexie | undefined;
databaseStatus: any; databaseStatus: DatabaseStatus;
databaseError: Error | undefined; databaseError: Error | undefined;
worker: Comlink.Remote<any>; worker: Comlink.Remote<DatabaseWorkerService>;
}; };
// TODO: check what default we want here
const DatabaseContext = const DatabaseContext =
React.createContext<DatabaseContext | undefined>(undefined); React.createContext<DatabaseContext | undefined>(undefined);
const worker = Comlink.wrap(new DatabaseWorker()); const worker: Comlink.Remote<DatabaseWorkerService> = Comlink.wrap(
new DatabaseWorker()
);
export function DatabaseProvider({ children }: { children: React.ReactNode }) { export function DatabaseProvider({ children }: { children: React.ReactNode }) {
const [database, setDatabase] = useState<Dexie>(); const [database, setDatabase] = useState<Dexie>();
const [databaseStatus, setDatabaseStatus] = const [databaseStatus, setDatabaseStatus] =
useState<"loading" | "disabled" | "upgrading" | "loaded">("loading"); useState<DatabaseStatus>("loading");
const [databaseError, setDatabaseError] = useState<Error>(); const [databaseError, setDatabaseError] = useState<Error>();
useEffect(() => { useEffect(() => {

View File

@ -1,12 +1,16 @@
import React, { useState, useContext, ReactChild } from "react"; import React, { useState, useContext, ReactChild } from "react";
type DiceLoadingContext = { export type AssetLoadStartEventHandler = () => void;
assetLoadStart: any, export type AssetLoadFinishEventHandler = () => void;
assetLoadFinish: any,
isLoading: boolean,
}
const DiceLoadingContext = React.createContext<DiceLoadingContext | undefined>(undefined); type DiceLoadingContext = {
assetLoadStart: AssetLoadStartEventHandler;
assetLoadFinish: AssetLoadFinishEventHandler;
isLoading: boolean;
};
const DiceLoadingContext =
React.createContext<DiceLoadingContext | undefined>(undefined);
export function DiceLoadingProvider({ children }: { children: ReactChild }) { export function DiceLoadingProvider({ children }: { children: ReactChild }) {
const [loadingAssetCount, setLoadingAssetCount] = useState(0); const [loadingAssetCount, setLoadingAssetCount] = useState(0);

View File

@ -2,8 +2,8 @@ import React, { useContext, useState, useEffect } from "react";
import Vector2 from "../helpers/Vector2"; import Vector2 from "../helpers/Vector2";
import Size from "../helpers/Size"; import Size from "../helpers/Size";
// eslint-disable-next-line no-unused-vars import { getGridPixelSize, getCellPixelSize } from "../helpers/grid";
import { getGridPixelSize, getCellPixelSize, Grid } from "../helpers/grid"; import { Grid } from "../types/Grid";
/** /**
* @typedef GridContextValue * @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 * @property {Vector2} gridCellPixelOffset Offset of the grid cells to convert the center position of hex cells to the top left
*/ */
type GridContextValue = { type GridContextValue = {
grid: Grid, grid: Grid;
gridPixelSize: Size, gridPixelSize: Size;
gridCellPixelSize: Size, gridCellPixelSize: Size;
gridCellNormalizedSize: Size, gridCellNormalizedSize: Size;
gridOffset: Vector2, gridOffset: Vector2;
gridStrokeWidth: number, gridStrokeWidth: number;
gridCellPixelOffset: Vector2 gridCellPixelOffset: Vector2;
} };
/** /**
* @type {GridContextValue} * @type {GridContextValue}
@ -66,11 +66,21 @@ export const GridCellPixelOffsetContext = React.createContext(
const defaultStrokeWidth = 1 / 10; const defaultStrokeWidth = 1 / 10;
export function GridProvider({ grid: inputGrid, width, height, children }: { grid: Required<Grid>, 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; let grid = inputGrid;
if (!grid.size.x || !grid.size.y) { if (!grid.size.x || !grid.size.y) {
grid = defaultValue.grid as Required<Grid>; grid = defaultValue.grid;
} }
const [gridPixelSize, setGridPixelSize] = useState( const [gridPixelSize, setGridPixelSize] = useState(

View File

@ -9,26 +9,41 @@ import { getGroupItems, groupsFromIds } from "../helpers/group";
import shortcuts from "../shortcuts"; import shortcuts from "../shortcuts";
import { Group, GroupContainer, GroupItem } from "../types/Group"; 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 = { type GroupContext = {
groups: Group[]; groups: Group[];
activeGroups: Group[]; activeGroups: Group[] | GroupItem[];
openGroupId: string | undefined; openGroupId: string | undefined;
openGroupItems: Group[]; openGroupItems: GroupItem[];
filter: string | undefined; filter: string | undefined;
filteredGroupItems: GroupItem[]; filteredGroupItems: GroupItem[];
selectedGroupIds: string[]; selectedGroupIds: string[];
selectMode: any; selectMode: GroupSelectMode;
onSelectModeChange: React.Dispatch< onSelectModeChange: GroupSelectModeChangeEventHandler;
React.SetStateAction<"single" | "multiple" | "range"> onGroupOpen: GroupOpenEventHandler;
>; onGroupClose: GroupCloseEventHandler;
onGroupOpen: (groupId: string) => void; onGroupsChange: GroupsChangeEventHandler;
onGroupClose: () => void; onSubgroupChange: SubgroupsChangeEventHandler;
onGroupsChange: ( onGroupSelect: GroupSelectEventHandler;
newGroups: Group[] | GroupItem[], onClearSelection: GroupClearSelectionEventHandler;
groupId: string | undefined onFilterChange: GroupFilterChangeEventHandler;
) => void; onFilterClear: GroupClearFilterEventHandler;
onGroupSelect: (groupId: string | undefined) => void;
onFilterChange: React.Dispatch<React.SetStateAction<string | undefined>>;
}; };
const GroupContext = React.createContext<GroupContext | undefined>(undefined); const GroupContext = React.createContext<GroupContext | undefined>(undefined);
@ -36,8 +51,8 @@ const GroupContext = React.createContext<GroupContext | undefined>(undefined);
type GroupProviderProps = { type GroupProviderProps = {
groups: Group[]; groups: Group[];
itemNames: Record<string, string>; itemNames: Record<string, string>;
onGroupsChange: (groups: Group[]) => void; onGroupsChange: GroupsChangeEventHandler;
onGroupsSelect: (groupIds: string[]) => void; onGroupsSelect: GroupsSelectEventHandler;
disabled: boolean; disabled: boolean;
children: React.ReactNode; children: React.ReactNode;
}; };
@ -51,15 +66,13 @@ export function GroupProvider({
children, children,
}: GroupProviderProps) { }: GroupProviderProps) {
const [selectedGroupIds, setSelectedGroupIds] = useState<string[]>([]); const [selectedGroupIds, setSelectedGroupIds] = useState<string[]>([]);
// Either single, multiple or range const [selectMode, setSelectMode] = useState<GroupSelectMode>("single");
const [selectMode, setSelectMode] =
useState<"single" | "multiple" | "range">("single");
/** /**
* Group Open * Group Open
*/ */
const [openGroupId, setOpenGroupId] = useState<string>(); const [openGroupId, setOpenGroupId] = useState<string>();
const [openGroupItems, setOpenGroupItems] = useState<Group[]>([]); const [openGroupItems, setOpenGroupItems] = useState<GroupItem[]>([]);
useEffect(() => { useEffect(() => {
if (openGroupId) { if (openGroupId) {
const openGroups = groupsFromIds([openGroupId], groups); const openGroups = groupsFromIds([openGroupId], groups);
@ -128,81 +141,78 @@ export function GroupProvider({
? filteredGroupItems ? filteredGroupItems
: groups; : groups;
/** function handleGroupsChange(newGroups: Group[]) {
* @param {Group[] | GroupItem[]} newGroups onGroupsChange(newGroups);
* @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object }
*/
function handleGroupsChange( function handleSubgroupChange(items: GroupItem[], groupId: string) {
newGroups: Group[] | GroupItem[], const groupIndex = groups.findIndex((group) => group.id === groupId);
groupId: string | undefined let updatedGroups = cloneDeep(groups);
) { const group = updatedGroups[groupIndex];
if (groupId) { if (group.type === "group") {
// 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];
updatedGroups[groupIndex] = { updatedGroups[groupIndex] = {
...group, ...group,
items: newGroups, items,
} as GroupContainer; };
onGroupsChange(updatedGroups); onGroupsChange(updatedGroups);
} else { } else {
onGroupsChange(newGroups); throw new Error(`Group ${group} not a subgroup`);
} }
} }
function handleGroupSelect(groupId: string | undefined) { function handleGroupSelect(groupId: string) {
let groupIds: string[] = []; let groupIds: string[] = [];
if (groupId) { switch (selectMode) {
switch (selectMode) { case "single":
case "single": groupIds = [groupId];
groupIds = [groupId]; break;
break; case "multiple":
case "multiple": if (selectedGroupIds.includes(groupId)) {
if (selectedGroupIds.includes(groupId)) { groupIds = selectedGroupIds.filter((id) => id !== groupId);
groupIds = selectedGroupIds.filter((id) => id !== groupId); } else {
} else { groupIds = [...selectedGroupIds, groupId];
groupIds = [...selectedGroupIds, groupId]; }
} break;
break; case "range":
case "range": if (selectedGroupIds.length > 0) {
if (selectedGroupIds.length > 0) { const currentIndex = activeGroups.findIndex((g) => g.id === groupId);
const currentIndex = activeGroups.findIndex( const lastIndex = activeGroups.findIndex(
(g) => g.id === groupId (g) => g.id === selectedGroupIds[selectedGroupIds.length - 1]
); );
const lastIndex = activeGroups.findIndex( let idsToAdd: string[] = [];
(g) => g.id === selectedGroupIds[selectedGroupIds.length - 1] let idsToRemove: string[] = [];
); const direction = currentIndex > lastIndex ? 1 : -1;
let idsToAdd: string[] = []; for (
let idsToRemove: string[] = []; let i = lastIndex + direction;
const direction = currentIndex > lastIndex ? 1 : -1; direction < 0 ? i >= currentIndex : i <= currentIndex;
for ( i += direction
let i = lastIndex + direction; ) {
direction < 0 ? i >= currentIndex : i <= currentIndex; const id = activeGroups[i].id;
i += direction if (selectedGroupIds.includes(id)) {
) { idsToRemove.push(id);
const id = activeGroups[i].id; } else {
if (selectedGroupIds.includes(id)) { idsToAdd.push(id);
idsToRemove.push(id);
} else {
idsToAdd.push(id);
}
} }
groupIds = [...selectedGroupIds, ...idsToAdd].filter(
(id) => !idsToRemove.includes(id)
);
} else {
groupIds = [groupId];
} }
break; groupIds = [...selectedGroupIds, ...idsToAdd].filter(
default: (id) => !idsToRemove.includes(id)
groupIds = []; );
} } else {
groupIds = [groupId];
}
break;
default:
groupIds = [];
} }
setSelectedGroupIds(groupIds); setSelectedGroupIds(groupIds);
onGroupsSelect(groupIds); onGroupsSelect(groupIds);
} }
function handleClearSelection() {
setSelectedGroupIds([]);
onGroupsSelect([]);
}
/** /**
* Shortcuts * Shortcuts
*/ */
@ -239,7 +249,7 @@ export function GroupProvider({
useBlur(handleBlur); useBlur(handleBlur);
const value = { const value: GroupContext = {
groups, groups,
activeGroups, activeGroups,
openGroupId, openGroupId,
@ -252,8 +262,11 @@ export function GroupProvider({
onGroupOpen: handleGroupOpen, onGroupOpen: handleGroupOpen,
onGroupClose: handleGroupClose, onGroupClose: handleGroupClose,
onGroupsChange: handleGroupsChange, onGroupsChange: handleGroupsChange,
onSubgroupChange: handleSubgroupChange,
onGroupSelect: handleGroupSelect, onGroupSelect: handleGroupSelect,
onClearSelection: handleClearSelection,
onFilterChange: setFilter, onFilterChange: setFilter,
onFilterClear: () => setFilter(undefined),
}; };
return ( return (

View File

@ -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<any>({});
// 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 (
<ImageSourcesStateContext.Provider value={imageSources}>
<ImageSourcesUpdaterContext.Provider value={setImageSources}>
{children}
</ImageSourcesUpdaterContext.Provider>
</ImageSourcesStateContext.Provider>
);
}
/**
* 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;
}

View File

@ -53,10 +53,7 @@ type MapDataContext = {
const MapDataContext = const MapDataContext =
React.createContext<MapDataContext | undefined>(undefined); React.createContext<MapDataContext | undefined>(undefined);
const defaultMapState: Pick< const defaultMapState: Omit<MapState, "mapId"> = {
MapState,
"tokens" | "drawShapes" | "fogShapes" | "editFlags" | "notes"
> = {
tokens: {}, tokens: {},
drawShapes: {}, drawShapes: {},
fogShapes: {}, fogShapes: {},

View File

@ -1,10 +1,16 @@
import React, { useState, useEffect, useContext } from "react"; import React, { useState, useEffect, useContext } from "react";
import { PartyState } from "../components/party/PartyState";
import Session from "../network/Session"; import Session from "../network/Session";
import { PartyState } from "../types/PartyState";
const PartyContext = React.createContext<PartyState | undefined>(undefined); const PartyContext = React.createContext<PartyState | undefined>(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({}); const [partyState, setPartyState] = useState({});
useEffect(() => { useEffect(() => {

View File

@ -5,29 +5,32 @@ import { useUserId } from "./UserIdContext";
import { getRandomMonster } from "../helpers/monsters"; import { getRandomMonster } from "../helpers/monsters";
import useNetworkedState from "../hooks/useNetworkedState"; import useNetworkedState, {
SetNetworkedState,
} from "../hooks/useNetworkedState";
import Session from "../network/Session"; import Session from "../network/Session";
import { PlayerInfo } from "../components/party/PartyState"; import { PlayerState } from "../types/PlayerState";
export const PlayerStateContext = React.createContext<any>(undefined); export const PlayerStateContext =
export const PlayerUpdaterContext = React.createContext<any>(() => {}); React.createContext<PlayerState | undefined>(undefined);
export const PlayerUpdaterContext =
React.createContext<SetNetworkedState<PlayerState> | undefined>(undefined);
export function PlayerProvider({ type PlayerProviderProps = {
session,
children,
}: {
session: Session; session: Session;
children: React.ReactNode; children: React.ReactNode;
}) { };
export function PlayerProvider({ session, children }: PlayerProviderProps) {
const userId = useUserId(); const userId = useUserId();
const { database, databaseStatus } = useDatabase(); const { database, databaseStatus } = useDatabase();
const [playerState, setPlayerState] = useNetworkedState( const [playerState, setPlayerState] = useNetworkedState<PlayerState>(
{ {
nickname: "", nickname: "",
timer: null, timer: undefined,
dice: { share: false, rolls: [] }, dice: { share: false, rolls: [] },
sessionId: null, sessionId: undefined,
userId, userId,
}, },
session, session,
@ -43,13 +46,13 @@ export function PlayerProvider({
async function loadNickname() { async function loadNickname() {
const storedNickname = await database?.table("user").get("nickname"); const storedNickname = await database?.table("user").get("nickname");
if (storedNickname !== undefined) { if (storedNickname !== undefined) {
setPlayerState((prevState: PlayerInfo) => ({ setPlayerState((prevState) => ({
...prevState, ...prevState,
nickname: storedNickname.value, nickname: storedNickname.value,
})); }));
} else { } else {
const name = getRandomMonster(); const name = getRandomMonster();
setPlayerState((prevState: any) => ({ ...prevState, nickname: name })); setPlayerState((prevState) => ({ ...prevState, nickname: name }));
database?.table("user").add({ key: "nickname", value: name }); database?.table("user").add({ key: "nickname", value: name });
} }
} }
@ -71,7 +74,7 @@ export function PlayerProvider({
useEffect(() => { useEffect(() => {
if (userId) { if (userId) {
setPlayerState((prevState: PlayerInfo) => { setPlayerState((prevState) => {
if (prevState) { if (prevState) {
return { return {
...prevState, ...prevState,
@ -85,8 +88,7 @@ export function PlayerProvider({
useEffect(() => { useEffect(() => {
function updateSessionId() { function updateSessionId() {
setPlayerState((prevState: PlayerInfo) => { setPlayerState((prevState) => {
// TODO: check useNetworkState requirements here
if (prevState) { if (prevState) {
return { return {
...prevState, ...prevState,

View File

@ -14,7 +14,7 @@ const SettingsContext =
const settingsProvider = getSettings(); const settingsProvider = getSettings();
export function SettingsProvider({ children }: { children: any }) { export function SettingsProvider({ children }: { children: React.ReactNode }) {
const [settings, setSettings] = useState<Settings>(settingsProvider.getAll()); const [settings, setSettings] = useState<Settings>(settingsProvider.getAll());
useEffect(() => { useEffect(() => {

View File

@ -19,6 +19,7 @@ import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
import Vector2 from "../helpers/Vector2"; import Vector2 from "../helpers/Vector2";
import usePreventSelect from "../hooks/usePreventSelect"; import usePreventSelect from "../hooks/usePreventSelect";
import { GroupItem } from "../types/Group";
const TileDragIdContext = const TileDragIdContext =
React.createContext<string | undefined | null>(undefined); React.createContext<string | undefined | null>(undefined);
@ -72,7 +73,9 @@ export function TileDragProvider({
openGroupId, openGroupId,
selectedGroupIds, selectedGroupIds,
onGroupsChange, onGroupsChange,
onSubgroupChange,
onGroupSelect, onGroupSelect,
onClearSelection,
filter, filter,
} = useGroup(); } = useGroup();
@ -145,24 +148,28 @@ export function TileDragProvider({
selectedIndices = selectedIndices.sort((a, b) => a - b); selectedIndices = selectedIndices.sort((a, b) => a - b);
if (over.id.startsWith(GROUP_ID_PREFIX)) { if (over.id.startsWith(GROUP_ID_PREFIX)) {
onGroupSelect(undefined); onClearSelection();
// Handle tile group // Handle tile group
const overId = over.id.slice(9); const overId = over.id.slice(9);
if (overId !== active.id) { if (overId !== active.id) {
const overGroupIndex = activeGroups.findIndex( const overGroupIndex = activeGroups.findIndex(
(group) => group.id === overId (group) => group.id === overId
); );
onGroupsChange( const newGroups = moveGroupsInto(
moveGroupsInto(activeGroups, overGroupIndex, selectedIndices), activeGroups,
openGroupId overGroupIndex,
selectedIndices
); );
if (!openGroupId) {
onGroupsChange(newGroups);
}
} }
} else if (over.id === UNGROUP_ID) { } else if (over.id === UNGROUP_ID) {
if (openGroupId) { if (openGroupId) {
onGroupSelect(undefined); onClearSelection();
// Handle tile ungroup // Handle tile ungroup
const newGroups = ungroup(groups, openGroupId, selectedIndices); const newGroups = ungroup(groups, openGroupId, selectedIndices);
onGroupsChange(newGroups, undefined); onGroupsChange(newGroups);
} }
} else if (over.id === ADD_TO_MAP_ID) { } else if (over.id === ADD_TO_MAP_ID) {
onDragAdd && onDragAdd &&
@ -173,10 +180,16 @@ export function TileDragProvider({
const overGroupIndex = activeGroups.findIndex( const overGroupIndex = activeGroups.findIndex(
(group) => group.id === over.id (group) => group.id === over.id
); );
onGroupsChange( const newGroups = moveGroups(
moveGroups(activeGroups, overGroupIndex, selectedIndices), activeGroups,
openGroupId overGroupIndex,
selectedIndices
); );
if (openGroupId) {
onSubgroupChange(newGroups as GroupItem[], openGroupId);
} else {
onGroupsChange(newGroups);
}
} }
} }

View File

@ -23,7 +23,7 @@ export type UpdateTokenEventHandler = (
export type GetTokenEventHandler = ( export type GetTokenEventHandler = (
tokenId: string tokenId: string
) => Promise<Token | undefined>; ) => Promise<Token | undefined>;
export type UpdateTokenGroupsEventHandler = (groups: any[]) => Promise<void>; export type UpdateTokenGroupsEventHandler = (groups: Group[]) => Promise<void>;
export type UpdateTokensHiddenEventHandler = ( export type UpdateTokensHiddenEventHandler = (
ids: string[], ids: string[],
hideInSidebar: boolean hideInSidebar: boolean

View File

@ -2,7 +2,7 @@
import Dexie, { DexieOptions } from "dexie"; import Dexie, { DexieOptions } from "dexie";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { loadVersions } from "./upgrade"; import { loadVersions, UpgradeEventHandler } from "./upgrade";
import { getDefaultMaps } from "./maps"; import { getDefaultMaps } from "./maps";
import { getDefaultTokens } from "./tokens"; import { getDefaultTokens } from "./tokens";
@ -10,7 +10,7 @@ import { getDefaultTokens } from "./tokens";
* Populate DB with initial data * Populate DB with initial data
* @param {Dexie} db * @param {Dexie} db
*/ */
function populate(db) { function populate(db: Dexie) {
db.on("populate", () => { db.on("populate", () => {
const userId = uuid(); const userId = uuid();
db.table("user").add({ key: "userId", value: userId }); db.table("user").add({ key: "userId", value: userId });
@ -35,16 +35,16 @@ function populate(db) {
* @param {string=} name * @param {string=} name
* @param {number=} versionNumber * @param {number=} versionNumber
* @param {boolean=} populateData * @param {boolean=} populateData
* @param {import("./upgrade").OnUpgrade=} onUpgrade * @param {UpgradeEventHandler=} onUpgrade
* @returns {Dexie} * @returns {Dexie}
*/ */
export function getDatabase( export function getDatabase(
options: DexieOptions, options: DexieOptions,
name = "OwlbearRodeoDB", name: string | undefined = "OwlbearRodeoDB",
versionNumber = undefined, versionNumber: number | undefined = undefined,
populateData = true, populateData: boolean | undefined = true,
onUpgrade = undefined onUpgrade: UpgradeEventHandler | undefined = undefined
) { ): Dexie {
let db = new Dexie(name, options); let db = new Dexie(name, options);
loadVersions(db, versionNumber, onUpgrade); loadVersions(db, versionNumber, onUpgrade);
if (populateData) { if (populateData) {

View File

@ -16,15 +16,13 @@ import d100Source from "./shared/d100.glb";
import { lerp } from "../helpers/shared"; import { lerp } from "../helpers/shared";
import { importTextureAsync } from "../helpers/babylon"; import { importTextureAsync } from "../helpers/babylon";
import { InstancedMesh, Material, Mesh, Scene } from "@babylonjs/core";
import { import {
BaseTexture, DiceType,
InstancedMesh, BaseDiceTextureSources,
Material, isDiceMeshes,
Mesh, DiceMeshes,
Scene, } from "../types/Dice";
Texture,
} from "@babylonjs/core";
import { DiceType } from "../types/Dice";
const minDiceRollSpeed = 600; const minDiceRollSpeed = 600;
const maxDiceRollSpeed = 800; const maxDiceRollSpeed = 800;
@ -35,13 +33,11 @@ class Dice {
static async loadMeshes( static async loadMeshes(
material: Material, material: Material,
scene: Scene, scene: Scene,
sourceOverrides?: any sourceOverrides?: Record<DiceType, string>
): Promise<Record<string, Mesh>> { ): Promise<DiceMeshes> {
let meshes: any = {}; let meshes: Partial<DiceMeshes> = {};
const addToMeshes = async (type: string | number, defaultSource: any) => { const addToMeshes = async (type: DiceType, defaultSource: string) => {
let source: string = sourceOverrides let source = sourceOverrides ? sourceOverrides[type] : defaultSource;
? sourceOverrides[type]
: defaultSource;
const mesh = await this.loadMesh(source, material, scene); const mesh = await this.loadMesh(source, material, scene);
meshes[type] = mesh; meshes[type] = mesh;
}; };
@ -54,12 +50,16 @@ class Dice {
addToMeshes("d20", d20Source), addToMeshes("d20", d20Source),
addToMeshes("d100", d100Source), 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) { static async loadMesh(source: string, material: Material, scene: Scene) {
let mesh = (await SceneLoader.ImportMeshAsync("", source, "", scene)) let mesh = (await SceneLoader.ImportMeshAsync("", source, "", scene))
.meshes[1]; .meshes[1] as Mesh;
mesh.setParent(null); mesh.setParent(null);
mesh.material = material; mesh.material = material;
@ -69,19 +69,18 @@ class Dice {
return mesh; 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 pbr = new PBRMaterial(materialName, scene);
let [albedo, normal, metalRoughness]: [ let [albedo, normal, metalRoughness] = await Promise.all([
albedo: BaseTexture,
normal: Texture,
metalRoughness: Texture
] = await Promise.all([
importTextureAsync(textures.albedo), importTextureAsync(textures.albedo),
importTextureAsync(textures.normal), importTextureAsync(textures.normal),
importTextureAsync(textures.metalRoughness), importTextureAsync(textures.metalRoughness),
]); ]);
pbr.albedoTexture = albedo; pbr.albedoTexture = albedo;
// pbr.normalTexture = normal;
pbr.bumpTexture = normal; pbr.bumpTexture = normal;
pbr.metallicTexture = metalRoughness; pbr.metallicTexture = metalRoughness;
pbr.useRoughnessFromMetallicTextureAlpha = false; pbr.useRoughnessFromMetallicTextureAlpha = false;
@ -98,12 +97,10 @@ class Dice {
) { ) {
let instance = mesh.createInstance(name); let instance = mesh.createInstance(name);
instance.position = mesh.position; instance.position = mesh.position;
for (let child of mesh.getChildTransformNodes()) { for (let child of mesh.getChildMeshes()) {
// TODO: type correctly another time -> should not be any const locator = child.clone(child.name, instance);
const locator: any = child.clone(child.name, instance);
// TODO: handle possible null value
if (!locator) { if (!locator) {
throw Error; throw new Error("Unable to clone dice locator");
} }
locator.setAbsolutePosition(child.getAbsolutePosition()); locator.setAbsolutePosition(child.getAbsolutePosition());
locator.name = child.name; locator.name = child.name;
@ -120,7 +117,7 @@ class Dice {
return instance; return instance;
} }
static getDicePhysicalProperties(diceType: string) { static getDicePhysicalProperties(diceType: DiceType) {
switch (diceType) { switch (diceType) {
case "d4": case "d4":
return { mass: 4, friction: 4 }; return { mass: 4, friction: 4 };
@ -133,7 +130,7 @@ class Dice {
return { mass: 7, friction: 4 }; return { mass: 7, friction: 4 };
case "d12": case "d12":
return { mass: 8, friction: 4 }; return { mass: 8, friction: 4 };
case "20": case "d20":
return { mass: 10, friction: 4 }; return { mass: 10, friction: 4 };
default: default:
return { mass: 10, friction: 4 }; return { mass: 10, friction: 4 };
@ -145,12 +142,14 @@ class Dice {
instance.physicsImpostor?.setAngularVelocity(Vector3.Zero()); instance.physicsImpostor?.setAngularVelocity(Vector3.Zero());
const scene = instance.getScene(); const scene = instance.getScene();
// TODO: remove any typing in this function -> this is just to get it working const diceTraySingle = scene.getMeshByID("dice_tray_single");
const diceTraySingle: any = scene.getNodeByID("dice_tray_single"); const diceTrayDouble = scene.getMeshByID("dice_tray_double");
const diceTrayDouble = scene.getNodeByID("dice_tray_double"); const visibleDiceTray = diceTraySingle?.isVisible
const visibleDiceTray: any = diceTraySingle?.isVisible
? diceTraySingle ? diceTraySingle
: diceTrayDouble; : diceTrayDouble;
if (!visibleDiceTray) {
throw new Error("No dice tray to roll in");
}
const trayBounds = visibleDiceTray?.getBoundingInfo().boundingBox; const trayBounds = visibleDiceTray?.getBoundingInfo().boundingBox;
const position = new Vector3( const position = new Vector3(

View File

@ -2,10 +2,9 @@ import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader";
import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial";
import { PhysicsImpostor } from "@babylonjs/core/Physics/physicsImpostor"; import { PhysicsImpostor } from "@babylonjs/core/Physics/physicsImpostor";
import { Mesh } from "@babylonjs/core/Meshes/mesh"; import { Mesh } from "@babylonjs/core/Meshes/mesh";
import { AbstractMesh, Scene, ShadowGenerator } from "@babylonjs/core";
//@ts-ignore
import singleMeshSource from "./single.glb"; import singleMeshSource from "./single.glb";
//@ts-ignore
import doubleMeshSource from "./double.glb"; import doubleMeshSource from "./double.glb";
import singleAlbedo from "./singleAlbedo.jpg"; import singleAlbedo from "./singleAlbedo.jpg";
@ -17,7 +16,6 @@ import doubleMetalRoughness from "./doubleMetalRoughness.jpg";
import doubleNormal from "./doubleNormal.jpg"; import doubleNormal from "./doubleNormal.jpg";
import { importTextureAsync } from "../../helpers/babylon"; import { importTextureAsync } from "../../helpers/babylon";
import { Scene, ShadowGenerator, Texture } from "@babylonjs/core";
class DiceTray { class DiceTray {
_size; _size;
@ -30,12 +28,12 @@ class DiceTray {
this._size = newSize; this._size = newSize;
const wallOffsetWidth = this.collisionSize / 2 + this.width / 2 - 0.5; const wallOffsetWidth = this.collisionSize / 2 + this.width / 2 - 0.5;
const wallOffsetHeight = this.collisionSize / 2 + this.height / 2 - 0.5; const wallOffsetHeight = this.collisionSize / 2 + this.height / 2 - 0.5;
this.wallTop.position.z = -wallOffsetHeight; if (this.wallTop) this.wallTop.position.z = -wallOffsetHeight;
this.wallRight.position.x = -wallOffsetWidth; if (this.wallRight) this.wallRight.position.x = -wallOffsetWidth;
this.wallBottom.position.z = wallOffsetHeight; if (this.wallBottom) this.wallBottom.position.z = wallOffsetHeight;
this.wallLeft.position.x = wallOffsetWidth; if (this.wallLeft) this.wallLeft.position.x = wallOffsetWidth;
this.singleMesh.isVisible = newSize === "single"; if (this.singleMesh) this.singleMesh.isVisible = newSize === "single";
this.doubleMesh.isVisible = newSize === "double"; if (this.doubleMesh) this.doubleMesh.isVisible = newSize === "double";
} }
scene; scene;
@ -44,17 +42,21 @@ class DiceTray {
get width() { get width() {
return this.size === "single" ? 10 : 20; return this.size === "single" ? 10 : 20;
} }
height = 20; height = 20;
collisionSize = 50; collisionSize = 50;
wallTop: any; wallTop?: Mesh;
wallRight: any; wallRight?: Mesh;
wallBottom: any; wallBottom?: Mesh;
wallLeft: any; wallLeft?: Mesh;
singleMesh: any; singleMesh?: AbstractMesh;
doubleMesh: any; doubleMesh?: AbstractMesh;
constructor(initialSize: string, scene: Scene, shadowGenerator: ShadowGenerator) { constructor(
initialSize: string,
scene: Scene,
shadowGenerator: ShadowGenerator
) {
this._size = initialSize; this._size = initialSize;
this.scene = scene; this.scene = scene;
this.shadowGenerator = shadowGenerator; this.shadowGenerator = shadowGenerator;
@ -65,7 +67,13 @@ class DiceTray {
await this.loadMeshes(); 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( let collision = Mesh.CreateBox(
name, name,
this.collisionSize, this.collisionSize,
@ -134,15 +142,6 @@ class DiceTray {
doubleAlbedoTexture, doubleAlbedoTexture,
doubleNormalTexture, doubleNormalTexture,
doubleMetalRoughnessTexture, doubleMetalRoughnessTexture,
]: [
singleMeshes: any,
doubleMeshes: any,
singleAlbedoTexture: Texture,
singleNormalTexture: Texture,
singleMetalRoughnessTexture: Texture,
doubleAlbedoTexture: Texture,
doubleNormalTexture: Texture,
doubleMetalRoughnessTexture: Texture
] = await Promise.all([ ] = await Promise.all([
SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene), SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene),
SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene), SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene),
@ -159,8 +158,6 @@ class DiceTray {
this.singleMesh.name = "dice_tray"; this.singleMesh.name = "dice_tray";
let singleMaterial = new PBRMaterial("dice_tray_mat_single", this.scene); let singleMaterial = new PBRMaterial("dice_tray_mat_single", this.scene);
singleMaterial.albedoTexture = singleAlbedoTexture; singleMaterial.albedoTexture = singleAlbedoTexture;
// TODO: ask Mitch about texture
// singleMaterial.normalTexture = singleNormalTexture;
singleMaterial.bumpTexture = singleNormalTexture; singleMaterial.bumpTexture = singleNormalTexture;
singleMaterial.metallicTexture = singleMetalRoughnessTexture; singleMaterial.metallicTexture = singleMetalRoughnessTexture;
singleMaterial.useRoughnessFromMetallicTextureAlpha = false; singleMaterial.useRoughnessFromMetallicTextureAlpha = false;
@ -177,8 +174,6 @@ class DiceTray {
this.doubleMesh.name = "dice_tray"; this.doubleMesh.name = "dice_tray";
let doubleMaterial = new PBRMaterial("dice_tray_mat_double", this.scene); let doubleMaterial = new PBRMaterial("dice_tray_mat_double", this.scene);
doubleMaterial.albedoTexture = doubleAlbedoTexture; doubleMaterial.albedoTexture = doubleAlbedoTexture;
// TODO: ask Mitch about texture
//doubleMaterial.normalTexture = doubleNormalTexture;
doubleMaterial.bumpTexture = doubleNormalTexture; doubleMaterial.bumpTexture = doubleNormalTexture;
doubleMaterial.metallicTexture = doubleMetalRoughnessTexture; doubleMaterial.metallicTexture = doubleMetalRoughnessTexture;
doubleMaterial.useRoughnessFromMetallicTextureAlpha = false; doubleMaterial.useRoughnessFromMetallicTextureAlpha = false;

View File

@ -1,12 +1,14 @@
import { InstancedMesh, Material, Mesh, Scene } from "@babylonjs/core"; import { InstancedMesh, Material, Scene } from "@babylonjs/core";
import Dice from "../Dice"; import Dice from "../Dice";
import albedo from "./albedo.jpg"; import albedo from "./albedo.jpg";
import metalRoughness from "./metalRoughness.jpg"; import metalRoughness from "./metalRoughness.jpg";
import normal from "./normal.jpg"; import normal from "./normal.jpg";
import { DiceMeshes, DiceType } from "../../types/Dice";
class GalaxyDice extends Dice { class GalaxyDice extends Dice {
static meshes: Record<string, Mesh>; static meshes: DiceMeshes;
static material: Material; static material: Material;
static async load(scene: Scene) { static async load(scene: Scene) {
@ -22,8 +24,7 @@ class GalaxyDice extends Dice {
} }
} }
// TODO: check static -> rename function? static createInstance(diceType: DiceType, scene: Scene): InstancedMesh {
static createInstance(diceType: string, scene: Scene): InstancedMesh {
if (!this.material || !this.meshes) { if (!this.material || !this.meshes) {
throw Error("Dice not loaded, call load before creating an instance"); throw Error("Dice not loaded, call load before creating an instance");
} }

View File

@ -1,5 +1,6 @@
import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial";
import { Color3 } from "@babylonjs/core/Maths/math"; import { Color3 } from "@babylonjs/core/Maths/math";
import { Material, Scene } from "@babylonjs/core";
import Dice from "../Dice"; import Dice from "../Dice";
@ -8,18 +9,22 @@ import metalRoughness from "./metalRoughness.jpg";
import normal from "./normal.jpg"; import normal from "./normal.jpg";
import { importTextureAsync } from "../../helpers/babylon"; import { importTextureAsync } from "../../helpers/babylon";
import { Material, Mesh, Scene } from "@babylonjs/core"; import { BaseDiceTextureSources, DiceMeshes, DiceType } from "../../types/Dice";
class GemstoneDice extends Dice { class GemstoneDice extends Dice {
static meshes: Record<string, Mesh>; static meshes: DiceMeshes;
static material: Material; static material: Material;
static getDicePhysicalProperties(diceType: string) { static getDicePhysicalProperties(diceType: DiceType) {
let properties = super.getDicePhysicalProperties(diceType); let properties = super.getDicePhysicalProperties(diceType);
return { mass: properties.mass * 1.5, friction: properties.friction }; 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 pbr = new PBRMaterial(materialName, scene);
let [albedo, normal, metalRoughness] = await Promise.all([ let [albedo, normal, metalRoughness] = await Promise.all([
importTextureAsync(textures.albedo), importTextureAsync(textures.albedo),
@ -27,7 +32,6 @@ class GemstoneDice extends Dice {
importTextureAsync(textures.metalRoughness), importTextureAsync(textures.metalRoughness),
]); ]);
pbr.albedoTexture = albedo; pbr.albedoTexture = albedo;
// TODO: ask Mitch about texture
pbr.bumpTexture = normal; pbr.bumpTexture = normal;
pbr.metallicTexture = metalRoughness; pbr.metallicTexture = metalRoughness;
pbr.useRoughnessFromMetallicTextureAlpha = false; 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) { if (!this.material || !this.meshes) {
throw Error("Dice not loaded, call load before creating an instance"); throw Error("Dice not loaded, call load before creating an instance");
} }

View File

@ -1,5 +1,6 @@
import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial";
import { Color3 } from "@babylonjs/core/Maths/math"; import { Color3 } from "@babylonjs/core/Maths/math";
import { Material, Scene } from "@babylonjs/core";
import Dice from "../Dice"; import Dice from "../Dice";
@ -8,18 +9,28 @@ import mask from "./mask.png";
import normal from "./normal.jpg"; import normal from "./normal.jpg";
import { importTextureAsync } from "../../helpers/babylon"; 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 { class GlassDice extends Dice {
static meshes: Record<string, Mesh>; static meshes: DiceMeshes;
static material: Material; static material: Material;
static getDicePhysicalProperties(diceType: string) { static getDicePhysicalProperties(diceType: DiceType) {
let properties = super.getDicePhysicalProperties(diceType); let properties = super.getDicePhysicalProperties(diceType);
return { mass: properties.mass * 1.5, friction: properties.friction }; 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 pbr = new PBRMaterial(materialName, scene);
let [albedo, normal, mask] = await Promise.all([ let [albedo, normal, mask] = await Promise.all([
importTextureAsync(textures.albedo), importTextureAsync(textures.albedo),
@ -27,7 +38,6 @@ class GlassDice extends Dice {
importTextureAsync(textures.mask), importTextureAsync(textures.mask),
]); ]);
pbr.albedoTexture = albedo; pbr.albedoTexture = albedo;
// pbr.normalTexture = normal;
pbr.bumpTexture = normal; pbr.bumpTexture = normal;
pbr.roughness = 0.25; pbr.roughness = 0.25;
pbr.metallic = 0; pbr.metallic = 0;
@ -47,7 +57,7 @@ class GlassDice extends Dice {
static async load(scene: Scene) { static async load(scene: Scene) {
if (!this.material) { if (!this.material) {
this.material = await this.loadMaterial( this.material = await this.loadGlassMaterial(
"glass_pbr", "glass_pbr",
{ albedo, mask, normal }, { albedo, mask, normal },
scene 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) { if (!this.material || !this.meshes) {
throw Error("Dice not loaded, call load before creating an instance"); throw Error("Dice not loaded, call load before creating an instance");
} }

View File

@ -1,15 +1,17 @@
import { Material, Mesh, Scene } from "@babylonjs/core"; import { Material, Scene } from "@babylonjs/core";
import Dice from "../Dice"; import Dice from "../Dice";
import albedo from "./albedo.jpg"; import albedo from "./albedo.jpg";
import metalRoughness from "./metalRoughness.jpg"; import metalRoughness from "./metalRoughness.jpg";
import normal from "./normal.jpg"; import normal from "./normal.jpg";
import { DiceMeshes, DiceType } from "../../types/Dice";
class IronDice extends Dice { class IronDice extends Dice {
static meshes: Record<string, Mesh>; static meshes: DiceMeshes;
static material: Material; static material: Material;
static getDicePhysicalProperties(diceType: string) { static getDicePhysicalProperties(diceType: DiceType) {
let properties = super.getDicePhysicalProperties(diceType); let properties = super.getDicePhysicalProperties(diceType);
return { mass: properties.mass * 2, friction: properties.friction }; 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) { if (!this.material || !this.meshes) {
throw Error("Dice not loaded, call load before creating an instance"); throw Error("Dice not loaded, call load before creating an instance");
} }

View File

@ -1,12 +1,14 @@
import { Material, Mesh, Scene } from "@babylonjs/core"; import { Material, Scene } from "@babylonjs/core";
import Dice from "../Dice"; import Dice from "../Dice";
import albedo from "./albedo.jpg"; import albedo from "./albedo.jpg";
import metalRoughness from "./metalRoughness.jpg"; import metalRoughness from "./metalRoughness.jpg";
import normal from "./normal.jpg"; import normal from "./normal.jpg";
import { DiceMeshes, DiceType } from "../../types/Dice";
class NebulaDice extends Dice { class NebulaDice extends Dice {
static meshes: Record<string, Mesh>; static meshes: DiceMeshes;
static material: Material; static material: Material;
static async load(scene: Scene) { 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) { if (!this.material || !this.meshes) {
throw Error("Dice not loaded, call load before creating an instance"); throw Error("Dice not loaded, call load before creating an instance");
} }

View File

@ -1,12 +1,14 @@
import { Material, Mesh, Scene } from "@babylonjs/core"; import { Material, Scene } from "@babylonjs/core";
import Dice from "../Dice"; import Dice from "../Dice";
import albedo from "./albedo.jpg"; import albedo from "./albedo.jpg";
import metalRoughness from "./metalRoughness.jpg"; import metalRoughness from "./metalRoughness.jpg";
import normal from "./normal.jpg"; import normal from "./normal.jpg";
import { DiceMeshes, DiceType } from "../../types/Dice";
class SunriseDice extends Dice { class SunriseDice extends Dice {
static meshes: Record<string, Mesh>; static meshes: DiceMeshes;
static material: Material; static material: Material;
static async load(scene: Scene) { 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) { if (!this.material || !this.meshes) {
throw Error("Dice not loaded, call load before creating an instance"); throw Error("Dice not loaded, call load before creating an instance");
} }

View File

@ -1,12 +1,14 @@
import { Material, Mesh, Scene } from "@babylonjs/core"; import { Material, Scene } from "@babylonjs/core";
import Dice from "../Dice"; import Dice from "../Dice";
import albedo from "./albedo.jpg"; import albedo from "./albedo.jpg";
import metalRoughness from "./metalRoughness.jpg"; import metalRoughness from "./metalRoughness.jpg";
import normal from "./normal.jpg"; import normal from "./normal.jpg";
import { DiceMeshes, DiceType } from "../../types/Dice";
class SunsetDice extends Dice { class SunsetDice extends Dice {
static meshes: Record<string, Mesh>; static meshes: DiceMeshes;
static material: Material; static material: Material;
static async load(scene: Scene) { 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) { if (!this.material || !this.meshes) {
throw Error("Dice not loaded, call load before creating an instance"); throw Error("Dice not loaded, call load before creating an instance");
} }

View File

@ -1,3 +1,4 @@
import { Material, Scene } from "@babylonjs/core";
import Dice from "../Dice"; import Dice from "../Dice";
import albedo from "./albedo.jpg"; import albedo from "./albedo.jpg";
@ -11,8 +12,7 @@ import d10Source from "./d10.glb";
import d12Source from "./d12.glb"; import d12Source from "./d12.glb";
import d20Source from "./d20.glb"; import d20Source from "./d20.glb";
import d100Source from "./d100.glb"; import d100Source from "./d100.glb";
import { Material, Mesh, Scene } from "@babylonjs/core"; import { DiceMeshes, DiceType } from "../../types/Dice";
import { DiceType } from "../../types/Dice";
const sourceOverrides = { const sourceOverrides = {
d4: d4Source, d4: d4Source,
@ -25,10 +25,10 @@ const sourceOverrides = {
}; };
class WalnutDice extends Dice { class WalnutDice extends Dice {
static meshes: Record<DiceType, Mesh>; static meshes: DiceMeshes;
static material: Material; static material: Material;
static getDicePhysicalProperties(diceType: string) { static getDicePhysicalProperties(diceType: DiceType) {
let properties = super.getDicePhysicalProperties(diceType); let properties = super.getDicePhysicalProperties(diceType);
return { mass: properties.mass * 1.4, friction: properties.friction }; return { mass: properties.mass * 1.4, friction: properties.friction };
} }

20
src/global.d.ts vendored
View File

@ -2,8 +2,20 @@ declare module "pepjs";
declare module "socket.io-msgpack-parser"; declare module "socket.io-msgpack-parser";
declare module "fake-indexeddb"; declare module "fake-indexeddb";
declare module "fake-indexeddb/lib/FDBKeyRange"; declare module "fake-indexeddb/lib/FDBKeyRange";
declare module "*.glb"; declare module "*.glb" {
declare module "*.png"; const source: string;
declare module "*.mp4"; export default source;
declare module "*.bin"; }
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"; declare module "react-router-hash-link";

View File

@ -2,11 +2,11 @@
* A faked local or session storage used when the user has disabled storage * A faked local or session storage used when the user has disabled storage
*/ */
class FakeStorage { class FakeStorage {
data: { [keyName: string ]: any} = {}; data: { [keyName: string]: any } = {};
key(index: number) { key(index: number) {
return Object.keys(this.data)[index] || null; return Object.keys(this.data)[index] || null;
} }
getItem(keyName: string ) { getItem(keyName: string) {
return this.data[keyName] || null; return this.data[keyName] || null;
} }
setItem(keyName: string, keyValue: any) { setItem(keyName: string, keyValue: any) {

View File

@ -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} a
* @param {(Vector2 | number)} [minimum] Value to compare * @param {Vector2 | number} b Value to compare
* @returns {(Vector2 | number)} * @returns {Vector2}
*/ */
static min(a: Vector2, minimum?: Vector2 | number): Vector2 | number { static min(a: Vector2, b: Vector2 | number): Vector2 {
if (minimum === undefined) { if (typeof b === "number") {
return a.x < a.y ? a.x : a.y; return { x: Math.min(a.x, b), y: Math.min(a.y, b) };
} else if (typeof minimum === "number") {
return { x: Math.min(a.x, minimum), y: Math.min(a.y, minimum) };
} else { } 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} a
* @param {(Vector2 | number)} [maximum] Value to compare * @returns {number}
* @returns {(Vector2 | number)}
*/ */
static max(a: Vector2, maximum?: Vector2 | number): Vector2 | number { static componentMin(a: Vector2): number {
if (maximum === undefined) { return a.x < a.y ? a.x : a.y;
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) }; /**
* 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 { } 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` * Rounds `p` to the nearest value of `to`
* @param {Vector2} p * @param {Vector2} p

View File

@ -1,20 +1,31 @@
import { MultiPolygon, Ring, Polygon, Geom } from "polygon-clipping";
import shortid from "shortid"; 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++) { for (let i = 0; i < difference.length; i++) {
let newId = shortid.generate(); let newId = shortid.generate();
// Holes detected // Holes detected
let holes = []; let holes = [];
if (difference[i].length > 1) { if (difference[i].length > 1) {
for (let j = 1; j < difference[i].length; j++) { 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] = { shapes[newId] = {
...shape, ...fog,
id: newId, id: newId,
data: { data: {
points, 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++) { for (let i = 0; i < intersection.length; i++) {
let newId = shortid.generate(); 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] = { shapes[newId] = {
...shape, ...shape,
@ -43,9 +61,9 @@ export function addPolygonIntersectionToShapes(shape: any, intersection: any, sh
} }
} }
export function shapeToGeometry(shape) { export function fogToGeometry(fog: Fog): Geom {
const shapePoints = shape.data.points.map(({ x, y }) => [x, y]); const shapePoints: Ring = fog.data.points.map(({ x, y }) => [x, y]);
const shapeHoles = shape.data.holes.map((hole) => const shapeHoles: Polygon = fog.data.holes.map((hole) =>
hole.map(({ x, y }) => [x, y]) hole.map(({ x, y }) => [x, y])
); );
return [[shapePoints, ...shapeHoles]]; return [[shapePoints, ...shapeHoles]];

View File

@ -7,16 +7,12 @@ async function blobToBuffer(blob: Blob): Promise<Uint8Array> {
const arrayBuffer = await blob.arrayBuffer(); const arrayBuffer = await blob.arrayBuffer();
return new Uint8Array(arrayBuffer); return new Uint8Array(arrayBuffer);
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
const reader = new FileReader(); const reader = new FileReader();
function onLoadEnd(event: any) { function onLoadEnd() {
reader.removeEventListener("loadend", onLoadEnd, false); reader.removeEventListener("loadend", onLoadEnd, false);
if (event.error) { resolve(Buffer.from(reader.result as ArrayBuffer));
reject(event.error);
} else {
resolve(Buffer.from(reader.result as ArrayBuffer));
}
} }
reader.addEventListener("loadend", onLoadEnd, false); reader.addEventListener("loadend", onLoadEnd, false);

View File

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

View File

@ -1,13 +1,15 @@
import { InstancedMesh, TransformNode } from "@babylonjs/core";
import { Vector3 } from "@babylonjs/core/Maths/math"; 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 * Find the number facing up on a mesh instance of a dice
* @param {Object} instance The dice instance * @param {Object} instance The dice instance
*/ */
export function getDiceInstanceRoll(instance: any) { export function getDiceInstanceRoll(instance: InstancedMesh) {
let highestDot = -1; let highestDot = -1;
let highestLocator; let highestLocator: TransformNode | undefined = undefined;
for (let locator of instance.getChildTransformNodes()) { for (let locator of instance.getChildTransformNodes()) {
let dif = locator let dif = locator
.getAbsolutePosition() .getAbsolutePosition()
@ -19,17 +21,19 @@ export function getDiceInstanceRoll(instance: any) {
highestLocator = locator; highestLocator = locator;
} }
} }
if (!highestLocator) {
return 0;
}
return parseInt(highestLocator.name.slice(12)); return parseInt(highestLocator.name.slice(12));
} }
/** /**
* Find the number facing up on a dice object * 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); let number = getDiceInstanceRoll(dice.instance);
// If the dice is a d100 add the d10 // If the dice is a d100 add the d10
if (dice.type === "d100") { if (dice.d10Instance) {
const d10Number = getDiceInstanceRoll(dice.d10Instance); const d10Number = getDiceInstanceRoll(dice.d10Instance);
// Both zero set to 100 // Both zero set to 100
if (d10Number === 0 && number === 0) { if (d10Number === 0 && number === 0) {
@ -44,7 +48,7 @@ export function getDiceRoll(dice: any) {
} }
export function getDiceRollTotal(diceRolls: DiceRoll[]) { export function getDiceRollTotal(diceRolls: DiceRoll[]) {
return diceRolls.reduce((accumulator: number, dice: any) => { return diceRolls.reduce((accumulator: number, dice) => {
if (dice.roll === "unknown") { if (dice.roll === "unknown") {
return accumulator; return accumulator;
} else { } else {

View File

@ -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"; import get from "lodash.get";
export function applyChanges<LHS>(target: LHS, changes: Diff<LHS, any>[]) { export function applyChanges<LHS>(target: LHS, changes: Diff<LHS>[]) {
for (let change of changes) { for (let change of changes) {
if (change.path && (change.kind === "E" || change.kind === "A")) { if (change.path && (change.kind === "E" || change.kind === "A")) {
// If editing an object or array ensure that the value exists // If editing an object or array ensure that the value exists
@ -15,7 +15,7 @@ export function applyChanges<LHS>(target: LHS, changes: Diff<LHS, any>[]) {
} }
} }
export function revertChanges<LHS>(target: LHS, changes: Diff<LHS, any>[]) { export function revertChanges<LHS>(target: LHS, changes: Diff<LHS>[]) {
for (let change of changes) { for (let change of changes) {
revertChange(target, true, change); revertChange(target, true, change);
} }

View File

@ -238,17 +238,6 @@ export function getFogShapesBoundingBoxes(
return boxes; return boxes;
} }
/**
* @typedef Edge
* @property {Vector2} start
* @property {Vector2} end
*/
// type Edge = {
// start: Vector2,
// end: Vector2
// }
/** /**
* @typedef Guide * @typedef Guide
* @property {Vector2} start * @property {Vector2} start
@ -257,7 +246,7 @@ export function getFogShapesBoundingBoxes(
* @property {number} distance * @property {number} distance
*/ */
type Guide = { export type Guide = {
start: Vector2; start: Vector2;
end: Vector2; end: Vector2;
orientation: "horizontal" | "vertical"; orientation: "horizontal" | "vertical";

View File

@ -4,6 +4,7 @@ import Vector2 from "./Vector2";
import Size from "./Size"; import Size from "./Size";
import { logError } from "./logging"; import { logError } from "./logging";
import { Grid, GridInset, GridScale } from "../types/Grid";
const SQRT3 = 1.73205; const SQRT3 = 1.73205;
const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented"); 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 * @param {number} gridWidth Width of the grid in pixels after inset
* @returns {number} * @returns {number}
*/ */
function getGridHeightFromWidth(grid: Grid, gridWidth: number): number { function getGridHeightFromWidth(
grid: Pick<Grid, "type" | "size">,
gridWidth: number
): number {
switch (grid.type) { switch (grid.type) {
case "square": case "square":
return (grid.size.y * gridWidth) / grid.size.x; return (grid.size.y * gridWidth) / grid.size.x;
@ -203,7 +207,7 @@ function getGridHeightFromWidth(grid: Grid, gridWidth: number): number {
* @returns {GridInset} * @returns {GridInset}
*/ */
export function getGridDefaultInset( export function getGridDefaultInset(
grid: Grid, grid: Pick<Grid, "type" | "size">,
mapWidth: number, mapWidth: number,
mapHeight: number mapHeight: number
): GridInset { ): GridInset {
@ -220,7 +224,7 @@ export function getGridDefaultInset(
* @returns {GridInset} * @returns {GridInset}
*/ */
export function getGridUpdatedInset( export function getGridUpdatedInset(
grid: Required<Grid>, grid: Grid,
mapWidth: number, mapWidth: number,
mapHeight: number mapHeight: number
): GridInset { ): GridInset {
@ -303,7 +307,7 @@ export function hexOffsetToCube(
* @param {Size} cellSize * @param {Size} cellSize
*/ */
export function gridDistance( export function gridDistance(
grid: Required<Grid>, grid: Grid,
a: Vector2, a: Vector2,
b: Vector2, b: Vector2,
cellSize: Size cellSize: Size
@ -313,12 +317,14 @@ export function gridDistance(
const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize); const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize);
if (grid.type === "square") { if (grid.type === "square") {
if (grid.measurement.type === "chebyshev") { 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") { } else if (grid.measurement.type === "alternating") {
// Alternating diagonal distance like D&D 3.5 and Pathfinder // Alternating diagonal distance like D&D 3.5 and Pathfinder
const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord)); const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord));
const max: any = Vector2.max(delta); const max = Vector2.componentMax(delta);
const min: any = Vector2.min(delta); const min = Vector2.componentMin(delta);
return max - min + Math.floor(1.5 * min); return max - min + Math.floor(1.5 * min);
} else if (grid.measurement.type === "euclidean") { } else if (grid.measurement.type === "euclidean") {
return Vector2.magnitude( 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 * 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 * @param {number[]} candidates
* @returns {Vector2 | null} * @returns {Vector2 | null}
*/ */
function gridSizeHeuristic( function gridSizeHeuristic(
image: CanvasImageSource, image: HTMLImageElement,
candidates: number[] candidates: number[]
): Vector2 | null { ): Vector2 | null {
// TODO: check type for Image and CanvasSourceImage const width = image.width;
const width: any = image.width; const height = image.height;
const height: any = image.height;
// Find the best candidate by comparing the absolute z-scores of each axis // Find the best candidate by comparing the absolute z-scores of each axis
let bestX = 1; let bestX = 1;
let bestY = 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 * 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 * @param {number[]} candidates
* @returns {Vector2 | null} * @returns {Vector2 | null}
*/ */
async function gridSizeML( async function gridSizeML(
image: CanvasImageSource, image: HTMLImageElement,
candidates: number[] candidates: number[]
): Promise<Vector2 | null> { ): Promise<Vector2 | null> {
// TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match // TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match
const width: any = image.width; const width = image.width;
const height: any = image.height; const height = image.height;
const ratio = width / height; const ratio = width / height;
let canvas = document.createElement("canvas"); let canvas = document.createElement("canvas");
let context = canvas.getContext("2d"); 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 * 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} * @returns {Vector2}
*/ */
export async function getGridSizeFromImage(image: CanvasImageSource) { export async function getGridSizeFromImage(image: HTMLImageElement) {
const width: any = image.width; const width = image.width;
const height: any = image.height; const height = image.height;
const candidates = dividers(width, height); const candidates = dividers(width, height);
let prediction; let prediction;

View File

@ -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 * Transform and item array to a record of item ids to item names
*/ */
export function getItemNames(items: any[], itemKey: string = "id") { export function getItemNames<Item extends { name: string; id: string }>(
items: Item[]
) {
let names: Record<string, string> = {}; let names: Record<string, string> = {};
for (let item of items) { for (let item of items) {
names[item[itemKey]] = item.name; names[item.id] = item.name;
} }
return names; return names;
} }

View File

@ -1,14 +1,23 @@
import React, { useState, useEffect, useRef } from "react"; 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 Konva from "konva";
import { Line, Group, Path, Circle } from "react-konva";
import { LineConfig } from "konva/types/shapes/Line";
import Color from "color"; import Color from "color";
import Vector2 from "./Vector2"; import Vector2 from "./Vector2";
type HoleyLineProps = {
holes: number[][];
} & LineConfig;
// Holes should be wound in the opposite direction as the containing points array // 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 // 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 length = points.length;
const tension = shape.tension(); const tension = shape.tension();
const closed = shape.closed(); const closed = shape.closed();
@ -76,7 +85,7 @@ export function HoleyLine({ holes, ...props }: { holes: any; props: [] }) {
} }
// Draw points and holes // Draw points and holes
function sceneFunc(context: any, shape: any) { function sceneFunc(context: Konva.Context, shape: Konva.Line) {
const points = shape.points(); const points = shape.points();
const closed = shape.closed(); const closed = shape.closed();
@ -106,22 +115,18 @@ export function HoleyLine({ holes, ...props }: { holes: any; props: [] }) {
} }
} }
return <Line sceneFunc={sceneFunc} {...props} />; return <Line {...props} sceneFunc={sceneFunc} />;
} }
export function Tick({ type TickProps = {
x, x: number;
y, y: number;
scale, scale: number;
onClick, onClick: (evt: Konva.KonvaEventObject<MouseEvent>) => void;
cross, cross: boolean;
}: { };
x: any;
y: any; export function Tick({ x, y, scale, onClick, cross }: TickProps) {
scale: any;
onClick: any;
cross: any;
}) {
const [fill, setFill] = useState("white"); const [fill, setFill] = useState("white");
function handleEnter() { function handleEnter() {
setFill("hsl(260, 100%, 80%)"); setFill("hsl(260, 100%, 80%)");
@ -160,19 +165,21 @@ interface TrailPoint extends Vector2 {
lifetime: number; lifetime: number;
} }
type TrailProps = {
position: Vector2;
size: number;
duration: number;
segments: number;
color: string;
};
export function Trail({ export function Trail({
position, position,
size, size,
duration, duration,
segments, segments,
color, color,
}: { }: TrailProps) {
position: Vector2;
size: any;
duration: number;
segments: any;
color: string;
}) {
const trailRef: React.MutableRefObject<Konva.Line | undefined> = useRef(); const trailRef: React.MutableRefObject<Konva.Line | undefined> = useRef();
const pointsRef: React.MutableRefObject<TrailPoint[]> = useRef([]); const pointsRef: React.MutableRefObject<TrailPoint[]> = useRef([]);
const prevPositionRef = useRef(position); const prevPositionRef = useRef(position);
@ -206,7 +213,7 @@ export function Trail({
useEffect(() => { useEffect(() => {
let prevTime = performance.now(); let prevTime = performance.now();
let request = requestAnimationFrame(animate); let request = requestAnimationFrame(animate);
function animate(time: any) { function animate(time: number) {
request = requestAnimationFrame(animate); request = requestAnimationFrame(animate);
const deltaTime = time - prevTime; const deltaTime = time - prevTime;
prevTime = time; prevTime = time;
@ -243,14 +250,13 @@ export function Trail({
}, []); }, []);
// Custom scene function for drawing a trail from a line // 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 // Resample points to ensure a smooth trail
const resampledPoints = Vector2.resample(pointsRef.current, segments); const resampledPoints = Vector2.resample(pointsRef.current, segments);
if (resampledPoints.length === 0) { if (resampledPoints.length === 0) {
return; return;
} }
// Draws a line offset in the direction perpendicular to its travel direction // 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 drawOffsetLine = (from: Vector2, to: Vector2, alpha: number) => {
const forward = Vector2.normalize(Vector2.subtract(from, to)); const forward = Vector2.normalize(Vector2.subtract(from, to));
// Rotate the forward vector 90 degrees based off of the direction // Rotate the forward vector 90 degrees based off of the direction
@ -328,7 +334,7 @@ Trail.defaultProps = {
*/ */
export function getRelativePointerPosition( export function getRelativePointerPosition(
node: Konva.Node node: Konva.Node
): { x: number; y: number } | undefined { ): Vector2 | undefined {
let transform = node.getAbsoluteTransform().copy(); let transform = node.getAbsoluteTransform().copy();
transform.invert(); transform.invert();
let position = node.getStage()?.getPointerPosition(); let position = node.getStage()?.getPointerPosition();
@ -340,10 +346,9 @@ export function getRelativePointerPosition(
export function getRelativePointerPositionNormalized( export function getRelativePointerPositionNormalized(
node: Konva.Node node: Konva.Node
): { x: number; y: number } | undefined { ): Vector2 | undefined {
const relativePosition = getRelativePointerPosition(node); const relativePosition = getRelativePointerPosition(node);
if (!relativePosition) { if (!relativePosition) {
// TODO: handle possible null value
return; return;
} }
return { return {
@ -357,8 +362,8 @@ export function getRelativePointerPositionNormalized(
* @param {number[]} points points in an x, y alternating array * @param {number[]} points points in an x, y alternating array
* @returns {Vector2[]} a `Vector2` array * @returns {Vector2[]} a `Vector2` array
*/ */
export function convertPointArray(points: number[]) { export function convertPointArray(points: number[]): Vector2[] {
return points.reduce((acc: any[], _, i, arr) => { return points.reduce((acc: Vector2[], _, i, arr) => {
if (i % 2 === 0) { if (i % 2 === 0) {
acc.push({ x: arr[i], y: arr[i + 1] }); acc.push({ x: arr[i], y: arr[i + 1] });
} }

View File

@ -1,6 +1,6 @@
import { captureException } from "@sentry/react"; import { captureException } from "@sentry/react";
export function logError(error: any): void { export function logError(error: Error): void {
console.error(error); console.error(error);
if (process.env.REACT_APP_LOGGING === "true") { if (process.env.REACT_APP_LOGGING === "true") {
captureException(error); captureException(error);

View File

@ -32,8 +32,6 @@ const mapResolutions: Resolution[] = [
/** /**
* Get the asset id of the preview file to send for a map * 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 { export function getMapPreviewAsset(map: Map): string | undefined {
if (map.type === "file") { if (map.type === "file") {
@ -126,7 +124,7 @@ export async function createMapFromFile(
) { ) {
const resized = await resizeImage( const resized = await resizeImage(
image, image,
Vector2.max(resolutionPixelSize) as number, Vector2.componentMax(resolutionPixelSize),
file.type, file.type,
resolution.quality resolution.quality
); );

View File

@ -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<React.SetStateAction<{}>>
] = useState({});
const [fuse, setFuse] = useState<any>();
// 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([]);
}
}

View File

@ -23,8 +23,9 @@ export function fromEntries(iterable: Iterable<[string | number, any]>) {
} }
// Check to see if all tracks are muted // Check to see if all tracks are muted
export function isStreamStopped(stream: MediaStream) { export function isStreamStopped(stream: MediaStream): boolean {
return stream.getTracks().reduce((a: any, b: any) => a && b, { mute: true }); // 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 { 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; return Object.keys(obj).length === 0 && obj.constructor === Object;
} }
export function keyBy<Type>(array: Type[], key: string): Record<string, Type> { export function keyBy<Type extends Record<PropertyKey, any>>(
array: Type[],
key: string
): Record<string, Type> {
return array.reduce( return array.reduce(
(prev: any, current: any) => ({ (prev, current) => ({
...prev, ...prev,
[key ? current[key] : current]: current, [key ? current[key] : current]: current,
}), }),

View File

@ -1,24 +1,14 @@
import { Duration } from "../types/Timer";
const MILLISECONDS_IN_HOUR = 3600000; const MILLISECONDS_IN_HOUR = 3600000;
const MILLISECONDS_IN_MINUTE = 60000; const MILLISECONDS_IN_MINUTE = 60000;
const MILLISECONDS_IN_SECOND = 1000; 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 * Returns a timers duration in milliseconds
* @param {Time} t The object with an hour, minute and second property * @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) { if (!t) {
return 0; return 0;
} }
@ -33,7 +23,7 @@ export function getHMSDuration(t: Time) {
* Returns an object with an hour, minute and second property * Returns an object with an hour, minute and second property
* @param {number} duration The duration in milliseconds * @param {number} duration The duration in milliseconds
*/ */
export function getDurationHMS(duration: number) { export function getDurationHMS(duration: number): Duration {
let workingDuration = duration; let workingDuration = duration;
const hour = Math.floor(workingDuration / MILLISECONDS_IN_HOUR); const hour = Math.floor(workingDuration / MILLISECONDS_IN_HOUR);
workingDuration -= hour * MILLISECONDS_IN_HOUR; workingDuration -= hour * MILLISECONDS_IN_HOUR;

View File

@ -73,7 +73,7 @@ function useGridSnapping(
const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint); const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint);
if ( if (
distanceToSnapPoint < distanceToSnapPoint <
(Vector2.min(gridCellPixelSize) as number) * gridSnappingSensitivity Vector2.componentMin(gridCellPixelSize) * gridSnappingSensitivity
) { ) {
// Reverse grid offset // Reverse grid offset
let offsetSnapPoint = Vector2.add( let offsetSnapPoint = Vector2.add(

View File

@ -1,5 +1,17 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
type useImageCenterProps = {
data:
stageRef:
stageWidth: number;
stageHeight: number;
stageTranslateRef:
setStageScale:
imageLayerRef:
containerRef:
responsive?: boolean
}
function useImageCenter( function useImageCenter(
data, data,
stageRef, stageRef,
@ -14,8 +26,8 @@ function useImageCenter(
const stageRatio = stageWidth / stageHeight; const stageRatio = stageWidth / stageHeight;
const imageRatio = data ? data.width / data.height : 1; const imageRatio = data ? data.width / data.height : 1;
let imageWidth; let imageWidth: number;
let imageHeight; let imageHeight: number;
if (stageRatio > imageRatio) { if (stageRatio > imageRatio) {
imageWidth = data ? stageHeight / (data.height / data.width) : stageWidth; imageWidth = data ? stageHeight / (data.height / data.width) : stageWidth;
imageHeight = stageHeight; imageHeight = stageHeight;

View File

@ -1,39 +1,45 @@
import { useEffect, useState, useRef, useCallback } from "react"; import { useEffect, useState, useRef, useCallback } from "react";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { Diff } from "deep-diff";
import useDebounce from "./useDebounce"; import useDebounce from "./useDebounce";
import { diff, applyChanges } from "../helpers/diff"; import { diff, applyChanges } from "../helpers/diff";
import Session from "../network/Session"; import Session from "../network/Session";
/** /**
* @callback setNetworkedState * @param update The updated state or a state function passed into setState
* @param {any} update The updated state or a state function passed into setState * @param sync Whether to sync the update with the session
* @param {boolean} sync Whether to sync the update with the session * @param force Whether to force a full update, usefull when partialUpdates is enabled
* @param {boolean} force Whether to force a full update, usefull when partialUpdates is enabled
*/ */
// TODO: check parameter requirements here export type SetNetworkedState<S> = (
type setNetworkedState = (update: any, sync?: boolean, force?: boolean) => void update: React.SetStateAction<S>,
sync?: boolean,
force?: boolean
) => void;
type Update<T> = {
id: string;
changes: Diff<T>[];
};
/** /**
* Helper to sync a react state to a `Session` * Helper to sync a react state to a `Session`
* *
* @param {any} initialState * @param {S} initialState
* @param {Session} session `Session` instance * @param {Session} session `Session` instance
* @param {string} eventName Name of the event to send to the session * @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 {number} debounceRate Amount to debounce before sending to the session (ms)
* @param {boolean} partialUpdates Allow sending of partial updates to the session * @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 * @param {string} partialUpdatesKey Key to lookup in the state to identify a partial update
*
* @returns {[any, setNetworkedState]}
*/ */
function useNetworkedState( function useNetworkedState<S extends { readonly [x: string]: any } | null>(
initialState: any, initialState: S,
session: Session, session: Session,
eventName: string, eventName: string,
debounceRate: number = 500, debounceRate: number = 500,
partialUpdates: boolean = true, partialUpdates: boolean = true,
partialUpdatesKey: string = "id" partialUpdatesKey: string = "id"
): [any, setNetworkedState] { ): [S, SetNetworkedState<S>] {
const [state, _setState] = useState(initialState); const [state, _setState] = useState(initialState);
// Used to control whether the state needs to be sent to the socket // Used to control whether the state needs to be sent to the socket
const dirtyRef = useRef(false); const dirtyRef = useRef(false);
@ -42,9 +48,9 @@ function useNetworkedState(
const forceUpdateRef = useRef(false); const forceUpdateRef = useRef(false);
// Update dirty at the same time as state // Update dirty at the same time as state
const setState = useCallback((update, sync = true, force = false) => { const setState = useCallback<SetNetworkedState<S>>((update, sync, force) => {
dirtyRef.current = sync; dirtyRef.current = sync || false;
forceUpdateRef.current = force; forceUpdateRef.current = force || false;
_setState(update); _setState(update);
}, []); }, []);
@ -54,7 +60,7 @@ function useNetworkedState(
}, [eventName]); }, [eventName]);
const debouncedState = useDebounce(state, debounceRate); const debouncedState = useDebounce(state, debounceRate);
const lastSyncedStateRef = useRef(); const lastSyncedStateRef = useRef<S>();
useEffect(() => { useEffect(() => {
if (session.socket && dirtyRef.current) { if (session.socket && dirtyRef.current) {
// If partial updates enabled, send just the changes to the socket // If partial updates enabled, send just the changes to the socket
@ -88,13 +94,13 @@ function useNetworkedState(
]); ]);
useEffect(() => { useEffect(() => {
function handleSocketEvent(data: any) { function handleSocketEvent(data: S) {
_setState(data); _setState(data);
lastSyncedStateRef.current = data; lastSyncedStateRef.current = data;
} }
function handleSocketUpdateEvent(update: any) { function handleSocketUpdateEvent(update: Update<S>) {
_setState((prevState: any) => { _setState((prevState) => {
if (prevState && prevState[partialUpdatesKey] === update.id) { if (prevState && prevState[partialUpdatesKey] === update.id) {
let newState = { ...prevState }; let newState = { ...prevState };
applyChanges(newState, update.changes); applyChanges(newState, update.changes);

View File

@ -10,8 +10,7 @@ class GridSizeModel extends Model {
static model: LayersModel; static model: LayersModel;
// Load tensorflow dynamically // Load tensorflow dynamically
// TODO: find type for tf static tf;
static tf: any;
constructor() { constructor() {
super(config as ModelJSON, { "group1-shard1of1.bin": weights }); super(config as ModelJSON, { "group1-shard1of1.bin": weights });
} }
@ -27,8 +26,7 @@ class GridSizeModel extends Model {
} }
const model = GridSizeModel.model; const model = GridSizeModel.model;
// TODO: check this mess -> changing type on prediction causes issues const prediction = tf.tidy(() => {
const prediction: any = tf.tidy(() => {
const image = tf.browser.fromPixels(imageData, 1).toFloat(); const image = tf.browser.fromPixels(imageData, 1).toFloat();
const normalized = image.div(tf.scalar(255.0)); const normalized = image.div(tf.scalar(255.0));
const batched = tf.expandDims(normalized); const batched = tf.expandDims(normalized);

View File

@ -8,7 +8,7 @@ function AddPartyMemberModal({
gameId, gameId,
}: { }: {
isOpen: boolean; isOpen: boolean;
onRequestClose: any; onRequestClose;
gameId: string; gameId: string;
}) { }) {
return ( return (

View File

@ -5,10 +5,10 @@ import Modal from "../components/Modal";
type ChangeNicknameModalProps = { type ChangeNicknameModalProps = {
isOpen: boolean; isOpen: boolean;
onRequestClose: () => void; onRequestClose;
onChangeSubmit: any; onChangeSubmit;
nickname: string; nickname: string;
onChange: any; onChange;
}; };
function ChangeNicknameModal({ function ChangeNicknameModal({

View File

@ -12,14 +12,18 @@ import { getGridDefaultInset } from "../helpers/grid";
import useResponsiveLayout from "../hooks/useResponsiveLayout"; import useResponsiveLayout from "../hooks/useResponsiveLayout";
import { Map } from "../types/Map"; import { Map } from "../types/Map";
import { MapState } from "../types/MapState"; import { MapState } from "../types/MapState";
import {
UpdateMapEventHanlder,
UpdateMapStateEventHandler,
} from "../contexts/MapDataContext";
type EditMapProps = { type EditMapProps = {
isOpen: boolean; isOpen: boolean;
onDone: () => void; onDone: () => void;
map: Map; map: Map;
mapState: MapState; mapState: MapState;
onUpdateMap: (id: string, update: Partial<Map>) => void; onUpdateMap: UpdateMapEventHanlder;
onUpdateMapState: (id: string, update: Partial<MapState>) => void; onUpdateMapState: UpdateMapStateEventHandler;
}; };
function EditMapModal({ function EditMapModal({
@ -48,52 +52,45 @@ function EditMapModal({
*/ */
// Local cache of map setting changes // Local cache of map setting changes
// Applied when done is clicked or map selection is changed // Applied when done is clicked or map selection is changed
const [mapSettingChanges, setMapSettingChanges] = useState<any>({}); const [mapSettingChanges, setMapSettingChanges] = useState<Partial<Map>>({});
const [mapStateSettingChanges, setMapStateSettingChanges] = useState<any>({}); const [mapStateSettingChanges, setMapStateSettingChanges] = useState<
Partial<MapState>
>({});
function handleMapSettingsChange(key: string, value: string) { function handleMapSettingsChange(change: Partial<Map>) {
setMapSettingChanges((prevChanges: any) => ({ setMapSettingChanges((prevChanges) => ({
...prevChanges, ...prevChanges,
[key]: value, ...change,
lastModified: Date.now(),
})); }));
} }
function handleMapStateSettingsChange(key: string, value: string) { function handleMapStateSettingsChange(change: Partial<MapState>) {
setMapStateSettingChanges((prevChanges: any) => ({ setMapStateSettingChanges((prevChanges) => ({
...prevChanges, ...prevChanges,
[key]: value, ...change,
})); }));
} }
async function applyMapChanges() { async function applyMapChanges() {
if (!isEmpty(mapSettingChanges) || !isEmpty(mapStateSettingChanges)) { if (!isEmpty(mapSettingChanges) || !isEmpty(mapStateSettingChanges)) {
// Ensure grid values are positive // Ensure grid values are positive
let verifiedChanges = { ...mapSettingChanges }; let verifiedChanges: Partial<Map> = { ...mapSettingChanges };
if ("grid" in verifiedChanges && "size" in verifiedChanges.grid) { if (verifiedChanges.grid) {
verifiedChanges.grid.size.x = verifiedChanges.grid.size.x || 1; verifiedChanges.grid.size.x = verifiedChanges.grid.size.x || 1;
verifiedChanges.grid.size.y = verifiedChanges.grid.size.y || 1; verifiedChanges.grid.size.y = verifiedChanges.grid.size.y || 1;
} }
// Ensure inset isn't flipped // Ensure inset isn't flipped
if ("grid" in verifiedChanges && "inset" in verifiedChanges.grid) { if (verifiedChanges.grid) {
const inset = verifiedChanges.grid.inset; const inset = verifiedChanges.grid.inset;
if ( if (
inset.topLeft.x > inset.bottomRight.x || inset.topLeft.x > inset.bottomRight.x ||
inset.topLeft.y > inset.bottomRight.y inset.topLeft.y > inset.bottomRight.y
) { ) {
if ("size" in verifiedChanges.grid) { verifiedChanges.grid.inset = getGridDefaultInset(
verifiedChanges.grid.inset = getGridDefaultInset( { size: verifiedChanges.grid.size, type: map.grid.type },
{ size: verifiedChanges.grid.size, type: map.grid.type }, map.width,
map.width, map.height
map.height );
);
} else {
verifiedChanges.grid.inset = getGridDefaultInset(
map.grid,
map.width,
map.height
);
}
} }
} }
await onUpdateMap(map.id, mapSettingChanges); await onUpdateMap(map.id, mapSettingChanges);

View File

@ -43,8 +43,9 @@ function EditTokenModal({
Partial<Token> Partial<Token>
>({}); >({});
// TODO: CHANGE MAP BACK? OR CHANGE THIS TO PARTIAL
function handleTokenSettingsChange(key: string, value: Pick<Token, any>) { function handleTokenSettingsChange(key: string, value: Pick<Token, any>) {
setTokenSettingChanges((prevChanges: any) => ({ setTokenSettingChanges((prevChanges) => ({
...prevChanges, ...prevChanges,
[key]: value, [key]: value,
})); }));

View File

@ -46,7 +46,7 @@ function ImportExportModal({
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const backgroundTaskRunningRef = useRef(false); const backgroundTaskRunningRef = useRef(false);
const fileInputRef = useRef<any>(); const fileInputRef = useRef();
const [showImportSelector, setShowImportSelector] = useState(false); const [showImportSelector, setShowImportSelector] = useState(false);
const [showExportSelector, setShowExportSelector] = useState(false); const [showExportSelector, setShowExportSelector] = useState(false);
@ -124,7 +124,7 @@ function ImportExportModal({
} }
useEffect(() => { useEffect(() => {
function handleBeforeUnload(event: any) { function handleBeforeUnload(event) {
if (backgroundTaskRunningRef.current) { if (backgroundTaskRunningRef.current) {
event.returnValue = event.returnValue =
"Database is still processing, are you sure you want to leave?"; "Database is still processing, are you sure you want to leave?";
@ -204,7 +204,7 @@ function ImportExportModal({
let newMaps: Map[] = []; let newMaps: Map[] = [];
let newStates: MapState[] = []; let newStates: MapState[] = [];
if (checkedMaps.length > 0) { 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); const mapsToAdd = await importDB.table("maps").bulkGet(mapIds);
for (let map of mapsToAdd) { for (let map of mapsToAdd) {
let state: MapState = await importDB.table("states").get(map.id); let state: MapState = await importDB.table("states").get(map.id);
@ -257,7 +257,7 @@ function ImportExportModal({
const assetsToAdd = await importDB const assetsToAdd = await importDB
.table("assets") .table("assets")
.bulkGet(Object.keys(newAssetIds)); .bulkGet(Object.keys(newAssetIds));
let newAssets: any[] = []; let newAssets = [];
for (let asset of assetsToAdd) { for (let asset of assetsToAdd) {
if (asset) { if (asset) {
newAssets.push({ newAssets.push({
@ -271,7 +271,7 @@ function ImportExportModal({
} }
// Add map groups with new ids // Add map groups with new ids
let newMapGroups: any[] = []; let newMapGroups = [];
if (checkedMapGroups.length > 0) { if (checkedMapGroups.length > 0) {
for (let group of checkedMapGroups) { for (let group of checkedMapGroups) {
if (group.type === "item") { if (group.type === "item") {
@ -290,7 +290,7 @@ function ImportExportModal({
} }
// Add token groups with new ids // Add token groups with new ids
let newTokenGroups: any[] = []; let newTokenGroups = [];
if (checkedTokenGroups.length > 0) { if (checkedTokenGroups.length > 0) {
for (let group of checkedTokenGroups) { for (let group of checkedTokenGroups) {
if (group.type === "item") { if (group.type === "item") {
@ -299,7 +299,7 @@ function ImportExportModal({
newTokenGroups.push({ newTokenGroups.push({
...group, ...group,
id: uuid(), id: uuid(),
items: group.items.map((item: any) => ({ items: group.items.map((item) => ({
...item, ...item,
id: newTokenIds[item.id], id: newTokenIds[item.id],
})), })),

View File

@ -81,7 +81,7 @@ function SelectMapModal({
* Image Upload * Image Upload
*/ */
const fileInputRef = useRef<any>(); const fileInputRef = useRef();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = const [isLargeImageWarningModalOpen, setShowLargeImageWarning] =

View File

@ -76,7 +76,7 @@ function SelectTokensModal({
* Image Upload * Image Upload
*/ */
const fileInputRef = useRef<any>(); const fileInputRef = useRef();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = const [isLargeImageWarningModalOpen, setShowLargeImageWarning] =

View File

@ -38,7 +38,7 @@ function StartModal({
history.push(`/game/${shortid.generate()}`); history.push(`/game/${shortid.generate()}`);
} }
const inputRef = useRef<any>(); const inputRef = useRef();
function focusInput() { function focusInput() {
inputRef.current && inputRef.current.focus(); inputRef.current && inputRef.current.focus();
} }

View File

@ -28,7 +28,7 @@ function StartTimerModal({
onTimerStop, onTimerStop,
timer, timer,
}: StartTimerProps) { }: StartTimerProps) {
const inputRef = useRef<any>(); const inputRef = useRef();
function focusInput() { function focusInput() {
inputRef.current && inputRef.current.focus(); inputRef.current && inputRef.current.focus();
} }

View File

@ -9,26 +9,26 @@ import blobToBuffer from "../helpers/blobToBuffer";
const MAX_BUFFER_SIZE = 16000; const MAX_BUFFER_SIZE = 16000;
class Connection extends SimplePeer { class Connection extends SimplePeer {
currentChunks: any; currentChunks;
dataChannels: any; dataChannels;
constructor(props: any) { constructor(props) {
super(props); super(props);
this.currentChunks = {} as Blob; this.currentChunks = {};
this.dataChannels = {}; this.dataChannels = {};
this.on("data", this.handleData); this.on("data", this.handleData);
this.on("datachannel", this.handleDataChannel); this.on("datachannel", this.handleDataChannel);
} }
// Intercept the data event with decoding and chunking support // Intercept the data event with decoding and chunking support
handleData(packed: any) { handleData(packed) {
const unpacked: any = decode(packed); const unpacked = decode(packed);
// If the special property __chunked is set and true // If the special property __chunked is set and true
// The data is a partial chunk of the a larger file // The data is a partial chunk of the a larger file
// So wait until all chunks are collected and assembled // So wait until all chunks are collected and assembled
// before emitting the dataComplete event // before emitting the dataComplete event
if (unpacked.__chunked) { if (unpacked.__chunked) {
let chunk: any = this.currentChunks[unpacked.id] || { let chunk = this.currentChunks[unpacked.id] || {
data: [], data: [],
count: 0, count: 0,
total: unpacked.total, total: unpacked.total,
@ -65,7 +65,7 @@ class Connection extends SimplePeer {
* @param {string=} channel * @param {string=} channel
* @param {string=} chunkId Optional ID to use for chunking * @param {string=} chunkId Optional ID to use for chunking
*/ */
sendObject(object: any, channel?: string, chunkId?: string) { sendObject(object, channel?: string, chunkId?: string) {
try { try {
const packedData = encode(object); const packedData = encode(object);
const chunks = this.chunk(packedData, chunkId); 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 // Override the create data channel function to store our own named reference to it
// and to use our custom data handler // and to use our custom data handler
createDataChannel(channelName: string, channelConfig: any, opts: any) { createDataChannel(channelName: string, channelConfig, opts) {
// TODO: resolve createDataChannel // TODO: resolve createDataChannel
// @ts-ignore // @ts-ignore
const channel = super.createDataChannel(channelName, channelConfig, opts); const channel = super.createDataChannel(channelName, channelConfig, opts);
@ -91,11 +91,11 @@ class Connection extends SimplePeer {
return channel; return channel;
} }
handleDataChannel(channel: any) { handleDataChannel(channel) {
const channelName = channel.channelName; const channelName = channel.channelName;
this.dataChannels[channelName] = channel; this.dataChannels[channelName] = channel;
channel.on("data", this.handleData.bind(this)); channel.on("data", this.handleData.bind(this));
channel.on("error", (error: any) => { channel.on("error", (error) => {
this.emit("error", error); this.emit("error", error);
}); });
} }

View File

@ -18,16 +18,40 @@ import Session from "./Session";
import Action from "../actions/Action"; import Action from "../actions/Action";
import Map, { import Map from "../components/map/Map";
MapState,
Map as MapType,
TokenState,
} from "../components/map/Map";
import TokenBar from "../components/token/TokenBar"; import TokenBar from "../components/token/TokenBar";
import GlobalImageDrop from "../components/image/GlobalImageDrop"; 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<Drawing>[];
mapDrawActionIndex: number;
fogDrawActions: Action<Fog>[];
fogDrawActionIndex: number;
};
type MapActionsKey = keyof Pick<
MapActions,
"mapDrawActions" | "fogDrawActions"
>;
type MapActionsIndexKey = keyof Pick<
MapActions,
"mapDrawActionIndex" | "fogDrawActionIndex"
>;
const defaultMapActions: MapActions = {
mapDrawActions: [], mapDrawActions: [],
mapDrawActionIndex: -1, mapDrawActionIndex: -1,
fogDrawActions: [], fogDrawActions: [],
@ -51,26 +75,32 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
const { updateMapState } = useMapData(); const { updateMapState } = useMapData();
const { getAsset, putAsset } = useAssets(); const { getAsset, putAsset } = useAssets();
const [currentMap, setCurrentMap] = useState<any>(null); const [currentMap, setCurrentMap] = useState<MapType | null>(null);
const [currentMapState, setCurrentMapState]: [ const [currentMapState, setCurrentMapState] =
currentMapState: MapState, useNetworkedState<MapState | null>(
setCurrentMapState: any null,
] = useNetworkedState(null, session, "map_state", 500, true, "mapId"); session,
const [assetManifest, setAssetManifest] = useNetworkedState( "map_state",
null, 500,
session, true,
"manifest", "mapId"
500, );
true, const [assetManifest, setAssetManifest] =
"mapId" useNetworkedState<AssetManifest | null>(
); null,
session,
"manifest",
500,
true,
"mapId"
);
async function loadAssetManifestFromMap(map: MapType, mapState: MapState) { async function loadAssetManifestFromMap(map: MapType, mapState: MapState) {
const assets = {}; const assets: AssetManifestAssets = {};
const { owner } = map; const { owner } = map;
let processedTokens = new Set(); let processedTokens = new Set();
for (let tokenState of Object.values(mapState.tokens)) { 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); processedTokens.add(tokenState.file);
assets[tokenState.file] = { assets[tokenState.file] = {
id: tokenState.file, id: tokenState.file,
@ -80,9 +110,11 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
} }
if (map.type === "file") { if (map.type === "file") {
assets[map.thumbnail] = { id: map.thumbnail, owner }; assets[map.thumbnail] = { id: map.thumbnail, owner };
const qualityId = map.resolutions[map.quality]; if (map.quality !== "original") {
if (qualityId) { const qualityId = map.resolutions[map.quality];
assets[qualityId] = { id: qualityId, owner }; if (qualityId) {
assets[qualityId] = { id: qualityId, owner };
}
} else { } else {
assets[map.file] = { id: map.file, owner }; assets[map.file] = { id: map.file, owner };
} }
@ -90,8 +122,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
setAssetManifest({ mapId: map.id, assets }, true, true); setAssetManifest({ mapId: map.id, assets }, true, true);
} }
function addAssetsIfNeeded(assets: any[]) { function addAssetsIfNeeded(assets: AssetManifestAsset[]) {
setAssetManifest((prevManifest: any) => { setAssetManifest((prevManifest) => {
if (prevManifest?.assets) { if (prevManifest?.assets) {
let newAssets = { ...prevManifest.assets }; let newAssets = { ...prevManifest.assets };
for (let asset of assets) { for (let asset of assets) {
@ -116,7 +148,10 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
} }
async function requestAssetsIfNeeded() { 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 ( if (
asset.owner === userId || asset.owner === userId ||
requestingAssetsRef.current.has(asset.id) requestingAssetsRef.current.has(asset.id)
@ -144,7 +179,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
if (cachedAsset) { if (cachedAsset) {
requestingAssetsRef.current.delete(asset.id); requestingAssetsRef.current.delete(asset.id);
} else { } else if (owner.sessionId) {
assetLoadStart(asset.id); assetLoadStart(asset.id);
session.sendTo(owner.sessionId, "assetRequest", asset); session.sendTo(owner.sessionId, "assetRequest", asset);
} }
@ -181,7 +216,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
} }
}, [currentMap, debouncedMapState, userId, database, updateMapState]); }, [currentMap, debouncedMapState, userId, database, updateMapState]);
async function handleMapChange(newMap: any, newMapState: any) { async function handleMapChange(newMap, newMapState) {
// Clear map before sending new one // Clear map before sending new one
setCurrentMap(null); setCurrentMap(null);
session.socket?.emit("map", null); session.socket?.emit("map", null);
@ -199,20 +234,20 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
await loadAssetManifestFromMap(newMap, newMapState); await loadAssetManifestFromMap(newMap, newMapState);
} }
function handleMapReset(newMapState: any) { function handleMapReset(newMapState) {
setCurrentMapState(newMapState, true, true); setCurrentMapState(newMapState, true, true);
setMapActions(defaultMapActions); setMapActions(defaultMapActions);
} }
const [mapActions, setMapActions] = useState<any>(defaultMapActions); const [mapActions, setMapActions] = useState(defaultMapActions);
function addMapActions( function addMapActions(
actions: Action[], actions: Action<DrawingState | FogState>[],
indexKey: string, indexKey: MapActionsIndexKey,
actionsKey: any, actionsKey: MapActionsKey,
shapesKey: any shapesKey: "drawShapes" | "fogShapes"
) { ) {
setMapActions((prevMapActions: any) => { setMapActions((prevMapActions) => {
const newActions = [ const newActions = [
...prevMapActions[actionsKey].slice(0, prevMapActions[indexKey] + 1), ...prevMapActions[actionsKey].slice(0, prevMapActions[indexKey] + 1),
...actions, ...actions,
@ -225,39 +260,40 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
}; };
}); });
// Update map state by performing the actions on it // Update map state by performing the actions on it
setCurrentMapState((prevMapState: any) => { setCurrentMapState((prevMapState) => {
if (prevMapState) { if (!prevMapState) {
let shapes = prevMapState[shapesKey]; return prevMapState;
for (let action of actions) {
shapes = action.execute(shapes);
}
return {
...prevMapState,
[shapesKey]: shapes,
};
} }
let shapes = prevMapState[shapesKey];
for (let action of actions) {
shapes = action.execute(shapes);
}
return {
...prevMapState,
[shapesKey]: shapes,
};
}); });
} }
function updateActionIndex( function updateActionIndex(
change: any, change,
indexKey: any, indexKey: MapActionsIndexKey,
actionsKey: any, actionsKey: MapActionsKey,
shapesKey: any shapesKey: "drawShapes" | "fogShapes"
) { ) {
const prevIndex: any = mapActions[indexKey]; const prevIndex = mapActions[indexKey];
const newIndex = Math.min( const newIndex = Math.min(
Math.max(mapActions[indexKey] + change, -1), Math.max(mapActions[indexKey] + change, -1),
mapActions[actionsKey].length - 1 mapActions[actionsKey].length - 1
); );
setMapActions((prevMapActions: Action[]) => ({ setMapActions((prevMapActions) => ({
...prevMapActions, ...prevMapActions,
[indexKey]: newIndex, [indexKey]: newIndex,
})); }));
// Update map state by either performing the actions or undoing them // Update map state by either performing the actions or undoing them
setCurrentMapState((prevMapState: any) => { setCurrentMapState((prevMapState) => {
if (prevMapState) { if (prevMapState) {
let shapes = prevMapState[shapesKey]; let shapes = prevMapState[shapesKey];
if (prevIndex < newIndex) { if (prevIndex < newIndex) {
@ -283,7 +319,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
return newIndex; return newIndex;
} }
function handleMapDraw(action: Action) { function handleMapDraw(action: Action<DrawingState>) {
addMapActions( addMapActions(
[action], [action],
"mapDrawActionIndex", "mapDrawActionIndex",
@ -300,7 +336,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
updateActionIndex(1, "mapDrawActionIndex", "mapDrawActions", "drawShapes"); updateActionIndex(1, "mapDrawActionIndex", "mapDrawActions", "drawShapes");
} }
function handleFogDraw(action: Action) { function handleFogDraw(action: Action<FogState>) {
addMapActions( addMapActions(
[action], [action],
"fogDrawActionIndex", "fogDrawActionIndex",
@ -318,7 +354,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
} }
// If map changes clear map actions // If map changes clear map actions
const previousMapIdRef = useRef<any>(); const previousMapIdRef = useRef();
useEffect(() => { useEffect(() => {
if (currentMap && currentMap?.id !== previousMapIdRef.current) { if (currentMap && currentMap?.id !== previousMapIdRef.current) {
setMapActions(defaultMapActions); setMapActions(defaultMapActions);
@ -326,8 +362,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
} }
}, [currentMap]); }, [currentMap]);
function handleNoteChange(note: any) { function handleNoteChange(note) {
setCurrentMapState((prevMapState: any) => ({ setCurrentMapState((prevMapState) => ({
...prevMapState, ...prevMapState,
notes: { notes: {
...prevMapState.notes, ...prevMapState.notes,
@ -337,7 +373,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
} }
function handleNoteRemove(noteId: string) { function handleNoteRemove(noteId: string) {
setCurrentMapState((prevMapState: any) => ({ setCurrentMapState((prevMapState) => ({
...prevMapState, ...prevMapState,
notes: omit(prevMapState.notes, [noteId]), notes: omit(prevMapState.notes, [noteId]),
})); }));
@ -352,7 +388,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
return; return;
} }
let assets = []; let assets: AssetManifestAsset[] = [];
for (let tokenState of tokenStates) { for (let tokenState of tokenStates) {
if (tokenState.type === "file") { if (tokenState.type === "file") {
assets.push({ id: tokenState.file, owner: tokenState.owner }); 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) { if (!currentMapState) {
return; return;
} }
setCurrentMapState((prevMapState: any) => { setCurrentMapState((prevMapState) => {
let tokens = { ...prevMapState.tokens }; let tokens = { ...prevMapState.tokens };
for (let id in change) { for (let id in change) {
if (id in tokens) { if (id in tokens) {
@ -390,8 +426,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
}); });
} }
function handleMapTokenStateRemove(tokenState: any) { function handleMapTokenStateRemove(tokenState) {
setCurrentMapState((prevMapState: any) => { setCurrentMapState((prevMapState) => {
const { [tokenState.id]: old, ...rest } = prevMapState.tokens; const { [tokenState.id]: old, ...rest } = prevMapState.tokens;
return { ...prevMapState, tokens: rest }; return { ...prevMapState, tokens: rest };
}); });
@ -404,8 +440,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
reply, reply,
}: { }: {
id: string; id: string;
data: any; data;
reply: any; reply;
}) { }) {
if (id === "assetRequest") { if (id === "assetRequest") {
const asset = await getAsset(data.id); const asset = await getAsset(data.id);
@ -440,7 +476,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
assetProgressUpdate({ id, total, count }); assetProgressUpdate({ id, total, count });
} }
async function handleSocketMap(map: any) { async function handleSocketMap(map) {
if (map) { if (map) {
setCurrentMap(map); setCurrentMap(map);
} else { } else {
@ -461,7 +497,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
const canChangeMap = !isLoading; const canChangeMap = !isLoading;
const canEditMapDrawing: any = const canEditMapDrawing =
currentMap && currentMap &&
currentMapState && currentMapState &&
(currentMapState.editFlags.includes("drawing") || (currentMapState.editFlags.includes("drawing") ||
@ -478,7 +514,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
(currentMapState.editFlags.includes("notes") || (currentMapState.editFlags.includes("notes") ||
currentMap?.owner === userId); currentMap?.owner === userId);
const disabledMapTokens: { [key: string]: any } = {}; const disabledMapTokens = {};
// If we have a map and state and have the token permission disabled // If we have a map and state and have the token permission disabled
// and are not the map owner // and are not the map owner
if ( if (

View File

@ -46,13 +46,13 @@ function NetworkedMapPointer({
// We use requestAnimationFrame as setInterval was being blocked during // We use requestAnimationFrame as setInterval was being blocked during
// re-renders on Chrome with Windows // re-renders on Chrome with Windows
const ownPointerUpdateRef: React.MutableRefObject< const ownPointerUpdateRef: React.MutableRefObject<
{ position: any; visible: boolean; id: any; color: any } | undefined | null { position; visible: boolean; id; color } | undefined | null
> = useRef(); > = useRef();
useEffect(() => { useEffect(() => {
let prevTime = performance.now(); let prevTime = performance.now();
let request = requestAnimationFrame(update); let request = requestAnimationFrame(update);
let counter = 0; let counter = 0;
function update(time: any) { function update(time) {
request = requestAnimationFrame(update); request = requestAnimationFrame(update);
const deltaTime = time - prevTime; const deltaTime = time - prevTime;
counter += deltaTime; counter += deltaTime;
@ -79,7 +79,7 @@ function NetworkedMapPointer({
}; };
}, []); }, []);
function updateOwnPointerState(position: any, visible: boolean) { function updateOwnPointerState(position, visible: boolean) {
setLocalPointerState((prev) => ({ setLocalPointerState((prev) => ({
...prev, ...prev,
[userId]: { position, visible, id: userId, color: pointerColor }, [userId]: { position, visible, id: userId, color: pointerColor },
@ -92,24 +92,24 @@ function NetworkedMapPointer({
}; };
} }
function handleOwnPointerDown(position: any) { function handleOwnPointerDown(position) {
updateOwnPointerState(position, true); updateOwnPointerState(position, true);
} }
function handleOwnPointerMove(position: any) { function handleOwnPointerMove(position) {
updateOwnPointerState(position, true); updateOwnPointerState(position, true);
} }
function handleOwnPointerUp(position: any) { function handleOwnPointerUp(position) {
updateOwnPointerState(position, false); updateOwnPointerState(position, false);
} }
// Handle pointer data receive // Handle pointer data receive
const interpolationsRef: React.MutableRefObject<any> = useRef({}); const interpolationsRef: React.MutableRefObject = useRef({});
useEffect(() => { useEffect(() => {
// TODO: Handle player disconnect while pointer visible // TODO: Handle player disconnect while pointer visible
function handleSocketPlayerPointer(pointer: any) { function handleSocketPlayerPointer(pointer) {
const interpolations: any = interpolationsRef.current; const interpolations = interpolationsRef.current;
const id = pointer.id; const id = pointer.id;
if (!(id in interpolations)) { if (!(id in interpolations)) {
interpolations[id] = { interpolations[id] = {
@ -154,8 +154,8 @@ function NetworkedMapPointer({
function animate() { function animate() {
request = requestAnimationFrame(animate); request = requestAnimationFrame(animate);
const time = performance.now(); const time = performance.now();
let interpolatedPointerState: any = {}; let interpolatedPointerState = {};
for (let interp of Object.values(interpolationsRef.current) as any) { for (let interp of Object.values(interpolationsRef.current)) {
if (!interp.from || !interp.to) { if (!interp.from || !interp.to) {
continue; continue;
} }
@ -200,7 +200,7 @@ function NetworkedMapPointer({
return ( return (
<Group> <Group>
{Object.values(localPointerState).map((pointer: any) => ( {Object.values(localPointerState).map((pointer) => (
<MapPointer <MapPointer
key={pointer.id} key={pointer.id}
active={pointer.id === userId ? active : false} active={pointer.id === userId ? active : false}

View File

@ -139,7 +139,13 @@ class Session extends EventEmitter {
* @param {string=} channel * @param {string=} channel
* @param {string=} chunkId * @param {string=} chunkId
*/ */
sendTo(sessionId: string, eventId: string, data: any, channel?: string, chunkId?: string) { sendTo(
sessionId: string,
eventId: string,
data,
channel?: string,
chunkId?: string
) {
if (!(sessionId in this.peers)) { if (!(sessionId in this.peers)) {
if (!this._addPeer(sessionId, true)) { if (!this._addPeer(sessionId, true)) {
return; return;
@ -248,11 +254,11 @@ class Session extends EventEmitter {
const peer = { id, connection, initiator, ready: false }; const peer = { id, connection, initiator, ready: false };
function reply(id: string, data: any, channel?: string, chunkId?: string) { function reply(id: string, data, channel?: string, chunkId?: string) {
peer.connection.sendObject({ id, data }, channel, chunkId); peer.connection.sendObject({ id, data }, channel, chunkId);
} }
const handleSignal = (signal: any) => { const handleSignal = (signal) => {
this.socket.emit("signal", JSON.stringify({ to: peer.id, signal })); this.socket.emit("signal", JSON.stringify({ to: peer.id, signal }));
}; };
@ -269,9 +275,9 @@ class Session extends EventEmitter {
* @property {peerReply} reply * @property {peerReply} reply
*/ */
this.emit("peerConnect", { peer, reply }); this.emit("peerConnect", { peer, reply });
} };
const handleDataComplete = (data: any) => { const handleDataComplete = (data) => {
/** /**
* Peer Data Event - Data received by a peer * Peer Data Event - Data received by a peer
* *
@ -285,7 +291,7 @@ class Session extends EventEmitter {
let peerDataEvent: { let peerDataEvent: {
peer: SessionPeer; peer: SessionPeer;
id: string; id: string;
data: any; data;
reply: peerReply; reply: peerReply;
} = { } = {
peer, peer,
@ -293,7 +299,7 @@ class Session extends EventEmitter {
data: data.data, data: data.data,
reply: reply, reply: reply,
}; };
console.log(`Data: ${JSON.stringify(data)}`) console.log(`Data: ${JSON.stringify(data)}`);
this.emit("peerData", peerDataEvent); this.emit("peerData", peerDataEvent);
}; };
@ -444,7 +450,7 @@ class Session extends EventEmitter {
} }
} }
_handleSignal(data: any) { _handleSignal(data) {
const { from, signal } = data; const { from, signal } = data;
if (!(from in this.peers)) { if (!(from in this.peers)) {
if (!this._addPeer(from, false)) { if (!this._addPeer(from, false)) {

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { ChangeEvent, FormEvent, useEffect, useState } from "react";
import { import {
Box, Box,
Flex, Flex,
@ -18,7 +18,7 @@ import LoadingOverlay from "../components/LoadingOverlay";
import { logError } from "../helpers/logging"; import { logError } from "../helpers/logging";
import { Stripe } from "@stripe/stripe-js"; 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[] = [ const prices: Price[] = [
{ price: "$5.00", name: "Small", value: 5 }, { price: "$5.00", name: "Small", value: 5 },
@ -32,11 +32,9 @@ function Donate() {
const hasDonated = query.has("success"); const hasDonated = query.has("success");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// TODO: check with Mitch about changes here from useState(null) const [error, setError] = useState<Error | undefined>(undefined);
// TODO: typing with error a little messy
const [error, setError]= useState<any>();
const [stripe, setStripe]: [ stripe: Stripe | undefined, setStripe: React.Dispatch<Stripe | undefined >] = useState(); const [stripe, setStripe] = useState<Stripe>();
useEffect(() => { useEffect(() => {
import("@stripe/stripe-js").then(({ loadStripe }) => { import("@stripe/stripe-js").then(({ loadStripe }) => {
loadStripe(process.env.REACT_APP_STRIPE_API_KEY as string) 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<HTMLDivElement>) {
event.preventDefault(); event.preventDefault();
if (loading) { if (loading) {
return; return;
@ -76,7 +74,8 @@ function Donate() {
const result = await stripe?.redirectToCheckout({ sessionId: session.id }); const result = await stripe?.redirectToCheckout({ sessionId: session.id });
if (result?.error) { 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); setValue(price.value);
setSelectedPrice(price.name); setSelectedPrice(price.name);
} }
return ( return (
<Flex <Flex
sx={{ sx={{
@ -159,7 +158,9 @@ function Donate() {
name="donation" name="donation"
min={1} min={1}
value={value} value={value}
onChange={(e: any) => setValue(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) =>
setValue(parseInt(e.target.value))
}
/> />
</Box> </Box>
)} )}
@ -169,7 +170,7 @@ function Donate() {
</Flex> </Flex>
<Footer /> <Footer />
{loading && <LoadingOverlay />} {loading && <LoadingOverlay />}
<ErrorBanner error={error as Error} onRequestClose={() => setError(undefined)} /> <ErrorBanner error={error} onRequestClose={() => setError(undefined)} />
</Flex> </Flex>
); );
} }

View File

@ -50,12 +50,9 @@ function Game() {
}, [session]); }, [session]);
// Handle session errors // Handle session errors
const [peerError, setPeerError]: [ const [peerError, setPeerError] = useState(null);
peerError: any,
setPeerError: React.Dispatch<any>
] = useState(null);
useEffect(() => { useEffect(() => {
function handlePeerError({ error }: { error: any }) { function handlePeerError({ error }) {
if (error.code === "ERR_WEBRTC_SUPPORT") { if (error.code === "ERR_WEBRTC_SUPPORT") {
setPeerError("WebRTC not supported."); setPeerError("WebRTC not supported.");
} else if (error.code === "ERR_CREATE_OFFER") { } else if (error.code === "ERR_CREATE_OFFER") {

5
src/types/Action.ts Normal file
View File

@ -0,0 +1,5 @@
/**
* Shared types for the Action class
*/
export type ID = { id: string };

View File

@ -6,3 +6,7 @@ export type Asset = {
owner: string; owner: string;
mime: string; mime: string;
}; };
export type AssetManifestAsset = Pick<Asset, "id" | "owner">;
export type AssetManifestAssets = Record<string, AssetManifestAsset>;
export type AssetManifest = { mapId: string; assets: AssetManifestAssets };

View File

@ -1,4 +1,4 @@
import { InstancedMesh } from "@babylonjs/core"; import { InstancedMesh, Mesh } from "@babylonjs/core";
import Dice from "../dice/Dice"; import Dice from "../dice/Dice";
export type DiceType = "d4" | "d6" | "d8" | "d10" | "d12" | "d20" | "d100"; export type DiceType = "d4" | "d6" | "d8" | "d10" | "d12" | "d20" | "d100";
@ -27,3 +27,25 @@ export type DefaultDice = {
class: typeof Dice; class: typeof Dice;
preview: string; preview: string;
}; };
export type BaseDiceTextureSources = {
albedo: string;
normal: string;
metalRoughness: string;
};
export type DiceMeshes = Record<DiceType, Mesh>;
export function isDiceMeshes(
meshes: Partial<DiceMeshes>
): meshes is DiceMeshes {
return (
!!meshes.d4 &&
!!meshes.d6 &&
!!meshes.d8 &&
!!meshes.d10 &&
!!meshes.d12 &&
!!meshes.d20 &&
!!meshes.d100
);
}

View File

@ -90,3 +90,5 @@ export function drawingToolIsShape(type: DrawingToolType): type is ShapeType {
type === "triangle" type === "triangle"
); );
} }
export type DrawingState = Record<string, Drawing>;

View File

@ -28,3 +28,5 @@ export type Fog = {
type: "fog"; type: "fog";
visible: boolean; visible: boolean;
}; };
export type FogState = Record<string, Fog>;

View File

@ -1,5 +1,21 @@
import React from "react";
import { Grid } from "./Grid"; import { Grid } from "./Grid";
export type MapToolId =
| "move"
| "fog"
| "drawing"
| "measure"
| "pointer"
| "note";
export type MapTool = {
id: MapToolId;
icon: React.ReactNode;
title: string;
SettingsComponent?: React.ElementType;
};
export type BaseMap = { export type BaseMap = {
id: string; id: string;
name: string; name: string;
@ -31,7 +47,7 @@ export type FileMap = BaseMap & {
file: string; file: string;
resolutions: FileMapResolutions; resolutions: FileMapResolutions;
thumbnail: string; thumbnail: string;
quality: "low" | "medium" | "high" | "ultra" | "original"; quality: keyof FileMapResolutions | "original";
}; };
export type Map = DefaultMap | FileMap; export type Map = DefaultMap | FileMap;

Some files were not shown because too many files have changed in this diff Show More