diff --git a/src/components/map/AddMapButton.js b/src/components/map/AddMapButton.js
index ac95b20..75dfcd4 100644
--- a/src/components/map/AddMapButton.js
+++ b/src/components/map/AddMapButton.js
@@ -13,10 +13,8 @@ function AddMapButton({ onMapChange }) {
setIsAddModalOpen(false);
}
- function handleDone(map) {
- if (map) {
- onMapChange(map);
- }
+ function handleDone(map, mapState) {
+ onMapChange(map, mapState);
closeModal();
}
diff --git a/src/components/map/Map.js b/src/components/map/Map.js
index 620ee94..f99d02f 100644
--- a/src/components/map/Map.js
+++ b/src/components/map/Map.js
@@ -18,15 +18,13 @@ const maxZoom = 5;
function Map({
map,
- tokens,
+ mapState,
onMapTokenChange,
onMapTokenRemove,
onMapChange,
onMapDraw,
onMapDrawUndo,
onMapDrawRedo,
- drawActions,
- drawActionIndex,
}) {
function handleProxyDragEnd(isOnMap, token) {
if (isOnMap && onMapTokenChange) {
@@ -63,9 +61,12 @@ function Map({
// Replay the draw actions and convert them to shapes for the map drawing
useEffect(() => {
+ if (!mapState) {
+ return;
+ }
let shapesById = {};
- for (let i = 0; i <= drawActionIndex; i++) {
- const action = drawActions[i];
+ for (let i = 0; i <= mapState.drawActionIndex; i++) {
+ const action = mapState.drawActions[i];
if (action.type === "add") {
for (let shape of action.shapes) {
shapesById[shape.id] = shape;
@@ -76,7 +77,7 @@ function Map({
}
}
setDrawnShapes(Object.values(shapesById));
- }, [drawActions, drawActionIndex]);
+ }, [mapState]);
const disabledTools = [];
if (!map) {
@@ -233,14 +234,15 @@ function Map({
pointerEvents: "none",
}}
>
- {Object.values(tokens).map((token) => (
-
- ))}
+ {mapState &&
+ Object.values(mapState.tokens).map((token) => (
+
+ ))}
);
@@ -299,8 +301,11 @@ function Map({
disabledTools={disabledTools}
onUndo={onMapDrawUndo}
onRedo={onMapDrawRedo}
- undoDisabled={drawActionIndex < 0}
- redoDisabled={drawActionIndex === drawActions.length - 1}
+ undoDisabled={!mapState || mapState.drawActionIndex < 0}
+ redoDisabled={
+ !mapState ||
+ mapState.drawActionIndex === mapState.drawActions.length - 1
+ }
brushColor={brushColor}
onBrushColorChange={setBrushColor}
onEraseAll={handleShapeRemoveAll}
diff --git a/src/database.js b/src/database.js
index cf9c91f..a88f8f0 100644
--- a/src/database.js
+++ b/src/database.js
@@ -1,6 +1,6 @@
import Dexie from "dexie";
const db = new Dexie("OwlbearRodeoDB");
-db.version(1).stores({ maps: "id" });
+db.version(1).stores({ maps: "id", states: "mapId" });
export default db;
diff --git a/src/helpers/useDebounce.js b/src/helpers/useDebounce.js
new file mode 100644
index 0000000..057068c
--- /dev/null
+++ b/src/helpers/useDebounce.js
@@ -0,0 +1,18 @@
+import { useEffect, useState } from "react";
+
+function useDebounce(value, delay) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+}
+
+export default useDebounce;
diff --git a/src/modals/AddMapModal.js b/src/modals/AddMapModal.js
index 1e68ecb..53088b0 100644
--- a/src/modals/AddMapModal.js
+++ b/src/modals/AddMapModal.js
@@ -10,6 +10,13 @@ import MapSelect from "../components/map/MapSelect";
import * as defaultMaps from "../maps";
const defaultMapSize = 22;
+const defaultMapState = {
+ tokens: {},
+ // An index into the draw actions array to which only actions before the
+ // index will be performed (used in undo and redo)
+ drawActionIndex: -1,
+ drawActions: [],
+};
function AddMapModal({ isOpen, onRequestClose, onDone }) {
const [imageLoading, setImageLoading] = useState(false);
@@ -81,6 +88,7 @@ function AddMapModal({ isOpen, onRequestClose, onDone }) {
async function handleMapAdd(map) {
await db.table("maps").add(map);
+ await db.table("states").add({ ...defaultMapState, mapId: map.id });
setMaps((prevMaps) => [map, ...prevMaps]);
setCurrentMapId(map.id);
setGridX(map.gridX);
@@ -89,6 +97,7 @@ function AddMapModal({ isOpen, onRequestClose, onDone }) {
async function handleMapRemove(id) {
await db.table("maps").delete(id);
+ await db.table("states").delete(id);
setMaps((prevMaps) => {
const filtered = prevMaps.filter((map) => map.id !== id);
setCurrentMapId(filtered[0].id);
@@ -102,9 +111,14 @@ function AddMapModal({ isOpen, onRequestClose, onDone }) {
setGridY(map.gridY);
}
- function handleSubmit(e) {
+ async function handleSubmit(e) {
e.preventDefault();
- onDone(maps.find((map) => map.id === currentMapId));
+ const currentMap = maps.find((map) => map.id === currentMapId);
+ if (currentMap) {
+ let currentMapState =
+ (await db.table("states").get(currentMap.id)) || defaultMapState;
+ onDone(currentMap, currentMapState);
+ }
}
async function handleGridXChange(e) {
diff --git a/src/routes/Game.js b/src/routes/Game.js
index ef3e955..dca2556 100644
--- a/src/routes/Game.js
+++ b/src/routes/Game.js
@@ -2,9 +2,12 @@ import React, { useState, useEffect, useCallback, useContext } from "react";
import { Flex, Box, Text, Link } from "theme-ui";
import { useParams } from "react-router-dom";
+import db from "../database";
+
import { omit, isStreamStopped } from "../helpers/shared";
import useSession from "../helpers/useSession";
import useNickname from "../helpers/useNickname";
+import useDebounce from "../helpers/useDebounce";
import Party from "../components/party/Party";
import Tokens from "../components/token/Tokens";
@@ -35,23 +38,32 @@ function Game() {
*/
const [map, setMap] = useState(null);
+ const [mapState, setMapState] = useState(null);
- function handleMapChange(newMap) {
+ // Sync the map state to the database after 500ms of inactivity
+ const debouncedMapState = useDebounce(mapState, 500);
+ useEffect(() => {
+ if (debouncedMapState && debouncedMapState.mapId) {
+ db.table("states").update(debouncedMapState.mapId, debouncedMapState);
+ }
+ }, [debouncedMapState]);
+
+ function handleMapChange(newMap, newMapState) {
setMap(newMap);
+ setMapState(newMapState);
for (let peer of Object.values(peers)) {
+ peer.connection.send({ id: "mapState", data: newMapState });
peer.connection.send({ id: "map", data: newMap });
}
}
- const [mapTokens, setMapTokens] = useState({});
-
- function handleMapTokenChange(token) {
- if (!map.source) {
- return;
- }
- setMapTokens((prevMapTokens) => ({
- ...prevMapTokens,
- [token.id]: token,
+ async function handleMapTokenChange(token) {
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ tokens: {
+ ...prevMapState.tokens,
+ [token.id]: token,
+ },
}));
for (let peer of Object.values(peers)) {
const data = { [token.id]: token };
@@ -60,9 +72,9 @@ function Game() {
}
function handleMapTokenRemove(token) {
- setMapTokens((prevMapTokens) => {
- const { [token.id]: old, ...rest } = prevMapTokens;
- return rest;
+ setMapState((prevMapState) => {
+ const { [token.id]: old, ...rest } = prevMapState.tokens;
+ return { ...prevMapState, tokens: rest };
});
for (let peer of Object.values(peers)) {
const data = { [token.id]: token };
@@ -70,19 +82,18 @@ function Game() {
}
}
- const [mapDrawActions, setMapDrawActions] = useState([]);
- // An index into the draw actions array to which only actions before the
- // index will be performed (used in undo and redo)
- const [mapDrawActionIndex, setMapDrawActionIndex] = useState(-1);
function addNewMapDrawActions(actions) {
- setMapDrawActions((prevActions) => {
+ setMapState((prevMapState) => {
const newActions = [
- ...prevActions.slice(0, mapDrawActionIndex + 1),
+ ...prevMapState.drawActions.slice(0, prevMapState.drawActionIndex + 1),
...actions,
];
const newIndex = newActions.length - 1;
- setMapDrawActionIndex(newIndex);
- return newActions;
+ return {
+ ...prevMapState,
+ drawActions: newActions,
+ drawActionIndex: newIndex,
+ };
});
}
@@ -94,8 +105,11 @@ function Game() {
}
function handleMapDrawUndo() {
- const newIndex = Math.max(mapDrawActionIndex - 1, -1);
- setMapDrawActionIndex(newIndex);
+ const newIndex = Math.max(mapState.drawActionIndex - 1, -1);
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ drawActionIndex: newIndex,
+ }));
for (let peer of Object.values(peers)) {
peer.connection.send({ id: "mapDrawIndex", data: newIndex });
}
@@ -103,10 +117,13 @@ function Game() {
function handleMapDrawRedo() {
const newIndex = Math.min(
- mapDrawActionIndex + 1,
- mapDrawActions.length - 1
+ mapState.drawActionIndex + 1,
+ mapState.drawActions.length - 1
);
- setMapDrawActionIndex(newIndex);
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ drawActionIndex: newIndex,
+ }));
for (let peer of Object.values(peers)) {
peer.connection.send({ id: "mapDrawIndex", data: newIndex });
}
@@ -145,14 +162,8 @@ function Game() {
if (map) {
peer.connection.send({ id: "map", data: map });
}
- if (mapTokens) {
- peer.connection.send({ id: "tokenEdit", data: mapTokens });
- }
- if (mapDrawActions) {
- peer.connection.send({ id: "mapDraw", data: mapDrawActions });
- }
- if (mapDrawActionIndex !== mapDrawActions.length - 1) {
- peer.connection.send({ id: "mapDrawIndex", data: mapDrawActionIndex });
+ if (mapState) {
+ peer.connection.send({ id: "mapState", data: mapState });
}
}
if (data.id === "map") {
@@ -166,16 +177,20 @@ function Game() {
setMap(data.data);
}
}
+ if (data.id === "mapState") {
+ setMapState(data.data);
+ }
if (data.id === "tokenEdit") {
- setMapTokens((prevMapTokens) => ({
- ...prevMapTokens,
- ...data.data,
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ tokens: { ...prevMapState.tokens, ...data.data },
}));
}
if (data.id === "tokenRemove") {
- setMapTokens((prevMapTokens) =>
- omit(prevMapTokens, Object.keys(data.data))
- );
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ tokens: omit(prevMapState.tokens, Object.keys(data.data)),
+ }));
}
if (data.id === "nickname") {
setPartyNicknames((prevNicknames) => ({
@@ -187,7 +202,10 @@ function Game() {
addNewMapDrawActions(data.data);
}
if (data.id === "mapDrawIndex") {
- setMapDrawActionIndex(data.data);
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ drawActionIndex: data.data,
+ }));
}
}
@@ -301,15 +319,13 @@ function Game() {
/>