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