369 lines
9.8 KiB
JavaScript
369 lines
9.8 KiB
JavaScript
import React, { useRef, useCallback, useEffect, useState } from "react";
|
|
import { Vector3 } from "@babylonjs/core/Maths/math";
|
|
import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight";
|
|
import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator";
|
|
import { CubeTexture } from "@babylonjs/core/Materials/Textures/cubeTexture";
|
|
import { Box } from "theme-ui";
|
|
|
|
import environment from "../../dice/environment.dds";
|
|
|
|
import DiceInteraction from "./DiceInteraction";
|
|
import Dice from "../../dice/Dice";
|
|
import LoadingOverlay from "../LoadingOverlay";
|
|
import DiceButtons from "./DiceButtons";
|
|
import DiceResults from "./DiceResults";
|
|
|
|
import DiceTray from "../../dice/diceTray/DiceTray";
|
|
|
|
import { useDiceLoading } from "../../contexts/DiceLoadingContext";
|
|
|
|
import { getDiceRoll } from "../../helpers/dice";
|
|
import useSetting from "../../hooks/useSetting";
|
|
|
|
function DiceTrayOverlay({
|
|
isOpen,
|
|
shareDice,
|
|
onShareDiceChage,
|
|
diceRolls,
|
|
onDiceRollsChange,
|
|
}) {
|
|
const sceneRef = useRef();
|
|
const shadowGeneratorRef = useRef();
|
|
const diceRefs = useRef([]);
|
|
const sceneVisibleRef = useRef(false);
|
|
const sceneInteractionRef = useRef(false);
|
|
// Add to the counter to ingore sleep values
|
|
const sceneKeepAwakeRef = useRef(0);
|
|
const diceTrayRef = useRef();
|
|
|
|
const [diceTraySize, setDiceTraySize] = useState("single");
|
|
const { assetLoadStart, assetLoadFinish, isLoading } = useDiceLoading();
|
|
const [fullScreen] = useSetting("map.fullScreen");
|
|
|
|
function handleAssetLoadStart() {
|
|
assetLoadStart();
|
|
}
|
|
|
|
function handleAssetLoadFinish() {
|
|
assetLoadFinish();
|
|
forceRender();
|
|
}
|
|
|
|
// Forces rendering for 1 second
|
|
function forceRender() {
|
|
// Force rerender
|
|
sceneKeepAwakeRef.current++;
|
|
let triggered = false;
|
|
let timeout = setTimeout(() => {
|
|
sceneKeepAwakeRef.current--;
|
|
triggered = true;
|
|
}, 1000);
|
|
|
|
return () => {
|
|
clearTimeout(timeout);
|
|
if (!triggered) {
|
|
sceneKeepAwakeRef.current--;
|
|
}
|
|
};
|
|
}
|
|
|
|
// Force render when changing dice tray size
|
|
useEffect(() => {
|
|
const diceTray = diceTrayRef.current;
|
|
let cleanup;
|
|
if (diceTray) {
|
|
diceTray.size = diceTraySize;
|
|
cleanup = forceRender();
|
|
}
|
|
return cleanup;
|
|
}, [diceTraySize]);
|
|
|
|
useEffect(() => {
|
|
let cleanup;
|
|
if (isOpen) {
|
|
sceneVisibleRef.current = true;
|
|
cleanup = forceRender();
|
|
} else {
|
|
sceneVisibleRef.current = false;
|
|
}
|
|
|
|
return cleanup;
|
|
}, [isOpen]);
|
|
|
|
const handleSceneMount = useCallback(async ({ scene, engine }) => {
|
|
sceneRef.current = scene;
|
|
await initializeScene(scene);
|
|
engine.runRenderLoop(() => update(scene));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
async function initializeScene(scene) {
|
|
handleAssetLoadStart();
|
|
let light = new DirectionalLight(
|
|
"DirectionalLight",
|
|
new Vector3(-0.5, -1, -0.5),
|
|
scene
|
|
);
|
|
light.position = new Vector3(5, 10, 5);
|
|
light.shadowMinZ = 1;
|
|
light.shadowMaxZ = 50;
|
|
let shadowGenerator = new ShadowGenerator(1024, light);
|
|
shadowGenerator.useCloseExponentialShadowMap = true;
|
|
shadowGenerator.darkness = 0.7;
|
|
shadowGeneratorRef.current = shadowGenerator;
|
|
|
|
scene.environmentTexture = CubeTexture.CreateFromPrefilteredData(
|
|
environment,
|
|
scene
|
|
);
|
|
scene.environmentIntensity = 1.0;
|
|
|
|
let diceTray = new DiceTray("single", scene, shadowGenerator);
|
|
await diceTray.load();
|
|
diceTrayRef.current = diceTray;
|
|
handleAssetLoadFinish();
|
|
}
|
|
|
|
function update(scene) {
|
|
function getDiceSpeed(dice) {
|
|
const diceSpeed = dice.instance.physicsImpostor
|
|
.getLinearVelocity()
|
|
.length();
|
|
// If the dice is a d100 check the d10 as well
|
|
if (dice.type === "d100") {
|
|
const d10Speed = dice.d10Instance.physicsImpostor
|
|
.getLinearVelocity()
|
|
.length();
|
|
return Math.max(diceSpeed, d10Speed);
|
|
} else {
|
|
return diceSpeed;
|
|
}
|
|
}
|
|
|
|
const die = diceRefs.current;
|
|
const sceneVisible = sceneVisibleRef.current;
|
|
if (!sceneVisible) {
|
|
return;
|
|
}
|
|
const forceSceneRender = sceneKeepAwakeRef.current > 0;
|
|
const sceneInteraction = sceneInteractionRef.current;
|
|
const diceAwake = die.map((dice) => dice.asleep).includes(false);
|
|
// Return early if scene doesn't need to be re-rendered
|
|
if (!forceSceneRender && !sceneInteraction && !diceAwake) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < die.length; i++) {
|
|
const dice = die[i];
|
|
const speed = getDiceSpeed(dice);
|
|
// If the speed has been below 0.01 for 1s set dice to sleep
|
|
if (speed < 0.01 && !dice.sleepTimout) {
|
|
dice.sleepTimout = setTimeout(() => {
|
|
dice.asleep = true;
|
|
}, 1000);
|
|
} else if (speed > 0.5 && (dice.asleep || dice.sleepTimout)) {
|
|
dice.asleep = false;
|
|
clearTimeout(dice.sleepTimout);
|
|
dice.sleepTimout = null;
|
|
}
|
|
}
|
|
|
|
if (scene) {
|
|
scene.render();
|
|
}
|
|
}
|
|
|
|
function handleDiceAdd(style, type) {
|
|
const scene = sceneRef.current;
|
|
const shadowGenerator = shadowGeneratorRef.current;
|
|
if (scene && shadowGenerator) {
|
|
const instance = style.createInstance(type, scene);
|
|
shadowGenerator.addShadowCaster(instance);
|
|
Dice.roll(instance);
|
|
let dice = { type, instance, asleep: false };
|
|
// If we have a d100 add a d10 as well
|
|
if (type === "d100") {
|
|
const d10Instance = style.createInstance("d10", scene);
|
|
shadowGenerator.addShadowCaster(d10Instance);
|
|
Dice.roll(d10Instance);
|
|
dice.d10Instance = d10Instance;
|
|
}
|
|
diceRefs.current.push(dice);
|
|
}
|
|
}
|
|
|
|
function handleDiceClear() {
|
|
const die = diceRefs.current;
|
|
for (let dice of die) {
|
|
dice.instance.dispose();
|
|
if (dice.type === "d100") {
|
|
dice.d10Instance.dispose();
|
|
}
|
|
}
|
|
diceRefs.current = [];
|
|
forceRender();
|
|
}
|
|
|
|
function handleDiceReroll() {
|
|
const die = diceRefs.current;
|
|
for (let dice of die) {
|
|
Dice.roll(dice.instance);
|
|
if (dice.type === "d100") {
|
|
Dice.roll(dice.d10Instance);
|
|
}
|
|
dice.asleep = false;
|
|
}
|
|
}
|
|
|
|
async function handleDiceLoad(dice) {
|
|
handleAssetLoadStart();
|
|
const scene = sceneRef.current;
|
|
if (scene) {
|
|
await dice.class.load(scene);
|
|
}
|
|
handleAssetLoadFinish();
|
|
}
|
|
|
|
const [traySize, setTraySize] = useState({
|
|
width: 0,
|
|
height: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
let renderTimeout;
|
|
let renderCleanup;
|
|
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;
|
|
}
|
|
|
|
// Debounce a timeout to force re-rendering on resize
|
|
renderTimeout = setTimeout(() => {
|
|
renderCleanup = forceRender();
|
|
}, 100);
|
|
|
|
setTraySize({ width, height });
|
|
}
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
|
|
handleResize();
|
|
|
|
return () => {
|
|
window.removeEventListener("resize", handleResize);
|
|
if (renderTimeout) {
|
|
clearTimeout(renderTimeout);
|
|
}
|
|
if (renderCleanup) {
|
|
renderCleanup();
|
|
}
|
|
};
|
|
}, [diceTraySize, fullScreen, isOpen]);
|
|
|
|
// 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 (
|
|
<Box
|
|
sx={{
|
|
width: `${traySize.width}px`,
|
|
height: `${traySize.height}px`,
|
|
borderRadius: "4px",
|
|
display: isOpen ? "block" : "none",
|
|
position: "relative",
|
|
overflow: "visible",
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
transform: "translateX(50px)",
|
|
width: "100%",
|
|
height: "100%",
|
|
pointerEvents: "all",
|
|
}}
|
|
>
|
|
<DiceInteraction
|
|
onSceneMount={handleSceneMount}
|
|
onPointerDown={() => {
|
|
sceneInteractionRef.current = true;
|
|
}}
|
|
onPointerUp={() => {
|
|
sceneInteractionRef.current = false;
|
|
}}
|
|
/>
|
|
<DiceResults
|
|
diceRolls={diceRolls}
|
|
onDiceClear={() => {
|
|
handleDiceClear();
|
|
onDiceRollsChange([]);
|
|
}}
|
|
onDiceReroll={handleDiceReroll}
|
|
/>
|
|
</Box>
|
|
<DiceButtons
|
|
diceRolls={diceRolls}
|
|
onDiceAdd={(style, type) => {
|
|
handleDiceAdd(style, type);
|
|
onDiceRollsChange([...diceRolls, { type, roll: "unknown" }]);
|
|
}}
|
|
onDiceLoad={handleDiceLoad}
|
|
onDiceTraySizeChange={setDiceTraySize}
|
|
diceTraySize={diceTraySize}
|
|
shareDice={shareDice}
|
|
onShareDiceChange={onShareDiceChage}
|
|
loading={isLoading}
|
|
/>
|
|
{isLoading && (
|
|
<Box
|
|
sx={{
|
|
width: "100%",
|
|
height: "100%",
|
|
position: "absolute",
|
|
top: 0,
|
|
left: "50px",
|
|
}}
|
|
>
|
|
<LoadingOverlay />
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export default DiceTrayOverlay;
|