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

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,
ml: 0,
mr: 0,
labelFunc: (value: any) => value,
labelFunc: (value: number) => value,
};
export default Slider;

View File

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

View File

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

View File

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

View File

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

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 RadioIconButton from "../RadioIconButton";
@ -21,21 +21,26 @@ import FullScreenExitIcon from "../../icons/FullScreenExitIcon";
import NoteToolIcon from "../../icons/NoteToolIcon";
import useSetting from "../../hooks/useSetting";
import { Map } from "../../types/Map";
import { Map, MapTool, MapToolId } from "../../types/Map";
import { MapState } from "../../types/MapState";
import {
MapChangeEventHandler,
MapResetEventHandler,
} from "../../types/Events";
import { Settings } from "../../types/Settings";
type MapControlsProps = {
onMapChange: () => void;
onMapReset: () => void;
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
currentMap?: Map;
currentMapState?: MapState;
selectedToolId: string;
onSelectedToolChange: () => void;
toolSettings: any;
onToolSettingChange: () => void;
onToolAction: () => void;
selectedToolId: MapToolId;
onSelectedToolChange: (toolId: MapToolId) => void;
toolSettings: Settings;
onToolSettingChange: (change: Partial<Settings>) => void;
onToolAction: (actionId: string) => void;
disabledControls: string[];
disabledSettings: string[];
disabledSettings: Partial<Record<keyof Settings, string[]>>;
};
function MapContols({
@ -54,7 +59,7 @@ function MapContols({
const [isExpanded, setIsExpanded] = useState(true);
const [fullScreen, setFullScreen] = useSetting("map.fullScreen");
const toolsById = {
const toolsById: Record<string, MapTool> = {
move: {
id: "move",
icon: <MoveToolIcon />,
@ -89,7 +94,14 @@ function MapContols({
title: "Note Tool (N)",
},
};
const tools = ["move", "fog", "drawing", "measure", "pointer", "note"];
const tools: MapToolId[] = [
"move",
"fog",
"drawing",
"measure",
"pointer",
"note",
];
const sections = [
{
@ -174,32 +186,41 @@ function MapContols({
function getToolSettings() {
const Settings = toolsById[selectedToolId].SettingsComponent;
if (Settings) {
return (
<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, change)
}
onToolAction={onToolAction}
disabledActions={disabledSettings[selectedToolId]}
/>
</Box>
);
} else {
if (
!Settings ||
selectedToolId === "move" ||
selectedToolId === "measure" ||
selectedToolId === "note"
) {
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 (

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { IconButton } from "theme-ui";
import SelectMapModal from "../../modals/SelectMapModal";
@ -6,6 +6,20 @@ import SelectMapIcon from "../../icons/SelectMapIcon";
import { useMapData } from "../../contexts/MapDataContext";
import { useUserId } from "../../contexts/UserIdContext";
import {
MapChangeEventHandler,
MapResetEventHandler,
} from "../../types/Events";
import { Map } from "../../types/Map";
import { MapState } from "../../types/MapState";
type SelectMapButtonProps = {
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
currentMap?: Map;
currentMapState?: MapState;
disabled: boolean;
};
function SelectMapButton({
onMapChange,
@ -13,7 +27,7 @@ function SelectMapButton({
currentMap,
currentMapState,
disabled,
}) {
}: SelectMapButtonProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
const { updateMapState } = useMapData();

View File

@ -4,7 +4,13 @@ import { IconButton } from "theme-ui";
import ChangeNicknameModal from "../../modals/ChangeNicknameModal";
import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon";
function ChangeNicknameButton({ nickname, onChange }: { nickname: string, onChange: any}) {
function ChangeNicknameButton({
nickname,
onChange,
}: {
nickname: string;
onChange;
}) {
const [isChangeModalOpen, setIsChangeModalOpen] = useState(false);
function openModal() {
setIsChangeModalOpen(true);

View File

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

View File

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

View File

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

View File

@ -4,7 +4,15 @@ import Stream from "./Stream";
import DiceRolls from "./DiceRolls";
// TODO: check if stream is a required or optional param
function Nickname({ nickname, stream, diceRolls }: { nickname: string, stream?: any, diceRolls: any}) {
function Nickname({
nickname,
stream,
diceRolls,
}: {
nickname: string;
stream?;
diceRolls;
}) {
return (
<Flex sx={{ flexDirection: "column" }}>
<Text

View File

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

View File

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

View File

@ -4,7 +4,15 @@ import { IconButton } from "theme-ui";
import StartTimerModal from "../../modals/StartTimerModal";
import StartTimerIcon from "../../icons/StartTimerIcon";
function StartTimerButton({ onTimerStart, onTimerStop, timer }: { onTimerStart: any, onTimerStop: any, timer: any }) {
function StartTimerButton({
onTimerStart,
onTimerStop,
timer,
}: {
onTimerStart;
onTimerStop;
timer;
}) {
const [isTimerModalOpen, setIsTimerModalOpen] = useState(false);
function openModal() {

View File

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

View File

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

View File

@ -10,10 +10,16 @@ import useDebounce from "../hooks/useDebounce";
import { omit } from "../helpers/shared";
import { Asset } from "../types/Asset";
export type GetAssetEventHanlder = (
assetId: string
) => Promise<Asset | undefined>;
export type AddAssetsEventHandler = (assets: Asset[]) => Promise<void>;
export type PutAssetEventsHandler = (asset: Asset) => Promise<void>;
type AssetsContext = {
getAsset: (assetId: string) => Promise<Asset | undefined>;
addAssets: (assets: Asset[]) => void;
putAsset: (asset: Asset) => void;
getAsset: GetAssetEventHanlder;
addAssets: AddAssetsEventHandler;
putAsset: PutAssetEventsHandler;
};
const AssetsContext = React.createContext<AssetsContext | undefined>(undefined);
@ -30,7 +36,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) {
}
}, [worker, databaseStatus]);
const getAsset = useCallback(
const getAsset = useCallback<GetAssetEventHanlder>(
async (assetId) => {
if (database) {
return await database.table("assets").get(assetId);
@ -39,7 +45,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) {
[database]
);
const addAssets = useCallback(
const addAssets = useCallback<AddAssetsEventHandler>(
async (assets) => {
if (database) {
await database.table("assets").bulkAdd(assets);
@ -48,7 +54,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) {
[database]
);
const putAsset = useCallback(
const putAsset = useCallback<PutAssetEventsHandler>(
async (asset) => {
if (database) {
// 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
import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax
import { DatabaseWorkerService } from "../workers/DatabaseWorker";
export type DatabaseStatus = "loading" | "disabled" | "upgrading" | "loaded";
type DatabaseContext = {
database: Dexie | undefined;
databaseStatus: any;
databaseStatus: DatabaseStatus;
databaseError: Error | undefined;
worker: Comlink.Remote<any>;
worker: Comlink.Remote<DatabaseWorkerService>;
};
// TODO: check what default we want here
const DatabaseContext =
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 }) {
const [database, setDatabase] = useState<Dexie>();
const [databaseStatus, setDatabaseStatus] =
useState<"loading" | "disabled" | "upgrading" | "loaded">("loading");
useState<DatabaseStatus>("loading");
const [databaseError, setDatabaseError] = useState<Error>();
useEffect(() => {

View File

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

View File

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

View File

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

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 =
React.createContext<MapDataContext | undefined>(undefined);
const defaultMapState: Pick<
MapState,
"tokens" | "drawShapes" | "fogShapes" | "editFlags" | "notes"
> = {
const defaultMapState: Omit<MapState, "mapId"> = {
tokens: {},
drawShapes: {},
fogShapes: {},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

20
src/global.d.ts vendored
View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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
export function isStreamStopped(stream: MediaStream) {
return stream.getTracks().reduce((a: any, b: any) => a && b, { mute: true });
export function isStreamStopped(stream: MediaStream): boolean {
// TODO: Check what this thing actually does
return stream.getTracks().reduce((a, b) => a && b, { muted: true }).muted;
}
export function roundTo(x: number, to: number): number {
@ -62,9 +63,12 @@ export function isEmpty(obj: Object): boolean {
return Object.keys(obj).length === 0 && obj.constructor === Object;
}
export function keyBy<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(
(prev: any, current: any) => ({
(prev, current) => ({
...prev,
[key ? current[key] : current]: current,
}),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,12 +50,9 @@ function Game() {
}, [session]);
// Handle session errors
const [peerError, setPeerError]: [
peerError: any,
setPeerError: React.Dispatch<any>
] = useState(null);
const [peerError, setPeerError] = useState(null);
useEffect(() => {
function handlePeerError({ error }: { error: any }) {
function handlePeerError({ error }) {
if (error.code === "ERR_WEBRTC_SUPPORT") {
setPeerError("WebRTC not supported.");
} 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;
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";
export type DiceType = "d4" | "d6" | "d8" | "d10" | "d12" | "d20" | "d100";
@ -27,3 +27,25 @@ export type DefaultDice = {
class: typeof Dice;
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"
);
}
export type DrawingState = Record<string, Drawing>;

View File

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

View File

@ -1,5 +1,21 @@
import React from "react";
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 = {
id: string;
name: string;
@ -31,7 +47,7 @@ export type FileMap = BaseMap & {
file: string;
resolutions: FileMapResolutions;
thumbnail: string;
quality: "low" | "medium" | "high" | "ultra" | "original";
quality: keyof FileMapResolutions | "original";
};
export type Map = DefaultMap | FileMap;

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