From 56944bc8d7e08f00a56b7344db3db414f5896124 Mon Sep 17 00:00:00 2001 From: nessi Date: Sun, 8 Feb 2026 11:02:43 +0100 Subject: [PATCH] Refactor dice components for true 3D rendering. Replaced the 2D dice logic with fully 3D-rendered cubes, including animations and proper face-to-value mappings. Updated the design to use CSS transformations and added visual enhancements for a more immersive user experience. --- frontend/src/App.jsx | 525 ++++++++++++++++++++----------------- frontend/src/AppLayout.css | 94 ++++++- 2 files changed, 369 insertions(+), 250 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b9e6611..8e91850 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -541,292 +541,327 @@ export default function App() { ); }; - // App.jsx (füge diese Komponente irgendwo unter deinen anderen Komponenten ein) -// ✅ 3 Würfel: 2x d6 + 1x Spezial (Häuser + Hilfkarte + Dunkles Deck) + // ✅ 3 Würfel: 2x d6 + 1x Spezial (Häuser + Hilfkarte + Dunkles Deck) -const DicePanel = ({ onRoll }) => { - const [d1, setD1] = useState(4); - const [d2, setD2] = useState(2); - const [special, setSpecial] = useState("gryffindor"); - const [rolling, setRolling] = useState(false); + const DicePanel = ({ onRoll }) => { + const [d1, setD1] = useState(4); + const [d2, setD2] = useState(2); + const [special, setSpecial] = useState("gryffindor"); + const [rolling, setRolling] = useState(false); - const specialFaces = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"]; + const specialFaces = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"]; + const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; - const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; - - const rollAll = () => { - if (rolling) return; - setRolling(true); - - // Während der Animation kurz "flackern" lassen (optional, wirkt lebendiger) - const flickerId = setInterval(() => { - setD1(randInt(1, 6)); - setD2(randInt(1, 6)); - setSpecial(specialFaces[randInt(0, specialFaces.length - 1)]); - }, 90); - - // Finales Ergebnis nach Animation - setTimeout(() => { - clearInterval(flickerId); + const rollAll = () => { + if (rolling) return; + setRolling(true); + // 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)]; - setD1(nd1); - setD2(nd2); - setSpecial(ns); + setTimeout(() => { + setD1(nd1); + setD2(nd2); + setSpecial(ns); + setRolling(false); + onRoll?.({ d1: nd1, d2: nd2, special: ns }); + }, 820); + }; - setRolling(false); + return ( +
+
+
+
+
+ Würfel +
+
+ Klicken zum Rollen +
+
- onRoll?.({ d1: nd1, d2: nd2, special: ns }); - }, 820); // muss zur CSS Animation passen - }; - - return ( -
-
-
-
- Würfel -
-
- Klicken zum Rollen + {rolling ? "ROLL…" : "READY"}
- {rolling ? "ROLL…" : "READY"} + + + +
+
+
+ ); + }; + + const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)", rolling = false, onClick }) => { + return ( + + ); + }; + + /* map value->cube rotation so that value face is FRONT + Face layout: + front=1, back=6, top=2, bottom=5, right=3, left=4 + */ + const cubeRotationForD6 = (value) => { + switch (value) { + case 1: return { rx: "0deg", ry: "0deg" }; + case 2: return { rx: "-90deg", ry: "0deg" }; + case 3: return { rx: "0deg", ry: "-90deg" }; + case 4: return { rx: "0deg", ry: "90deg" }; + case 5: return { rx: "90deg", ry: "0deg" }; + case 6: return { rx: "0deg", ry: "180deg" }; + default: return { rx: "0deg", ry: "0deg" }; + } + }; + + const PipFace = ({ value }) => { + // pip positions: 0..2 grid + const pos = (gx, gy) => ({ left: `${gx * 50}%`, top: `${gy * 50}%` }); + + const faces = { + 1: [[1, 1]], + 2: [[0, 0], [2, 2]], + 3: [[0, 0], [1, 1], [2, 2]], + 4: [[0, 0], [2, 0], [0, 2], [2, 2]], + 5: [[0, 0], [2, 0], [1, 1], [0, 2], [2, 2]], + 6: [[0, 0], [0, 1], [0, 2], [2, 0], [2, 1], [2, 2]], + }; + + const arr = faces[value] || faces[1]; + + return ( +
+ {arr.map(([x, y], idx) => ( +
+ ))} +
+ ); + }; + + const DieD6 = ({ value = 1, rolling = false, onClick }) => { + const rot = cubeRotationForD6(value); + + return ( + +
+
+ {/* faces show correct pips like a real die */} +
+
+
+
+
+
- - - + d6
-
-
- ); -}; - -const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)", rolling = false, onClick }) => { - return ( - - ); -}; - -const DieD6 = ({ value = 1, rolling = false, onClick }) => { - const P = ({ x, y }) => ( -
- ); - - const faces = { - 1: [{ x: 1, y: 1 }], - 2: [{ x: 0, y: 0 }, { x: 2, y: 2 }], - 3: [{ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }], - 4: [{ x: 0, y: 0 }, { x: 2, y: 0 }, { x: 0, y: 2 }, { x: 2, y: 2 }], - 5: [{ x: 0, y: 0 }, { x: 2, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 2 }, { x: 2, y: 2 }], - 6: [{ x: 0, y: 0 }, { x: 0, y: 1 }, { x: 0, y: 2 }, { x: 2, y: 0 }, { x: 2, y: 1 }, { x: 2, y: 2 }], + + ); }; - return ( - -
- {(faces[value] || faces[1]).map((p, idx) => ( -

- ))} -

+ const HouseDie = ({ face = "gryffindor", rolling = false, onClick }) => { + // icon instead of letters + const faces = { + gryffindor: { icon: "🦁", color: "#ef4444", sub: "Gryff." }, + slytherin: { icon: "🐍", color: "#22c55e", sub: "Slyth." }, + ravenclaw: { icon: "🦅", color: "#3b82f6", sub: "Raven." }, + hufflepuff: { icon: "🦡", color: "#facc15", sub: "Huff." }, + help: { icon: "🃏", color: "#f2d27a", sub: "Hilf" }, + dark: { icon: "🌙", color: "rgba(255,255,255,0.85)", sub: "Dark" }, + }; -
- d6 -
-
- ); -}; + const order = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"]; + const idx = Math.max(0, order.indexOf(face)); + const value = idx + 1; // 1..6 + const rot = cubeRotationForD6(value); -const HouseDie = ({ face = "gryffindor", rolling = false, onClick }) => { - const faces = { - gryffindor: { label: "G", color: "#ef4444", sub: "Gryff." }, - slytherin: { label: "S", color: "#22c55e", sub: "Slyth." }, - ravenclaw: { label: "R", color: "#3b82f6", sub: "Raven." }, - hufflepuff: { label: "H", color: "#facc15", sub: "Huff." }, - help: { label: "?", color: "#f2d27a", sub: "Hilf" }, - dark: { label: "☾", color: "rgba(255,255,255,0.78)", sub: "Dark" }, - }; + const f = faces[face] || faces.gryffindor; + const ring = `${f.color}55`; - const f = faces[face] || faces.gryffindor; + return ( + +
+
+ {/* order mapped to 6 faces */} + {order.map((key, i) => { + const item = faces[key]; + const faceClass = + i === 0 ? "front" : + i === 1 ? "top" : + i === 2 ? "right" : + i === 3 ? "left" : + i === 4 ? "bottom" : + "back"; + + return ( +
+
+ {item.icon} +
+
+ ); + })} +
+
- return ( - -
- {f.label} + Spezial
-
- -
- Spezial -
-
- ); -}; +
+ ); + }; + const PlayerIdentityCard = ({ diff --git a/frontend/src/AppLayout.css b/frontend/src/AppLayout.css index 172d751..5062cfd 100644 --- a/frontend/src/AppLayout.css +++ b/frontend/src/AppLayout.css @@ -176,11 +176,7 @@ body { opacity: 0.98; } -/* --- Dice 3D feel --- */ -.diceRow3d { - perspective: 900px; -} - +/* ===== True 3D Cube Dice ===== */ .die3d { transform-style: preserve-3d; will-change: transform; @@ -190,6 +186,94 @@ body { transform: translateY(-3px) rotateX(8deg) rotateY(-10deg); } +/* Cube container inside the button */ +.dieCubeWrap { + width: 44px; + height: 44px; + position: relative; + transform-style: preserve-3d; +} + +/* actual cube */ +.dieCube { + width: 44px; + height: 44px; + position: absolute; + inset: 0; + transform-style: preserve-3d; + transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)); + transition: transform 260ms cubic-bezier(0.22, 1.0, 0.25, 1); +} + +/* rolling animation (spins, then we snap by updating --rx/--ry) */ +@keyframes cubeRoll { + 0% { transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)); } + 100% { + transform: + rotateX(calc(var(--rx, 0deg) + 720deg)) + rotateY(calc(var(--ry, 0deg) + 540deg)); + } +} + +.dieCube.rolling { + animation: cubeRoll 820ms cubic-bezier(0.18, 0.88, 0.22, 1) both; +} + +/* one face */ +.dieFace { + position: absolute; + inset: 0; + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.10); + background: + radial-gradient(120% 120% at 25% 18%, rgba(255,255,255,0.14), rgba(0,0,0,0.40) 60%, rgba(0,0,0,0.72)); + box-shadow: + inset 0 0 0 1px rgba(255,255,255,0.06), + inset 0 12px 20px rgba(255,255,255,0.05); + display: grid; + place-items: center; + backface-visibility: hidden; +} + +/* face positions */ +.dieFace.front { transform: rotateY( 0deg) translateZ(22px); } +.dieFace.back { transform: rotateY(180deg) translateZ(22px); } +.dieFace.right { transform: rotateY( 90deg) translateZ(22px); } +.dieFace.left { transform: rotateY(-90deg) translateZ(22px); } +.dieFace.top { transform: rotateX( 90deg) translateZ(22px); } +.dieFace.bottom { transform: rotateX(-90deg) translateZ(22px); } + +/* pips */ +.pipGrid { + width: 70%; + height: 70%; + position: relative; +} + +.pip { + position: absolute; + width: 7px; + height: 7px; + border-radius: 999px; + background: rgba(245,245,245,0.92); + box-shadow: 0 3px 10px rgba(0,0,0,0.35), inset 0 1px 2px rgba(0,0,0,0.20); +} + +/* special icon */ +.specialIcon { + font-size: 20px; + line-height: 1; + filter: drop-shadow(0 10px 18px rgba(0,0,0,0.55)); +} + +/* optional: subtle face ring tint via CSS var */ +.dieFace { + box-shadow: + inset 0 0 0 1px rgba(255,255,255,0.06), + inset 0 0 0 2px var(--ring, rgba(255,255,255,0.00)), + inset 0 12px 20px rgba(255,255,255,0.05); +} + /* Roll animation */ @keyframes dieRoll { 0% {