From 85bb30ae73e14f96a8f37080e81393bcb9bc6945 Mon Sep 17 00:00:00 2001 From: nessi Date: Sun, 8 Feb 2026 11:39:25 +0100 Subject: [PATCH] 3d dice --- frontend/src/App.jsx | 500 ++++++++++++++++++------------------- frontend/src/AppLayout.css | 75 ++++-- 2 files changed, 301 insertions(+), 274 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e6d7784..bb49433 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -544,116 +544,180 @@ export default function App() { // ✅ 3 Würfel: 2x d6 + 1x Spezial (Häuser + Hilfkarte + Dunkles Deck) const DicePanel = ({ onRoll }) => { - const LS_KEY = "hp_cluedo_dice_v1"; - const [d1, setD1] = useState(4); - const [d2, setD2] = useState(2); - const [special, setSpecial] = useState("gryffindor"); - const [rolling, setRolling] = useState(false); + const LS_KEY = "hp_cluedo_dice_v1"; - useEffect(() => { - try { - const raw = localStorage.getItem(LS_KEY); - if (!raw) return; - const parsed = JSON.parse(raw); - if (parsed?.d1) setD1(parsed.d1); - if (parsed?.d2) setD2(parsed.d2); - if (parsed?.special) setSpecial(parsed.special); - } catch {} - }, []); + const [d1, setD1] = useState(4); + const [d2, setD2] = useState(2); + const [special, setSpecial] = useState("gryffindor"); - useEffect(() => { - try { - localStorage.setItem(LS_KEY, JSON.stringify({ d1, d2, special })); - } catch {} - }, [d1, d2, special]); + // angles (absolute) + const [a1, setA1] = useState({ x: 0, y: 90 }); // start showing 4 (ry=90) + const [a2, setA2] = useState({ x: -90, y: 0 }); // start showing 2 (rx=-90) + const [as, setAs] = useState({ x: 0, y: 0 }); // gryffindor mapped below - const specialFaces = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"]; - const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; + const [rolling, setRolling] = useState(false); - const rollAll = () => { - if (rolling) return; - setRolling(true); + // commit targets (avoid “result earlier than animation”) + const pendingRef = useRef(null); + const doneCountRef = useRef(0); - // wir picken final direkt (kein Sound, kein Flicker nötig – Animation macht den Job) - const nd1 = randInt(1, 6); - const nd2 = randInt(1, 6); - const ns = specialFaces[randInt(0, specialFaces.length - 1)]; + const specialFaces = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"]; + const order = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"]; + const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; - setTimeout(() => { - setD1(nd1); - setD2(nd2); - setSpecial(ns); - setRolling(false); - onRoll?.({ d1: nd1, d2: nd2, special: ns }); - }, 820); + // restore last result + useEffect(() => { + try { + const raw = localStorage.getItem(LS_KEY); + if (!raw) return; + const parsed = JSON.parse(raw); + + if (parsed?.d1) setD1(parsed.d1); + if (parsed?.d2) setD2(parsed.d2); + if (parsed?.special) setSpecial(parsed.special); + + // set angles matching restored values + if (parsed?.d1) { + const r = cubeRotationForD6(parsed.d1); + setA1({ x: r.rx, y: r.ry }); + } + if (parsed?.d2) { + const r = cubeRotationForD6(parsed.d2); + setA2({ x: r.rx, y: r.ry }); + } + if (parsed?.special) { + const idx = Math.max(0, order.indexOf(parsed.special)); + const r = cubeRotationForD6(idx + 1); + setAs({ x: r.rx, y: r.ry }); + } + } catch {} + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + try { + localStorage.setItem(LS_KEY, JSON.stringify({ d1, d2, special })); + } catch {} + }, [d1, d2, special]); + + const normalize360 = (n) => ((n % 360) + 360) % 360; + + const rollTo = (currentAngles, targetBase) => { + // add big spins but land exactly on base orientation modulo 360 + const spinX = 720 + randInt(0, 2) * 360; + const spinY = 720 + randInt(0, 2) * 360; + + const currX = normalize360(currentAngles.x); + const currY = normalize360(currentAngles.y); + + const dx = targetBase.rx - currX; + const dy = targetBase.ry - currY; + + return { + x: currentAngles.x + spinX + dx, + y: currentAngles.y + spinY + dy, }; - - return ( -
-
-
-
-
- Würfel -
-
- Klicken zum Rollen -
-
- -
- {rolling ? "ROLL…" : "READY"} -
-
- -
- - - -
-
-
- ); }; - const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)", rolling = false, onClick }) => { + const rollAll = () => { + if (rolling) return; + + const nd1 = randInt(1, 6); + const nd2 = randInt(1, 6); + const ns = specialFaces[randInt(0, specialFaces.length - 1)]; + + const r1 = cubeRotationForD6(nd1); + const r2 = cubeRotationForD6(nd2); + const rs = cubeRotationForD6(Math.max(0, order.indexOf(ns)) + 1); + + pendingRef.current = { nd1, nd2, ns }; + doneCountRef.current = 0; + + setRolling(true); + + // start roll immediately (angles change drives the animation) + setA1((cur) => rollTo(cur, r1)); + setA2((cur) => rollTo(cur, r2)); + setAs((cur) => rollTo(cur, rs)); + }; + + const onOneDone = () => { + doneCountRef.current += 1; + if (doneCountRef.current < 3) return; + + const p = pendingRef.current; + if (!p) return; + + setD1(p.nd1); + setD2(p.nd2); + setSpecial(p.ns); + + setRolling(false); + pendingRef.current = null; + + onRoll?.({ d1: p.nd1, d2: p.nd2, special: p.ns }); + }; + + return ( +
+ {/* ✅ no container background/border anymore */} +
+
+
+ Würfel +
+
+ {rolling ? "ROLL…" : "READY"} +
+
+ +
+ + + +
+ +
+ Klicken zum Rollen +
+
+
+ ); +}; + + + const DieShell = ({ children, rolling = false, onClick }) => { return (