<!-- wp:html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Xyndoria - Dice Battle</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Import Google Fonts: Inter for UI, MedievalSharp for thematic titles */
@import url('https://fonts.googleapis.com/css2?family=MedievalSharp&family=Inter:wght@400;500;700&display=swap');
/* Base font for the body - Inter for readability */
body {
font-family: 'Inter', sans-serif;
}
/* Apply MedievalSharp to the main title */
h1.text-4xl { /* Targeting the main Xyndoria title */
font-family: 'MedievalSharp', cursive; /* 'cursive' is a generic fallback, MedievalSharp is primary */
}
/* Apply MedievalSharp to Attacker/Defender titles */
#player1-area h2,
#player2-area h2 {
font-family: 'MedievalSharp', cursive; /* 'cursive' is a generic fallback, MedievalSharp is primary */
}
/* Animation for the flame appearance */
.rolling {
animation: flame-appear-animation 1s ease-out;
}
@keyframes flame-appear-animation {
0% {
opacity: 0;
transform: scale(0.4) translateY(20px);
filter: brightness(2) saturate(3) hue-rotate(-30deg);
}
30% {
opacity: 0.9;
transform: scale(1.3) translateY(-15px);
filter: brightness(2.5) saturate(3.5) hue-rotate(-20deg);
}
50% {
opacity: 0.7;
transform: scale(1.1) translateY(-10px) rotateZ(3deg);
filter: brightness(2.2) saturate(3) hue-rotate(-25deg);
}
70% {
opacity: 1;
transform: scale(0.95) translateY(-5px) rotateZ(-2deg);
filter: brightness(1.5) saturate(1.5) hue-rotate(-10deg);
}
100% {
opacity: 1;
transform: scale(1) translateY(0px) rotateZ(0deg);
filter: brightness(1) saturate(1) hue-rotate(0deg);
}
}
.perspective-800 {
perspective: 800px;
}
.transform-style-preserve-3d {
transform-style: preserve-3d;
}
/* Styling for die selection buttons */
.die-option {
transition: background-color 0.2s ease-in-out, transform 0.1s ease;
}
/* Ensure SVGs scale within their container */
.die-visual-container svg {
width: 100%;
height: 100%;
}
</style>
</head>
<body class="bg-slate-900 flex flex-col items-center justify-center min-h-screen text-white p-4 overflow-hidden">
<h1 class="text-4xl font-bold mb-8 text-center">Dice Roller</h1>
<div class="flex flex-col md:flex-row justify-around items-start w-full max-w-4xl mx-auto">
<div id="player1-area" class="flex flex-col items-center mb-8 md:mb-0 md:mr-4 perspective-800 w-full md:w-1/2">
<h2 class="text-2xl font-semibold mb-3 text-sky-400">Attacker</h2>
<div id="player1-die-options" class="flex flex-wrap justify-center gap-2 mb-4 pt-1">
<button data-value="4" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D4</button>
<button data-value="6" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D6</button>
<button data-value="8" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D8</button>
<button data-value="10" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D10</button>
<button data-value="12" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D12</button>
</div>
<div id="die1-visual" class="die-visual-container w-24 h-24 md:w-32 md:h-32 flex items-center justify-center p-1 transform-style-preserve-3d mb-4">
{/* SVG spinner will be injected here by JS */}
</div>
</div>
<div id="player2-area" class="flex flex-col items-center md:ml-4 perspective-800 w-full md:w-1/2">
<h2 class="text-2xl font-semibold mb-3 text-pink-400">Defender</h2>
<div id="player2-die-options" class="flex flex-wrap justify-center gap-2 mb-4 pt-1">
<button data-value="4" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D4</button>
<button data-value="6" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D6</button>
<button data-value="8" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D8</button>
<button data-value="10" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D10</button>
<button data-value="12" class="die-option bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-md">D12</button>
</div>
<div id="die2-visual" class="die-visual-container w-24 h-24 md:w-32 md:h-32 flex items-center justify-center p-1 transform-style-preserve-3d mb-4">
{/* SVG spinner will be injected here by JS */}
</div>
</div>
</div>
<div class="w-full flex justify-center">
<button id="roll-button" class="mt-10 bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-lg text-xl shadow-lg transition-all duration-150 ease-in-out active:scale-95 focus:outline-none focus:ring-2 focus:ring-indigo-400">
Roll for Battle!
</button>
</div>
<div id="battle-result-area" class="mt-8 text-3xl font-bold h-12 text-center">
</div>
<script>
// Get DOM elements
const die1VisualElement = document.getElementById('die1-visual');
const player1DieOptionsContainer = document.getElementById('player1-die-options');
const die2VisualElement = document.getElementById('die2-visual');
const player2DieOptionsContainer = document.getElementById('player2-die-options');
const rollButton = document.getElementById('roll-button');
const numberColor = "#FFFFFF";
const numberOutlineColor = "#111827";
const shapeColorPlayer1 = '#4ade80';
const shapeColorPlayer2 = '#c084fc';
const spinnerBaseMetalColor = "#334155";
const spinnerEngravingColor = "#64748b";
const spinnerGemEdgeColor = "#fde047";
const compassBezelColor = "#4a3b31";
const compassFaceColor = "#d4c8b0";
const compassMarkingColor = "#3e2723";
const compassNeedleHighlight = "#f5e5c3";
const gemSettingColor = "#b08d57";
let player1SelectedDieMax = 6;
let player2SelectedDieMax = 6;
let lastRoll1 = 1;
let lastRoll2 = 1;
const selectedOptionClassPlayer1 = 'bg-sky-500';
const selectedOptionClassPlayer2 = 'bg-pink-500';
const defaultOptionClass = 'bg-slate-700';
const hoverOptionClass = 'hover:bg-slate-600';
let isRolling = false;
let rollingEffectInterval1 = { id: null };
let rollingEffectInterval2 = { id: null };
const animationDuration = 1000;
const faceChangeInterval = 80;
/**
* Sets up the die selection buttons for a player.
*/
function setupDieSelection(container, onSelectCallback, defaultDieValue, selectedClass, playerIndex) {
const buttons = container.querySelectorAll('.die-option');
buttons.forEach(button => {
if (parseInt(button.dataset.value) === defaultDieValue) {
button.classList.remove(defaultOptionClass, hoverOptionClass);
button.classList.add(selectedClass);
}
button.addEventListener('click', () => {
buttons.forEach(btn => {
btn.classList.remove(selectedClass);
btn.classList.add(defaultOptionClass, hoverOptionClass);
});
button.classList.remove(defaultOptionClass, hoverOptionClass);
button.classList.add(selectedClass);
onSelectCallback(parseInt(button.dataset.value));
if (playerIndex === 1) {
drawFantasySpinner(die1VisualElement, lastRoll1, player1SelectedDieMax, shapeColorPlayer1);
} else {
drawFantasySpinner(die2VisualElement, lastRoll2, player2SelectedDieMax, shapeColorPlayer2);
}
});
});
}
/**
* Draws the D&D themed fantasy compass spinner with the rolled number.
* @param {HTMLElement} dieVisualElement - The div element of the die to draw on.
* @param {number} number - The number to display.
* @param {number} dieType - The type of die (max value).
* @param {string} playerGemColor - The base color for the central gem.
*/
function drawFantasySpinner(dieVisualElement, number, dieType, playerGemColor) {
const fontSize = Math.max(20, 45 - (String(number).length * 5));
const uniqueGradId = `gemGrad-${playerGemColor.replace(/[^a-zA-Z0-9]/g, '')}${Math.random().toString(36).substring(2,7)}`;
const compassFaceGradId = `compassFaceGrad-${Math.random().toString(36).substring(2,7)}`;
const legibleFontFamily = "'Inter', Arial, sans-serif"; // Define legible font stack for SVG text
const svgString = `
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
<defs>
<radialGradient id="${uniqueGradId}" cx="50%" cy="50%" r="70%" fx="50%" fy="40%">
<stop offset="0%" style="stop-color:white;stop-opacity:0.8" />
<stop offset="50%" style="stop-color:${playerGemColor};stop-opacity:1" />
<stop offset="100%" style="stop-color:${playerGemColor};stop-opacity:0.7" />
</radialGradient>
<linearGradient id="${compassFaceGradId}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:${compassNeedleHighlight};stop-opacity:1" />
<stop offset="100%" style="stop-color:${compassFaceColor};stop-opacity:1" />
</linearGradient>
<filter id="dropshadow" height="130%">
<feGaussianBlur in="SourceAlpha" stdDeviation="1"/>
<feOffset dx="0.5" dy="0.5" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.5"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<path id="runicBorderPath" d="M50,2
C 20,2 2,20 2,50
S 20,98 50,98
S 98,80 98,50
S 80,2 50,2 Z
M50,8
C 24,8 8,24 8,50
S 24,92 50,92
S 92,76 92,50
S 76,8 50,8 Z" fill-rule="evenodd"/>
<clipPath id="clipRunic">
<use href="#runicBorderPath"/>
</clipPath>
</defs>
<use href="#runicBorderPath" fill="${spinnerBaseMetalColor}" />
<use href="#runicBorderPath" fill="none" stroke="${spinnerEngravingColor}" stroke-width="1" />
<g clip-path="url(#clipRunic)">
${[...Array(12)].map((_, i) => {
const angle = i * 30;
const rOuter = 46;
const rInner = 42;
const x1 = 50 + rOuter * Math.cos(angle * Math.PI / 180);
const y1 = 50 + rOuter * Math.sin(angle * Math.PI / 180);
const x2 = 50 + rInner * Math.cos((angle + 5) * Math.PI / 180);
const y2 = 50 + rInner * Math.sin((angle + 5) * Math.PI / 180);
const x3 = 50 + rInner * Math.cos((angle - 5) * Math.PI / 180);
const y3 = 50 + rInner * Math.sin((angle - 5) * Math.PI / 180);
return `<polygon points="${x1},${y1} ${x2},${y2} ${x3},${y3}" fill="${spinnerEngravingColor}99" />`;
}).join('')}
</g>
<circle cx="50" cy="50" r="49" fill="${compassBezelColor}" stroke="#00000033" stroke-width="0.5" /> {/* Outer Bezel */}
<circle cx="50" cy="50" r="47" fill="none" stroke="${compassMarkingColor}" stroke-width="1.5" /> {/* Inner Bezel Line */}
<circle cx="50" cy="50" r="46" fill="none" stroke="${compassBezelColor}" stroke-width="2" stroke-dasharray="4 2" /> {/* Dashed Detail */}
<circle cx="50" cy="50" r="44" fill="url(#${compassFaceGradId})" stroke="${compassMarkingColor}" stroke-width="0.5" /> {/* Compass Face */}
<g stroke="${compassMarkingColor}" stroke-width="1.5" fill="${compassMarkingColor}"> {/* Compass Markings */}
<polygon points="50,8 47,15 53,15" />
<text x="50" y="6.5" text-anchor="middle" font-size="4" fill="${compassMarkingColor}" font-weight="bold" font-family="${legibleFontFamily}">N</text>
<polygon points="92,50 85,47 85,53" />
<text x="95" y="51.5" text-anchor="middle" font-size="4" fill="${compassMarkingColor}" font-weight="bold" font-family="${legibleFontFamily}">E</text>
<polygon points="50,92 47,85 53,85" />
<text x="50" y="97" text-anchor="middle" font-size="4" fill="${compassMarkingColor}" font-weight="bold" font-family="${legibleFontFamily}">S</text>
<polygon points="8,50 15,47 15,53" />
<text x="4.5" y="51.5" text-anchor="middle" font-size="4" fill="${compassMarkingColor}" font-weight="bold" font-family="${legibleFontFamily}">W</text>
${[45, 135, 225, 315].map(angle => `
<line x1="50" y1="50"
x2="${50 + 38 * Math.cos(angle * Math.PI / 180)}"
y2="${50 + 38 * Math.sin(angle * Math.PI / 180)}"
stroke-width="1" />
<circle cx="${50 + 41 * Math.cos(angle * Math.PI / 180)}"
cy="${50 + 41 * Math.sin(angle * Math.PI / 180)}"
r="1" fill="${compassMarkingColor}" />
`).join('')}
${[...Array(12)].map((_, i) => {
const angle = i * 30;
if (angle % 90 !== 0) {
return `<line x1="${50 + 40 * Math.cos(angle * Math.PI/180)}"
y1="${50 + 40 * Math.sin(angle * Math.PI/180)}"
x2="${50 + 43 * Math.cos(angle * Math.PI/180)}"
y2="${50 + 43 * Math.sin(angle * Math.PI/180)}"
stroke-width="0.75" />`;
} return '';
}).join('')}
</g>
<circle cx="50" cy="50" r="28" fill="${gemSettingColor}" filter="url(#dropshadow)" /> {/* Gem Setting */}
<circle cx="50" cy="50" r="26" fill="none" stroke="${compassNeedleHighlight}" stroke-width="1" /> {/* Gem Setting Highlight */}
<circle cx="50" cy="50" r="25" fill="url(#${uniqueGradId})" stroke="${playerGemColor}" stroke-width="0.5"/> {/* Central Gem */}
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle"
font-size="${fontSize}" fill="${numberColor}" font-weight="bold"
font-family="${legibleFontFamily}"
stroke="${numberOutlineColor}" stroke-width="0.8" paint-order="stroke"
style="text-shadow: 0.5px 0.5px 1.5px ${numberOutlineColor}cc; user-select: none;">
${number}
</text>
</svg>
`;
dieVisualElement.innerHTML = svgString;
}
/**
* Starts the visual effect of a die rolling (rapidly changing faces).
*/
function startRollingEffect(dieVisualElement, intervalHolder, maxDieValue, playerColor) {
if (intervalHolder.id) clearInterval(intervalHolder.id);
intervalHolder.id = setInterval(() => {
const randomFace = Math.floor(Math.random() * maxDieValue) + 1;
drawFantasySpinner(dieVisualElement, randomFace, maxDieValue, playerColor);
}, faceChangeInterval);
}
/**
* Stops the visual rolling effect and displays the final number on a die.
*/
function stopRollingEffect(dieVisualElement, intervalHolder, finalNumber, dieType, playerColor) {
clearInterval(intervalHolder.id);
intervalHolder.id = null;
drawFantasySpinner(dieVisualElement, finalNumber, dieType, playerColor);
}
/**
* Handles the dice roll event for the battle.
*/
function handleRoll() {
if (isRolling) return;
isRolling = true;
rollButton.disabled = true;
rollButton.textContent = "Rolling...";
die1VisualElement.classList.add('rolling');
die2VisualElement.classList.add('rolling');
startRollingEffect(die1VisualElement, rollingEffectInterval1, player1SelectedDieMax, shapeColorPlayer1);
startRollingEffect(die2VisualElement, rollingEffectInterval2, player2SelectedDieMax, shapeColorPlayer2);
setTimeout(() => {
const finalResult1 = Math.floor(Math.random() * player1SelectedDieMax) + 1;
const finalResult2 = Math.floor(Math.random() * player2SelectedDieMax) + 1;
lastRoll1 = finalResult1;
lastRoll2 = finalResult2;
stopRollingEffect(die1VisualElement, rollingEffectInterval1, finalResult1, player1SelectedDieMax, shapeColorPlayer1);
stopRollingEffect(die2VisualElement, rollingEffectInterval2, finalResult2, player2SelectedDieMax, shapeColorPlayer2);
die1VisualElement.classList.remove('rolling');
die2VisualElement.classList.remove('rolling');
isRolling = false;
rollButton.disabled = false;
rollButton.textContent = "Roll for Battle!";
}, animationDuration);
}
rollButton.addEventListener('click', handleRoll);
// Initial setup
document.addEventListener('DOMContentLoaded', () => {
setupDieSelection(player1DieOptionsContainer, (val) => {
player1SelectedDieMax = val;
drawFantasySpinner(die1VisualElement, lastRoll1, player1SelectedDieMax, shapeColorPlayer1);
}, player1SelectedDieMax, 1);
setupDieSelection(player2DieOptionsContainer, (val) => {
player2SelectedDieMax = val;
drawFantasySpinner(die2VisualElement, lastRoll2, player2SelectedDieMax, shapeColorPlayer2);
}, player2SelectedDieMax, 2);
drawFantasySpinner(die1VisualElement, lastRoll1, player1SelectedDieMax, shapeColorPlayer1);
drawFantasySpinner(die2VisualElement, lastRoll2, player2SelectedDieMax, shapeColorPlayer2);
rollButton.textContent = "Roll for Battle!";
});
</script>
</body>
</html>
<!-- /wp:html -->
Dice Roller
Attacker
{/* SVG spinner will be injected here by JS */}
Defender
{/* SVG spinner will be injected here by JS */}
