Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Epic D&D Dice Roller</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/cannon-es@0.19.0/dist/cannon-es.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=MedievalSharp&display=swap'); | |
| body { | |
| background-color: #0a0a0a; | |
| color: #e0d4b0; | |
| font-family: 'Cinzel', serif; | |
| overflow-x: hidden; | |
| } | |
| .dice-container { | |
| perspective: 1000px; | |
| } | |
| .roll-history-item { | |
| border-left: 3px solid #8b5a2b; | |
| transition: all 0.3s ease; | |
| } | |
| .roll-history-item:hover { | |
| background-color: rgba(139, 90, 43, 0.2); | |
| } | |
| .dice-selector { | |
| transition: all 0.2s ease; | |
| min-height: 80px; | |
| } | |
| .dice-selector:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 15px rgba(0, 0, 0, 0.3); | |
| } | |
| .dice-selector.active { | |
| border-color: #d4af37; | |
| box-shadow: 0 0 15px rgba(212, 175, 55, 0.5); | |
| } | |
| .glow-text { | |
| text-shadow: 0 0 5px rgba(212, 175, 55, 0.7); | |
| } | |
| .roll-button { | |
| background: linear-gradient(135deg, #8b5a2b 0%, #5d3a1a 100%); | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); | |
| transition: all 0.3s ease; | |
| } | |
| .roll-button:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 7px 20px rgba(139, 90, 43, 0.6); | |
| } | |
| .roll-button:active { | |
| transform: translateY(1px); | |
| } | |
| #canvas-container { | |
| position: relative; | |
| width: 100%; | |
| height: 400px; | |
| background: radial-gradient(ellipse at center, #1a1a1a 0%, #0a0a0a 100%); | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| #canvas-container::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="none" stroke="%23222222" stroke-width="1"/></svg>'); | |
| opacity: 0.3; | |
| pointer-events: none; | |
| } | |
| .critical { | |
| animation: pulse 1s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { color: #e0d4b0; } | |
| 50% { color: #d4af37; } | |
| 100% { color: #e0d4b0; } | |
| } | |
| .shape-name { | |
| display: -webkit-box; | |
| -webkit-line-clamp: 2; | |
| -webkit-box-orient: vertical; | |
| overflow: hidden; | |
| font-size: 0.7rem; | |
| line-height: 1.1; | |
| } | |
| .dice-preview { | |
| width: 40px; | |
| height: 40px; | |
| margin: 0 auto 5px; | |
| position: relative; | |
| } | |
| .dice-preview-container { | |
| width: 100%; | |
| height: 100%; | |
| transform-style: preserve-3d; | |
| transition: transform 1s; | |
| } | |
| .dice-face { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| border: 1px solid rgba(255,255,255,0.2); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| font-weight: bold; | |
| color: white; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <!-- Header --> | |
| <header class="text-center mb-12"> | |
| <h1 class="text-4xl md:text-6xl font-bold mb-4 glow-text">Dragon's Fortune</h1> | |
| <p class="text-xl md:text-2xl font-medium text-amber-200">Epic Dice Roller for Adventurers</p> | |
| <div class="w-32 h-1 bg-amber-700 mx-auto mt-4 rounded-full"></div> | |
| </header> | |
| <div class="flex flex-col lg:flex-row gap-8"> | |
| <!-- Left Panel - Controls --> | |
| <div class="w-full lg:w-1/3"> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-lg p-6 shadow-xl border border-gray-800"> | |
| <!-- Dice Selection --> | |
| <h2 class="text-2xl font-bold mb-4 text-amber-200 border-b border-amber-900 pb-2">Choose Your Dice</h2> | |
| <div class="grid grid-cols-3 gap-4 mb-6"> | |
| <div class="dice-selector p-3 bg-gray-800 rounded-lg cursor-pointer text-center border border-gray-700" data-sides="4"> | |
| <div class="dice-preview"> | |
| <div class="dice-preview-container" id="d4-preview"> | |
| <!-- Faces will be added by JS --> | |
| </div> | |
| </div> | |
| <div class="text-amber-200 text-lg font-bold">D4</div> | |
| <div class="shape-name text-gray-400">Tetrahedron</div> | |
| </div> | |
| <div class="dice-selector p-3 bg-gray-800 rounded-lg cursor-pointer text-center border border-gray-700" data-sides="6"> | |
| <div class="dice-preview"> | |
| <div class="dice-preview-container" id="d6-preview"> | |
| <!-- Faces will be added by JS --> | |
| </div> | |
| </div> | |
| <div class="text-amber-200 text-lg font-bold">D6</div> | |
| <div class="shape-name text-gray-400">Cube</div> | |
| </div> | |
| <div class="dice-selector p-3 bg-gray-800 rounded-lg cursor-pointer text-center border border-gray-700" data-sides="8"> | |
| <div class="dice-preview"> | |
| <div class="dice-preview-container" id="d8-preview"> | |
| <!-- Faces will be added by JS --> | |
| </div> | |
| </div> | |
| <div class="text-amber-200 text-lg font-bold">D8</div> | |
| <div class="shape-name text-gray-400">Octahedron</div> | |
| </div> | |
| <div class="dice-selector p-3 bg-gray-800 rounded-lg cursor-pointer text-center border border-gray-700" data-sides="10"> | |
| <div class="dice-preview"> | |
| <div class="dice-preview-container" id="d10-preview"> | |
| <!-- Faces will be added by JS --> | |
| </div> | |
| </div> | |
| <div class="text-amber-200 text-lg font-bold">D10</div> | |
| <div class="shape-name text-gray-400">Pentagonal Trapezohedron</div> | |
| </div> | |
| <div class="dice-selector p-3 bg-gray-800 rounded-lg cursor-pointer text-center border border-gray-700" data-sides="12"> | |
| <div class="dice-preview"> | |
| <div class="dice-preview-container" id="d12-preview"> | |
| <!-- Faces will be added by JS --> | |
| </div> | |
| </div> | |
| <div class="text-amber-200 text-lg font-bold">D12</div> | |
| <div class="shape-name text-gray-400">Dodecahedron</div> | |
| </div> | |
| <div class="dice-selector p-3 bg-gray-800 rounded-lg cursor-pointer text-center border border-gray-700" data-sides="20"> | |
| <div class="dice-preview"> | |
| <div class="dice-preview-container" id="d20-preview"> | |
| <!-- Faces will be added by JS --> | |
| </div> | |
| </div> | |
| <div class="text-amber-200 text-lg font-bold">D20</div> | |
| <div class="shape-name text-gray-400">Icosahedron</div> | |
| </div> | |
| </div> | |
| <!-- Quantity and Modifiers --> | |
| <div class="mb-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <label class="text-amber-200">Quantity:</label> | |
| <input type="number" id="diceQuantity" min="1" max="10" value="1" class="w-16 bg-gray-800 border border-gray-700 text-center text-amber-200 rounded px-2 py-1"> | |
| </div> | |
| <div class="flex items-center justify-between mb-2"> | |
| <label class="text-amber-200">Modifier:</label> | |
| <input type="number" id="diceModifier" min="-20" max="20" value="0" class="w-16 bg-gray-800 border border-gray-700 text-center text-amber-200 rounded px-2 py-1"> | |
| </div> | |
| </div> | |
| <!-- Roll Button --> | |
| <button id="rollButton" class="roll-button w-full py-4 rounded-lg text-xl font-bold text-amber-200 uppercase tracking-wider"> | |
| Roll the Dice | |
| </button> | |
| <!-- Result Display --> | |
| <div id="resultDisplay" class="mt-6 p-4 bg-gray-800 rounded-lg text-center hidden"> | |
| <div class="text-sm text-gray-400 mb-1">Result</div> | |
| <div id="totalResult" class="text-4xl font-bold text-amber-200"></div> | |
| <div id="individualResults" class="mt-2 text-gray-300"></div> | |
| <div id="criticalText" class="mt-2 text-xl font-bold text-red-400 hidden">CRITICAL HIT!</div> | |
| </div> | |
| </div> | |
| <!-- Roll History --> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-lg p-6 shadow-xl border border-gray-800 mt-6"> | |
| <h2 class="text-2xl font-bold mb-4 text-amber-200 border-b border-amber-900 pb-2">Roll History</h2> | |
| <div id="rollHistory" class="space-y-2 max-h-64 overflow-y-auto pr-2"> | |
| <!-- History items will be added here --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Panel - 3D Dice --> | |
| <div class="w-full lg:w-2/3"> | |
| <div id="canvas-container" class="dice-container"> | |
| <!-- Three.js canvas will be inserted here --> | |
| </div> | |
| <!-- Dice Table --> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-lg p-6 shadow-xl border border-gray-800 mt-6"> | |
| <h2 class="text-2xl font-bold mb-4 text-amber-200 border-b border-amber-900 pb-2">Dice Reference</h2> | |
| <div class="grid grid-cols-2 md:grid-cols-3 gap-4"> | |
| <div class="p-3 bg-gray-800 rounded-lg"> | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 bg-amber-700 rounded flex items-center justify-center mr-2">4</div> | |
| <div> | |
| <div>D4</div> | |
| <div class="text-xs text-gray-400">Tetrahedron</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-3 bg-gray-800 rounded-lg"> | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 bg-amber-700 rounded flex items-center justify-center mr-2">6</div> | |
| <div> | |
| <div>D6</div> | |
| <div class="text-xs text-gray-400">Cube</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-3 bg-gray-800 rounded-lg"> | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 bg-amber-700 rounded flex items-center justify-center mr-2">8</div> | |
| <div> | |
| <div>D8</div> | |
| <div class="text-xs text-gray-400">Octahedron</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-3 bg-gray-800 rounded-lg"> | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 bg-amber-700 rounded flex items-center justify-center mr-2">10</div> | |
| <div> | |
| <div>D10</div> | |
| <div class="text-xs text-gray-400">Pentagonal Trapezohedron</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-3 bg-gray-800 rounded-lg"> | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 bg-amber-700 rounded flex items-center justify-center mr-2">12</div> | |
| <div> | |
| <div>D12</div> | |
| <div class="text-xs text-gray-400">Dodecahedron</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-3 bg-gray-800 rounded-lg"> | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 bg-amber-700 rounded flex items-center justify-center mr-2">20</div> | |
| <div> | |
| <div>D20</div> | |
| <div class="text-xs text-gray-400">Icosahedron</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize variables | |
| let selectedSides = 20; | |
| let scene, camera, renderer, world; | |
| let diceObjects = []; | |
| let diceMeshes = []; | |
| let diceValues = {}; | |
| let isRolling = false; | |
| // Initialize Three.js and Cannon.js | |
| function init() { | |
| // Set up Three.js scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x0a0a0a); | |
| // Add ambient light | |
| const ambientLight = new THREE.AmbientLight(0x404040, 1.5); | |
| scene.add(ambientLight); | |
| // Add directional light | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
| directionalLight.position.set(1, 1, 1).normalize(); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.mapSize.width = 1024; | |
| directionalLight.shadow.mapSize.height = 1024; | |
| scene.add(directionalLight); | |
| // Add point light | |
| const pointLight = new THREE.PointLight(0xd4af37, 1, 10); | |
| pointLight.position.set(0, 3, 2); | |
| scene.add(pointLight); | |
| // Set up camera | |
| camera = new THREE.PerspectiveCamera(45, document.getElementById('canvas-container').clientWidth / document.getElementById('canvas-container').clientHeight, 0.1, 1000); | |
| camera.position.set(0, 5, 10); | |
| camera.lookAt(0, 0, 0); | |
| // Set up renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(document.getElementById('canvas-container').clientWidth, document.getElementById('canvas-container').clientHeight); | |
| renderer.shadowMap.enabled = true; | |
| document.getElementById('canvas-container').appendChild(renderer.domElement); | |
| // Set up Cannon.js world | |
| world = new CANNON.World(); | |
| world.gravity.set(0, -9.82, 0); | |
| world.broadphase = new CANNON.NaiveBroadphase(); | |
| world.solver.iterations = 10; | |
| // Create a ground plane | |
| const groundShape = new CANNON.Plane(); | |
| const groundBody = new CANNON.Body({ mass: 0 }); | |
| groundBody.addShape(groundShape); | |
| groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); | |
| groundBody.position.y = -2; | |
| world.addBody(groundBody); | |
| // Add a ground plane visual | |
| const groundGeometry = new THREE.PlaneGeometry(20, 20, 1, 1); | |
| const groundMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x333333, | |
| roughness: 0.8, | |
| metalness: 0.2 | |
| }); | |
| const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial); | |
| groundMesh.rotation.x = -Math.PI / 2; | |
| groundMesh.receiveShadow = true; | |
| scene.add(groundMesh); | |
| // Add decorative elements | |
| addDecorativeElements(); | |
| // Handle window resize | |
| window.addEventListener('resize', onWindowResize); | |
| // Set up dice selection | |
| setupDiceSelection(); | |
| // Set up dice previews | |
| setupDicePreviews(); | |
| // Set up roll button | |
| document.getElementById('rollButton').addEventListener('click', rollDice); | |
| // Start animation loop | |
| animate(); | |
| } | |
| // Set up dice previews | |
| function setupDicePreviews() { | |
| // D4 Preview | |
| createDicePreview('d4-preview', 4, '#8b5a2b'); | |
| // D6 Preview | |
| createDicePreview('d6-preview', 6, '#9b111e'); | |
| // D8 Preview | |
| createDicePreview('d8-preview', 8, '#005b96'); | |
| // D10 Preview | |
| createDicePreview('d10-preview', 10, '#5e8c31'); | |
| // D12 Preview | |
| createDicePreview('d12-preview', 12, '#7d26cd'); | |
| // D20 Preview | |
| createDicePreview('d20-preview', 20, '#d4af37'); | |
| } | |
| // Create a dice preview | |
| function createDicePreview(containerId, sides, color) { | |
| const container = document.getElementById(containerId); | |
| container.innerHTML = ''; | |
| // Create faces based on dice type | |
| if (sides === 4) { | |
| // Tetrahedron (4 triangular faces) | |
| for (let i = 0; i < 4; i++) { | |
| const face = document.createElement('div'); | |
| face.className = 'dice-face'; | |
| face.style.backgroundColor = color; | |
| face.style.clipPath = 'polygon(50% 0%, 0% 100%, 100% 100%)'; | |
| face.style.transform = `rotateX(${i * 120}deg) rotateY(0deg) translateZ(20px)`; | |
| face.textContent = i+1; | |
| container.appendChild(face); | |
| } | |
| container.style.transform = 'rotateX(20deg) rotateY(20deg)'; | |
| } | |
| else if (sides === 6) { | |
| // Cube (6 square faces) | |
| const positions = [ | |
| { transform: 'rotateY(0deg) translateZ(20px)', color: color }, // front | |
| { transform: 'rotateY(180deg) translateZ(20px)', color: color }, // back | |
| { transform: 'rotateY(90deg) translateZ(20px)', color: color }, // right | |
| { transform: 'rotateY(-90deg) translateZ(20px)', color: color }, // left | |
| { transform: 'rotateX(90deg) translateZ(20px)', color: color }, // top | |
| { transform: 'rotateX(-90deg) translateZ(20px)', color: color } // bottom | |
| ]; | |
| for (let i = 0; i < 6; i++) { | |
| const face = document.createElement('div'); | |
| face.className = 'dice-face'; | |
| face.style.backgroundColor = positions[i].color; | |
| face.style.transform = positions[i].transform; | |
| face.textContent = i+1; | |
| container.appendChild(face); | |
| } | |
| container.style.transform = 'rotateX(20deg) rotateY(20deg)'; | |
| } | |
| else if (sides === 8) { | |
| // Octahedron (8 triangular faces) | |
| for (let i = 0; i < 8; i++) { | |
| const face = document.createElement('div'); | |
| face.className = 'dice-face'; | |
| face.style.backgroundColor = color; | |
| face.style.clipPath = 'polygon(50% 0%, 0% 100%, 100% 100%)'; | |
| face.style.transform = `rotateX(${i * 90}deg) rotateY(${i % 2 === 0 ? 0 : 180}deg) translateZ(15px)`; | |
| face.textContent = i+1; | |
| container.appendChild(face); | |
| } | |
| container.style.transform = 'rotateX(20deg) rotateY(20deg)'; | |
| } | |
| else if (sides === 10) { | |
| // Pentagonal Trapezohedron (10 kite-shaped faces) | |
| for (let i = 0; i < 10; i++) { | |
| const face = document.createElement('div'); | |
| face.className = 'dice-face'; | |
| face.style.backgroundColor = color; | |
| face.style.clipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)'; | |
| face.style.transform = `rotateX(${i * 36}deg) rotateY(${i % 2 === 0 ? 0 : 180}deg) translateZ(15px)`; | |
| face.textContent = i+1; | |
| container.appendChild(face); | |
| } | |
| container.style.transform = 'rotateX(20deg) rotateY(20deg)'; | |
| } | |
| else if (sides === 12) { | |
| // Dodecahedron (12 pentagonal faces) | |
| for (let i = 0; i < 12; i++) { | |
| const face = document.createElement('div'); | |
| face.className = 'dice-face'; | |
| face.style.backgroundColor = color; | |
| face.style.clipPath = 'polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)'; | |
| face.style.transform = `rotateX(${i * 30}deg) rotateY(${i % 2 === 0 ? 0 : 180}deg) translateZ(15px)`; | |
| face.textContent = i+1; | |
| container.appendChild(face); | |
| } | |
| container.style.transform = 'rotateX(20deg) rotateY(20deg)'; | |
| } | |
| else if (sides === 20) { | |
| // Icosahedron (20 triangular faces) | |
| for (let i = 0; i < 20; i++) { | |
| const face = document.createElement('div'); | |
| face.className = 'dice-face'; | |
| face.style.backgroundColor = color; | |
| face.style.clipPath = 'polygon(50% 0%, 0% 100%, 100% 100%)'; | |
| face.style.transform = `rotateX(${i * 72}deg) rotateY(${i % 2 === 0 ? 0 : 180}deg) translateZ(15px)`; | |
| face.textContent = i+1; | |
| container.appendChild(face); | |
| } | |
| container.style.transform = 'rotateX(20deg) rotateY(20deg)'; | |
| } | |
| // Add rotation animation | |
| setInterval(() => { | |
| const currentTransform = container.style.transform; | |
| const matches = currentTransform.match(/rotateX\((\d+)deg\) rotateY\((\d+)deg\)/); | |
| if (matches) { | |
| const x = parseInt(matches[1]); | |
| const y = parseInt(matches[2]); | |
| container.style.transform = `rotateX(${x}deg) rotateY(${y + 2}deg)`; | |
| } | |
| }, 100); | |
| } | |
| // Set up dice selection | |
| function setupDiceSelection() { | |
| const diceSelectors = document.querySelectorAll('.dice-selector'); | |
| diceSelectors.forEach(selector => { | |
| selector.addEventListener('click', function() { | |
| // Remove active class from all selectors | |
| diceSelectors.forEach(s => s.classList.remove('active')); | |
| // Add active class to clicked selector | |
| this.classList.add('active'); | |
| // Update selected sides | |
| selectedSides = parseInt(this.getAttribute('data-sides')); | |
| }); | |
| }); | |
| // Activate D20 by default | |
| document.querySelector('.dice-selector[data-sides="20"]').classList.add('active'); | |
| } | |
| // Add decorative elements to the scene | |
| function addDecorativeElements() { | |
| // Add some floating runes | |
| const runeGeometry = new THREE.TetrahedronGeometry(0.3); | |
| const runeMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0xd4af37, | |
| transparent: true, | |
| opacity: 0.3 | |
| }); | |
| for (let i = 0; i < 10; i++) { | |
| const rune = new THREE.Mesh(runeGeometry, runeMaterial); | |
| rune.position.set( | |
| Math.random() * 10 - 5, | |
| Math.random() * 5, | |
| Math.random() * 10 - 5 | |
| ); | |
| scene.add(rune); | |
| } | |
| } | |
| // Handle window resize | |
| function onWindowResize() { | |
| camera.aspect = document.getElementById('canvas-container').clientWidth / document.getElementById('canvas-container').clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(document.getElementById('canvas-container').clientWidth, document.getElementById('canvas-container').clientHeight); | |
| } | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| // Update physics | |
| if (world) { | |
| world.step(1/60); | |
| } | |
| // Update dice meshes to match physics bodies | |
| for (let i = 0; i < diceObjects.length; i++) { | |
| if (diceObjects[i] && diceMeshes[i]) { | |
| diceMeshes[i].position.copy(diceObjects[i].position); | |
| diceMeshes[i].quaternion.copy(diceObjects[i].quaternion); | |
| } | |
| } | |
| if (renderer && scene && camera) { | |
| renderer.render(scene, camera); | |
| } | |
| } | |
| // Create dice geometry based on number of sides | |
| function createDiceGeometry(sides) { | |
| let geometry; | |
| switch(sides) { | |
| case 4: | |
| geometry = new THREE.TetrahedronGeometry(1); | |
| break; | |
| case 6: | |
| geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5); | |
| break; | |
| case 8: | |
| geometry = new THREE.OctahedronGeometry(1); | |
| break; | |
| case 10: | |
| geometry = new THREE.DodecahedronGeometry(1); | |
| break; | |
| case 12: | |
| geometry = new THREE.DodecahedronGeometry(1); | |
| break; | |
| case 20: | |
| geometry = new THREE.IcosahedronGeometry(1); | |
| break; | |
| default: | |
| geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5); | |
| } | |
| return geometry; | |
| } | |
| // Create dice physics body based on number of sides | |
| function createDiceBody(sides, position) { | |
| let shape; | |
| switch(sides) { | |
| case 4: | |
| shape = new CANNON.ConvexPolyhedron({ | |
| vertices: [ | |
| new CANNON.Vec3(1, 1, 1), | |
| new CANNON.Vec3(-1, -1, 1), | |
| new CANNON.Vec3(-1, 1, -1), | |
| new CANNON.Vec3(1, -1, -1) | |
| ], | |
| faces: [ | |
| [0, 1, 2], | |
| [0, 1, 3], | |
| [0, 2, 3], | |
| [1, 2, 3] | |
| ] | |
| }); | |
| break; | |
| case 6: | |
| shape = new CANNON.Box(new CANNON.Vec3(0.75, 0.75, 0.75)); | |
| break; | |
| case 8: | |
| shape = new CANNON.ConvexPolyhedron({ | |
| vertices: [ | |
| new CANNON.Vec3(1, 0, 0), | |
| new CANNON.Vec3(-1, 0, 0), | |
| new CANNON.Vec3(0, 1, 0), | |
| new CANNON.Vec3(0, -1, 0), | |
| new CANNON.Vec3(0, 0, 1), | |
| new CANNON.Vec3(0, 0, -1) | |
| ], | |
| faces: [ | |
| [0, 2, 4], [0, 4, 3], [0, 3, 5], [0, 5, 2], | |
| [1, 2, 5], [1, 5, 3], [1, 3, 4], [1, 4, 2] | |
| ] | |
| }); | |
| break; | |
| case 10: | |
| case 12: | |
| case 20: | |
| // For simplicity, we'll use a sphere for these | |
| shape = new CANNON.Sphere(1); | |
| break; | |
| default: | |
| shape = new CANNON.Box(new CANNON.Vec3(0.75, 0.75, 0.75)); | |
| } | |
| const body = new CANNON.Body({ | |
| mass: 1, | |
| shape: shape, | |
| position: position, | |
| linearDamping: 0.1, | |
| angularDamping: 0.1 | |
| }); | |
| return body; | |
| } | |
| // Create dice material | |
| function createDiceMaterial(sides) { | |
| let color; | |
| switch(sides) { | |
| case 4: | |
| color = 0x8b5a2b; // Brown | |
| break; | |
| case 6: | |
| color = 0x9b111e; // Red | |
| break; | |
| case 8: | |
| color = 0x005b96; // Blue | |
| break; | |
| case 10: | |
| color = 0x5e8c31; // Green | |
| break; | |
| case 12: | |
| color = 0x7d26cd; // Purple | |
| break; | |
| case 20: | |
| color = 0xd4af37; // Gold | |
| break; | |
| default: | |
| color = 0xffffff; // White | |
| } | |
| return new THREE.MeshStandardMaterial({ | |
| color: color, | |
| roughness: 0.3, | |
| metalness: 0.5, | |
| emissive: color, | |
| emissiveIntensity: 0.1 | |
| }); | |
| } | |
| // Roll the dice | |
| function rollDice() { | |
| if (isRolling) return; | |
| isRolling = true; | |
| // Clear previous dice | |
| clearDice(); | |
| // Get quantity | |
| const quantity = parseInt(document.getElementById('diceQuantity').value) || 1; | |
| const modifier = parseInt(document.getElementById('diceModifier').value) || 0; | |
| // Hide result display | |
| document.getElementById('resultDisplay').classList.add('hidden'); | |
| document.getElementById('criticalText').classList.add('hidden'); | |
| // Create new dice | |
| for (let i = 0; i < quantity; i++) { | |
| // Position the dice in a line above the ground | |
| const x = (i - (quantity - 1) / 2) * 2; | |
| const position = new CANNON.Vec3(x, 5, 0); | |
| // Create physics body | |
| const body = createDiceBody(selectedSides, position); | |
| // Add some random force to make it roll | |
| body.velocity.set( | |
| Math.random() * 5 - 2.5, | |
| Math.random() * 2, | |
| Math.random() * 5 - 2.5 | |
| ); | |
| body.angularVelocity.set( | |
| Math.random() * 5, | |
| Math.random() * 5, | |
| Math.random() * 5 | |
| ); | |
| world.addBody(body); | |
| diceObjects.push(body); | |
| // Create visual mesh | |
| const geometry = createDiceGeometry(selectedSides); | |
| const material = createDiceMaterial(selectedSides); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| scene.add(mesh); | |
| diceMeshes.push(mesh); | |
| } | |
| // Wait for dice to settle | |
| setTimeout(() => { | |
| checkDiceResults(quantity, modifier); | |
| }, 2000); | |
| } | |
| // Clear all dice from scene | |
| function clearDice() { | |
| // Remove physics bodies | |
| diceObjects.forEach(body => { | |
| world.removeBody(body); | |
| }); | |
| // Remove meshes | |
| diceMeshes.forEach(mesh => { | |
| scene.remove(mesh); | |
| if (mesh.geometry) mesh.geometry.dispose(); | |
| if (mesh.material) mesh.material.dispose(); | |
| }); | |
| diceObjects = []; | |
| diceMeshes = []; | |
| diceValues = {}; | |
| } | |
| // Check dice results after they've settled | |
| function checkDiceResults(quantity, modifier) { | |
| const results = []; | |
| let total = 0; | |
| let isCritical = false; | |
| // Generate random results (since we can't actually detect dice faces in this simplified version) | |
| for (let i = 0; i < quantity; i++) { | |
| const value = Math.floor(Math.random() * selectedSides) + 1; | |
| results.push(value); | |
| total += value; | |
| // Check for critical (20 on d20) | |
| if (selectedSides === 20 && value === 20) { | |
| isCritical = true; | |
| } | |
| } | |
| // Add modifier | |
| total += modifier; | |
| // Display results | |
| displayResults(results, total, modifier, isCritical); | |
| // Add to history | |
| addToHistory(results, total, modifier); | |
| isRolling = false; | |
| } | |
| // Display the results | |
| function displayResults(results, total, modifier, isCritical) { | |
| const resultDisplay = document.getElementById('resultDisplay'); | |
| const totalResult = document.getElementById('totalResult'); | |
| const individualResults = document.getElementById('individualResults'); | |
| const criticalText = document.getElementById('criticalText'); | |
| // Show total | |
| totalResult.textContent = total; | |
| // Show individual results | |
| let individualText = `Rolls: ${results.join(', ')}`; | |
| if (modifier !== 0) { | |
| individualText += ` + ${modifier}`; | |
| } | |
| individualResults.textContent = individualText; | |
| // Show critical if applicable | |
| if (isCritical) { | |
| criticalText.classList.remove('hidden'); | |
| } | |
| // Show the display | |
| resultDisplay.classList.remove('hidden'); | |
| } | |
| // Add roll to history | |
| function addToHistory(results, total, modifier) { | |
| const historyContainer = document.getElementById('rollHistory'); | |
| const historyItem = document.createElement('div'); | |
| historyItem.className = 'roll-history-item p-3 bg-gray-800 rounded'; | |
| let historyText = `D${selectedSides}: ${results.join(' + ')}`; | |
| if (modifier !== 0) { | |
| historyText += ` + ${modifier}`; | |
| } | |
| historyText += ` = ${total}`; | |
| historyItem.textContent = historyText; | |
| historyContainer.insertBefore(historyItem, historyContainer.firstChild); | |
| // Limit history to 10 items | |
| if (historyContainer.children.length > 10) { | |
| historyContainer.removeChild(historyContainer.lastChild); | |
| } | |
| } | |
| // Initialize when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', init); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=dricampbell/d-d-dice" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |