- >
- );
-}
-
-export default DiceControls;
diff --git a/src/components/dice/DiceInteraction.js b/src/components/dice/DiceInteraction.js
index e573352..24bff59 100644
--- a/src/components/dice/DiceInteraction.js
+++ b/src/components/dice/DiceInteraction.js
@@ -1,7 +1,19 @@
import React, { useRef, useEffect } from "react";
-import * as BABYLON from "babylonjs";
+import { Engine } from "@babylonjs/core/Engines/engine";
+import { Scene } from "@babylonjs/core/scene";
+import { Vector3, Color4, Matrix } from "@babylonjs/core/Maths/math";
+import { AmmoJSPlugin } from "@babylonjs/core/Physics/Plugins/ammoJSPlugin";
+import { TargetCamera } from "@babylonjs/core/Cameras/targetCamera";
import * as AMMO from "ammo.js";
-import "babylonjs-loaders";
+
+import "@babylonjs/core/Physics/physicsEngineComponent";
+import "@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent";
+import "@babylonjs/core/Materials/Textures/Loaders/ddsTextureLoader";
+import "@babylonjs/core/Meshes/Builders/boxBuilder";
+import "@babylonjs/core/Actions/actionManager";
+import "@babylonjs/core/Culling/ray";
+import "@babylonjs/loaders/glTF";
+
import ReactResizeDetector from "react-resize-detector";
import usePreventTouch from "../../helpers/usePreventTouch";
@@ -16,25 +28,18 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
useEffect(() => {
const canvas = canvasRef.current;
- const engine = new BABYLON.Engine(canvas, true, {
+ const engine = new Engine(canvas, true, {
preserveDrawingBuffer: true,
stencil: true,
});
- const scene = new BABYLON.Scene(engine);
- scene.clearColor = new BABYLON.Color4(0, 0, 0, 0);
+ const scene = new Scene(engine);
+ scene.clearColor = new Color4(0, 0, 0, 0);
// Enable physics
- scene.enablePhysics(
- new BABYLON.Vector3(0, -98, 0),
- new BABYLON.AmmoJSPlugin(true, AMMO)
- );
+ scene.enablePhysics(new Vector3(0, -98, 0), new AmmoJSPlugin(true, AMMO));
- let camera = new BABYLON.TargetCamera(
- "camera",
- new BABYLON.Vector3(0, 33.5, 0),
- scene
- );
+ let camera = new TargetCamera("camera", new Vector3(0, 33.5, 0), scene);
camera.fov = 0.65;
- camera.setTarget(BABYLON.Vector3.Zero());
+ camera.setTarget(Vector3.Zero());
onSceneMount && onSceneMount({ scene, engine, canvas });
@@ -48,7 +53,7 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
const ray = scene.createPickingRay(
scene.pointerX,
scene.pointerY,
- BABYLON.Matrix.Identity(),
+ Matrix.Identity(),
camera
);
const currentPosition = selectedMesh.getAbsolutePosition();
@@ -72,17 +77,19 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
const selectedMeshRef = useRef();
const selectedMeshVelocityWindowRef = useRef([]);
const selectedMeshVelocityWindowSize = 4;
+ const selectedMeshMassRef = useRef();
function handlePointerDown() {
const scene = sceneRef.current;
if (scene) {
const pickInfo = scene.pick(scene.pointerX, scene.pointerY);
if (pickInfo.hit && pickInfo.pickedMesh.name !== "dice_tray") {
- pickInfo.pickedMesh.physicsImpostor.setLinearVelocity(
- BABYLON.Vector3.Zero()
- );
- pickInfo.pickedMesh.physicsImpostor.setAngularVelocity(
- BABYLON.Vector3.Zero()
- );
+ pickInfo.pickedMesh.physicsImpostor.setLinearVelocity(Vector3.Zero());
+ pickInfo.pickedMesh.physicsImpostor.setAngularVelocity(Vector3.Zero());
+
+ // Save the meshes mass and set it to 0 so we can pick it up
+ selectedMeshMassRef.current = pickInfo.pickedMesh.physicsImpostor.mass;
+ pickInfo.pickedMesh.physicsImpostor.setMass(0);
+
selectedMeshRef.current = pickInfo.pickedMesh;
}
}
@@ -95,7 +102,7 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
const scene = sceneRef.current;
if (selectedMesh && scene) {
// Average velocity window
- let velocity = BABYLON.Vector3.Zero();
+ let velocity = Vector3.Zero();
for (let v of velocityWindow) {
velocity.addInPlace(v);
}
@@ -103,6 +110,10 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
velocity.scaleInPlace(1 / velocityWindow.length);
}
+ // Re-apply the meshes mass
+ selectedMesh.physicsImpostor.setMass(selectedMeshMassRef.current);
+ selectedMesh.physicsImpostor.forceUpdate();
+
selectedMesh.physicsImpostor.applyImpulse(
velocity.scale(diceThrowSpeed * selectedMesh.physicsImpostor.mass),
selectedMesh.physicsImpostor.getObjectCenter()
@@ -110,6 +121,7 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
}
selectedMeshRef.current = null;
selectedMeshVelocityWindowRef.current = [];
+ selectedMeshMassRef.current = null;
onPointerUp();
}
diff --git a/src/components/dice/DiceResults.js b/src/components/dice/DiceResults.js
index 7ce4a2d..c0f1965 100644
--- a/src/components/dice/DiceResults.js
+++ b/src/components/dice/DiceResults.js
@@ -4,42 +4,49 @@ import { Flex, Text, Button, IconButton } from "theme-ui";
import ClearDiceIcon from "../../icons/ClearDiceIcon";
import RerollDiceIcon from "../../icons/RerollDiceIcon";
+import { getDiceRollTotal } from "../../helpers/dice";
+
const maxDiceRollsShown = 6;
function DiceResults({ diceRolls, onDiceClear, onDiceReroll }) {
const [isExpanded, setIsExpanded] = useState(false);
- if (
- diceRolls.map((dice) => dice.roll).includes("unknown") ||
- diceRolls.length === 0
- ) {
+ if (diceRolls.length === 0) {
return null;
}
let rolls = [];
if (diceRolls.length > 1) {
- rolls = diceRolls.map((dice, index) => (
-
-
- {dice.roll}
-
-
- {index === diceRolls.length - 1 ? "" : "+"}
-
-
- ));
+ rolls = diceRolls
+ .filter((dice) => dice.roll !== "unknown")
+ .map((dice, index) => (
+
+
+ {dice.roll}
+
+
+ {index === diceRolls.length - 1 ? "" : "+"}
+
+
+ ));
}
return (
- {diceRolls.reduce((accumulator, dice) => accumulator + dice.roll, 0)}
+ {getDiceRollTotal(diceRolls)}
{rolls.length > maxDiceRollsShown ? (
{
+ function handleResize() {
+ const map = document.querySelector(".map");
+ const mapRect = map.getBoundingClientRect();
+
+ const availableWidth = mapRect.width - 108; // Subtract padding
+ const availableHeight = mapRect.height - 80; // Subtract paddding and open icon
+
+ let height = Math.min(availableHeight, 1000);
+ let width = diceTraySize === "single" ? height / 2 : height;
+
+ if (width > availableWidth) {
+ width = availableWidth;
+ height = diceTraySize === "single" ? width * 2 : width;
+ }
+
+ setTraySize({ width, height });
+ }
+
+ window.addEventListener("resize", handleResize);
+
+ handleResize();
+
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ };
+ }, [diceTraySize]);
+
+ // Update dice rolls
+ useEffect(() => {
+ function updateDiceRolls() {
+ const die = diceRefs.current;
+ const sceneVisible = sceneVisibleRef.current;
+ if (!sceneVisible) {
+ return;
+ }
+ const diceAwake = die.map((dice) => dice.asleep).includes(false);
+ if (!diceAwake) {
+ return;
+ }
+
+ let newRolls = [];
+ for (let i = 0; i < die.length; i++) {
+ const dice = die[i];
+ let roll = getDiceRoll(dice);
+ newRolls[i] = roll;
+ }
+ onDiceRollsChange(newRolls);
+ }
+
+ const updateInterval = setInterval(updateDiceRolls, 100);
+ return () => {
+ clearInterval(updateInterval);
+ };
+ }, [diceRefs, sceneVisibleRef, onDiceRollsChange]);
+
return (
- {
- sceneInteractionRef.current = true;
+ {
- sceneInteractionRef.current = false;
+ >
+ {
+ sceneInteractionRef.current = true;
+ }}
+ onPointerUp={() => {
+ sceneInteractionRef.current = false;
+ }}
+ />
+ {
+ handleDiceClear();
+ onDiceRollsChange([]);
+ }}
+ onDiceReroll={handleDiceReroll}
+ />
+
+ {
+ handleDiceAdd(style, type);
+ onDiceRollsChange([...diceRolls, { type, roll: "unknown" }]);
}}
- />
-
- {isLoading && }
+ {isLoading && (
+
+
+
+ )}
);
}
diff --git a/src/components/dice/SelectDiceButton.js b/src/components/dice/SelectDiceButton.js
index 933ccee..f5c9fda 100644
--- a/src/components/dice/SelectDiceButton.js
+++ b/src/components/dice/SelectDiceButton.js
@@ -4,7 +4,7 @@ import { IconButton } from "theme-ui";
import SelectDiceIcon from "../../icons/SelectDiceIcon";
import SelectDiceModal from "../../modals/SelectDiceModal";
-function SelectDiceButton({ onDiceChange, currentDice }) {
+function SelectDiceButton({ onDiceChange, currentDice, disabled }) {
const [isModalOpen, setIsModalOpen] = useState(false);
function openModal() {
@@ -24,8 +24,8 @@ function SelectDiceButton({ onDiceChange, currentDice }) {
diff --git a/src/components/map/Map.js b/src/components/map/Map.js
index df9384a..ae09b02 100644
--- a/src/components/map/Map.js
+++ b/src/components/map/Map.js
@@ -6,12 +6,13 @@ import MapInteraction from "./MapInteraction";
import MapToken from "./MapToken";
import MapDrawing from "./MapDrawing";
import MapFog from "./MapFog";
-import MapDice from "./MapDice";
import MapGrid from "./MapGrid";
import MapMeasure from "./MapMeasure";
import MapLoadingOverlay from "./MapLoadingOverlay";
+import NetworkedMapPointer from "../../network/NetworkedMapPointer";
import TokenDataContext from "../../contexts/TokenDataContext";
+import SettingsContext from "../../contexts/SettingsContext";
import TokenMenu from "../token/TokenMenu";
import TokenDragOverlay from "../token/TokenDragOverlay";
@@ -35,6 +36,7 @@ function Map({
allowFogDrawing,
allowMapChange,
disabledTokens,
+ session,
}) {
const { tokensById } = useContext(TokenDataContext);
@@ -47,20 +49,10 @@ function Map({
const tokenSizePercent = gridSizeNormalized.x;
const [selectedToolId, setSelectedToolId] = useState("pan");
- const [toolSettings, setToolSettings] = useState({
- fog: { type: "polygon", useEdgeSnapping: false, useFogCut: false },
- drawing: {
- color: "red",
- type: "brush",
- useBlending: true,
- },
- measure: {
- type: "chebyshev",
- },
- });
+ const { settings, setSettings } = useContext(SettingsContext);
function handleToolSettingChange(tool, change) {
- setToolSettings((prevSettings) => ({
+ setSettings((prevSettings) => ({
...prevSettings,
[tool]: {
...prevSettings[tool],
@@ -139,6 +131,7 @@ function Map({
if (!map) {
disabledControls.push("pan");
disabledControls.push("measure");
+ disabledControls.push("pointer");
}
if (!allowFogDrawing) {
disabledControls.push("fog");
@@ -178,7 +171,7 @@ function Map({
currentMapState={mapState}
onSelectedToolChange={setSelectedToolId}
selectedToolId={selectedToolId}
- toolSettings={toolSettings}
+ toolSettings={settings}
onToolSettingChange={handleToolSettingChange}
onToolAction={handleToolAction}
disabledControls={disabledControls}
@@ -238,6 +231,8 @@ function Map({
selectedToolId === "pan" && !(tokenState.id in disabledTokens)
}
mapState={mapState}
+ fadeOnHover={selectedToolId === "drawing"}
+ map={map}
/>
))}
@@ -270,25 +265,30 @@ function Map({
const mapDrawing = (
);
const mapFog = (
);
@@ -298,9 +298,18 @@ function Map({
const mapMeasure = (
+ );
+
+ const mapPointer = (
+
);
@@ -312,7 +321,6 @@ function Map({
{mapControls}
{tokenMenu}
{tokenDragOverlay}
-
>
}
@@ -324,6 +332,7 @@ function Map({
{mapDrawing}
{mapTokens}
{mapFog}
+ {mapPointer}
{mapMeasure}
);
diff --git a/src/components/map/MapControls.js b/src/components/map/MapControls.js
index 33873ba..ca46ae1 100644
--- a/src/components/map/MapControls.js
+++ b/src/components/map/MapControls.js
@@ -15,6 +15,7 @@ import FogToolIcon from "../../icons/FogToolIcon";
import BrushToolIcon from "../../icons/BrushToolIcon";
import MeasureToolIcon from "../../icons/MeasureToolIcon";
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
+import PointerToolIcon from "../../icons/PointerToolIcon";
function MapContols({
onMapChange,
@@ -55,8 +56,13 @@ function MapContols({
title: "Measure Tool",
SettingsComponent: MeasureToolSettings,
},
+ pointer: {
+ id: "pointer",
+ icon: ,
+ title: "Pointer Tool",
+ },
};
- const tools = ["pan", "fog", "drawing", "measure"];
+ const tools = ["pan", "fog", "drawing", "measure", "pointer"];
const sections = [
{
diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js
index 370baee..11c54df 100644
--- a/src/components/map/MapDrawing.js
+++ b/src/components/map/MapDrawing.js
@@ -18,11 +18,13 @@ import { getRelativePointerPositionNormalized } from "../../helpers/konva";
import colors from "../../helpers/colors";
function MapDrawing({
+ map,
shapes,
onShapeAdd,
onShapesRemove,
- selectedToolId,
- selectedToolSettings,
+ active,
+ toolId,
+ toolSettings,
gridSize,
}) {
const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext(
@@ -33,22 +35,17 @@ function MapDrawing({
const [isBrushDown, setIsBrushDown] = useState(false);
const [erasingShapes, setErasingShapes] = useState([]);
- const shouldHover =
- selectedToolSettings && selectedToolSettings.type === "erase";
- const isEditing = selectedToolId === "drawing";
+ const shouldHover = toolSettings.type === "erase";
const isBrush =
- selectedToolSettings &&
- (selectedToolSettings.type === "brush" ||
- selectedToolSettings.type === "paint");
+ toolSettings.type === "brush" || toolSettings.type === "paint";
const isShape =
- selectedToolSettings &&
- (selectedToolSettings.type === "line" ||
- selectedToolSettings.type === "rectangle" ||
- selectedToolSettings.type === "circle" ||
- selectedToolSettings.type === "triangle");
+ toolSettings.type === "line" ||
+ toolSettings.type === "rectangle" ||
+ toolSettings.type === "circle" ||
+ toolSettings.type === "triangle";
useEffect(() => {
- if (!isEditing) {
+ if (!active) {
return;
}
const mapStage = mapStageRef.current;
@@ -56,9 +53,10 @@ function MapDrawing({
function getBrushPosition() {
const mapImage = mapStage.findOne("#mapImage");
return getBrushPositionForTool(
+ map,
getRelativePointerPositionNormalized(mapImage),
- selectedToolId,
- selectedToolSettings,
+ toolId,
+ toolSettings,
gridSize,
shapes
);
@@ -67,24 +65,24 @@ function MapDrawing({
function handleBrushDown() {
const brushPosition = getBrushPosition();
const commonShapeData = {
- color: selectedToolSettings && selectedToolSettings.color,
- blend: selectedToolSettings && selectedToolSettings.useBlending,
+ color: toolSettings.color,
+ blend: toolSettings.useBlending,
id: shortid.generate(),
};
if (isBrush) {
setDrawingShape({
type: "path",
- pathType: selectedToolSettings.type === "brush" ? "stroke" : "fill",
+ pathType: toolSettings.type === "brush" ? "stroke" : "fill",
data: { points: [brushPosition] },
- strokeWidth: selectedToolSettings.type === "brush" ? 1 : 0,
+ strokeWidth: toolSettings.type === "brush" ? 1 : 0,
...commonShapeData,
});
} else if (isShape) {
setDrawingShape({
type: "shape",
- shapeType: selectedToolSettings.type,
- data: getDefaultShapeData(selectedToolSettings.type, brushPosition),
- strokeWidth: selectedToolSettings.type === "line" ? 1 : 0,
+ shapeType: toolSettings.type,
+ data: getDefaultShapeData(toolSettings.type, brushPosition),
+ strokeWidth: toolSettings.type === "line" ? 1 : 0,
...commonShapeData,
});
}
@@ -157,23 +155,7 @@ function MapDrawing({
interactionEmitter.off("drag", handleBrushMove);
interactionEmitter.off("dragEnd", handleBrushUp);
};
- }, [
- drawingShape,
- erasingShapes,
- gridSize,
- isBrush,
- isBrushDown,
- isEditing,
- isShape,
- mapStageRef,
- onShapeAdd,
- onShapesRemove,
- selectedToolId,
- selectedToolSettings,
- shapes,
- stageScale,
- interactionEmitter,
- ]);
+ });
function handleShapeOver(shape, isDown) {
if (shouldHover && isDown) {
@@ -206,6 +188,7 @@ function MapDrawing({
closed={shape.pathType === "fill"}
fillEnabled={shape.pathType === "fill"}
lineCap="round"
+ lineJoin="round"
strokeWidth={getStrokeWidth(
shape.strokeWidth,
gridSize,
diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js
index 7a03c81..96bc1df 100644
--- a/src/components/map/MapFog.js
+++ b/src/components/map/MapFog.js
@@ -22,14 +22,17 @@ import {
} from "../../helpers/konva";
function MapFog({
+ map,
shapes,
onShapeAdd,
onShapeSubtract,
onShapesRemove,
onShapesEdit,
- selectedToolId,
- selectedToolSettings,
+ active,
+ toolId,
+ toolSettings,
gridSize,
+ transparent,
}) {
const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext(
MapInteractionContext
@@ -39,16 +42,14 @@ function MapFog({
const [isBrushDown, setIsBrushDown] = useState(false);
const [editingShapes, setEditingShapes] = useState([]);
- const isEditing = selectedToolId === "fog";
const shouldHover =
- isEditing &&
- (selectedToolSettings.type === "toggle" ||
- selectedToolSettings.type === "remove");
+ active &&
+ (toolSettings.type === "toggle" || toolSettings.type === "remove");
const [patternImage] = useImage(diagonalPattern);
useEffect(() => {
- if (!isEditing) {
+ if (!active) {
return;
}
@@ -57,9 +58,10 @@ function MapFog({
function getBrushPosition() {
const mapImage = mapStage.findOne("#mapImage");
return getBrushPositionForTool(
+ map,
getRelativePointerPositionNormalized(mapImage),
- selectedToolId,
- selectedToolSettings,
+ toolId,
+ toolSettings,
gridSize,
shapes
);
@@ -67,7 +69,7 @@ function MapFog({
function handleBrushDown() {
const brushPosition = getBrushPosition();
- if (selectedToolSettings.type === "brush") {
+ if (toolSettings.type === "brush") {
setDrawingShape({
type: "fog",
data: {
@@ -75,7 +77,7 @@ function MapFog({
holes: [],
},
strokeWidth: 0.5,
- color: selectedToolSettings.useFogSubtract ? "red" : "black",
+ color: toolSettings.useFogSubtract ? "red" : "black",
blend: false,
id: shortid.generate(),
visible: true,
@@ -85,11 +87,7 @@ function MapFog({
}
function handleBrushMove() {
- if (
- selectedToolSettings.type === "brush" &&
- isBrushDown &&
- drawingShape
- ) {
+ if (toolSettings.type === "brush" && isBrushDown && drawingShape) {
const brushPosition = getBrushPosition();
setDrawingShape((prevShape) => {
const prevPoints = prevShape.data.points;
@@ -114,8 +112,8 @@ function MapFog({
}
function handleBrushUp() {
- if (selectedToolSettings.type === "brush" && drawingShape) {
- const subtract = selectedToolSettings.useFogSubtract;
+ if (toolSettings.type === "brush" && drawingShape) {
+ const subtract = toolSettings.useFogSubtract;
if (drawingShape.data.points.length > 1) {
let shapeData = {};
@@ -147,9 +145,9 @@ function MapFog({
// Erase
if (editingShapes.length > 0) {
- if (selectedToolSettings.type === "remove") {
+ if (toolSettings.type === "remove") {
onShapesRemove(editingShapes.map((shape) => shape.id));
- } else if (selectedToolSettings.type === "toggle") {
+ } else if (toolSettings.type === "toggle") {
onShapesEdit(
editingShapes.map((shape) => ({
...shape,
@@ -164,7 +162,7 @@ function MapFog({
}
function handlePolygonClick() {
- if (selectedToolSettings.type === "polygon") {
+ if (toolSettings.type === "polygon") {
const brushPosition = getBrushPosition();
setDrawingShape((prevDrawingShape) => {
if (prevDrawingShape) {
@@ -183,7 +181,7 @@ function MapFog({
holes: [],
},
strokeWidth: 0.5,
- color: selectedToolSettings.useFogSubtract ? "red" : "black",
+ color: toolSettings.useFogSubtract ? "red" : "black",
blend: false,
id: shortid.generate(),
visible: true,
@@ -194,7 +192,7 @@ function MapFog({
}
function handlePolygonMove() {
- if (selectedToolSettings.type === "polygon" && drawingShape) {
+ if (toolSettings.type === "polygon" && drawingShape) {
const brushPosition = getBrushPosition();
setDrawingShape((prevShape) => {
if (!prevShape) {
@@ -227,26 +225,10 @@ function MapFog({
mapStage.off("mousemove touchmove", handlePolygonMove);
mapStage.off("click tap", handlePolygonClick);
};
- }, [
- mapStageRef,
- isEditing,
- drawingShape,
- editingShapes,
- gridSize,
- isBrushDown,
- onShapeAdd,
- onShapeSubtract,
- onShapesEdit,
- onShapesRemove,
- selectedToolId,
- selectedToolSettings,
- shapes,
- stageScale,
- interactionEmitter,
- ]);
+ });
const finishDrawingPolygon = useCallback(() => {
- const subtract = selectedToolSettings.useFogSubtract;
+ const subtract = toolSettings.useFogSubtract;
const data = {
...drawingShape.data,
// Remove the last point as it hasn't been placed yet
@@ -263,16 +245,12 @@ function MapFog({
}
setDrawingShape(null);
- }, [selectedToolSettings, drawingShape, onShapeSubtract, onShapeAdd]);
+ }, [toolSettings, drawingShape, onShapeSubtract, onShapeAdd]);
// Add keyboard shortcuts
useEffect(() => {
function handleKeyDown({ key }) {
- if (
- key === "Enter" &&
- selectedToolSettings.type === "polygon" &&
- drawingShape
- ) {
+ if (key === "Enter" && toolSettings.type === "polygon" && drawingShape) {
finishDrawingPolygon();
}
if (key === "Escape" && drawingShape) {
@@ -296,7 +274,7 @@ function MapFog({
}
return {
...prevShape,
- color: selectedToolSettings.useFogSubtract ? "black" : "red",
+ color: toolSettings.useFogSubtract ? "black" : "red",
};
});
}
@@ -307,12 +285,7 @@ function MapFog({
interactionEmitter.off("keyDown", handleKeyDown);
interactionEmitter.off("keyUp", handleKeyUp);
};
- }, [
- finishDrawingPolygon,
- interactionEmitter,
- drawingShape,
- selectedToolSettings,
- ]);
+ }, [finishDrawingPolygon, interactionEmitter, drawingShape, toolSettings]);
function handleShapeOver(shape, isDown) {
if (shouldHover && isDown) {
@@ -343,17 +316,21 @@ function MapFog({
fill={colors[shape.color] || shape.color}
closed
lineCap="round"
+ lineJoin="round"
strokeWidth={getStrokeWidth(
shape.strokeWidth,
gridSize,
mapWidth,
mapHeight
)}
- visible={isEditing || shape.visible}
- opacity={isEditing ? 0.5 : 1}
+ visible={(active && !toolSettings.preview) || shape.visible}
+ opacity={transparent ? 0.5 : 1}
fillPatternImage={patternImage}
- fillPriority={isEditing && !shape.visible ? "pattern" : "color"}
+ fillPriority={active && !shape.visible ? "pattern" : "color"}
holes={holes}
+ // Disable collision if the fog is transparent and we're not editing it
+ // This allows tokens to be moved under the fog
+ hitFunc={transparent && !active ? () => {} : undefined}
/>
);
}
@@ -394,8 +371,8 @@ function MapFog({
{shapes.map(renderShape)}
{drawingShape && renderShape(drawingShape)}
{drawingShape &&
- selectedToolSettings &&
- selectedToolSettings.type === "polygon" &&
+ toolSettings &&
+ toolSettings.type === "polygon" &&
renderPolygonAcceptTick(drawingShape)}
{editingShapes.length > 0 && editingShapes.map(renderEditingShape)}
diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js
index 2a69f48..60908ff 100644
--- a/src/components/map/MapInteraction.js
+++ b/src/components/map/MapInteraction.js
@@ -74,9 +74,11 @@ function MapInteraction({
const stageTranslateRef = useRef({ x: 0, y: 0 });
// Reset transform when map changes
+ const previousMapIdRef = useRef();
useEffect(() => {
const layer = mapLayerRef.current;
- if (map && layer && !mapLoaded) {
+ const previousMapId = previousMapIdRef.current;
+ if (map && layer && previousMapId !== map.id) {
const mapHeight = stageWidthRef.current * (map.height / map.width);
const newTranslate = {
x: 0,
@@ -89,7 +91,8 @@ function MapInteraction({
setStageScale(1);
}
- }, [map, mapLoaded]);
+ previousMapIdRef.current = map && map.id;
+ }, [map]);
const pinchPreviousDistanceRef = useRef();
const pinchPreviousOriginRef = useRef();
@@ -256,6 +259,9 @@ function MapInteraction({
if (event.key === "m" && !disabledControls.includes("measure")) {
onSelectedToolChange("measure");
}
+ if (event.key === "q" && !disabledControls.includes("pointer")) {
+ onSelectedToolChange("pointer");
+ }
}
function handleKeyUp(event) {
@@ -284,6 +290,7 @@ function MapInteraction({
case "fog":
case "drawing":
case "measure":
+ case "pointer":
return "crosshair";
default:
return "default";
diff --git a/src/components/map/MapMeasure.js b/src/components/map/MapMeasure.js
index 5ca4678..c3544a7 100644
--- a/src/components/map/MapMeasure.js
+++ b/src/components/map/MapMeasure.js
@@ -13,7 +13,7 @@ import {
import { getRelativePointerPositionNormalized } from "../../helpers/konva";
import * as Vector2 from "../../helpers/vector2";
-function MapMeasure({ selectedToolSettings, active, gridSize }) {
+function MapMeasure({ map, selectedToolSettings, active, gridSize }) {
const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext(
MapInteractionContext
);
@@ -21,6 +21,12 @@ function MapMeasure({ selectedToolSettings, active, gridSize }) {
const [drawingShapeData, setDrawingShapeData] = useState(null);
const [isBrushDown, setIsBrushDown] = useState(false);
+ const toolScale =
+ active && selectedToolSettings.scale.match(/(\d*)([a-zA-Z]*)/);
+ const toolMultiplier =
+ active && !isNaN(parseInt(toolScale[1])) ? parseInt(toolScale[1]) : 1;
+ const toolUnit = active && toolScale[2];
+
useEffect(() => {
if (!active) {
return;
@@ -30,6 +36,7 @@ function MapMeasure({ selectedToolSettings, active, gridSize }) {
function getBrushPosition() {
const mapImage = mapStage.findOne("#mapImage");
return getBrushPositionForTool(
+ map,
getRelativePointerPositionNormalized(mapImage),
"drawing",
{ type: "line" },
@@ -81,15 +88,7 @@ function MapMeasure({ selectedToolSettings, active, gridSize }) {
interactionEmitter.off("drag", handleBrushMove);
interactionEmitter.off("dragEnd", handleBrushUp);
};
- }, [
- drawingShapeData,
- gridSize,
- isBrushDown,
- mapStageRef,
- interactionEmitter,
- active,
- selectedToolSettings,
- ]);
+ });
function renderShape(shapeData) {
const linePoints = shapeData.points.reduce(
@@ -126,7 +125,9 @@ function MapMeasure({ selectedToolSettings, active, gridSize }) {
>
{
+ if (!active) {
+ return;
+ }
+
+ const mapStage = mapStageRef.current;
+
+ function getBrushPosition() {
+ const mapImage = mapStage.findOne("#mapImage");
+ return getRelativePointerPositionNormalized(mapImage);
+ }
+
+ function handleBrushDown() {
+ onPointerDown && onPointerDown(getBrushPosition());
+ }
+
+ function handleBrushMove() {
+ onPointerMove && onPointerMove(getBrushPosition());
+ }
+
+ function handleBrushUp() {
+ onPointerMove && onPointerUp(getBrushPosition());
+ }
+
+ interactionEmitter.on("dragStart", handleBrushDown);
+ interactionEmitter.on("drag", handleBrushMove);
+ interactionEmitter.on("dragEnd", handleBrushUp);
+
+ return () => {
+ interactionEmitter.off("dragStart", handleBrushDown);
+ interactionEmitter.off("drag", handleBrushMove);
+ interactionEmitter.off("dragEnd", handleBrushUp);
+ };
+ });
+
+ const size = getStrokeWidth(2, gridSize, mapWidth, mapHeight);
+
+ return (
+
+ {visible && (
+
+ )}
+
+ );
+}
+
+export default MapPointer;
diff --git a/src/components/map/MapSettings.js b/src/components/map/MapSettings.js
index 31743eb..e5526a7 100644
--- a/src/components/map/MapSettings.js
+++ b/src/components/map/MapSettings.js
@@ -64,7 +64,7 @@ function MapSettings({
onSettingsChange("gridX", parseInt(e.target.value))
}
@@ -78,7 +78,7 @@ function MapSettings({
onSettingsChange("gridY", parseInt(e.target.value))
}
@@ -103,7 +103,7 @@ function MapSettings({
@@ -116,14 +116,28 @@ function MapSettings({
-
+
+
+
+
{!mapEmpty && map.type !== "default" && (
diff --git a/src/components/map/MapTile.js b/src/components/map/MapTile.js
index 6e76a37..c8675d4 100644
--- a/src/components/map/MapTile.js
+++ b/src/components/map/MapTile.js
@@ -109,7 +109,8 @@ function MapTile({
}}
m={2}
bg="muted"
- onClick={() => {
+ onClick={(e) => {
+ e.stopPropagation();
setIsTileMenuOpen(false);
if (!isSelected) {
onMapSelect(map);
diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js
index 88d7961..96060c9 100644
--- a/src/components/map/MapTiles.js
+++ b/src/components/map/MapTiles.js
@@ -31,6 +31,7 @@ function MapTiles({
width: "500px",
borderRadius: "4px",
}}
+ onClick={() => onMapSelect(null)}
>
onFogPreviewChange(!useFogPreview)}
+ >
+ {useFogPreview ? : }
+
+ );
+}
+
+export default FogPreviewToggle;
diff --git a/src/components/map/controls/FogToolSettings.js b/src/components/map/controls/FogToolSettings.js
index f94b386..09ece28 100644
--- a/src/components/map/controls/FogToolSettings.js
+++ b/src/components/map/controls/FogToolSettings.js
@@ -4,6 +4,7 @@ import { useMedia } from "react-media";
import EdgeSnappingToggle from "./EdgeSnappingToggle";
import RadioIconButton from "./RadioIconButton";
+import FogPreviewToggle from "./FogPreviewToggle";
import FogBrushIcon from "../../../icons/FogBrushIcon";
import FogPolygonIcon from "../../../icons/FogPolygonIcon";
@@ -43,6 +44,8 @@ function BrushToolSettings({
onSettingChange({ type: "remove" });
} else if (key === "s") {
onSettingChange({ useEdgeSnapping: !settings.useEdgeSnapping });
+ } else if (key === "f") {
+ onSettingChange({ preview: !settings.preview });
} else if (
(key === "z" || key === "Z") &&
(ctrlKey || metaKey) &&
@@ -142,6 +145,10 @@ function BrushToolSettings({
onSettingChange({ useEdgeSnapping })
}
/>
+ onSettingChange({ preview })}
+ />
onToolAction("fogUndo")}
diff --git a/src/components/map/controls/MeasureToolSettings.js b/src/components/map/controls/MeasureToolSettings.js
index d29f447..8886dbc 100644
--- a/src/components/map/controls/MeasureToolSettings.js
+++ b/src/components/map/controls/MeasureToolSettings.js
@@ -1,11 +1,13 @@
import React, { useEffect, useContext } from "react";
-import { Flex } from "theme-ui";
+import { Flex, Input, Text } from "theme-ui";
import ToolSection from "./ToolSection";
import MeasureChebyshevIcon from "../../../icons/MeasureChebyshevIcon";
import MeasureEuclideanIcon from "../../../icons/MeasureEuclideanIcon";
import MeasureManhattanIcon from "../../../icons/MeasureManhattanIcon";
+import Divider from "../../Divider";
+
import MapInteractionContext from "../../../contexts/MapInteractionContext";
function MeasureToolSettings({ settings, onSettingChange }) {
@@ -50,14 +52,31 @@ function MeasureToolSettings({ settings, onSettingChange }) {
},
];
- // TODO Add keyboard shortcuts
-
return (
onSettingChange({ type: tool.id })}
/>
+
+
+ Scale:
+
+ onSettingChange({ scale: e.target.value })}
+ autoComplete="off"
+ />
);
}
diff --git a/src/components/party/DiceRoll.js b/src/components/party/DiceRoll.js
new file mode 100644
index 0000000..f04d11b
--- /dev/null
+++ b/src/components/party/DiceRoll.js
@@ -0,0 +1,19 @@
+import React from "react";
+import { Flex, Box, Text } from "theme-ui";
+
+function DiceRoll({ rolls, type, children }) {
+ return (
+
+ {children}
+ {rolls
+ .filter((d) => d.type === type && d.roll !== "unknown")
+ .map((dice, index) => (
+
+ {dice.roll}
+
+ ))}
+
+ );
+}
+
+export default DiceRoll;
diff --git a/src/components/party/DiceRolls.js b/src/components/party/DiceRolls.js
new file mode 100644
index 0000000..01d1ccc
--- /dev/null
+++ b/src/components/party/DiceRolls.js
@@ -0,0 +1,72 @@
+import React, { useState } from "react";
+import { Flex, Text, IconButton } from "theme-ui";
+
+import DiceRollsIcon from "../../icons/DiceRollsIcon";
+import D20Icon from "../../icons/D20Icon";
+import D12Icon from "../../icons/D12Icon";
+import D10Icon from "../../icons/D10Icon";
+import D8Icon from "../../icons/D8Icon";
+import D6Icon from "../../icons/D6Icon";
+import D4Icon from "../../icons/D4Icon";
+import D100Icon from "../../icons/D100Icon";
+
+import DiceRoll from "./DiceRoll";
+
+import { getDiceRollTotal } from "../../helpers/dice";
+
+function DiceRolls({ rolls }) {
+ const total = getDiceRollTotal(rolls);
+
+ const [expanded, setExpanded] = useState(false);
+
+ return (
+ total > 0 && (
+
+
+ setExpanded(!expanded)}
+ >
+
+
+
+ {total}
+
+
+ {expanded && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+ );
+}
+
+export default DiceRolls;
diff --git a/src/components/map/MapDice.js b/src/components/party/DiceTrayButton.js
similarity index 73%
rename from src/components/map/MapDice.js
rename to src/components/party/DiceTrayButton.js
index e40f109..475324a 100644
--- a/src/components/map/MapDice.js
+++ b/src/components/party/DiceTrayButton.js
@@ -6,7 +6,12 @@ import DiceTrayOverlay from "../dice/DiceTrayOverlay";
import { DiceLoadingProvider } from "../../contexts/DiceLoadingContext";
-function MapDice() {
+function DiceTrayButton({
+ shareDice,
+ onShareDiceChage,
+ diceRolls,
+ onDiceRollsChange,
+}) {
const [isExpanded, setIsExpanded] = useState(false);
return (
@@ -14,11 +19,12 @@ function MapDice() {
sx={{
position: "absolute",
top: 0,
- left: 0,
+ left: "100%",
bottom: 0,
flexDirection: "column",
alignItems: "flex-start",
pointerEvents: "none",
+ zIndex: 1,
}}
ml={1}
>
@@ -37,10 +43,16 @@ function MapDice() {
-
+
);
}
-export default MapDice;
+export default DiceTrayButton;
diff --git a/src/components/party/Nickname.js b/src/components/party/Nickname.js
index c20a588..12cdd33 100644
--- a/src/components/party/Nickname.js
+++ b/src/components/party/Nickname.js
@@ -2,8 +2,9 @@ import React from "react";
import { Text, Flex } from "theme-ui";
import Stream from "./Stream";
+import DiceRolls from "./DiceRolls";
-function Nickname({ nickname, stream }) {
+function Nickname({ nickname, stream, diceRolls }) {
return (
{stream && }
+ {diceRolls && }
);
}
diff --git a/src/components/party/Party.js b/src/components/party/Party.js
index e2c436d..e60c776 100644
--- a/src/components/party/Party.js
+++ b/src/components/party/Party.js
@@ -1,11 +1,15 @@
import React from "react";
import { Flex, Box, Text } from "theme-ui";
+import SimpleBar from "simplebar-react";
import AddPartyMemberButton from "./AddPartyMemberButton";
import Nickname from "./Nickname";
import ChangeNicknameButton from "./ChangeNicknameButton";
import StartStreamButton from "./StartStreamButton";
import SettingsButton from "../SettingsButton";
+import StartTimerButton from "./StartTimerButton";
+import Timer from "./Timer";
+import DiceTrayButton from "./DiceTrayButton";
function Party({
nickname,
@@ -16,6 +20,15 @@ function Party({
partyStreams,
onStreamStart,
onStreamEnd,
+ timer,
+ partyTimers,
+ onTimerStart,
+ onTimerStop,
+ shareDice,
+ onShareDiceChage,
+ diceRolls,
+ onDiceRollsChange,
+ partyDiceRolls,
}) {
return (
-
-
+
{Object.entries(partyNicknames).map(([id, partyNickname]) => (
))}
-
+ {timer && }
+ {Object.entries(partyTimers).map(([id, partyTimer], index) => (
+
+ ))}
+
@@ -61,8 +91,19 @@ function Party({
onStreamEnd={onStreamEnd}
stream={stream}
/>
+
+
);
}
diff --git a/src/components/party/StartTimerButton.js b/src/components/party/StartTimerButton.js
new file mode 100644
index 0000000..266ba6f
--- /dev/null
+++ b/src/components/party/StartTimerButton.js
@@ -0,0 +1,38 @@
+import React, { useState } from "react";
+import { IconButton } from "theme-ui";
+
+import StartTimerModal from "../../modals/StartTimerModal";
+import StartTimerIcon from "../../icons/StartTimerIcon";
+
+function StartTimerButton({ onTimerStart, onTimerStop, timer }) {
+ const [isTimerModalOpen, setIsTimerModalOpen] = useState(false);
+
+ function openModal() {
+ setIsTimerModalOpen(true);
+ }
+ function closeModal() {
+ setIsTimerModalOpen(false);
+ }
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default StartTimerButton;
diff --git a/src/components/party/Stream.js b/src/components/party/Stream.js
index 403b532..68ef167 100644
--- a/src/components/party/Stream.js
+++ b/src/components/party/Stream.js
@@ -102,15 +102,14 @@ function Stream({ stream, nickname }) {
>
- {isVolumeControlAvailable && (
-
- )}
+
{stream && }
{
+ if (progressBarRef.current && timer) {
+ progressBarRef.current.value = timer.current;
+ }
+ }, [timer]);
+
+ useEffect(() => {
+ let request = requestAnimationFrame(animate);
+ let previousTime = performance.now();
+ function animate(time) {
+ request = requestAnimationFrame(animate);
+ const deltaTime = time - previousTime;
+ previousTime = time;
+
+ if (progressBarRef.current && progressBarRef.current.value > 0) {
+ progressBarRef.current.value -= deltaTime;
+ }
+ }
+
+ return () => {
+ cancelAnimationFrame(request);
+ };
+ }, []);
+
+ const timerContainer = usePortal("root");
+
+ return ReactDOM.createPortal(
+
+
+ ,
+ timerContainer
+ );
+}
+
+export default Timer;
diff --git a/src/components/token/TokenSettings.js b/src/components/token/TokenSettings.js
index 92d2f20..e11b9e8 100644
--- a/src/components/token/TokenSettings.js
+++ b/src/components/token/TokenSettings.js
@@ -19,7 +19,7 @@ function TokenSettings({
onSettingsChange("defaultSize", parseInt(e.target.value))
}
diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js
index 1a3bc84..57bdc71 100644
--- a/src/contexts/MapDataContext.js
+++ b/src/contexts/MapDataContext.js
@@ -44,6 +44,7 @@ export function MapDataProvider({ children }) {
lastModified: Date.now() + i,
gridType: "grid",
showGrid: false,
+ snapToGrid: true,
});
// Add a state for the map if there isn't one already
const state = await database.table("states").get(id);
diff --git a/src/contexts/SettingsContext.js b/src/contexts/SettingsContext.js
new file mode 100644
index 0000000..b396577
--- /dev/null
+++ b/src/contexts/SettingsContext.js
@@ -0,0 +1,31 @@
+import React, { useState, useEffect } from "react";
+
+import { getSettings } from "../settings";
+
+const SettingsContext = React.createContext({
+ settings: {},
+ setSettings: () => {},
+});
+
+const settingsProvider = getSettings();
+
+export function SettingsProvider({ children }) {
+ const [settings, setSettings] = useState(settingsProvider.getAll());
+
+ useEffect(() => {
+ settingsProvider.setAll(settings);
+ }, [settings]);
+
+ const value = {
+ settings,
+ setSettings,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default SettingsContext;
diff --git a/src/database.js b/src/database.js
index 2e726ff..56ad355 100644
--- a/src/database.js
+++ b/src/database.js
@@ -129,6 +129,32 @@ function loadVersions(db) {
map.quality = "original";
});
});
+ // v1.5.0 - Fixed default token rogue spelling
+ db.version(7)
+ .stores({})
+ .upgrade((tx) => {
+ return tx
+ .table("states")
+ .toCollection()
+ .modify((state) => {
+ for (let id in state.tokens) {
+ if (state.tokens[id].tokenId === "__default-Rouge") {
+ state.tokens[id].tokenId = "__default-Rogue";
+ }
+ }
+ });
+ });
+ // v1.5.0 - Added map snap to grid option
+ db.version(8)
+ .stores({})
+ .upgrade((tx) => {
+ return tx
+ .table("maps")
+ .toCollection()
+ .modify((map) => {
+ map.snapToGrid = true;
+ });
+ });
}
// Get the dexie database used in DatabaseContext
diff --git a/src/dice/Dice.js b/src/dice/Dice.js
index e40e352..21dcbfa 100644
--- a/src/dice/Dice.js
+++ b/src/dice/Dice.js
@@ -1,4 +1,7 @@
-import * as BABYLON from "babylonjs";
+import { Vector3 } from "@babylonjs/core/Maths/math";
+import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader";
+import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial";
+import { PhysicsImpostor } from "@babylonjs/core/Physics/physicsImpostor";
import d4Source from "./shared/d4.glb";
import d6Source from "./shared/d6.glb";
@@ -35,9 +38,8 @@ class Dice {
}
static async loadMesh(source, material, scene) {
- let mesh = (
- await BABYLON.SceneLoader.ImportMeshAsync("", source, "", scene)
- ).meshes[1];
+ let mesh = (await SceneLoader.ImportMeshAsync("", source, "", scene))
+ .meshes[1];
mesh.setParent(null);
mesh.material = material;
@@ -48,7 +50,7 @@ class Dice {
}
static async loadMaterial(materialName, textures, scene) {
- let pbr = new BABYLON.PBRMaterial(materialName, scene);
+ let pbr = new PBRMaterial(materialName, scene);
pbr.albedoTexture = await importTextureAsync(textures.albedo);
pbr.normalTexture = await importTextureAsync(textures.normal);
pbr.metallicTexture = await importTextureAsync(textures.metalRoughness);
@@ -68,9 +70,9 @@ class Dice {
instance.addChild(locator);
}
- instance.physicsImpostor = new BABYLON.PhysicsImpostor(
+ instance.physicsImpostor = new PhysicsImpostor(
instance,
- BABYLON.PhysicsImpostor.ConvexHullImpostor,
+ PhysicsImpostor.ConvexHullImpostor,
physicalProperties,
scene
);
@@ -99,8 +101,8 @@ class Dice {
}
static roll(instance) {
- instance.physicsImpostor.setLinearVelocity(BABYLON.Vector3.Zero());
- instance.physicsImpostor.setAngularVelocity(BABYLON.Vector3.Zero());
+ instance.physicsImpostor.setLinearVelocity(Vector3.Zero());
+ instance.physicsImpostor.setAngularVelocity(Vector3.Zero());
const scene = instance.getScene();
const diceTraySingle = scene.getNodeByID("dice_tray_single");
@@ -110,7 +112,7 @@ class Dice {
: diceTrayDouble;
const trayBounds = visibleDiceTray.getBoundingInfo().boundingBox;
- const position = new BABYLON.Vector3(
+ const position = new Vector3(
trayBounds.center.x + (Math.random() * 2 - 1),
8,
trayBounds.center.z + (Math.random() * 2 - 1)
@@ -122,13 +124,13 @@ class Dice {
Math.random() * Math.PI * 2
);
- const throwTarget = new BABYLON.Vector3(
+ const throwTarget = new Vector3(
lerp(trayBounds.minimumWorld.x, trayBounds.maximumWorld.x, Math.random()),
5,
lerp(trayBounds.minimumWorld.z, trayBounds.maximumWorld.z, Math.random())
);
- const impulse = new BABYLON.Vector3(0, 0, 0)
+ const impulse = new Vector3(0, 0, 0)
.subtract(throwTarget)
.normalizeToNew()
.scale(lerp(minDiceRollSpeed, maxDiceRollSpeed, Math.random()));
diff --git a/src/dice/diceTray/DiceTray.js b/src/dice/diceTray/DiceTray.js
index 0e32e8b..72bd936 100644
--- a/src/dice/diceTray/DiceTray.js
+++ b/src/dice/diceTray/DiceTray.js
@@ -1,4 +1,7 @@
-import * as BABYLON from "babylonjs";
+import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader";
+import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial";
+import { PhysicsImpostor } from "@babylonjs/core/Physics/physicsImpostor";
+import { Mesh } from "@babylonjs/core/Meshes/mesh";
import singleMeshSource from "./single.glb";
import doubleMeshSource from "./double.glb";
@@ -55,19 +58,19 @@ class DiceTray {
}
createCollision(name, x, y, z, friction) {
- let collision = BABYLON.Mesh.CreateBox(
+ let collision = Mesh.CreateBox(
name,
this.collisionSize,
this.scene,
true,
- BABYLON.Mesh.DOUBLESIDE
+ Mesh.DOUBLESIDE
);
collision.position.x = x;
collision.position.y = y;
collision.position.z = z;
- collision.physicsImpostor = new BABYLON.PhysicsImpostor(
+ collision.physicsImpostor = new PhysicsImpostor(
collision,
- BABYLON.PhysicsImpostor.BoxImpostor,
+ PhysicsImpostor.BoxImpostor,
{ mass: 0, friction: friction },
this.scene
);
@@ -115,19 +118,11 @@ class DiceTray {
async loadMeshes() {
this.singleMesh = (
- await BABYLON.SceneLoader.ImportMeshAsync(
- "",
- singleMeshSource,
- "",
- this.scene
- )
+ await SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene)
).meshes[1];
this.singleMesh.id = "dice_tray_single";
this.singleMesh.name = "dice_tray";
- let singleMaterial = new BABYLON.PBRMaterial(
- "dice_tray_mat_single",
- this.scene
- );
+ let singleMaterial = new PBRMaterial("dice_tray_mat_single", this.scene);
singleMaterial.albedoTexture = await importTextureAsync(singleAlbedo);
singleMaterial.normalTexture = await importTextureAsync(singleNormal);
singleMaterial.metallicTexture = await importTextureAsync(
@@ -143,19 +138,11 @@ class DiceTray {
this.singleMesh.isVisible = this.size === "single";
this.doubleMesh = (
- await BABYLON.SceneLoader.ImportMeshAsync(
- "",
- doubleMeshSource,
- "",
- this.scene
- )
+ await SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene)
).meshes[1];
this.doubleMesh.id = "dice_tray_double";
this.doubleMesh.name = "dice_tray";
- let doubleMaterial = new BABYLON.PBRMaterial(
- "dice_tray_mat_double",
- this.scene
- );
+ let doubleMaterial = new PBRMaterial("dice_tray_mat_double", this.scene);
doubleMaterial.albedoTexture = await importTextureAsync(doubleAlbedo);
doubleMaterial.normalTexture = await importTextureAsync(doubleNormal);
doubleMaterial.metallicTexture = await importTextureAsync(
diff --git a/src/dice/gemstone/GemstoneDice.js b/src/dice/gemstone/GemstoneDice.js
index 347c489..fc20df7 100644
--- a/src/dice/gemstone/GemstoneDice.js
+++ b/src/dice/gemstone/GemstoneDice.js
@@ -1,4 +1,5 @@
-import * as BABYLON from "babylonjs";
+import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial";
+import { Color3 } from "@babylonjs/core/Maths/math";
import Dice from "../Dice";
@@ -18,7 +19,7 @@ class GemstoneDice extends Dice {
}
static async loadMaterial(materialName, textures, scene) {
- let pbr = new BABYLON.PBRMaterial(materialName, scene);
+ let pbr = new PBRMaterial(materialName, scene);
pbr.albedoTexture = await importTextureAsync(textures.albedo);
pbr.normalTexture = await importTextureAsync(textures.normal);
pbr.metallicTexture = await importTextureAsync(textures.metalRoughness);
@@ -30,7 +31,7 @@ class GemstoneDice extends Dice {
pbr.subSurface.translucencyIntensity = 1.0;
pbr.subSurface.minimumThickness = 5;
pbr.subSurface.maximumThickness = 10;
- pbr.subSurface.tintColor = new BABYLON.Color3(190 / 255, 0, 220 / 255);
+ pbr.subSurface.tintColor = new Color3(190 / 255, 0, 220 / 255);
return pbr;
}
diff --git a/src/dice/glass/GlassDice.js b/src/dice/glass/GlassDice.js
index 50de8f3..2631f53 100644
--- a/src/dice/glass/GlassDice.js
+++ b/src/dice/glass/GlassDice.js
@@ -1,4 +1,5 @@
-import * as BABYLON from "babylonjs";
+import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial";
+import { Color3 } from "@babylonjs/core/Maths/math";
import Dice from "../Dice";
@@ -18,7 +19,7 @@ class GlassDice extends Dice {
}
static async loadMaterial(materialName, textures, scene) {
- let pbr = new BABYLON.PBRMaterial(materialName, scene);
+ let pbr = new PBRMaterial(materialName, scene);
pbr.albedoTexture = await importTextureAsync(textures.albedo);
pbr.normalTexture = await importTextureAsync(textures.normal);
pbr.roughness = 0.25;
@@ -30,7 +31,7 @@ class GlassDice extends Dice {
pbr.subSurface.translucencyIntensity = 2.5;
pbr.subSurface.minimumThickness = 10;
pbr.subSurface.maximumThickness = 10;
- pbr.subSurface.tintColor = new BABYLON.Color3(43 / 255, 1, 115 / 255);
+ pbr.subSurface.tintColor = new Color3(43 / 255, 1, 115 / 255);
pbr.subSurface.thicknessTexture = await importTextureAsync(textures.mask);
pbr.subSurface.useMaskFromThicknessTexture = true;
diff --git a/src/docs/assets/CustomMapsAdvanced.jpg b/src/docs/assets/CustomMapsAdvanced.jpg
index 63562cd..7cad55c 100644
Binary files a/src/docs/assets/CustomMapsAdvanced.jpg and b/src/docs/assets/CustomMapsAdvanced.jpg differ
diff --git a/src/docs/assets/DiceRolling.mp4 b/src/docs/assets/DiceRolling.mp4
index 94b9711..d48a7ba 100644
Binary files a/src/docs/assets/DiceRolling.mp4 and b/src/docs/assets/DiceRolling.mp4 differ
diff --git a/src/docs/assets/DiceSharing.mp4 b/src/docs/assets/DiceSharing.mp4
new file mode 100644
index 0000000..d1019fb
Binary files /dev/null and b/src/docs/assets/DiceSharing.mp4 differ
diff --git a/src/docs/assets/DiceStyles.mp4 b/src/docs/assets/DiceStyles.mp4
index d612302..3dadece 100644
Binary files a/src/docs/assets/DiceStyles.mp4 and b/src/docs/assets/DiceStyles.mp4 differ
diff --git a/src/docs/assets/DiceTraySize.mp4 b/src/docs/assets/DiceTraySize.mp4
index ab17ef4..385b6b8 100644
Binary files a/src/docs/assets/DiceTraySize.mp4 and b/src/docs/assets/DiceTraySize.mp4 differ
diff --git a/src/docs/assets/OpenDiceTray.mp4 b/src/docs/assets/OpenDiceTray.mp4
index 17baf1c..3571528 100644
Binary files a/src/docs/assets/OpenDiceTray.mp4 and b/src/docs/assets/OpenDiceTray.mp4 differ
diff --git a/src/docs/assets/UsingFog.mp4 b/src/docs/assets/UsingFog.mp4
index 2012c9a..53cbc5d 100644
Binary files a/src/docs/assets/UsingFog.mp4 and b/src/docs/assets/UsingFog.mp4 differ
diff --git a/src/docs/assets/UsingMeasure.mp4 b/src/docs/assets/UsingMeasure.mp4
index e59b0e3..321d314 100644
Binary files a/src/docs/assets/UsingMeasure.mp4 and b/src/docs/assets/UsingMeasure.mp4 differ
diff --git a/src/docs/assets/UsingPointer.mp4 b/src/docs/assets/UsingPointer.mp4
new file mode 100644
index 0000000..d0b76ea
Binary files /dev/null and b/src/docs/assets/UsingPointer.mp4 differ
diff --git a/src/docs/assets/UsingTimer.mp4 b/src/docs/assets/UsingTimer.mp4
new file mode 100644
index 0000000..c06ab7e
Binary files /dev/null and b/src/docs/assets/UsingTimer.mp4 differ
diff --git a/src/docs/assets/index.js b/src/docs/assets/index.js
index 5e207b7..62f7e6e 100644
--- a/src/docs/assets/index.js
+++ b/src/docs/assets/index.js
@@ -18,6 +18,9 @@ import addPartyMember from "./AddPartyMember.mp4";
import changeNickname from "./ChangeNickname.mp4";
import sharingAudio from "./SharingAudio.mp4";
import startGame from "./StartGame.mp4";
+import diceSharing from "./DiceSharing.mp4";
+import usingTimer from "./UsingTimer.mp4";
+import usingPointer from "./UsingPointer.mp4";
export default {
defaultMaps,
@@ -40,4 +43,7 @@ export default {
changeNickname,
sharingAudio,
startGame,
+ diceSharing,
+ usingTimer,
+ usingPointer,
};
diff --git a/src/docs/faq/saving.md b/src/docs/faq/saving.md
index 14520e5..7190cbe 100644
--- a/src/docs/faq/saving.md
+++ b/src/docs/faq/saving.md
@@ -2,4 +2,4 @@
### Database is disabled.
-Owlbear Rodeo uses a local database to store saved data. If you are seeing a database is disabled message this usually means you have data storage disabled. The most common occurances of this is if you are using Private Browsing modes or in Firefox have the Never Remember History option enabled. The site will still function in these cases however all data will be lost when the page closes or reloads.
+Owlbear Rodeo uses a local database to store saved data. If you are seeing a database is disabled message this usually means you have data storage disabled. The most common occurrences of this is if you are using Private Browsing modes or in Firefox have the Never Remember History option enabled. The site will still function in these cases however all data will be lost when the page closes or reloads.
diff --git a/src/docs/howTo/sharingAudio.md b/src/docs/howTo/sharingAudio.md
index 0512e27..3c918d5 100644
--- a/src/docs/howTo/sharingAudio.md
+++ b/src/docs/howTo/sharingAudio.md
@@ -1,10 +1,10 @@
-While using Owlbear Rodeo you can share your computers audio to other party memebers.
+While using Owlbear Rodeo you can share your computer's audio to other party members.
To accomplish this Owlbear Rodeo uses the audio portion of a browsers screen share support. This means that sharing audio relies on a browser that supports this functionality (currently this is Google Chrome and the new Microsoft Edge). What kind of audio you can share depends on the operating system you are using. Currently Google Chrome on Windows allows you to share the audio of any tab or an entire screen while on MacOS you can only share the audio of a tab. The limited support is why this feature is marked as experimental.
`Note: Even though sharing audio requires a supported browser, receiving audio works on all browsers`
-To use audio sharing click the Start Radio Stream button in the bottom left to open the Radio Screen then click Start Radio. The browser will then ask to share your screen, click on the Chrome Tab option and select your tab. Insure to select the Share Audio Checkbox and finally click Share.
+To use audio sharing click the Start Radio Stream button in the bottom left to open the Radio Screen then click Start Radio. The browser will then ask to share your screen, click on the Chrome Tab option and select your tab. Ensure to select the Share Audio Checkbox and finally click Share.
![Sharing Audio](sharingAudio)
diff --git a/src/docs/howTo/sharingMaps.md b/src/docs/howTo/sharingMaps.md
index 4c18de2..6dd767e 100644
--- a/src/docs/howTo/sharingMaps.md
+++ b/src/docs/howTo/sharingMaps.md
@@ -20,7 +20,7 @@ To do this open the Map Select Screen and then either click the Add Map button i
Once a custom map has been added you must configure the size of the map.
-To do this there is the Column and Row properties. Columns represents how many grid cells your map has in the horizontal direction while Rows represents the amount of cells in the vertical direction.
+To do this there are the Column and Row properties. Columns represent how many grid cells your map has in the horizontal direction while Rows represents the amount of cells in the vertical direction.
`Tip: Owlbear Rodeo can automatically fill the Column and Row properties for you if you include them in the file name of the uploaded map. E.g. River [10x15] will create a map named River with 10 columns and 15 rows`
@@ -37,7 +37,8 @@ A brief summary of these settings is listed below.
- Name: The name of the map shown in the Map Select Screen.
- Grid Type: Change the type of grid to use for the map. Currently only the Square type is supported however Hex will be added in a future release.
-- Show Grid: When enabled Owlbear Rodeo will draw a grid on top of your map, this is useful if a custom map you have uploaded does't include a grid.
+- Show Grid: When enabled Owlbear Rodeo will draw a grid on top of your map, this is useful if a custom map you have uploaded doesn't include a grid.
+- Snap to Grid: When enabled tokens, drawing, fog and measurements will attempt to snap to the grid.
- Quality: When uploading a map Owlbear Rodeo will automatically generate various quality options, selecting a lower quality may help speed up map sending in resource constrained environments.
- Allow others to edit: These properties control what other party members can edit when viewing your map.
- Fog: Controls whether others can edit the maps fog (default disabled).
diff --git a/src/docs/howTo/usingDice.md b/src/docs/howTo/usingDice.md
index ceee00f..4260b92 100644
--- a/src/docs/howTo/usingDice.md
+++ b/src/docs/howTo/usingDice.md
@@ -20,15 +20,21 @@ To reroll all your dice you can click the Reroll Dice icon in the bottom right o
To clear the dice in your dice tray you can click the Clear Dice button in the bottom left of the dice tray.
+## Sharing Dice Rolls
+
+To share dice rolls with other party members you can click the Share Dice Rolls button when the dice tray is open. Others can then see your dice roll total as well as a breakdown of each dice rolled updated in real time.
+
+![Dice Sharing](diceSharing)
+
## Styling Your Dice
Owlbear Rodeo has a bunch of varying dice styles to choose from.
-To change your dice style click Select Dice Style button in the top left of the dice tray.
+To change your dice style click the Select Dice Style button in the top left of the dice tray.
![Dice Styles](diceStyles)
## Expanding Your Dice Tray
-The dice tray comes in two different sizes to change the size click the Expand Dice Tray button in the top right of the dice tray.
+The dice tray comes in two different sizes. To change the size click the Expand Dice Tray button in the top right of the dice tray.
![Dice Tray Size](diceTraySize)
diff --git a/src/docs/howTo/usingFog.md b/src/docs/howTo/usingFog.md
index 475b8e6..79fc896 100644
--- a/src/docs/howTo/usingFog.md
+++ b/src/docs/howTo/usingFog.md
@@ -4,7 +4,7 @@ The Fog Tool allows you to add hidden areas to control what the other party memb
![Using Fog](usingFog)
-`Note: When using the Fog Tool the fog will be transparent, this is to make it easier to align your fog with the map below. When the Fog Tool is no longer in use the fog will be opaque. This extends to other party members meaning if others are denied fog editing permissions they can not see under the fog until you reveal what's underneath.`
+`Note: If it is your map the fog will be transparent for you. This is to make it easier to edit the fog and also allows you to move any tokens that are underneath the fog. For other members (who don't have edit permissions for fog) the fog will be opaque. If you want to preview what other people will see you can enable the Fog Preview option in the fog tool settings.`
A summary of the Fog Tool options are listed below.
@@ -17,5 +17,6 @@ A summary of the Fog Tool options are listed below.
| Add Fog | When selected drawing a fog shape will add it to the scene | Alt (Toggle) |
| Subtract Fog | When selected drawing a fog shape will subtract it from other shapes | Alt (Toggle) |
| Edge Snapping | Enables/Disables edge snapping | S |
+| Fog Preview | Enables/Disables a preview of the final fog shapes | F |
| Undo | Undo a fog action | Ctrl + Z |
| Redo | Redo a fog action | Ctrl + Shift + Z |
diff --git a/src/docs/howTo/usingMeasure.md b/src/docs/howTo/usingMeasure.md
index 5e0eaca..8334909 100644
--- a/src/docs/howTo/usingMeasure.md
+++ b/src/docs/howTo/usingMeasure.md
@@ -6,6 +6,7 @@ A summary of the Measure Tool options are listed below.
| Option | Description | Shortcut |
| ------------------- | ---------------------------------------------------------------------------------- | -------- |
-| Grid Distance | This is the distance on a grid and is the metric use in D&D | G |
+| Grid Distance | This is the distance on a grid and is the metric used in D&D | G |
| Line Distance | This is the actual distance between the two points of the measure tool | L |
| City Block Distance | This is the distance when only travelling in the horizontal or vertical directions | C |
+| Scale | This allows you to enter a custom scale and unit value to apply | |
diff --git a/src/docs/howTo/usingPointer.md b/src/docs/howTo/usingPointer.md
new file mode 100644
index 0000000..c252dee
--- /dev/null
+++ b/src/docs/howTo/usingPointer.md
@@ -0,0 +1,3 @@
+The pointer tool allows you to temporarily highlight parts of the map for other members to see.
+
+![Using Pointer](usingPointer)
diff --git a/src/docs/howTo/usingTimer.md b/src/docs/howTo/usingTimer.md
new file mode 100644
index 0000000..dff5aa9
--- /dev/null
+++ b/src/docs/howTo/usingTimer.md
@@ -0,0 +1,3 @@
+The countdown timer allows you to run timed encounters. When clicking the Start Timer button you can set the duration of the timer and simply click the Start button to begin a timer that is shared between all party members.
+
+![Using Timer](usingTimer)
diff --git a/src/docs/howTo/usingTokens.md b/src/docs/howTo/usingTokens.md
index 7e27f95..ef34459 100644
--- a/src/docs/howTo/usingTokens.md
+++ b/src/docs/howTo/usingTokens.md
@@ -41,13 +41,13 @@ Once a token has been uploaded you can adjust the default size that is used when
`Note: The size input for a non-square image represents the number of grid cells a token takes up on the horizontal axis. The number of cells in the vertical axis is determined by the aspect ratio of the uploaded image.`
-`Tip: Owlbear Rodeo has full transparancy support for tokens. This means that players can only interact with visible parts of a token so feel free to upload creatures that might have large extended areas like wings.`
+`Tip: Owlbear Rodeo has full transparency support for tokens. This means that players can only interact with visible parts of a token so feel free to upload creatures that might have large extended areas like wings.`
## Custom Tokens (Advanced)
When uploading a custom token there are a couple of more advanced options that may come in handy.
-To get access to these settings select the desired token in the Edit Token Screen and click the Show More Button under the Deafult Size Input.
+To get access to these settings select the desired token in the Edit Token Screen and click the Show More Button under the Default Size Input.
![Custom Tokens Advanced](customTokensAdvanced)
diff --git a/src/docs/releaseNotes/v1.2.0.md b/src/docs/releaseNotes/v1.2.0.md
index 010c388..d15dea3 100644
--- a/src/docs/releaseNotes/v1.2.0.md
+++ b/src/docs/releaseNotes/v1.2.0.md
@@ -5,7 +5,7 @@
### Saved Maps
Added the ability to load and save multiple maps also added a selection of default maps.
-The map upload modal now shows a grid of maps at first this will be the new set of default maps (Blank, Grass, Sand, Stone, Water and Wood) but when you add your own they will show here as well.
+The map upload modal now shows a grid of maps. At first this will be the new set of default maps (Blank, Grass, Sand, Stone, Water and Wood) but when you add your own they will show here as well.
### Edit Permissions
@@ -26,7 +26,7 @@ First fog and drawing have been separated into three tools. The first is the fog
The second tool is a new brush tool. The main update to this tool is a new line only drawing option which is good for making plans or annotations. The auto-shape detection has been removed in favour of the third tool.
-The third tool is a new shape tool, hopefully this should be more precise then the auto-shape option from v1.1. Rectangles, circles and triangles can be created by selecting a point and dragging outwards to create the required shape. While drawing shapes they will snap to the grid to make it easier draw area of effect spells.
+The third tool is a new shape tool, hopefully this should be more precise then the auto-shape option from v1.1. Rectangles, circles and triangles can be created by selecting a point and dragging outwards to create the required shape. While drawing shapes they will snap to the grid to make it easier to draw area of effect spells.
## Minor Changes
diff --git a/src/docs/releaseNotes/v1.3.0.md b/src/docs/releaseNotes/v1.3.0.md
index 836902e..f6d0c44 100644
--- a/src/docs/releaseNotes/v1.3.0.md
+++ b/src/docs/releaseNotes/v1.3.0.md
@@ -11,7 +11,7 @@ Added a physically simulated dice tray and dice with a bunch of features.
- Automatic dice total and roll breakdown.
- Physically based rendering for beautiful metal, plastic, glass, wood and stone dice.
- Two dice tray sizes, a small one for rolling while still seeing the map or a large one for when you have a lot of dice to roll.
-- Intelligent renderering to allow the dice to only be drawn when there is movement, this allows there to be almost zero cost on battery life while the dice are inactive.
+- Intelligent rendering to allow the dice to only be drawn when there is movement, this allows there to be almost zero cost on battery life while the dice are inactive.
### Custom and New Default Tokens
diff --git a/src/docs/releaseNotes/v1.3.1.md b/src/docs/releaseNotes/v1.3.1.md
index a3ac353..381689c 100644
--- a/src/docs/releaseNotes/v1.3.1.md
+++ b/src/docs/releaseNotes/v1.3.1.md
@@ -1,7 +1,7 @@
## Minor Changes
- Fixed a bug where tokens that were placed on the map then removed from the token select screen could no longer be deleted from the map.
-- Fixed a bug where fog drawing couldn't be undone if there the last fog shape was deleted.
+- Fixed a bug where fog drawing couldn't be undone if the last fog shape was deleted.
- Added the ability to add multiple new maps or tokens at the same time.
- Added a Show Grid option for maps that will overlay a grid on the map. This can be useful for when you have a map with no grid or you want to verify your current grid settings.
- Added the ability to erase multiple shapes at a time by dragging over a shape with the eraser tool. This works for fog erase and toggle as well.
diff --git a/src/docs/releaseNotes/v1.3.3.md b/src/docs/releaseNotes/v1.3.3.md
index 6f4a160..a6280f7 100644
--- a/src/docs/releaseNotes/v1.3.3.md
+++ b/src/docs/releaseNotes/v1.3.3.md
@@ -1,7 +1,7 @@
## Minor Changes
- Fixed a bug that would cause the game to crash when a player would lose internet connection.
-- Added an automatic reconnection feature for when internet connection is lost. This should also help when players put the site into the background and try and return to the game later.
+- Added an automatic reconnection feature for when internet connection is lost. This should also help when players put the site into the background and try to return to the game later.
[Reddit](https://www.reddit.com/r/OwlbearRodeo/comments/ha301n/beta_v133_release_bug_fix_and_auto_reconnect/)
[Twitter](https://twitter.com/OwlbearRodeo/status/1272868031014727680?s=20)
diff --git a/src/docs/releaseNotes/v1.4.0.md b/src/docs/releaseNotes/v1.4.0.md
index aaffc62..d3a8960 100644
--- a/src/docs/releaseNotes/v1.4.0.md
+++ b/src/docs/releaseNotes/v1.4.0.md
@@ -13,7 +13,7 @@ The fog tool now has more options for supporting different interaction paradigms
A new measure tool has been added to allow you to easily find out how far areas of the map are from one another. This tool has three options for calculating distance.
-- Grid Distance (default) or Chebyshev distance, this is the distance on a grid and is the metric use in D&D.
+- Grid Distance (default) or Chebyshev distance, this is the distance on a grid and is the metric used in D&D.
- Line Distance or Euclidean distance is the actual distance between the two points of the measure tool.
- City Block Distance or Manhattan distance is the distance when only travelling in the horizontal or vertical directions.
@@ -60,12 +60,12 @@ When interacting with the map we now support keyboard shortcuts for quickly swit
## Minor Changes
-- The brush tool, shape tool and erase tool have been combined in to one drawing tool. Having these tools combined should hopefully make the drawing experience a little simpler.
-- Added a new line tool that will allow you to draw staight lines.
+- The brush tool, shape tool and erase tool have been combined into one drawing tool. Having these tools combined should hopefully make the drawing experience a little simpler.
+- Added a new line tool that will allow you to draw straight lines.
- Fixed performance regression for drawing tools that was introduced in v1.3.0.
- Fixed performance issues when editing map and token settings.
- Added a notification for when a user can connect to the server but not to other party members.
-- Fixed a bug that lead to a token getting stuck to the cursor when moving.
+- Fixed a bug that led to a token getting stuck to the cursor when moving.
- Added a new loading indicator showing the progress of map and tokens when downloading them from other party members.
- Fixed a bug that stopped the undo and redo buttons for fog editing being synced to other party members.
diff --git a/src/docs/releaseNotes/v1.4.2.md b/src/docs/releaseNotes/v1.4.2.md
index ed17be7..6c5666b 100644
--- a/src/docs/releaseNotes/v1.4.2.md
+++ b/src/docs/releaseNotes/v1.4.2.md
@@ -9,7 +9,7 @@ A lot of the underlying network code was changed to support this so there may st
Along with this there are a few bug fixes and small enhancements as well:
-- Fixed a bug that caused the drawing tools to dissapear on smaller screens when changing tools.
+- Fixed a bug that caused the drawing tools to disappear on smaller screens when changing tools.
- Fixed keyboard shortcuts not working when interacting with elements other than the map.
- Added a volume slider for audio sharing on platforms that support controlling audio.
- Added a better function for determining which tokens are sitting on a token with the Vehicle / Mount option set.
diff --git a/src/docs/releaseNotes/v1.5.0.md b/src/docs/releaseNotes/v1.5.0.md
new file mode 100644
index 0000000..ec4f17a
--- /dev/null
+++ b/src/docs/releaseNotes/v1.5.0.md
@@ -0,0 +1,43 @@
+[embed:](https://www.youtube.com/embed/i4JvZboAPhQ)
+
+## Major Changes
+
+### Pointer Tool
+
+Use a virtual laser pointer with the new pointer tool. With the tool selected simply click and drag and all party members will be able to see your pointer. This should be helpful for when you want to quickly draw the attention of the other players to a specific part of the map or token.
+
+### Countdown Timer
+
+A new countdown timer tool allows you to create timed encounters. Whether it's for a room filling with water or a crumbling tower about to collapse, the timer tool allows you to start a countdown that all players can see.
+
+### Dice Sharing
+
+A lot of changes have gone into the virtual dice this release.
+
+The major two are:
+
+1. There is a new share dice option that will show both your dice total but also a breakdown of all your rolled dice to each party member.
+2. The UI to add dice is now to the side of the dice tray and displayed vertically which allows the dice UI to now scale across all screen sizes.
+
+## Minor Changes
+
+- The measure tool now has a scale option that allows you to add a scale and unit to the measurement values. The default is 5ft which will set each grid cell size to 5ft. This is then taken into account when taking measurements.
+- For party members that are allowed to edit the fog it is now transparent while using other tools. This means GMs can always see what's under the fog now and can even move tokens that are hidden by fog. If you wish to get a preview of what other players will see there is a new option in the fog tool called Show Preview that can be enabled or disabled at any time.
+- Fixed the spelling for the default rogue token.
+- Fixed a bug that caused the dice throwing to not work if the dice was stationary for too long on Windows.
+- Fixed a bug that caused the close button of the map select screen to select a map anyway.
+- Added the ability to deselect maps in the map select screen by clicking outside of a map tile.
+- Added a snap to grid option in the advanced map settings. Disabling this will disable all grid snapping features for a map.
+- Added grid snapping for tokens. I was planning to add this when hex grid support was added but I liked it enough to add it now. For those who use hex grids you can disable the new snap to grid option to get the old functionality.
+- Number inputs now behave better when typing by removing the prepended 0.
+- Fog and drawing shapes now use different line endings which fixes issues with overhangs on three sided shapes.
+- Tool settings are now saved across page refreshes this includes things like the shape tool colour, measurement units and also selected dice style.
+- Fixed a bug that would stop map transforms from resetting when changing to a new map.
+- Fixed a bug that caused the state of default maps to be overridden when joining another person's game.
+
+[Reddit]()
+[Twitter]()
+
+---
+
+Aug 11 2020
diff --git a/src/helpers/Settings.js b/src/helpers/Settings.js
new file mode 100644
index 0000000..d9ac9da
--- /dev/null
+++ b/src/helpers/Settings.js
@@ -0,0 +1,43 @@
+/**
+ * An interface to a local storage back settings store with a versioning mechanism
+ */
+class Settings {
+ name;
+ currentVersion;
+
+ constructor(name) {
+ this.name = name;
+ this.currentVersion = this.get("__version");
+ }
+
+ version(versionNumber, upgradeFunction) {
+ if (versionNumber > this.currentVersion) {
+ this.currentVersion = versionNumber;
+ this.setAll(upgradeFunction(this.getAll()));
+ }
+ }
+
+ getAll() {
+ return JSON.parse(localStorage.getItem(this.name));
+ }
+
+ get(key) {
+ const settings = this.getAll();
+ return settings && settings[key];
+ }
+
+ setAll(newSettings) {
+ localStorage.setItem(
+ this.name,
+ JSON.stringify({ ...newSettings, __version: this.currentVersion })
+ );
+ }
+
+ set(key, value) {
+ let settings = this.getAll();
+ settings[key] = value;
+ this.setAll(settings);
+ }
+}
+
+export default Settings;
diff --git a/src/helpers/babylon.js b/src/helpers/babylon.js
index 82d735b..a70c79c 100644
--- a/src/helpers/babylon.js
+++ b/src/helpers/babylon.js
@@ -1,9 +1,9 @@
-import * as BABYLON from "babylonjs";
+import { Texture } from "@babylonjs/core/Materials/Textures/texture";
// Turn texture load into an async function so it can be awaited
export async function importTextureAsync(url) {
return new Promise((resolve, reject) => {
- let texture = new BABYLON.Texture(
+ let texture = new Texture(
url,
null,
undefined,
diff --git a/src/helpers/dice.js b/src/helpers/dice.js
new file mode 100644
index 0000000..d30cab8
--- /dev/null
+++ b/src/helpers/dice.js
@@ -0,0 +1,53 @@
+import { Vector3 } from "@babylonjs/core/Maths/math";
+
+/**
+ * Find the number facing up on a mesh instance of a dice
+ * @param {Object} instance The dice instance
+ */
+export function getDiceInstanceRoll(instance) {
+ let highestDot = -1;
+ let highestLocator;
+ for (let locator of instance.getChildTransformNodes()) {
+ let dif = locator
+ .getAbsolutePosition()
+ .subtract(instance.getAbsolutePosition());
+ let direction = dif.normalize();
+ const dot = Vector3.Dot(direction, Vector3.Up());
+ if (dot > highestDot) {
+ highestDot = dot;
+ highestLocator = locator;
+ }
+ }
+ return parseInt(highestLocator.name.slice(12));
+}
+
+/**
+ * Find the number facing up on a dice object
+ * @param {Object} dice The Dice object
+ */
+export function getDiceRoll(dice) {
+ let number = getDiceInstanceRoll(dice.instance);
+ // If the dice is a d100 add the d10
+ if (dice.type === "d100") {
+ const d10Number = getDiceInstanceRoll(dice.d10Instance);
+ // Both zero set to 100
+ if (d10Number === 0 && number === 0) {
+ number = 100;
+ } else {
+ number += d10Number;
+ }
+ } else if (dice.type === "d10" && number === 0) {
+ number = 10;
+ }
+ return { type: dice.type, roll: number };
+}
+
+export function getDiceRollTotal(diceRolls) {
+ return diceRolls.reduce((accumulator, dice) => {
+ if (dice.roll === "unknown") {
+ return accumulator;
+ } else {
+ return accumulator + dice.roll;
+ }
+ }, 0);
+}
diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js
index 363bac2..5c5c4d8 100644
--- a/src/helpers/drawing.js
+++ b/src/helpers/drawing.js
@@ -6,6 +6,7 @@ import { toDegrees, omit } from "./shared";
const snappingThreshold = 1 / 5;
export function getBrushPositionForTool(
+ map,
brushPosition,
tool,
toolSettings,
@@ -14,12 +15,13 @@ export function getBrushPositionForTool(
) {
let position = brushPosition;
const useGridSnappning =
- (tool === "drawing" &&
+ map.snapToGrid &&
+ ((tool === "drawing" &&
(toolSettings.type === "line" ||
toolSettings.type === "rectangle" ||
toolSettings.type === "circle" ||
toolSettings.type === "triangle")) ||
- (tool === "fog" && toolSettings.type === "polygon");
+ (tool === "fog" && toolSettings.type === "polygon"));
if (useGridSnappning) {
// Snap to corners of grid
diff --git a/src/helpers/konva.js b/src/helpers/konva.js
index 83da4dd..bfa602d 100644
--- a/src/helpers/konva.js
+++ b/src/helpers/konva.js
@@ -1,5 +1,7 @@
-import React, { useState } from "react";
+import React, { useState, useEffect, useRef } from "react";
import { Line, Group, Path, Circle } from "react-konva";
+import { lerp } from "./shared";
+import * as Vector2 from "./vector2";
// Holes should be wound in the opposite direction as the containing points array
export function HoleyLine({ holes, ...props }) {
@@ -140,6 +142,93 @@ export function Tick({ x, y, scale, onClick, cross }) {
);
}
+export function Trail({ position, size, duration, segments }) {
+ const trailRef = useRef();
+ const pointsRef = useRef([]);
+ const prevPositionRef = useRef(position);
+ // Add a new point every time position is changed
+ useEffect(() => {
+ if (Vector2.compare(position, prevPositionRef.current, 0.0001)) {
+ return;
+ }
+ pointsRef.current.push({ ...position, lifetime: duration });
+ prevPositionRef.current = position;
+ }, [position, duration]);
+
+ // Advance lifetime of trail
+ useEffect(() => {
+ let prevTime = performance.now();
+ let request = requestAnimationFrame(animate);
+ function animate(time) {
+ request = requestAnimationFrame(animate);
+ const deltaTime = time - prevTime;
+ prevTime = time;
+
+ if (pointsRef.current.length === 0) {
+ return;
+ }
+
+ let expired = 0;
+ for (let point of pointsRef.current) {
+ point.lifetime -= deltaTime;
+ if (point.lifetime < 0) {
+ expired++;
+ }
+ }
+ if (expired > 0) {
+ pointsRef.current = pointsRef.current.slice(expired);
+ }
+ if (trailRef.current) {
+ trailRef.current.getLayer().draw();
+ }
+ }
+
+ return () => {
+ cancelAnimationFrame(request);
+ };
+ }, []);
+
+ // Custom scene function for drawing a trail from a line
+ function sceneFunc(context) {
+ // Resample points to ensure a smooth trail
+ const resampledPoints = Vector2.resample(pointsRef.current, segments);
+ for (let i = 1; i < resampledPoints.length; i++) {
+ const from = resampledPoints[i - 1];
+ const to = resampledPoints[i];
+ const alpha = i / resampledPoints.length;
+ context.beginPath();
+ context.lineJoin = "round";
+ context.lineCap = "round";
+ context.lineWidth = alpha * size;
+ context.strokeStyle = `hsl(0, 63%, ${lerp(90, 50, alpha)}%)`;
+ context.moveTo(from.x, from.y);
+ context.lineTo(to.x, to.y);
+ context.stroke();
+ context.closePath();
+ }
+ }
+
+ return (
+
+
+
+
+ );
+}
+
+Trail.defaultProps = {
+ // Duration of each point in milliseconds
+ duration: 200,
+ // Number of segments in the trail, resampled from the points
+ segments: 20,
+};
+
export function getRelativePointerPosition(node) {
let transform = node.getAbsoluteTransform().copy();
transform.invert();
diff --git a/src/helpers/timer.js b/src/helpers/timer.js
new file mode 100644
index 0000000..bcbe425
--- /dev/null
+++ b/src/helpers/timer.js
@@ -0,0 +1,33 @@
+const MILLISECONDS_IN_HOUR = 3600000;
+const MILLISECONDS_IN_MINUTE = 60000;
+const MILLISECONDS_IN_SECOND = 1000;
+
+/**
+ * Returns a timers duration in milliseconds
+ * @param {Object} t The object with an hour, minute and second property
+ */
+export function getHMSDuration(t) {
+ if (!t) {
+ return 0;
+ }
+ return (
+ t.hour * MILLISECONDS_IN_HOUR +
+ t.minute * MILLISECONDS_IN_MINUTE +
+ t.second * MILLISECONDS_IN_SECOND
+ );
+}
+
+/**
+ * Returns an object with an hour, minute and second property
+ * @param {number} duration The duration in milliseconds
+ */
+export function getDurationHMS(duration) {
+ let workingDuration = duration;
+ const hour = Math.floor(workingDuration / MILLISECONDS_IN_HOUR);
+ workingDuration -= hour * MILLISECONDS_IN_HOUR;
+ const minute = Math.floor(workingDuration / MILLISECONDS_IN_MINUTE);
+ workingDuration -= minute * MILLISECONDS_IN_MINUTE;
+ const second = Math.floor(workingDuration / MILLISECONDS_IN_SECOND);
+
+ return { hour, minute, second };
+}
diff --git a/src/helpers/useSetting.js b/src/helpers/useSetting.js
new file mode 100644
index 0000000..ca38152
--- /dev/null
+++ b/src/helpers/useSetting.js
@@ -0,0 +1,26 @@
+import { useContext } from "react";
+
+import get from "lodash.get";
+import set from "lodash.set";
+
+import SettingsContext from "../contexts/SettingsContext";
+
+/**
+ * Helper to get and set nested settings that are saved in local storage
+ * @param {String} path The path to the setting within the Settings object provided by the SettingsContext
+ */
+function useSetting(path) {
+ const { settings, setSettings } = useContext(SettingsContext);
+
+ const setting = get(settings, path);
+
+ const setSetting = (value) =>
+ setSettings((prev) => {
+ const updated = set({ ...prev }, path, value);
+ return updated;
+ });
+
+ return [setting, setSetting];
+}
+
+export default useSetting;
diff --git a/src/helpers/vector2.js b/src/helpers/vector2.js
index de79f9b..7697b9b 100644
--- a/src/helpers/vector2.js
+++ b/src/helpers/vector2.js
@@ -1,4 +1,8 @@
-import { toRadians, roundTo as roundToNumber } from "./shared";
+import {
+ toRadians,
+ roundTo as roundToNumber,
+ lerp as lerpNumber,
+} from "./shared";
export function lengthSquared(p) {
return p.x * p.x + p.y * p.y;
@@ -238,3 +242,55 @@ export function distance(a, b, type) {
return length(subtract(a, b));
}
}
+
+export function lerp(a, b, alpha) {
+ return { x: lerpNumber(a.x, b.x, alpha), y: lerpNumber(a.y, b.y, alpha) };
+}
+
+/**
+ * Returns total length of a an array of points treated as a path
+ * @param {Array} points the array of points in the path
+ */
+export function pathLength(points) {
+ let l = 0;
+ for (let i = 1; i < points.length; i++) {
+ l += distance(points[i - 1], points[i], "euclidean");
+ }
+ return l;
+}
+
+/**
+ * Resample a path to n number of evenly distributed points
+ * based off of http://depts.washington.edu/acelab/proj/dollar/index.html
+ * @param {Array} points the points to resample
+ * @param {number} n the number of new points
+ */
+export function resample(points, n) {
+ if (points.length === 0 || n <= 0) {
+ return [];
+ }
+ let localPoints = [...points];
+ const intervalLength = pathLength(localPoints) / (n - 1);
+ let resampledPoints = [localPoints[0]];
+ let currentDistance = 0;
+ for (let i = 1; i < localPoints.length; i++) {
+ let d = distance(localPoints[i - 1], localPoints[i], "euclidean");
+ if (currentDistance + d >= intervalLength) {
+ let newPoint = lerp(
+ localPoints[i - 1],
+ localPoints[i],
+ (intervalLength - currentDistance) / d
+ );
+ resampledPoints.push(newPoint);
+ localPoints.splice(i, 0, newPoint);
+ currentDistance = 0;
+ } else {
+ currentDistance += d;
+ }
+ }
+ if (resampledPoints.length === n - 1) {
+ resampledPoints.push(localPoints[localPoints.length - 1]);
+ }
+
+ return resampledPoints;
+}
diff --git a/src/icons/DiceRollsIcon.js b/src/icons/DiceRollsIcon.js
new file mode 100644
index 0000000..572a707
--- /dev/null
+++ b/src/icons/DiceRollsIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function DiceRollsIcon() {
+ return (
+
+ );
+}
+
+export default DiceRollsIcon;
diff --git a/src/icons/FogPreviewOffIcon.js b/src/icons/FogPreviewOffIcon.js
new file mode 100644
index 0000000..458c9d6
--- /dev/null
+++ b/src/icons/FogPreviewOffIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogPreviewOffIcon() {
+ return (
+
+ );
+}
+
+export default FogPreviewOffIcon;
diff --git a/src/icons/FogPreviewOnIcon.js b/src/icons/FogPreviewOnIcon.js
new file mode 100644
index 0000000..baacee1
--- /dev/null
+++ b/src/icons/FogPreviewOnIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogPreviewOnIcon() {
+ return (
+
+ );
+}
+
+export default FogPreviewOnIcon;
diff --git a/src/icons/PointerToolIcon.js b/src/icons/PointerToolIcon.js
new file mode 100644
index 0000000..1e7bf24
--- /dev/null
+++ b/src/icons/PointerToolIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function PointerToolIcon() {
+ return (
+
+ );
+}
+
+export default PointerToolIcon;
diff --git a/src/icons/ShareDiceOffIcon.js b/src/icons/ShareDiceOffIcon.js
new file mode 100644
index 0000000..8834ac4
--- /dev/null
+++ b/src/icons/ShareDiceOffIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ShareDiceOffIcon() {
+ return (
+
+ );
+}
+
+export default ShareDiceOffIcon;
diff --git a/src/icons/ShareDiceOnIcon.js b/src/icons/ShareDiceOnIcon.js
new file mode 100644
index 0000000..0f4401d
--- /dev/null
+++ b/src/icons/ShareDiceOnIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ShareDiceOnIcon() {
+ return (
+
+ );
+}
+
+export default ShareDiceOnIcon;
diff --git a/src/icons/StartTimerIcon.js b/src/icons/StartTimerIcon.js
new file mode 100644
index 0000000..f55517f
--- /dev/null
+++ b/src/icons/StartTimerIcon.js
@@ -0,0 +1,19 @@
+import React from "react";
+
+function StartTimerIcon() {
+ return (
+
+ );
+}
+
+export default StartTimerIcon;
diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js
index dac0603..def4c2b 100644
--- a/src/modals/SelectMapModal.js
+++ b/src/modals/SelectMapModal.js
@@ -22,6 +22,7 @@ const defaultMapProps = {
// TODO: add support for hex horizontal and hex vertical
gridType: "grid",
showGrid: false,
+ snapToGrid: true,
quality: "original",
};
@@ -184,7 +185,11 @@ function SelectMapModal({
async function handleMapSelect(map) {
await applyMapChanges();
- setSelectedMapId(map.id);
+ if (map) {
+ setSelectedMapId(map.id);
+ } else {
+ setSelectedMapId(null);
+ }
}
async function handleMapReset(id) {
@@ -195,6 +200,13 @@ function SelectMapModal({
}
}
+ async function handleClose() {
+ if (selectedMapId) {
+ await applyMapChanges();
+ }
+ onDone();
+ }
+
async function handleDone() {
if (imageLoading) {
return;
@@ -202,6 +214,8 @@ function SelectMapModal({
if (selectedMapId) {
await applyMapChanges();
onMapChange(selectedMapWithChanges, selectedMapStateWithChanges);
+ } else {
+ onMapChange(null, null);
}
onDone();
}
@@ -235,7 +249,15 @@ function SelectMapModal({
selectedMapId &&
(!isEmpty(mapSettingChanges) || !isEmpty(mapStateSettingChanges))
) {
- await updateMap(selectedMapId, mapSettingChanges);
+ // Ensure grid values are positive
+ let verifiedChanges = { ...mapSettingChanges };
+ if ("gridX" in verifiedChanges) {
+ verifiedChanges.gridX = verifiedChanges.gridX || 1;
+ }
+ if ("gridY" in verifiedChanges) {
+ verifiedChanges.gridY = verifiedChanges.gridY || 1;
+ }
+ await updateMap(selectedMapId, verifiedChanges);
await updateMapState(selectedMapId, mapStateSettingChanges);
setMapSettingChanges({});
@@ -250,7 +272,7 @@ function SelectMapModal({
};
return (
-
+
handleImagesUpload(event.target.files)}
diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js
index d6b3f14..0feea1b 100644
--- a/src/modals/SelectTokensModal.js
+++ b/src/modals/SelectTokensModal.js
@@ -112,7 +112,13 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
async function applyTokenChanges() {
if (selectedTokenId && !isEmpty(tokenSettingChanges)) {
- await updateToken(selectedTokenId, tokenSettingChanges);
+ // Ensure size value is positive
+ let verifiedChanges = { ...tokenSettingChanges };
+ if ("defaultSize" in verifiedChanges) {
+ verifiedChanges.defaultSize = verifiedChanges.defaultSize || 1;
+ }
+
+ await updateToken(selectedTokenId, verifiedChanges);
setTokenSettingChanges({});
}
}
diff --git a/src/modals/SettingsModal.js b/src/modals/SettingsModal.js
index 61a75ee..0a9316b 100644
--- a/src/modals/SettingsModal.js
+++ b/src/modals/SettingsModal.js
@@ -12,11 +12,15 @@ function SettingsModal({ isOpen, onRequestClose }) {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
async function handleEraseAllData() {
+ localStorage.clear();
await database.delete();
window.location.reload();
}
async function handleClearCache() {
+ // Clear saved settings
+ localStorage.clear();
+ // Clear map cache
await database.table("maps").where("owner").notEqual(userId).delete();
// Find all other peoples tokens who aren't benig used in a map state and delete them
const tokens = await database
diff --git a/src/modals/StartTimerModal.js b/src/modals/StartTimerModal.js
new file mode 100644
index 0000000..b8481d6
--- /dev/null
+++ b/src/modals/StartTimerModal.js
@@ -0,0 +1,127 @@
+import React, { useRef } from "react";
+import { Box, Label, Input, Button, Flex, Text } from "theme-ui";
+
+import Modal from "../components/Modal";
+
+import { getHMSDuration, getDurationHMS } from "../helpers/timer";
+
+import useSetting from "../helpers/useSetting";
+
+function StartTimerModal({
+ isOpen,
+ onRequestClose,
+ onTimerStart,
+ onTimerStop,
+ timer,
+}) {
+ const inputRef = useRef();
+ function focusInput() {
+ inputRef.current && inputRef.current.focus();
+ }
+
+ const [hour, setHour] = useSetting("timer.hour");
+ const [minute, setMinute] = useSetting("timer.minute");
+ const [second, setSecond] = useSetting("timer.second");
+
+ function handleSubmit(event) {
+ event.preventDefault();
+ if (timer) {
+ onTimerStop();
+ } else {
+ const duration = getHMSDuration({ hour, minute, second });
+ onTimerStart({ current: duration, max: duration });
+ }
+ }
+
+ const inputStyle = {
+ width: "70px",
+ border: "none",
+ ":focus": {
+ outline: "none",
+ },
+ fontSize: "32px",
+ padding: 2,
+ paddingLeft: 0,
+ };
+
+ function parseValue(value, max) {
+ const num = parseInt(value);
+ if (isNaN(num)) {
+ return 0;
+ }
+ return Math.min(num, max);
+ }
+
+ const timerHMS = timer && getDurationHMS(timer.current);
+
+ return (
+
+
+
+
+
+
+ H:
+
+ setHour(parseValue(e.target.value, 24))}
+ type="number"
+ disabled={timer}
+ min={0}
+ max={24}
+ />
+
+ M:
+
+ setMinute(parseValue(e.target.value, 59))}
+ type="number"
+ ref={inputRef}
+ disabled={timer}
+ min={0}
+ max={59}
+ />
+
+ S:
+
+ setSecond(parseValue(e.target.value, 59))}
+ type="number"
+ disabled={timer}
+ min={0}
+ max={59}
+ />
+
+
+
+
+
+
+
+ );
+}
+
+export default StartTimerModal;
diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js
index c3e99e4..9355c1e 100644
--- a/src/network/NetworkedMapAndTokens.js
+++ b/src/network/NetworkedMapAndTokens.js
@@ -427,6 +427,7 @@ function NetworkedMapAndTokens({ session }) {
allowFogDrawing={canEditFogDrawing}
allowMapChange={canChangeMap}
disabledTokens={disabledMapTokens}
+ session={session}
/>
>
diff --git a/src/network/NetworkedMapPointer.js b/src/network/NetworkedMapPointer.js
new file mode 100644
index 0000000..5cce3e4
--- /dev/null
+++ b/src/network/NetworkedMapPointer.js
@@ -0,0 +1,178 @@
+import React, { useState, useContext, useEffect, useRef } from "react";
+import { Group } from "react-konva";
+
+import AuthContext from "../contexts/AuthContext";
+
+import MapPointer from "../components/map/MapPointer";
+import { isEmpty } from "../helpers/shared";
+import { lerp } from "../helpers/vector2";
+
+// Send pointer updates every 33ms
+const sendTickRate = 33;
+
+function NetworkedMapPointer({ session, active, gridSize }) {
+ const { userId } = useContext(AuthContext);
+ const [pointerState, setPointerState] = useState({});
+ useEffect(() => {
+ if (userId && !(userId in pointerState)) {
+ setPointerState({
+ [userId]: { position: { x: 0, y: 0 }, visible: false, id: userId },
+ });
+ }
+ }, [userId, pointerState]);
+
+ const sessionRef = useRef(session);
+ useEffect(() => {
+ sessionRef.current = session;
+ }, [session]);
+
+ // Send pointer updates every sendTickRate to peers to save on bandwidth
+ // We use requestAnimationFrame as setInterval was being blocked during
+ // re-renders on Chrome with Windows
+ const ownPointerUpdateRef = useRef();
+ useEffect(() => {
+ let prevTime = performance.now();
+ let request = requestAnimationFrame(update);
+ let counter = 0;
+ function update(time) {
+ request = requestAnimationFrame(update);
+ const deltaTime = time - prevTime;
+ counter += deltaTime;
+ prevTime = time;
+
+ if (counter > sendTickRate) {
+ counter -= sendTickRate;
+ if (ownPointerUpdateRef.current && sessionRef.current) {
+ sessionRef.current.send("pointer", ownPointerUpdateRef.current);
+ ownPointerUpdateRef.current = null;
+ }
+ }
+ }
+
+ return () => {
+ cancelAnimationFrame(request);
+ };
+ }, []);
+
+ function updateOwnPointerState(position, visible) {
+ setPointerState((prev) => ({
+ ...prev,
+ [userId]: { position, visible, id: userId },
+ }));
+ ownPointerUpdateRef.current = { position, visible, id: userId };
+ }
+
+ function handleOwnPointerDown(position) {
+ updateOwnPointerState(position, true);
+ }
+
+ function handleOwnPointerMove(position) {
+ updateOwnPointerState(position, true);
+ }
+
+ function handleOwnPointerUp(position) {
+ updateOwnPointerState(position, false);
+ }
+
+ // Handle pointer data receive
+ const syncedPointerStateRef = useRef({});
+ useEffect(() => {
+ function handlePeerData({ id, data }) {
+ if (id === "pointer") {
+ // Setup an interpolation to the current pointer data when receiving a pointer event
+ if (syncedPointerStateRef.current[data.id]) {
+ const from = syncedPointerStateRef.current[data.id].to;
+ syncedPointerStateRef.current[data.id] = {
+ id: data.id,
+ from: {
+ ...from,
+ time: performance.now(),
+ },
+ to: {
+ ...data,
+ time: performance.now() + sendTickRate,
+ },
+ };
+ } else {
+ syncedPointerStateRef.current[data.id] = {
+ from: null,
+ to: { ...data, time: performance.now() + sendTickRate },
+ };
+ }
+ }
+ }
+
+ session.on("data", handlePeerData);
+
+ return () => {
+ session.off("data", handlePeerData);
+ };
+ });
+
+ // Animate to the peer pointer positions
+ useEffect(() => {
+ let request = requestAnimationFrame(animate);
+
+ function animate(time) {
+ request = requestAnimationFrame(animate);
+ let interpolatedPointerState = {};
+ for (let syncState of Object.values(syncedPointerStateRef.current)) {
+ if (!syncState.from || !syncState.to) {
+ continue;
+ }
+ const totalInterpTime = syncState.to.time - syncState.from.time;
+ const currentInterpTime = time - syncState.from.time;
+ const alpha = currentInterpTime / totalInterpTime;
+
+ if (alpha >= 0 && alpha <= 1) {
+ interpolatedPointerState[syncState.id] = {
+ id: syncState.to.id,
+ visible: syncState.from.visible,
+ position: lerp(
+ syncState.from.position,
+ syncState.to.position,
+ alpha
+ ),
+ };
+ }
+ if (alpha > 1 && !syncState.to.visible) {
+ interpolatedPointerState[syncState.id] = {
+ id: syncState.id,
+ visible: syncState.to.visible,
+ position: syncState.to.position,
+ };
+ delete syncedPointerStateRef.current[syncState.to.id];
+ }
+ }
+ if (!isEmpty(interpolatedPointerState)) {
+ setPointerState((prev) => ({
+ ...prev,
+ ...interpolatedPointerState,
+ }));
+ }
+ }
+
+ return () => {
+ cancelAnimationFrame(request);
+ };
+ }, []);
+
+ return (
+
+ {Object.values(pointerState).map((pointer) => (
+
+ ))}
+
+ );
+}
+
+export default NetworkedMapPointer;
diff --git a/src/network/NetworkedParty.js b/src/network/NetworkedParty.js
index 68bd59a..3e03326 100644
--- a/src/network/NetworkedParty.js
+++ b/src/network/NetworkedParty.js
@@ -3,9 +3,10 @@ import React, { useContext, useState, useEffect, useCallback } from "react";
// Load session for auto complete
// eslint-disable-next-line no-unused-vars
import Session from "../helpers/Session";
-import { isStreamStopped, omit } from "../helpers/shared";
+import { isStreamStopped, omit, fromEntries } from "../helpers/shared";
import AuthContext from "../contexts/AuthContext";
+import useSetting from "../helpers/useSetting";
import Party from "../components/party/Party";
@@ -23,10 +24,16 @@ function NetworkedParty({ gameId, session }) {
const [partyNicknames, setPartyNicknames] = useState({});
const [stream, setStream] = useState(null);
const [partyStreams, setPartyStreams] = useState({});
+ const [timer, setTimer] = useState(null);
+ const [partyTimers, setPartyTimers] = useState({});
+ const [diceRolls, setDiceRolls] = useState([]);
+ const [partyDiceRolls, setPartyDiceRolls] = useState({});
- function handleNicknameChange(nickname) {
- setNickname(nickname);
- session.send("nickname", { [session.id]: nickname });
+ const [shareDice, setShareDice] = useSetting("dice.shareDice");
+
+ function handleNicknameChange(newNickname) {
+ setNickname(newNickname);
+ session.send("nickname", { [session.id]: newNickname });
}
function handleStreamStart(localStream) {
@@ -59,16 +66,82 @@ function NetworkedParty({ gameId, session }) {
[session]
);
+ function handleTimerStart(newTimer) {
+ setTimer(newTimer);
+ session.send("timer", { [session.id]: newTimer });
+ }
+
+ function handleTimerStop() {
+ setTimer(null);
+ session.send("timer", { [session.id]: null });
+ }
+
+ useEffect(() => {
+ let prevTime = performance.now();
+ let request = requestAnimationFrame(update);
+ let counter = 0;
+ function update(time) {
+ request = requestAnimationFrame(update);
+ const deltaTime = time - prevTime;
+ prevTime = time;
+
+ if (timer) {
+ counter += deltaTime;
+ // Update timer every second
+ if (counter > 1000) {
+ const newTimer = {
+ ...timer,
+ current: timer.current - counter,
+ };
+ if (newTimer.current < 0) {
+ setTimer(null);
+ session.send("timer", { [session.id]: null });
+ } else {
+ setTimer(newTimer);
+ session.send("timer", { [session.id]: newTimer });
+ }
+ counter = 0;
+ }
+ }
+ }
+ return () => {
+ cancelAnimationFrame(request);
+ };
+ }, [timer, session]);
+
+ function handleDiceRollsChange(newDiceRolls) {
+ setDiceRolls(newDiceRolls);
+ if (shareDice) {
+ session.send("dice", { [session.id]: newDiceRolls });
+ }
+ }
+
+ function handleShareDiceChange(newShareDice) {
+ setShareDice(newShareDice);
+ if (newShareDice) {
+ session.send("dice", { [session.id]: diceRolls });
+ } else {
+ session.send("dice", { [session.id]: null });
+ }
+ }
+
useEffect(() => {
function handlePeerConnect({ peer, reply }) {
reply("nickname", { [session.id]: nickname });
if (stream) {
peer.connection.addStream(stream);
}
+ if (timer) {
+ reply("timer", { [session.id]: timer });
+ }
+ if (shareDice) {
+ reply("dice", { [session.id]: diceRolls });
+ }
}
function handlePeerDisconnect({ peer }) {
setPartyNicknames((prevNicknames) => omit(prevNicknames, [peer.id]));
+ setPartyTimers((prevTimers) => omit(prevTimers, [peer.id]));
}
function handlePeerData({ id, data }) {
@@ -78,6 +151,26 @@ function NetworkedParty({ gameId, session }) {
...data,
}));
}
+ if (id === "timer") {
+ setPartyTimers((prevTimers) => {
+ const newTimers = { ...prevTimers, ...data };
+ // filter out timers that are null
+ const filtered = Object.entries(newTimers).filter(
+ ([, value]) => value !== null
+ );
+ return fromEntries(filtered);
+ });
+ }
+ if (id === "dice") {
+ setPartyDiceRolls((prevDiceRolls) => {
+ const newRolls = { ...prevDiceRolls, ...data };
+ // filter out dice rolls that are null
+ const filtered = Object.entries(newRolls).filter(
+ ([, value]) => value !== null
+ );
+ return fromEntries(filtered);
+ });
+ }
}
function handlePeerTrackAdded({ peer, stream: remoteStream }) {
@@ -111,7 +204,7 @@ function NetworkedParty({ gameId, session }) {
session.off("trackAdded", handlePeerTrackAdded);
session.off("trackRemoved", handlePeerTrackRemoved);
};
- }, [session, nickname, stream]);
+ }, [session, nickname, stream, timer, shareDice, diceRolls]);
useEffect(() => {
if (stream) {
@@ -139,6 +232,15 @@ function NetworkedParty({ gameId, session }) {
partyNicknames={partyNicknames}
stream={stream}
partyStreams={partyStreams}
+ timer={timer}
+ partyTimers={partyTimers}
+ onTimerStart={handleTimerStart}
+ onTimerStop={handleTimerStop}
+ shareDice={shareDice}
+ onShareDiceChage={handleShareDiceChange}
+ diceRolls={diceRolls}
+ onDiceRollsChange={handleDiceRollsChange}
+ partyDiceRolls={partyDiceRolls}
/>
);
}
diff --git a/src/routes/Home.js b/src/routes/Home.js
index 20e6bf9..d1ca57b 100644
--- a/src/routes/Home.js
+++ b/src/routes/Home.js
@@ -55,7 +55,7 @@ function Home() {
Join Game
- Beta v1.4.2
+ Beta v{process.env.REACT_APP_VERSION}
+