diff --git a/.env b/.env index f2f811d..00465e2 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ REACT_APP_BROKER_URL=http://localhost:9000 -REACT_APP_ICE_SERVERS_URL=http://localhost:9000/iceservers \ No newline at end of file +REACT_APP_ICE_SERVERS_URL=http://localhost:9000/iceservers +REACT_APP_VERSION=$npm_package_version \ No newline at end of file diff --git a/.env.production b/.env.production index 3305ecc..c37dac6 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,3 @@ REACT_APP_BROKER_URL=https://broker.owlbear.app -REACT_APP_ICE_SERVERS_URL=https://broker.owlbear.app/iceservers \ No newline at end of file +REACT_APP_ICE_SERVERS_URL=https://broker.owlbear.app/iceservers +REACT_APP_VERSION=$npm_package_version \ No newline at end of file diff --git a/package.json b/package.json index 6376851..48da415 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,23 @@ { "name": "owlbear-rodeo", - "version": "1.4.2", + "version": "1.5.0", "private": true, "dependencies": { + "@babylonjs/core": "^4.1.0", + "@babylonjs/loaders": "^4.1.0", "@msgpack/msgpack": "^1.12.1", "@stripe/stripe-js": "^1.3.2", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "ammo.js": "kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778", - "babylonjs": "^4.1.0", - "babylonjs-loaders": "^4.1.0", "case": "^1.6.3", "dexie": "^2.0.4", "fake-indexeddb": "^3.0.0", "interactjs": "^1.9.7", "konva": "^6.0.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", "polygon-clipping": "^0.14.3", "raw.macro": "^0.3.0", "react": "^16.13.0", @@ -35,11 +37,13 @@ "simplebar-react": "^2.1.0", "simplify-js": "^1.2.4", "socket.io-client": "^2.3.0", + "source-map-explorer": "^2.4.2", "theme-ui": "^0.3.1", "use-image": "^1.0.5", "webrtc-adapter": "^7.5.1" }, "scripts": { + "analyze": "source-map-explorer 'build/static/js/*.js'", "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", diff --git a/src/App.js b/src/App.js index 80455d7..d6bd778 100644 --- a/src/App.js +++ b/src/App.js @@ -15,41 +15,44 @@ import { DatabaseProvider } from "./contexts/DatabaseContext"; import { MapDataProvider } from "./contexts/MapDataContext"; import { TokenDataProvider } from "./contexts/TokenDataContext"; import { MapLoadingProvider } from "./contexts/MapLoadingContext"; +import { SettingsProvider } from "./contexts/SettingsContext.js"; function App() { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/src/components/Modal.js b/src/components/Modal.js index 94b9eda..28526e0 100644 --- a/src/components/Modal.js +++ b/src/components/Modal.js @@ -16,7 +16,7 @@ function StyledModal({ isOpen={isOpen} onRequestClose={onRequestClose} style={{ - overlay: { backgroundColor: "rgba(0, 0, 0, 0.73)" }, + overlay: { backgroundColor: "rgba(0, 0, 0, 0.73)", zIndex: 100 }, content: { backgroundColor: theme.colors.background, top: "50%", diff --git a/src/components/dice/DiceButton.js b/src/components/dice/DiceButton.js index a7f1f69..45c9d74 100644 --- a/src/components/dice/DiceButton.js +++ b/src/components/dice/DiceButton.js @@ -3,14 +3,14 @@ import { IconButton } from "theme-ui"; import Count from "./DiceButtonCount"; -function DiceButton({ title, children, count, onClick }) { +function DiceButton({ title, children, count, onClick, disabled }) { return ( {children} {count && {count}} diff --git a/src/components/dice/DiceButtonCount.js b/src/components/dice/DiceButtonCount.js index b18caba..4b17aac 100644 --- a/src/components/dice/DiceButtonCount.js +++ b/src/components/dice/DiceButtonCount.js @@ -6,9 +6,9 @@ function DiceButtonCount({ children }) { - {children}× + {children} ); diff --git a/src/components/dice/DiceButtons.js b/src/components/dice/DiceButtons.js index c2900da..b44ffd1 100644 --- a/src/components/dice/DiceButtons.js +++ b/src/components/dice/DiceButtons.js @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; -import { Flex, IconButton } from "theme-ui"; +import { Flex, IconButton, Box } from "theme-ui"; +import SimpleBar from "simplebar-react"; import D20Icon from "../../icons/D20Icon"; import D12Icon from "../../icons/D12Icon"; @@ -9,6 +10,8 @@ import D6Icon from "../../icons/D6Icon"; import D4Icon from "../../icons/D4Icon"; import D100Icon from "../../icons/D100Icon"; import ExpandMoreDiceTrayIcon from "../../icons/ExpandMoreDiceTrayIcon"; +import ShareDiceOnIcon from "../../icons/ShareDiceOnIcon"; +import ShareDiceOffIcon from "../../icons/ShareDiceOffIcon"; import DiceButton from "./DiceButton"; import SelectDiceButton from "./SelectDiceButton"; @@ -16,6 +19,7 @@ import SelectDiceButton from "./SelectDiceButton"; import Divider from "../Divider"; import { dice } from "../../dice"; +import useSetting from "../../helpers/useSetting"; function DiceButtons({ diceRolls, @@ -23,11 +27,17 @@ function DiceButtons({ onDiceLoad, diceTraySize, onDiceTraySizeChange, + shareDice, + onShareDiceChange, + loading, }) { - const [currentDice, setCurrentDice] = useState(dice[0]); + const [currentDiceStyle, setCurrentDiceStyle] = useSetting("dice.style"); + const [currentDice, setCurrentDice] = useState( + dice.find((d) => d.key === currentDiceStyle) + ); useEffect(() => { - const initialDice = dice[0]; + const initialDice = dice.find((d) => d.key === currentDiceStyle); onDiceLoad(initialDice); setCurrentDice(initialDice); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -45,89 +55,129 @@ function DiceButtons({ async function handleDiceChange(dice) { await onDiceLoad(dice); setCurrentDice(dice); + setCurrentDiceStyle(dice.key); } + let buttons = [ + { + key: "d20", + title: "Add D20", + count: diceCounts.d20, + onClick: () => onDiceAdd(currentDice.class, "d20"), + children: , + }, + { + key: "d12", + title: "Add D12", + count: diceCounts.d12, + onClick: () => onDiceAdd(currentDice.class, "d12"), + children: , + }, + { + key: "d10", + title: "Add D10", + count: diceCounts.d10, + onClick: () => onDiceAdd(currentDice.class, "d10"), + children: , + }, + { + key: "d8", + title: "Add D8", + count: diceCounts.d8, + onClick: () => onDiceAdd(currentDice.class, "d8"), + children: , + }, + { + key: "d6", + title: "Add D6", + count: diceCounts.d6, + onClick: () => onDiceAdd(currentDice.class, "d6"), + children: , + }, + { + key: "d4", + title: "Add D4", + count: diceCounts.d4, + onClick: () => onDiceAdd(currentDice.class, "d4"), + children: , + }, + { + key: "d100", + title: "Add D100", + count: diceCounts.d100, + onClick: () => onDiceAdd(currentDice.class, "d100"), + children: , + }, + ]; + return ( - - - - onDiceAdd(currentDice.class, "d20")} - > - - - onDiceAdd(currentDice.class, "d12")} - > - - - onDiceAdd(currentDice.class, "d10")} - > - - - onDiceAdd(currentDice.class, "d8")} - > - - - onDiceAdd(currentDice.class, "d6")} - > - - - onDiceAdd(currentDice.class, "d4")} - > - - - onDiceAdd(currentDice.class, "d100")} - > - - - - - onDiceTraySizeChange(diceTraySize === "single" ? "double" : "single") - } - > - - - + + + + + {buttons.map((button) => ( + + ))} + + + onDiceTraySizeChange( + diceTraySize === "single" ? "double" : "single" + ) + } + disabled={loading} + > + + + + onShareDiceChange(!shareDice)} + disabled={loading} + > + {shareDice ? : } + + + + ); } diff --git a/src/components/dice/DiceControls.js b/src/components/dice/DiceControls.js deleted file mode 100644 index 9baabc9..0000000 --- a/src/components/dice/DiceControls.js +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useEffect, useState } from "react"; -import * as BABYLON from "babylonjs"; - -import DiceButtons from "./DiceButtons"; -import DiceResults from "./DiceResults"; - -function DiceControls({ - diceRefs, - sceneVisibleRef, - onDiceAdd, - onDiceClear, - onDiceReroll, - onDiceLoad, - diceTraySize, - onDiceTraySizeChange, -}) { - const [diceRolls, setDiceRolls] = useState([]); - - // Update dice rolls - useEffect(() => { - // Find the number facing up on a dice object - 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 }; - } - - // Find the number facing up on a mesh instance of a dice - 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 = BABYLON.Vector3.Dot(direction, BABYLON.Vector3.Up()); - if (dot > highestDot) { - highestDot = dot; - highestLocator = locator; - } - } - return parseInt(highestLocator.name.slice(12)); - } - - 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; - } - setDiceRolls(newRolls); - } - - const updateInterval = setInterval(updateDiceRolls, 100); - return () => { - clearInterval(updateInterval); - }; - }, [diceRefs, sceneVisibleRef]); - - return ( - <> -
- { - onDiceClear(); - setDiceRolls([]); - }} - onDiceReroll={onDiceReroll} - /> -
-
- { - onDiceAdd(style, type); - setDiceRolls((prevRolls) => [ - ...prevRolls, - { type, roll: "unknown" }, - ]); - }} - onDiceLoad={onDiceLoad} - onDiceTraySizeChange={onDiceTraySizeChange} - diceTraySize={diceTraySize} - /> -
- - ); -} - -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 ? ( + + + + + ); +} + +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}