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 (