typescript
This commit is contained in:
parent
68c1c6db0c
commit
d80bfa2f1e
@ -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);
|
@ -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;
|
21
src/actions/AddStatesAction.ts
Normal file
21
src/actions/AddStatesAction.ts
Normal 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;
|
41
src/actions/CutFogAction.ts
Normal file
41
src/actions/CutFogAction.ts
Normal 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;
|
@ -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;
|
@ -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;
|
23
src/actions/EditStatesAction.ts
Normal file
23
src/actions/EditStatesAction.ts
Normal 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;
|
@ -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;
|
21
src/actions/RemoveStatesAction.ts
Normal file
21
src/actions/RemoveStatesAction.ts
Normal 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;
|
32
src/actions/SubtractFogAction.ts
Normal file
32
src/actions/SubtractFogAction.ts
Normal 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;
|
@ -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;
|
@ -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
13
src/actions/index.ts
Normal 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,
|
||||
};
|
@ -78,7 +78,7 @@ Slider.defaultProps = {
|
||||
value: 0,
|
||||
ml: 0,
|
||||
mr: 0,
|
||||
labelFunc: (value: any) => value,
|
||||
labelFunc: (value: number) => value,
|
||||
};
|
||||
|
||||
export default Slider;
|
||||
|
@ -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%",
|
||||
|
@ -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}>
|
||||
|
@ -35,7 +35,7 @@ type DiceInteractionProps = {
|
||||
canvas: HTMLCanvasElement | WebGLRenderingContext;
|
||||
}) => void;
|
||||
onPointerDown: () => void;
|
||||
onPointerUp: () => any;
|
||||
onPointerUp: () => void;
|
||||
};
|
||||
|
||||
function DiceInteraction({
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -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 (
|
||||
|
@ -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 }}
|
@ -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) {
|
@ -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]);
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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
|
||||
|
@ -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[]) {
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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(() => {
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
}
|
@ -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: {},
|
||||
|
@ -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(() => {
|
||||
|
@ -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,
|
||||
|
@ -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(() => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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
20
src/global.d.ts
vendored
@ -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";
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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]];
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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] });
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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([]);
|
||||
}
|
||||
}
|
@ -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,
|
||||
}),
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
|
@ -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;
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -8,7 +8,7 @@ function AddPartyMemberModal({
|
||||
gameId,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onRequestClose: any;
|
||||
onRequestClose;
|
||||
gameId: string;
|
||||
}) {
|
||||
return (
|
||||
|
@ -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({
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
}));
|
||||
|
@ -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],
|
||||
})),
|
||||
|
@ -81,7 +81,7 @@ function SelectMapModal({
|
||||
* Image Upload
|
||||
*/
|
||||
|
||||
const fileInputRef = useRef<any>();
|
||||
const fileInputRef = useRef();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [isLargeImageWarningModalOpen, setShowLargeImageWarning] =
|
||||
|
@ -76,7 +76,7 @@ function SelectTokensModal({
|
||||
* Image Upload
|
||||
*/
|
||||
|
||||
const fileInputRef = useRef<any>();
|
||||
const fileInputRef = useRef();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [isLargeImageWarningModalOpen, setShowLargeImageWarning] =
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ function StartTimerModal({
|
||||
onTimerStop,
|
||||
timer,
|
||||
}: StartTimerProps) {
|
||||
const inputRef = useRef<any>();
|
||||
const inputRef = useRef();
|
||||
function focusInput() {
|
||||
inputRef.current && inputRef.current.focus();
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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}
|
||||
|
@ -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)) {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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
5
src/types/Action.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Shared types for the Action class
|
||||
*/
|
||||
|
||||
export type ID = { id: string };
|
@ -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 };
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -90,3 +90,5 @@ export function drawingToolIsShape(type: DrawingToolType): type is ShapeType {
|
||||
type === "triangle"
|
||||
);
|
||||
}
|
||||
|
||||
export type DrawingState = Record<string, Drawing>;
|
||||
|
@ -28,3 +28,5 @@ export type Fog = {
|
||||
type: "fog";
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export type FogState = Record<string, Fog>;
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user