专栏文章
3D Dice Roll
休闲·娱乐参与者 2已保存评论 4
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 4 条
- 当前快照
- 1 份
- 快照标识符
- @min8gfb4
- 此快照首次捕获于
- 2025/12/01 22:16 3 个月前
- 此快照最后确认于
- 2025/12/01 22:16 3 个月前
访问次数 :
。由于我极其可能会咕咕咕所以你们也可以直接把代码扔给 AI 进行修改。
食用方法:建一个文本文档,后缀名 .html,用记事本编辑,把代码复制进去,双击运行。
Update
- v.1.0:初版;
- v.1.1:将 Lucky Streak Card 的效果改为了:接下来的 3 次投掷中,扔到 1,2 不会计入总共投掷次数。
- v.1.2:修复 Lucky Streak Card 的 bug;通过修改投掷结果的出现方式,解决页面元素上下移动的问题;骰子被投掷时,原则上不能做别的动作(升级、使用技能卡);优化 Roll History,冻结表头,修改奇怪 bug。
- v.1.2.1:修改了 Roll history 中升级后积分变化错误的问题。
To do list
- 增加一种新的技能卡。
- 增加历史获胜所需次数并记录最好成绩(前提是修改 Play Again 的逻辑,这个类似多测清空,bug 极其的多)。
Code
v.1.0
HTML<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Dice Roll Animation</title>
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#f97316',
dark: '#1e293b',
light: '#f8fafc'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'shake': 'shake 0.5s ease-in-out',
},
keyframes: {
shake: {
'0%, 100%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(-5px)' },
'50%': { transform: 'translateX(5px)' },
'75%': { transform: 'translateX(-5px)' },
}
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.preserve-3d {
transform-style: preserve-3d;
}
.perspective {
perspective: 1000px;
}
.backface-hidden {
backface-visibility: hidden;
}
.dice-shadow {
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
}
.glass-effect {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.modal-content {
transform: translateY(-20px);
opacity: 0;
transition: all 0.3s ease-out;
}
.modal-open .modal-content {
transform: translateY(0);
opacity: 1;
}
.modal-open .modal-backdrop {
opacity: 1;
visibility: visible;
}
}
/* Custom styles for 3D dice */
.dice-scene {
perspective: 1500px;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.dice-container {
position: relative;
width: 100px;
height: 100px;
transform-style: preserve-3d;
transition: transform 1s ease-out;
transform: rotateX(20deg) rotateY(20deg);
}
.dice-face {
position: absolute;
width: 100px;
height: 100px;
border-radius: 8px;
background-color: white;
border: 2px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
/* Dice face positions */
.face-1 { transform: translateZ(50px); }
.face-2 { transform: rotateY(90deg) translateZ(50px); }
.face-3 { transform: rotateY(180deg) translateZ(50px); }
.face-4 { transform: rotateY(-90deg) translateZ(50px); }
.face-5 { transform: rotateX(90deg) translateZ(50px); }
.face-6 { transform: rotateX(-90deg) translateZ(50px); }
/* Dots on dice faces */
.dot {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: black;
}
/* Dot positions for each face */
.face-1 .dot { top: 40px; left: 40px; }
.face-2 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-2 .dot:nth-child(2) { top: 60px; left: 60px; }
.face-3 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-3 .dot:nth-child(2) { top: 40px; left: 40px; }
.face-3 .dot:nth-child(3) { top: 60px; left: 60px; }
.face-4 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-4 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-4 .dot:nth-child(3) { top: 60px; left: 20px; }
.face-4 .dot:nth-child(4) { top: 60px; left: 60px; }
.face-5 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-5 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-5 .dot:nth-child(3) { top: 40px; left: 40px; }
.face-5 .dot:nth-child(4) { top: 60px; left: 20px; }
.face-5 .dot:nth-child(5) { top: 60px; left: 60px; }
.face-6 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-6 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-6 .dot:nth-child(3) { top: 40px; left: 20px; }
.face-6 .dot:nth-child(4) { top: 40px; left: 60px; }
.face-6 .dot:nth-child(5) { top: 60px; left: 20px; }
.face-6 .dot:nth-child(6) { top: 60px; left: 60px; }
.dice-result {
transition: all 0.5s ease-out;
}
.result-shine {
animation: shine 0.5s ease-out;
}
.score-increase {
color: #10b981;
animation: scorePopup 1s ease-out;
}
.score-decrease {
color: #ef4444;
animation: scorePopup 1s ease-out;
}
.score-neutral {
color: #6b7280;
animation: scorePopup 1s ease-out;
}
/* Locked color option styles */
.color-option.locked {
position: relative;
opacity: 0.5;
cursor: not-allowed;
}
.color-option.locked::after {
content: '\f023'; /* Lock icon from Font Awesome */
font-family: 'FontAwesome';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(0, 0, 0, 0.5);
font-size: 16px;
}
@keyframes shine {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
@keyframes scorePopup {
0% {
transform: translateY(0);
opacity: 0;
}
50% {
transform: translateY(-10px);
opacity: 1;
}
100% {
transform: translateY(-20px);
opacity: 0;
}
}
.confetti {
position: absolute;
width: 10px;
height: 10px;
background-color: #f97316;
animation: confetti-fall 3s ease-in-out infinite;
}
@keyframes confetti-fall {
0% {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
@keyframes lock-particle {
0% {
transform: translate(0, 0);
opacity: 1;
}
100% {
transform: translate(var(--tx), var(--ty));
opacity: 0;
}
}
/* Game Rules Modal Styles */
.game-rules-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
visibility: hidden;
box-sizing: border-box;
}
.game-rules-modal .modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease-out;
}
.game-rules-modal .modal-content {
position: relative;
background-color: white;
border-radius: 1rem;
max-width: 90%;
max-height: 80vh;
width: 500px;
overflow-y: auto;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.game-rules-modal .modal-header {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.game-rules-modal .modal-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
transition: color 0.2s ease-in-out;
}
.game-rules-modal .close-button:hover {
color: #1e293b;
}
.game-rules-modal .modal-body {
padding: 1.5rem;
line-height: 1.6;
color: #374151;
}
.game-rules-modal .modal-body h3 {
font-size: 1.1rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #1e293b;
}
.game-rules-modal .modal-body p {
margin-bottom: 1rem;
}
.game-rules-modal .modal-body ul {
margin-bottom: 1rem;
padding-left: 1.5rem;
list-style-type: disc;
}
.game-rules-modal .modal-body li {
margin-bottom: 0.5rem;
}
.game-rules-modal .modal-body strong {
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
}
.game-rules-modal .modal-footer button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-weight: 500;
transition: background-color 0.2s ease-in-out;
}
.game-rules-modal .modal-footer button:hover {
background-color: #2563eb;
}
/* Game Rules Button Styles */
.game-rules-button {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 40;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease-in-out;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.game-rules-button:hover {
background-color: #2563eb;
transform: translateY(-2px);
}
.game-rules-button i {
font-size: 1.1rem;
}
</style>
</head>
<body class="bg-gradient-to-br from-light to-gray-200 min-h-screen flex flex-col items-center justify-center p-4 m-0">
<!-- Game Rules Button -->
<button id="game-rules-button" class="game-rules-button">
<i class="fa fa-book"></i>
<span>Game Rules</span>
</button>
<!-- Game Rules Modal -->
<div id="game-rules-modal" class="game-rules-modal">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">3D Dice Game Rules</h2>
<button class="close-button">×</button>
</div>
<div class="modal-body">
<p>Welcome to the 3D Dice Game! This is an exciting game that combines luck, strategy, and upgrading mechanics. Below is the complete game rules explanation:</p>
<h3>Basic Gameplay</h3>
<ul>
<li>Click the "Roll Dice" button to roll the dice</li>
<li>The dice will randomly show a number between 1-6</li>
<li>Gain or lose points based on the number rolled</li>
<li>The goal is to obtain the highest tier "Unique" dice</li>
</ul>
<h3>Score System</h3>
<ul>
<li>Rolling 1 or 2: Lose 1 point (multiplier not applied)</li>
<li>Rolling 3: No points gained or lost</li>
<li>Rolling 4: Gain 1 point</li>
<li>Rolling 5: Gain 2 points</li>
<li>Rolling 6: Gain 3 points</li>
</ul>
<h3>Dice Upgrade System</h3>
<p>There are 10 different tiers of dice in the game, from common to rare:</p>
<ul>
<li><strong>Empty</strong> (Initial) - Score Multiplier ×1.0</li>
<li><strong>Common</strong> - Score Multiplier ×1.1</li>
<li><strong>Unusual</strong> - Score Multiplier ×1.2</li>
<li><strong>Rare</strong> - Score Multiplier ×1.3</li>
<li><strong>Epic</strong> - Score Multiplier ×1.4</li>
<li><strong>Legendary</strong> - Score Multiplier ×1.5</li>
<li><strong>Mythic</strong> - Score Multiplier ×1.6</li>
<li><strong>Ultra</strong> - Score Multiplier ×1.7</li>
<li><strong>Super</strong> - Score Multiplier ×1.8</li>
<li><strong>Unique</strong> - Score Multiplier ×1.9</li>
</ul>
<p>Each upgrade costs 5 points. After upgrading, you can choose to use the new dice or continue using the old one.</p>
<h3>Item System</h3>
<p>When upgrading, you have a chance to obtain item cards that can be used at critical moments:</p>
<ul>
<li><strong>Double Points Card</strong>: Doubles the points from one roll</li>
<li><strong>No Penalty Card</strong>: Prevents point loss when rolling 1 or 2</li>
<li><strong>Lucky Streak Card</strong>: Gives you 3 free dice rolls that don't count towards your total</li>
</ul>
<p>Items can be stockpiled and each card can only be used once. Double Points Cards and No Penalty Cards can be used simultaneously. Lucky Streak Cards give you 3 free rolls that don't count towards your total roll count.</p>
<h3>Game Rules</h3>
<ul>
<li>Initial score is 10 points</li>
<li>Score multiplier changes based on the current dice tier being used</li>
<li>Game over when score is less than 0</li>
<li>Game victory when obtaining the Unique dice</li>
<li>Unlocked dice colors can be switched at any time</li>
</ul>
<h3>Operation Tips</h3>
<ul>
<li>Click on dice color options to switch the dice being used</li>
<li>Click the "Upgrade Dice" button to upgrade your dice (requires 5 points)</li>
<li>Click the "Use" button next to item cards to use them</li>
<li>Press the ESC key to close the rules window</li>
</ul>
<p>Good luck, and may you successfully obtain the highest tier Unique dice!</p>
</div>
<div class="modal-footer">
<button class="close-button">Got it</button>
</div>
</div>
</div>
<div class="w-full max-w-md mx-auto glass-effect rounded-2xl p-6 dice-shadow relative z-10">
<h1 class="text-3xl font-bold text-center text-dark mb-8">3D Dice Roll</h1>
<div class="flex flex-col items-center justify-center mb-8">
<!-- Dice display area -->
<div id="dice-display" class="w-48 h-48 flex items-center justify-center mb-4 bg-gray-100 rounded-lg">
<!-- Dice scene for 3D perspective -->
<div class="dice-scene">
<div id="dice" class="dice-container">
<!-- Dice faces will be inserted here by JavaScript -->
</div>
</div>
</div>
<!-- Result display -->
<div id="result-display" class="text-2xl font-bold text-center mb-4 hidden">
Result: <span id="result-value" class="text-primary">0</span>
</div>
<!-- Score display -->
<div id="score-display" class="text-xl font-bold text-center mb-2">
Score: <span id="score-value" class="text-secondary">10</span>
<span id="score-change" class="ml-2 text-sm font-normal"></span>
</div>
<!-- Roll count display -->
<div id="roll-count-display" class="text-lg font-medium text-center mb-4">
Rolls: <span id="roll-count-value" class="text-gray-700">0</span>
</div>
<!-- Roll button -->
<button id="roll-button" class="bg-primary hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-opacity-50">
<i class="fa fa-random mr-2"></i> Roll Dice
</button>
<!-- Upgrade button and current tier display -->
<div class="flex flex-col items-center mt-4">
<div id="current-tier" class="text-sm text-gray-600 mb-2">
Current Dice: <span class="font-semibold text-primary">Empty</span>
</div>
<div id="tier-multiplier-display" class="text-sm text-gray-600 mb-2">
Tier Multiplier: <span class="font-semibold text-purple-500">×1.0</span>
</div>
<button id="upgrade-button" class="bg-secondary hover:bg-orange-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-orange-300 focus:ring-opacity-50">
<i class="fa fa-arrow-up mr-1"></i> Upgrade Dice <span id="upgrade-cost">(Cost: 5)</span>
</button>
</div>
<!-- Game message display -->
<div id="game-message" class="mt-4 text-center font-semibold hidden"></div>
<!-- Items display -->
<div id="items-display" class="mt-6 grid grid-cols-2 gap-4">
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-plus-circle text-green-500 text-xl mr-2"></i>
<span class="font-medium">Double Points Card</span>
</div>
<div class="flex items-center">
<span id="double-points-count" class="bg-green-100 text-green-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-double-points" class="bg-green-500 hover:bg-green-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Doubles your score change for one roll</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-shield text-blue-500 text-xl mr-2"></i>
<span class="font-medium">No Penalty Card</span>
</div>
<div class="flex items-center">
<span id="no-penalty-count" class="bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-no-penalty" class="bg-blue-500 hover:bg-blue-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Prevents score loss when rolling 1 or 2</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-star text-yellow-500 text-xl mr-2"></i>
<span class="font-medium">Lucky Streak Card</span>
</div>
<div class="flex items-center">
<span id="lucky-streak-count" class="bg-yellow-100 text-yellow-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-lucky-streak" class="bg-yellow-500 hover:bg-yellow-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Get 3 free dice rolls (not counted in total)</p>
</div>
</div>
<!-- Free rolls indicator -->
<div id="free-rolls-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fa fa-star mr-1" style="color: #f59e0b;"></i>
<span>Free Rolls: <span id="free-rolls-count">0</span></span>
</span>
</div>
<!-- Active item indicator -->
<div id="active-item-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
<i id="active-item-icon" class="mr-1"></i>
<span id="active-item-text">Active Item: None</span>
</span>
</div>
</div>
<!-- Dice color selection -->
<div class="mb-8">
<label class="block text-sm font-medium text-gray-700 mb-2">Dice Rarity</label>
<div class="grid grid-cols-5 gap-4">
<!-- Empty -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #FFFFFF; border: 1px solid #dddddd;" data-color="#FFFFFF"></button>
<span class="text-xs text-center text-gray-600">Empty</span>
</div>
<!-- Common -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #7EEF6D;" data-color="#7EEF6D"></button>
<span class="text-xs text-center text-gray-600">Common</span>
</div>
<!-- Unusual -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #FFE65D;" data-color="#FFE65D"></button>
<span class="text-xs text-center text-gray-600">Unusual</span>
</div>
<!-- Rare -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #4d52e3;" data-color="#4d52e3"></button>
<span class="text-xs text-center text-gray-600">Rare</span>
</div>
<!-- Epic -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #861FDE;" data-color="#861FDE"></button>
<span class="text-xs text-center text-gray-600">Epic</span>
</div>
<!-- Legendary -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #DE1F1F;" data-color="#DE1F1F"></button>
<span class="text-xs text-center text-gray-600">Legendary</span>
</div>
<!-- Mythic -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #1fdbde;" data-color="#1fdbde"></button>
<span class="text-xs text-center text-gray-600">Mythic</span>
</div>
<!-- Ultra -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #ff2b75;" data-color="#ff2b75"></button>
<span class="text-xs text-center text-gray-600">Ultra</span>
</div>
<!-- Super -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #2bffa3;" data-color="#2bffa3"></button>
<span class="text-xs text-center text-gray-600">Super</span>
</div>
<!-- Unique -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #555555;" data-color="#555555"></button>
<span class="text-xs text-center text-gray-600">Unique</span>
</div>
</div>
</div>
<!-- History log -->
<div class="bg-white rounded-lg p-4 h-32 overflow-y-auto">
<h2 class="text-lg font-semibold text-center mb-2">Roll History</h2>
<ul id="history-list" class="text-sm">
<!-- History items will be inserted here by JavaScript -->
</ul>
</div>
</div>
<footer class="mt-8 text-center text-gray-600 text-sm">
<p>Click the button to roll the dice and see the result!</p>
</footer>
<script>
// Set up global error handler
window.addEventListener('error', function(event) {
console.error('Global error caught:', event.error);
// Try to get roll button element
const rollButton = document.getElementById('roll-button');
if (rollButton && rollButton.disabled) {
console.warn('Error during dice roll, re-enabling button');
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
// Try to show error message
const gameMessage = document.getElementById('game-message');
if (gameMessage) {
gameMessage.textContent = 'An error occurred. Please try again.';
gameMessage.className = 'mt-4 text-center font-semibold text-red-500';
gameMessage.classList.remove('hidden');
}
}
});
// DOM elements
const diceElement = document.getElementById('dice');
const rollButton = document.getElementById('roll-button');
const resultDisplay = document.getElementById('result-display');
const resultValue = document.getElementById('result-value');
const historyList = document.getElementById('history-list');
const colorOptions = document.querySelectorAll('.color-option');
const scoreDisplay = document.getElementById('score-display');
const scoreValue = document.getElementById('score-value');
const scoreChange = document.getElementById('score-change');
const upgradeButton = document.getElementById('upgrade-button');
const currentTierDisplay = document.getElementById('current-tier');
const gameMessageDisplay = document.getElementById('game-message');
const rollCountDisplay = document.getElementById('roll-count-display');
const rollCountValue = document.getElementById('roll-count-value');
const upgradeCost = document.getElementById('upgrade-cost');
// Item related DOM elements
const doublePointsCount = document.getElementById('double-points-count');
const noPenaltyCount = document.getElementById('no-penalty-count');
const luckyStreakCount = document.getElementById('lucky-streak-count');
const useDoublePointsButton = document.getElementById('use-double-points');
const useNoPenaltyButton = document.getElementById('use-no-penalty');
const useLuckyStreakButton = document.getElementById('use-lucky-streak');
const activeItemIndicator = document.getElementById('active-item-indicator');
const freeRollsIndicator = document.getElementById('free-rolls-indicator');
const freeRollsCountElement = document.getElementById('free-rolls-count');
// Game state
let currentScore = 10; // Initial score
let currentTier = 0; // Initial dice tier (0 = Empty)
let gameOver = false; // Game over flag
let rollCount = 0; // Number of dice rolls
// Item system
let doublePointsCards = 0; // Number of double points cards
let noPenaltyCards = 0; // Number of no penalty cards
let luckyStreakCards = 0; // Number of lucky streak cards
let freeRolls = 0; // Number of free rolls available
let activeItems = { // Currently active items
double: 0, // Number of active double points cards
noPenalty: false // Whether no penalty card is active
};
// Dice tiers configuration
const diceTiers = [
{ name: 'Empty', color: '#FFFFFF' },
{ name: 'Common', color: '#7EEF6D' },
{ name: 'Unusual', color: '#FFE65D' },
{ name: 'Rare', color: '#4d52e3' },
{ name: 'Epic', color: '#861FDE' },
{ name: 'Legendary', color: '#DE1F1F' },
{ name: 'Mythic', color: '#1fdbde' },
{ name: 'Ultra', color: '#ff2b75' },
{ name: 'Super', color: '#2bffa3' },
{ name: 'Unique', color: '#555555' }
];
// Initialize 3D dice
function initializeDice() {
console.log('Initializing dice...');
// Create 6 faces for the dice
const faces = [1, 2, 3, 4, 5, 6];
faces.forEach(faceNumber => {
const face = document.createElement('div');
face.className = `dice-face face-${faceNumber}`;
// Add dots to the face based on the number
for (let i = 0; i < faceNumber; i++) {
const dot = document.createElement('div');
dot.className = 'dot';
face.appendChild(dot);
}
diceElement.appendChild(face);
console.log(`Added face ${faceNumber}`);
});
console.log('Dice initialized with faces:', diceElement.children.length);
// Set initial position to show face 1 clearly
diceElement.style.transform = 'rotateX(0deg) rotateY(0deg)';
}
// Get random rotation values for the dice
function getRandomRotation() {
// Determine which face we want to show
const targetFace = Math.floor(Math.random() * 6) + 1;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let currentX = 0, currentY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
currentX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
currentY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Set target rotation based on target face
// These values are precisely calibrated to show the correct face
let targetX = 0, targetY = 0;
switch(targetFace) {
case 1: // Front face (Z+) - visible when no rotation
targetX = 0;
targetY = 0;
break;
case 2: // Left face (X-) - visible when Y rotated 270 degrees
targetX = 0;
targetY = 270;
break;
case 3: // Back face (Z-) - visible when Y rotated 180 degrees
targetX = 0;
targetY = 180;
break;
case 4: // Right face (X+) - visible when Y rotated 90 degrees
targetX = 0;
targetY = 90;
break;
case 5: // Top face (Y-) - visible when X rotated -90 degrees
targetX = -90;
targetY = 0;
break;
case 6: // Bottom face (Y+) - visible when X rotated 90 degrees
targetX = 90;
targetY = 0;
break;
}
// Calculate the shortest path to the target rotation
// This prevents large rotation values from accumulating
let diffX = targetX - currentX;
let diffY = targetY - currentY;
// Normalize the difference to the range [-180, 180] to find the shortest path
diffX = ((diffX + 180) % 360) - 180;
diffY = ((diffY + 180) % 360) - 180;
// Add multiple full rotations for spinning effect (2-4 full rotations)
const fullRotations = 2 + Math.floor(Math.random() * 3);
// Calculate final rotation with full spins
// We add full rotations in the direction of the shortest path
const spinDirectionX = diffX >= 0 ? 1 : -1;
const spinDirectionY = diffY >= 0 ? 1 : -1;
const finalX = currentX + diffX + spinDirectionX * fullRotations * 360;
const finalY = currentY + diffY + spinDirectionY * fullRotations * 360;
// Add a tiny bit of randomness to make it look more natural
// But not enough to change which face is visible
const randomX = (Math.random() - 0.5) * 2;
const randomY = (Math.random() - 0.5) * 2;
return {
x: finalX + randomX,
y: finalY + randomY,
targetFace: targetFace // Return the target face so we don't have to recalculate it
};
}
// Roll the dice function
function rollDice() {
// Check if game is over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if there are free rolls available
const isFreeRoll = freeRolls > 0;
// Increment roll count only if not a free roll
if (!isFreeRoll) {
rollCount++;
console.log(`Roll count: ${rollCount}`);
} else {
// Decrement free rolls count
freeRolls--;
updateFreeRollsDisplay();
console.log(`Free roll used. Remaining free rolls: ${freeRolls}`);
}
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
console.log('Rolling dice...');
console.log('Button state before disable:', rollButton.disabled);
// Disable button during animation
rollButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
console.log('Button state after disable:', rollButton.disabled);
// Hide result display
resultDisplay.classList.add('hidden');
try {
// Set animation duration
const duration = 2000; // Fixed duration for consistent experience
// Get random rotation values for the final position
const rotationData = getRandomRotation();
console.log('Rotation data:', rotationData);
console.log('Target face:', rotationData.targetFace);
// Animate the dice using JavaScript for more control
animateDice(duration, rotationData);
// Set a safety timeout to ensure button is re-enabled even if something goes wrong
setTimeout(() => {
if (rollButton.disabled) {
console.warn('Safety timeout: Re-enabling roll button');
enableRollButton();
showGameMessage('The dice roll took longer than expected. Please try again.', 'text-orange-500');
}
}, duration + 1000); // Add 1 second buffer
} catch (error) {
console.error('Error during dice roll:', error);
// Re-enable button if there's an error
enableRollButton();
showGameMessage('An error occurred during the dice roll. Please try again.', 'text-red-500');
}
}
// Animate the dice with spin animation
function animateDice(duration, rotationData) {
console.log('Animate dice called with rotationData:', rotationData);
const startTime = performance.now();
const finalRotation = rotationData;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let startX = 0, startY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
startX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
startY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Function to handle each animation frame
function animate(currentTime) {
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
// Apply easing function for smooth, natural animation
const easedProgress = easeOutCubic(progress);
// Calculate current rotation - smooth continuous rotation
const currentX = startX + (finalRotation.x - startX) * easedProgress;
const currentY = startY + (finalRotation.y - startY) * easedProgress;
// Spin animation: rotate in place
diceElement.style.transform = `rotateX(${currentX}deg) rotateY(${currentY}deg)`;
// Continue animation if not complete
if (progress < 1) {
requestAnimationFrame(animate);
} else {
// Animation complete - ensure we're at the exact target rotation
diceElement.style.transform = `rotateX(${finalRotation.x}deg) rotateY(${finalRotation.y}deg)`;
// Show result after a short delay to ensure rotation is complete
console.log('Animation complete, calling finalizeAnimation...');
setTimeout(() => {
finalizeAnimation(finalRotation);
}, 50);
}
}
// Start the animation
requestAnimationFrame(animate);
}
// Helper function to normalize angles to the range [-180, 180]
function normalizeAngle(angle) {
angle = angle % 360;
if (angle > 180) angle -= 360;
if (angle < -180) angle += 360;
return angle;
}
// Easing function for smooth, natural animation with gentle acceleration and deceleration
// Uses a cubic easing function that starts slow, accelerates, then slows down at the end
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// Calculate score change based on dice roll result
function calculateScoreChange(result) {
let baseChange = 0;
switch(result) {
case 1:
case 2:
baseChange = -1;
break;
case 3:
baseChange = 0;
break;
case 4:
baseChange = 1;
break;
case 5:
baseChange = 2;
break;
case 6:
baseChange = 3;
break;
default:
baseChange = 0;
}
// Apply current tier multiplier
// If losing points (baseChange < 0), use multiplier of 1 instead of tier multiplier
const tierMultiplier = 1 + currentTier / 10;
let finalChange;
if (baseChange < 0) {
// For point loss, use multiplier of 1 regardless of tier
finalChange = baseChange * 1;
} else {
// For point gain or neutral, use tier multiplier
finalChange = baseChange * tierMultiplier;
}
// Apply active item effects
const itemsUsed = {
double: activeItems.double,
noPenalty: activeItems.noPenalty
};
// Apply no penalty card first
if (activeItems.noPenalty && baseChange < 0) {
finalChange = 0;
}
// Apply double points cards
if (activeItems.double > 0) {
finalChange = finalChange * Math.pow(2, activeItems.double);
}
// Round to 2 decimal places to avoid floating point precision issues
finalChange = Math.round(finalChange * 100) / 100;
return {
baseChange: baseChange,
finalChange: finalChange,
itemsUsed: itemsUsed,
tierMultiplier: tierMultiplier
};
}
// Enable roll button
function enableRollButton() {
console.log('Button state before enable:', rollButton.disabled);
// Directly enable the button
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
console.log('Button state after enable:', rollButton.disabled);
// Double-check and force enable if needed
if (rollButton.disabled) {
console.warn('Forcing button enable');
setTimeout(() => {
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
console.log('Button state after forced enable:', rollButton.disabled);
}, 100);
}
}
// Finalize the animation and show result
function finalizeAnimation(rotationData) {
console.log('=== Finalize animation called ===');
console.log('Rotation data:', rotationData);
const finalRotation = rotationData;
// Use the target face directly instead of recalculating
const result = finalRotation.targetFace;
console.log(`Final result: ${result} (should show face ${result})`);
// Show result display
resultValue.textContent = result;
resultDisplay.classList.remove('hidden');
// Add shine effect to result
resultDisplay.classList.add('result-shine');
setTimeout(() => {
resultDisplay.classList.remove('result-shine');
}, 500);
// Calculate and update score
console.log('Calculating score change...');
const scoreChangeData = calculateScoreChange(result);
console.log('Score change data:', scoreChangeData);
currentScore += scoreChangeData.finalChange;
console.log('Updated score:', currentScore);
// Update score display with animation
console.log('Updating score display...');
updateScoreDisplay(scoreChangeData);
// Enable button
console.log('Enabling roll button...');
enableRollButton();
// Add to history - include whether it was a free roll
addToHistory(result, scoreChangeData, isFreeRoll);
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Create confetti effect if result is 6
if (result === 6) {
createConfetti();
}
// Debug: log final state
console.log(`Final precise rotation: X=${finalRotation.x}°, Y=${finalRotation.y}°`);
console.log(`Displayed result: ${result}`);
console.log(`Score change: ${scoreChangeData.finalChange}, Current score: ${currentScore}`);
console.log('=== finalizeAnimation completed ===');
}
// Update score display with animation
function updateScoreDisplay(scoreChangeData) {
console.log('=== updateScoreDisplay called ===');
console.log('Score change data:', scoreChangeData);
const change = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
const tierMultiplier = scoreChangeData.tierMultiplier;
console.log('Change:', change, 'Base change:', baseChange, 'Items used:', itemsUsed);
// Update the score value - round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
scoreValue.textContent = currentScore;
console.log('Score value updated to:', currentScore);
// Clear previous score change display
scoreChange.textContent = '';
scoreChange.className = 'ml-2 text-sm font-normal';
console.log('Cleared previous score change display');
// Show score change with appropriate styling
if (change > 0) {
scoreChange.textContent = `+${change}`;
scoreChange.classList.add('score-increase');
console.log('Score increase:', change);
} else if (change < 0) {
scoreChange.textContent = `${change}`;
scoreChange.classList.add('score-decrease');
console.log('Score decrease:', change);
} else {
scoreChange.textContent = `±0`;
scoreChange.classList.add('score-neutral');
console.log('Score neutral');
}
// Show item effect message if items were used
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
// Create container for item effects
const itemEffectsContainer = document.createElement('div');
itemEffectsContainer.className = 'flex flex-wrap gap-2 mt-1';
// Add tier multiplier effect if applicable
if (tierMultiplier !== 1) {
const tierEffect = document.createElement('div');
tierEffect.className = 'text-xs text-purple-500';
tierEffect.textContent = `Tier Multiplier ×${tierMultiplier.toFixed(1)}!`;
itemEffectsContainer.appendChild(tierEffect);
}
// Add no penalty effect
if (itemsUsed.noPenalty) {
const noPenaltyEffect = document.createElement('div');
noPenaltyEffect.className = 'text-xs text-blue-500';
noPenaltyEffect.textContent = `No Penalty! (Score protected from ${baseChange < 0 ? baseChange : 0} loss)`;
itemEffectsContainer.appendChild(noPenaltyEffect);
}
// Add double points effect
if (itemsUsed.double > 0) {
const doublePointsEffect = document.createElement('div');
doublePointsEffect.className = 'text-xs text-green-500';
// Calculate multiplier
const multiplier = Math.pow(2, itemsUsed.double);
let calculationText = `${baseChange}`;
// Apply tier multiplier for display
let displayChange = baseChange * tierMultiplier;
// Apply no penalty first for display
if (itemsUsed.noPenalty && baseChange < 0) {
displayChange = 0;
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} → 0)`;
} else if (tierMultiplier !== 1) {
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} = ${displayChange.toFixed(2)})`;
}
// Show multiplication steps if multiple double cards used
if (itemsUsed.double > 1) {
for (let i = 0; i < itemsUsed.double; i++) {
calculationText += ` × 2`;
}
calculationText += ` = ${(displayChange * multiplier).toFixed(2)}`;
} else {
calculationText += ` × 2 = ${(displayChange * multiplier).toFixed(2)}`;
}
doublePointsEffect.textContent = `Double Points ×${itemsUsed.double}! (${calculationText})`;
itemEffectsContainer.appendChild(doublePointsEffect);
}
// Add to score display
scoreDisplay.appendChild(itemEffectsContainer);
// Remove after animation
setTimeout(() => {
scoreDisplay.removeChild(itemEffectsContainer);
}, 1000);
}
// Reset score change display after animation completes
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
// Clear active items after score update
console.log('Checking if items need to be cleared...');
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
console.log('Clearing active items...');
clearActiveItems();
console.log('Updating items display...');
updateItemsDisplay();
}
// Check game state after score update
console.log('Checking game state...');
checkGameState();
console.log('=== updateScoreDisplay completed ===');
}
// Handle dice upgrade
function upgradeDice() {
// Check if game is already over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if already at maximum tier
if (currentTier >= diceTiers.length - 1) {
// If not already game over, end the game with win condition
if (!gameOver) {
gameOver = true;
endGame(true);
} else {
showGameMessage('Congratulations! You already have the Unique dice!', 'text-green-500');
}
return;
}
// Calculate required score for upgrade (5 + currentTier)
const requiredScore = 5 + currentTier;
// Check if enough score to upgrade
if (currentScore < requiredScore) {
showGameMessage(`Not enough score to upgrade! Need ${requiredScore} points.`, 'text-orange-500');
// Add shake animation to score display
scoreDisplay.classList.add('animate-shake');
setTimeout(() => {
scoreDisplay.classList.remove('animate-shake');
}, 500);
return;
}
// Deduct score for upgrade
currentScore -= requiredScore;
// Round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
// Update score display
scoreValue.textContent = currentScore;
showScoreChange(-5);
// Increase tier
currentTier++;
// Update current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Update tier multiplier display
updateTierMultiplierDisplay();
// Unlock the new dice color
unlockDiceColor(currentTier);
// Update color selection UI
updateColorSelection(currentTier);
// Update upgrade cost display
updateUpgradeCostDisplay();
// Change to the new dice color
console.log(`Changing dice color to ${diceTiers[currentTier].color} (${diceTiers[currentTier].name})`);
changeDiceColor(diceTiers[currentTier].color);
// Check if this upgrade reached the maximum tier
checkGameState();
// Randomly get an item
const itemChance = Math.random();
if (itemChance < 0.32) { // 32% chance to get double points card
doublePointsCards++;
} else if (itemChance < 0.64) { // 32% chance to get no penalty card
noPenaltyCards++;
} else if (itemChance < 0.98) { // 34% chance to get nothing
// Do nothing
} else { // 2% chance to get lucky streak card
luckyStreakCards++;
}
// Update items display to show new counts
updateItemsDisplay();
// Update items display
updateItemsDisplay();
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Add to history
addToHistory('UPGRADE', -5);
// Add item to history if obtained
if (itemChance < 0.32) {
addToHistory('ITEM', 'Double Points Card');
} else if (itemChance < 0.64) {
addToHistory('ITEM', 'No Penalty Card');
} else if (itemChance >= 0.98) {
addToHistory('ITEM', 'Lucky Streak Card');
}
// Check game state after upgrade
checkGameState();
}
// Show score change temporarily
function showScoreChange(change) {
scoreChange.textContent = change > 0 ? `+${change}` : change;
scoreChange.className = `ml-2 text-sm font-normal ${change > 0 ? 'score-increase' : 'score-decrease'}`;
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
}
// Unlock a dice color option
function unlockDiceColor(tierIndex) {
if (tierIndex >= 0 && tierIndex < colorOptions.length) {
const option = colorOptions[tierIndex];
// Show particle effect on the lock before unlocking
createLockParticles(option);
// Add a small delay to show the particles before unlocking
setTimeout(() => {
option.disabled = false;
option.classList.remove('locked');
}, 300);
}
}
// Show game message
function showGameMessage(message, className) {
gameMessageDisplay.textContent = message;
gameMessageDisplay.className = `mt-4 text-center font-semibold ${className}`;
gameMessageDisplay.classList.remove('hidden');
// Hide message after 3 seconds
setTimeout(() => {
if (!gameOver) {
gameMessageDisplay.classList.add('hidden');
}
}, 3000);
}
// Check game state (win/lose conditions)
function checkGameState() {
console.log('=== checkGameState called ===');
console.log('Current score:', currentScore, 'Game over:', gameOver);
// Check if score is negative (lose condition)
if (currentScore < 0 && !gameOver) {
console.log('Score is negative, ending game...');
gameOver = true;
endGame(false);
}
// Check if reached Unique dice (win condition)
if (currentTier === diceTiers.length - 1 && !gameOver) {
console.log('Reached Unique dice, ending game...');
gameOver = true;
endGame(true);
}
// Check if score is low (warning)
if (currentScore >= 0 && currentScore <= 5 && !gameOver) {
showLowScoreWarning();
}
console.log('=== checkGameState completed ===');
}
// Update items display
function updateItemsDisplay() {
// Update counts
doublePointsCount.textContent = doublePointsCards;
noPenaltyCount.textContent = noPenaltyCards;
luckyStreakCount.textContent = luckyStreakCards;
// Enable/disable buttons based on available items
useDoublePointsButton.disabled = doublePointsCards <= 0 || gameOver;
useNoPenaltyButton.disabled = noPenaltyCards <= 0 || activeItems.noPenalty || gameOver;
useLuckyStreakButton.disabled = luckyStreakCards <= 0 || gameOver;
// Update free rolls display
updateFreeRollsDisplay();
}
// Update free rolls display
function updateFreeRollsDisplay() {
if (freeRolls > 0) {
freeRollsCountElement.textContent = freeRolls;
freeRollsIndicator.classList.remove('hidden');
} else {
freeRollsIndicator.classList.add('hidden');
}
}
// Update upgrade cost display
function updateUpgradeCostDisplay() {
if (currentTier >= diceTiers.length - 1) {
// Already at maximum tier
upgradeCost.textContent = '(Max Tier)';
} else {
const requiredScore = 5 + currentTier;
upgradeCost.textContent = `(Cost: ${requiredScore})`;
}
}
// Use double points card
function useDoublePointsCard() {
if (doublePointsCards > 0 && !gameOver) {
activeItems.double++;
doublePointsCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('Double Points Card activated! Next roll will double your score change.', 'text-green-500');
}
}
// Use no penalty card
function useNoPenaltyCard() {
if (noPenaltyCards > 0 && !activeItems.noPenalty && !gameOver) {
activeItems.noPenalty = true;
noPenaltyCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('No Penalty Card activated! Next roll won\'t lose points.', 'text-blue-500');
}
}
// Use lucky streak card
function useLuckyStreakCard() {
if (luckyStreakCards > 0 && !gameOver) {
freeRolls += 3;
luckyStreakCards--;
// Update UI
updateItemsDisplay();
// Show message
showGameMessage('Lucky Streak Card activated! You have 3 free rolls!', 'text-yellow-500');
// Add to history
addToHistory('ITEM USE', 'Lucky Streak Card');
}
}
// Show active item indicator
function showActiveItemIndicator() {
// Clear previous content
activeItemIndicator.innerHTML = '';
// Check if any items are active
if (activeItems.double === 0 && !activeItems.noPenalty) {
activeItemIndicator.className = 'mt-3 text-center hidden';
return;
}
// Create container for active items
const itemsContainer = document.createElement('div');
itemsContainer.className = 'flex flex-wrap justify-center gap-2';
// Add double points cards indicator
if (activeItems.double > 0) {
const doublePointsIndicator = document.createElement('span');
doublePointsIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800';
doublePointsIndicator.innerHTML = `
<i class="fa fa-plus-circle mr-1" style="color: #10b981;"></i>
<span>Double Points ×${activeItems.double}</span>
`;
itemsContainer.appendChild(doublePointsIndicator);
}
// Add no penalty card indicator
if (activeItems.noPenalty) {
const noPenaltyIndicator = document.createElement('span');
noPenaltyIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800';
noPenaltyIndicator.innerHTML = `
<i class="fa fa-shield mr-1" style="color: #3b82f6;"></i>
<span>No Penalty</span>
`;
itemsContainer.appendChild(noPenaltyIndicator);
}
// Add indicators to container
activeItemIndicator.appendChild(itemsContainer);
activeItemIndicator.className = 'mt-3 text-center';
}
// Clear active items
function clearActiveItems() {
activeItems = {
double: 0,
noPenalty: false
};
showActiveItemIndicator();
}
// End the game and display appropriate message
function endGame(isWin) {
console.log('=== endGame called ===');
console.log('Is win:', isWin, 'Current score:', currentScore);
// Disable all interactive elements
rollButton.disabled = true;
upgradeButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
// Disable all interactive elements
colorOptions.forEach(option => {
option.disabled = true;
option.classList.add('opacity-50', 'cursor-not-allowed');
});
// Disable item buttons
useDoublePointsButton.disabled = true;
useNoPenaltyButton.disabled = true;
// Hide active item indicator
activeItemIndicator.classList.add('hidden');
// Create game over message element
const gameOverMessage = document.createElement('div');
gameOverMessage.className = `mt-4 p-4 rounded-lg text-center font-bold ${isWin ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`;
// Format final score to 2 decimal places
const formattedScore = currentScore.toFixed(2);
if (isWin) {
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Congratulations!</div>
<p>You won the game!</p>
<p>You obtained the Unique dice!</p>
<p class="mt-2 text-sm">Final Score: ${formattedScore}</p>
<p class="mt-1 text-sm">Total Rolls: ${rollCount}</p>
<p class="mt-1 text-sm">Double Points Cards: ${doublePointsCards}</p>
<p class="mt-1 text-sm">No Penalty Cards: ${noPenaltyCards}</p>
<p class="mt-1 text-sm">Lucky Streak Cards: ${luckyStreakCards}</p>
<p class="mt-1 text-sm">Final Tier Multiplier: ×${(1 + currentTier / 10).toFixed(1)}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
createConfetti();
} else {
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Game Over!</div>
<p>Your score went negative.</p>
<p>Better luck next time!</p>
<p class="mt-2 text-sm">Final Score: ${formattedScore}</p>
<p class="mt-1 text-sm">Total Rolls: ${rollCount}</p>
<p class="mt-1 text-sm">Double Points Cards: ${doublePointsCards}</p>
<p class="mt-1 text-sm">No Penalty Cards: ${noPenaltyCards}</p>
<p class="mt-1 text-sm">Lucky Streak Cards: ${luckyStreakCards}</p>
<p class="mt-1 text-sm">Final Tier Multiplier: ×${(1 + currentTier / 10).toFixed(1)}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
}
// Add restart button event listener
gameOverMessage.querySelector('#play-again').addEventListener('click', () => {
window.location.reload();
});
// Replace game message display with game over message
const gameMessageContainer = gameMessageDisplay.parentElement;
gameMessageContainer.replaceChild(gameOverMessage, gameMessageDisplay);
gameMessageDisplay = gameOverMessage;
// Add game end to history
addToHistory(isWin ? 'GAME WIN' : 'GAME OVER', 0);
console.log('=== endGame completed ===');
}
// Show warning when score is low
function showLowScoreWarning() {
// Only show warning if not already showing
if (gameMessageDisplay.classList.contains('hidden')) {
showGameMessage('Warning: Low score! Risk of game over.', 'text-orange-500');
}
}
// Update color selection UI to show the currently selected color
function updateColorSelection(selectedIndex) {
// Remove active state from all color options
colorOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-offset-2', 'ring-primary'));
// Add active state to the selected color option
if (selectedIndex >= 0 && selectedIndex < colorOptions.length) {
colorOptions[selectedIndex].classList.add('ring-2', 'ring-offset-2', 'ring-primary');
}
}
// Add result to history
function addToHistory(result, scoreChangeData, isFreeRoll = false) {
const now = new Date();
const timeString = now.toLocaleTimeString();
// Determine score change display and color
let scoreChangeText = '';
let scoreChangeClass = '';
let actionText = '';
if (result === 'UPGRADE') {
actionText = 'Upgraded dice';
scoreChangeText = `${scoreChangeData}`;
scoreChangeClass = 'text-red-500';
} else if (result === 'ITEM') {
actionText = `Received <span class="font-bold text-purple-500">${scoreChangeData}</span>`;
scoreChangeText = '+1';
scoreChangeClass = 'text-purple-500';
} else if (result === 'GAME WIN' || result === 'GAME OVER') {
actionText = result;
scoreChangeText = '';
scoreChangeClass = '';
} else {
const scoreChange = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
actionText = `Rolled a <span class="font-bold text-primary">${result}</span>`;
// Add free roll indicator if applicable
if (isFreeRoll) {
actionText += ` <span class="text-yellow-500">(Free Roll)</span>`;
}
// Add item effect indicators if applicable
const activeItemsText = [];
if (itemsUsed && itemsUsed.double > 0) {
activeItemsText.push(`<span class="text-green-500">(Double Points ×${itemsUsed.double})</span>`);
}
if (itemsUsed && itemsUsed.noPenalty) {
activeItemsText.push(`<span class="text-blue-500">(No Penalty)</span>`);
}
if (activeItemsText.length > 0) {
actionText += ` ${activeItemsText.join(' ')}`;
}
if (scoreChange > 0) {
scoreChangeText = `+${scoreChange}`;
scoreChangeClass = 'text-green-500';
} else if (scoreChange < 0) {
scoreChangeText = `${scoreChange}`;
scoreChangeClass = 'text-red-500';
} else {
scoreChangeText = '±0';
scoreChangeClass = 'text-gray-500';
}
// Show base change if different from final change (items were used)
if ((itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) && baseChange !== scoreChange) {
scoreChangeText += ` <span class="text-xs">(Base: ${baseChange > 0 ? '+' : ''}${baseChange})</span>`;
}
}
const listItem = document.createElement('li');
listItem.className = 'py-1 border-b border-gray-100 flex justify-between items-center';
listItem.innerHTML = `
<span><span class="text-gray-500">${timeString}</span>: ${actionText}</span>
<span class="font-medium ${scoreChangeClass}">${scoreChangeText}</span>
`;
historyList.prepend(listItem);
// Keep only last 5 history items
if (historyList.children.length > 5) {
historyList.removeChild(historyList.lastChild);
}
}
// Create confetti effect
function createConfetti() {
const confettiContainer = document.createElement('div');
confettiContainer.className = 'fixed inset-0 pointer-events-none overflow-hidden';
document.body.appendChild(confettiContainer);
// Create 50 confetti pieces
for (let i = 0; i < 50; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
// Random position
const posX = Math.random() * 100;
const delay = Math.random() * 3;
const duration = 3 + Math.random() * 2;
// Random colors
const colors = ['#3b82f6', '#f97316', '#10b981', '#ef4444', '#8b5cf6'];
const color = colors[Math.floor(Math.random() * colors.length)];
confetti.style.left = `${posX}%`;
confetti.style.backgroundColor = color;
confetti.style.animationDelay = `${delay}s`;
confetti.style.animationDuration = `${duration}s`;
confettiContainer.appendChild(confetti);
}
// Remove confetti container after animation completes
setTimeout(() => {
document.body.removeChild(confettiContainer);
}, 5000);
}
// Create particle effect on lock
function createLockParticles(lockElement) {
// Get lock element position
const rect = lockElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// Create particle container
const particleContainer = document.createElement('div');
particleContainer.className = 'absolute pointer-events-none';
particleContainer.style.left = `${centerX}px`;
particleContainer.style.top = `${centerY}px`;
particleContainer.style.transform = 'translate(-50%, -50%)';
document.body.appendChild(particleContainer);
// Create 15 particles
for (let i = 0; i < 15; i++) {
const particle = document.createElement('div');
particle.className = 'lock-particle';
// Random direction and distance
const angle = Math.random() * Math.PI * 2;
const distance = 10 + Math.random() * 20;
const tx = Math.cos(angle) * distance;
const ty = Math.sin(angle) * distance;
// Random color (use the color of the unlocked dice)
const color = lockElement.style.backgroundColor;
// Random animation duration
const duration = 0.5 + Math.random() * 0.5;
// Set particle styles
particle.style.backgroundColor = color;
particle.style.setProperty('--tx', `${tx}px`);
particle.style.setProperty('--ty', `${ty}px`);
particle.style.animation = `lock-particle ${duration}s ease-out forwards`;
particleContainer.appendChild(particle);
}
// Remove particle container after animation completes
setTimeout(() => {
document.body.removeChild(particleContainer);
}, 1000);
}
// Function to darken a color by a certain percentage
function darkenColor(color, percent) {
const hex = color.replace('#', '');
let r = parseInt(hex.substr(0, 2), 16);
let g = parseInt(hex.substr(2, 2), 16);
let b = parseInt(hex.substr(4, 2), 16);
// Darken each channel by the percentage
r = Math.max(0, Math.floor(r * (1 - percent / 100)));
g = Math.max(0, Math.floor(g * (1 - percent / 100)));
b = Math.max(0, Math.floor(b * (1 - percent / 100)));
// Convert back to hex
const darkenedHex = '#' +
r.toString(16).padStart(2, '0') +
g.toString(16).padStart(2, '0') +
b.toString(16).padStart(2, '0');
return darkenedHex;
}
// Change dice color function
function changeDiceColor(color) {
console.log(`changeDiceColor called with color: ${color}`);
const faces = document.querySelectorAll('.dice-face');
console.log(`Found ${faces.length} dice faces`);
let dotColor, borderColor;
// Special cases for high contrast
if (color.toUpperCase() === '#FFFFFF') {
// White dice - use black dots and light gray borders
dotColor = '#000000';
borderColor = '#CCCCCC';
} else if (color.toUpperCase() === '#555555') {
// Dark gray dice - use white dots and slightly lighter gray borders
dotColor = '#FFFFFF';
borderColor = '#777777';
} else {
// Calculate darker color for dots and borders (40% darker for more contrast)
dotColor = darkenColor(color, 40);
borderColor = darkenColor(color, 30); // Slightly lighter border than dots
}
faces.forEach(face => {
// Set face background color
face.style.backgroundColor = color;
// Set face border color
face.style.border = `3px solid ${borderColor}`;
// Set dots color
const dots = face.querySelectorAll('.dot');
dots.forEach(dot => {
dot.style.backgroundColor = dotColor;
// Add slight border to dots for better definition
dot.style.border = color.toUpperCase() === '#FFFFFF' ? '1px solid rgba(0, 0, 0, 0.2)' : '1px solid rgba(0, 0, 0, 0.1)';
});
});
}
// Update tier multiplier display
function updateTierMultiplierDisplay() {
const display = document.getElementById('tier-multiplier-display');
if (display) {
display.innerHTML = `Tier Multiplier: <span class="font-semibold text-purple-500">×${(1 + currentTier / 10).toFixed(1)}</span>`;
}
}
// Event listener for roll button
rollButton.addEventListener('click', rollDice);
// Event listener for upgrade button
upgradeButton.addEventListener('click', upgradeDice);
// Event listeners for item buttons
useDoublePointsButton.addEventListener('click', useDoublePointsCard);
useNoPenaltyButton.addEventListener('click', useNoPenaltyCard);
useLuckyStreakButton.addEventListener('click', useLuckyStreakCard);
// Event listeners for color options
colorOptions.forEach((option, index) => {
// Disable all color options except the first one initially
if (index !== 0) {
option.disabled = true;
option.classList.add('locked');
}
option.addEventListener('click', () => {
// Check if game is over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if color is unlocked
if (option.disabled) {
showGameMessage('You need to upgrade to unlock this dice color!', 'text-orange-500');
return;
}
const color = option.getAttribute('data-color');
changeDiceColor(color);
// Add active state to selected color
colorOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-offset-2', 'ring-primary'));
option.classList.add('ring-2', 'ring-offset-2', 'ring-primary');
// Update current tier to the selected color's tier
currentTier = index;
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
updateTierMultiplierDisplay();
});
});
// Initialize dice on page load
window.addEventListener('DOMContentLoaded', () => {
console.log('DOM fully loaded');
initializeDice();
// Set default color (first option)
if (colorOptions.length > 0) {
const defaultColor = colorOptions[0].getAttribute('data-color');
changeDiceColor(defaultColor);
colorOptions[0].classList.add('ring-2', 'ring-offset-2', 'ring-primary');
}
// Initialize current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Initialize tier multiplier display
updateTierMultiplierDisplay();
// Initialize items display
updateItemsDisplay();
// Initialize upgrade cost display
updateUpgradeCostDisplay();
// Show welcome message
setTimeout(() => {
showGameMessage('Welcome! Roll the dice to earn points and upgrade your dice!', 'text-blue-500');
}, 1000);
});
// Also try initializing on window load
window.addEventListener('load', () => {
console.log('Window loaded');
// If dice not already initialized, try again
if (diceElement.children.length === 0) {
console.log('Dice not initialized, trying again...');
initializeDice();
}
});
// Game Rules Modal Functionality
(function() {
// Get elements
const rulesButton = document.getElementById('game-rules-button');
const rulesModal = document.getElementById('game-rules-modal');
const closeButtons = rulesModal.querySelectorAll('.close-button');
const modalBackdrop = rulesModal.querySelector('.modal-backdrop');
const modalContent = rulesModal.querySelector('.modal-content');
// Function to prevent background scrolling
function preventBackgroundScroll(event) {
// Allow scrolling inside the modal content
if (modalContent.contains(event.target)) {
// Check if we're at the top or bottom of the modal content
const isAtTop = modalContent.scrollTop === 0;
const isAtBottom = modalContent.scrollTop + modalContent.clientHeight >= modalContent.scrollHeight;
// Prevent scrolling if at the top and scrolling up, or at the bottom and scrolling down
if ((isAtTop && event.deltaY < 0) || (isAtBottom && event.deltaY > 0)) {
event.preventDefault();
}
} else {
// Prevent scrolling outside the modal content
event.preventDefault();
}
}
// Function to open modal
function openModal() {
// Add event listeners to prevent background scrolling
document.addEventListener('wheel', preventBackgroundScroll, { passive: false });
document.addEventListener('touchmove', preventBackgroundScroll, { passive: false });
// Add open class to trigger animations
rulesModal.classList.add('modal-open');
// Show the modal
rulesModal.style.visibility = 'visible';
}
// Function to close modal
function closeModal() {
// Remove open class to trigger animations
rulesModal.classList.remove('modal-open');
// Remove event listeners that prevent background scrolling
document.removeEventListener('wheel', preventBackgroundScroll);
document.removeEventListener('touchmove', preventBackgroundScroll);
// Hide the modal after animation completes
setTimeout(() => {
rulesModal.style.visibility = 'hidden';
}, 300);
}
// Add event listeners
if (rulesButton) {
rulesButton.addEventListener('click', openModal);
}
// Close buttons
closeButtons.forEach(button => {
button.addEventListener('click', closeModal);
});
// Close when clicking outside the modal content
modalBackdrop.addEventListener('click', closeModal);
// Close when pressing Escape key
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && rulesModal.classList.contains('modal-open')) {
closeModal();
}
});
// Add hover effect to rules button
if (rulesButton) {
rulesButton.addEventListener('mouseenter', () => {
rulesButton.classList.add('scale-105');
});
rulesButton.addEventListener('mouseleave', () => {
rulesButton.classList.remove('scale-105');
});
}
// Add click effect to close buttons
closeButtons.forEach(button => {
button.addEventListener('mousedown', () => {
button.classList.add('scale-95');
});
button.addEventListener('mouseup', () => {
button.classList.remove('scale-95');
});
button.addEventListener('mouseleave', () => {
button.classList.remove('scale-95');
});
});
})();
</script>
</body>
</html>
v.1.1
HTML<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Dice Roll Animation</title>
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#f97316',
dark: '#1e293b',
light: '#f8fafc'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'shake': 'shake 0.5s ease-in-out',
},
keyframes: {
shake: {
'0%, 100%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(-5px)' },
'50%': { transform: 'translateX(5px)' },
'75%': { transform: 'translateX(-5px)' },
}
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.preserve-3d {
transform-style: preserve-3d;
}
.perspective {
perspective: 1000px;
}
.backface-hidden {
backface-visibility: hidden;
}
.dice-shadow {
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
}
.glass-effect {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.modal-content {
transform: translateY(-20px);
opacity: 0;
transition: all 0.3s ease-out;
}
.modal-open .modal-content {
transform: translateY(0);
opacity: 1;
}
.modal-open .modal-backdrop {
opacity: 1;
visibility: visible;
}
.trophy-animation {
animation: trophy-pulse 2s ease-in-out infinite;
}
.record-animation {
animation: record-shine 2s ease-in-out;
}
}
@keyframes trophy-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes record-shine {
0% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.7); }
70% { box-shadow: 0 0 0 15px rgba(251, 191, 36, 0); }
100% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0); }
}
/* Custom styles for 3D dice */
.dice-scene {
perspective: 1500px;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.dice-container {
position: relative;
width: 100px;
height: 100px;
transform-style: preserve-3d;
transition: transform 1s ease-out;
transform: rotateX(20deg) rotateY(20deg);
}
.dice-face {
position: absolute;
width: 100px;
height: 100px;
border-radius: 8px;
background-color: white;
border: 2px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
/* Dice face positions */
.face-1 { transform: translateZ(50px); }
.face-2 { transform: rotateY(90deg) translateZ(50px); }
.face-3 { transform: rotateY(180deg) translateZ(50px); }
.face-4 { transform: rotateY(-90deg) translateZ(50px); }
.face-5 { transform: rotateX(90deg) translateZ(50px); }
.face-6 { transform: rotateX(-90deg) translateZ(50px); }
/* Dots on dice faces */
.dot {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: black;
}
/* Dot positions for each face */
.face-1 .dot { top: 40px; left: 40px; }
.face-2 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-2 .dot:nth-child(2) { top: 60px; left: 60px; }
.face-3 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-3 .dot:nth-child(2) { top: 40px; left: 40px; }
.face-3 .dot:nth-child(3) { top: 60px; left: 60px; }
.face-4 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-4 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-4 .dot:nth-child(3) { top: 60px; left: 20px; }
.face-4 .dot:nth-child(4) { top: 60px; left: 60px; }
.face-5 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-5 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-5 .dot:nth-child(3) { top: 40px; left: 40px; }
.face-5 .dot:nth-child(4) { top: 60px; left: 20px; }
.face-5 .dot:nth-child(5) { top: 60px; left: 60px; }
.face-6 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-6 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-6 .dot:nth-child(3) { top: 40px; left: 20px; }
.face-6 .dot:nth-child(4) { top: 40px; left: 60px; }
.face-6 .dot:nth-child(5) { top: 60px; left: 20px; }
.face-6 .dot:nth-child(6) { top: 60px; left: 60px; }
.dice-result {
transition: all 0.5s ease-out;
}
.result-shine {
animation: shine 0.5s ease-out;
}
.score-increase {
color: #10b981;
animation: scorePopup 1s ease-out;
}
.score-decrease {
color: #ef4444;
animation: scorePopup 1s ease-out;
}
.score-neutral {
color: #6b7280;
animation: scorePopup 1s ease-out;
}
/* Locked color option styles */
.color-option.locked {
position: relative;
opacity: 0.5;
cursor: not-allowed;
}
.color-option.locked::after {
content: '\f023'; /* Lock icon from Font Awesome */
font-family: 'FontAwesome';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(0, 0, 0, 0.5);
font-size: 16px;
}
@keyframes shine {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
@keyframes scorePopup {
0% {
transform: translateY(0);
opacity: 0;
}
50% {
transform: translateY(-10px);
opacity: 1;
}
100% {
transform: translateY(-20px);
opacity: 0;
}
}
.confetti {
position: absolute;
width: 10px;
height: 10px;
background-color: #f97316;
animation: confetti-fall 3s ease-in-out infinite;
}
@keyframes confetti-fall {
0% {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
@keyframes lock-particle {
0% {
transform: translate(0, 0);
opacity: 1;
}
100% {
transform: translate(var(--tx), var(--ty));
opacity: 0;
}
}
/* Game Rules Modal Styles */
.game-rules-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
visibility: hidden;
box-sizing: border-box;
}
.game-rules-modal .modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease-out;
}
.game-rules-modal .modal-content {
position: relative;
background-color: white;
border-radius: 1rem;
max-width: 90%;
max-height: 80vh;
width: 500px;
overflow-y: auto;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.game-rules-modal .modal-header {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.game-rules-modal .modal-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
transition: color 0.2s ease-in-out;
}
.game-rules-modal .close-button:hover {
color: #1e293b;
}
.game-rules-modal .modal-body {
padding: 1.5rem;
line-height: 1.6;
color: #374151;
}
.game-rules-modal .modal-body h3 {
font-size: 1.1rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #1e293b;
}
.game-rules-modal .modal-body p {
margin-bottom: 1rem;
}
.game-rules-modal .modal-body ul {
margin-bottom: 1rem;
padding-left: 1.5rem;
list-style-type: disc;
}
.game-rules-modal .modal-body li {
margin-bottom: 0.5rem;
}
.game-rules-modal .modal-body strong {
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
}
.game-rules-modal .modal-footer button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-size: 1.25rem;
font-weight: 500;
transition: background-color 0.2s ease-in-out;
}
.game-rules-modal .modal-footer button:hover {
background-color: #2563eb;
}
/* Game Rules Button Styles */
.game-rules-button {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 40;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease-in-out;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.game-rules-button:hover {
background-color: #2563eb;
transform: translateY(-2px);
}
.game-rules-button i {
font-size: 1.1rem;
}
</style>
</head>
<body class="bg-gradient-to-br from-light to-gray-200 min-h-screen flex flex-col items-center justify-center p-4 m-0">
<!-- Game Rules Button -->
<button id="game-rules-button" class="game-rules-button">
<i class="fa fa-book"></i>
<span>Game Rules</span>
</button>
<!-- Game Rules Modal -->
<div id="game-rules-modal" class="game-rules-modal">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">3D Dice Game Rules</h2>
<button class="close-button">×</button>
</div>
<div class="modal-body">
<p>Welcome to the 3D Dice Game! This is an exciting game that combines luck, strategy, and upgrading mechanics. Below is the complete game rules explanation:</p>
<h3>Basic Gameplay</h3>
<ul>
<li>Click the "Roll Dice" button to roll the dice</li>
<li>The dice will randomly show a number between 1-6</li>
<li>Gain or lose points based on the number rolled</li>
<li>The goal is to obtain the highest tier "Unique" dice</li>
</ul>
<h3>Score System</h3>
<ul>
<li>Rolling 1 or 2: Lose 1 point (multiplier not applied)</li>
<li>Rolling 3: No points gained or lost</li>
<li>Rolling 4: Gain 1 point</li>
<li>Rolling 5: Gain 2 points</li>
<li>Rolling 6: Gain 3 points</li>
</ul>
<h3>Dice Upgrade System</h3>
<p>There are 10 different tiers of dice in the game, from common to rare:</p>
<ul>
<li><strong>Empty</strong> (Initial) - Score Multiplier ×1.0</li>
<li><strong>Common</strong> - Score Multiplier ×1.1</li>
<li><strong>Unusual</strong> - Score Multiplier ×1.2</li>
<li><strong>Rare</strong> - Score Multiplier ×1.3</li>
<li><strong>Epic</strong> - Score Multiplier ×1.4</li>
<li><strong>Legendary</strong> - Score Multiplier ×1.5</li>
<li><strong>Mythic</strong> - Score Multiplier ×1.6</li>
<li><strong>Ultra</strong> - Score Multiplier ×1.7</li>
<li><strong>Super</strong> - Score Multiplier ×1.8</li>
<li><strong>Unique</strong> - Score Multiplier ×1.9</li>
</ul>
<p>Each upgrade costs (5 + current tier) points. For example, upgrading from Empty (tier 0) costs 5 points, upgrading from Common (tier 1) costs 6 points, and so on. After upgrading, you can choose to use the new dice or continue using the old one.</p>
<h3>Item System</h3>
<p>When upgrading, you have a chance to obtain item cards that can be used at critical moments:</p>
<ul>
<li><strong>Double Points Card</strong>: Doubles the points from one roll</li>
<li><strong>No Penalty Card</strong>: Prevents point loss when rolling 1 or 2</li>
<li><strong>Lucky Streak Card</strong>: Next 3 rolls of 1 or 2 won't count towards your total roll count</li>
</ul>
<p>Items can be stockpiled and each card can only be used once. Double Points Cards and No Penalty Cards can be used simultaneously. Lucky Streak Cards make your next 3 rolls of 1 or 2 not count towards your total roll count, giving you a chance to recover from bad luck.</p>
<h3>Game Rules</h3>
<ul>
<li>Initial score is 10 points</li>
<li>Score multiplier changes based on the current dice tier being used</li>
<li>Game over when score is less than 0</li>
<li>Game victory when obtaining the Unique dice</li>
<li>Unlocked dice colors can be switched at any time</li>
</ul>
<h3>Operation Tips</h3>
<ul>
<li>Click on dice color options to switch the dice being used</li>
<li>Click the "Upgrade Dice" button to upgrade your dice (requires 5 points)</li>
<li>Click the "Use" button next to item cards to use them</li>
<li>Press the ESC key to close the rules window</li>
</ul>
<p>Good luck, and may you successfully obtain the highest tier Unique dice!</p>
</div>
<div class="modal-footer">
<button class="close-button">Got it</button>
</div>
</div>
</div>
<div class="w-full max-w-md mx-auto glass-effect rounded-2xl p-6 dice-shadow relative z-10">
<h1 class="text-3xl font-bold text-center text-dark mb-8">3D Dice Roll</h1>
<div class="flex flex-col items-center justify-center mb-8">
<!-- Dice display area -->
<div id="dice-display" class="w-48 h-48 flex items-center justify-center mb-4 bg-gray-100 rounded-lg">
<!-- Dice scene for 3D perspective -->
<div class="dice-scene">
<div id="dice" class="dice-container">
<!-- Dice faces will be inserted here by JavaScript -->
</div>
</div>
</div>
<!-- Result display -->
<div id="result-display" class="text-2xl font-bold text-center mb-4 hidden">
Result: <span id="result-value" class="text-primary">0</span>
</div>
<!-- Score display -->
<div id="score-display" class="text-xl font-bold text-center mb-2">
Score: <span id="score-value" class="text-secondary">10</span>
<span id="score-change" class="ml-2 text-sm font-normal"></span>
</div>
<!-- Roll count display -->
<div id="roll-count-display" class="text-lg font-medium text-center mb-4">
Rolls: <span id="roll-count-value" class="text-gray-700">0</span>
</div>
<!-- Roll button -->
<button id="roll-button" class="bg-primary hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-opacity-50">
<i class="fa fa-random mr-2"></i> Roll Dice
</button>
<!-- Upgrade button and current tier display -->
<div class="flex flex-col items-center mt-4">
<div id="current-tier" class="text-sm text-gray-600 mb-2">
Current Dice: <span class="font-semibold text-primary">Empty</span>
</div>
<div id="tier-multiplier-display" class="text-sm text-gray-600 mb-2">
Tier Multiplier: <span class="font-semibold text-purple-500">×1.0</span>
</div>
<button id="upgrade-button" class="bg-secondary hover:bg-orange-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-orange-300 focus:ring-opacity-50">
<i class="fa fa-arrow-up mr-1"></i> Upgrade Dice <span id="upgrade-cost">(Cost: 5)</span>
</button>
</div>
<!-- Game message display -->
<div id="game-message" class="mt-4 text-center font-semibold hidden"></div>
<!-- Items display -->
<div id="items-display" class="mt-6 grid grid-cols-2 gap-4">
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-plus-circle text-green-500 text-xl mr-2"></i>
<span class="font-medium">Double Points Card</span>
</div>
<div class="flex items-center">
<span id="double-points-count" class="bg-green-100 text-green-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-double-points" class="bg-green-500 hover:bg-green-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Doubles your score change for one roll</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-shield text-blue-500 text-xl mr-2"></i>
<span class="font-medium">No Penalty Card</span>
</div>
<div class="flex items-center">
<span id="no-penalty-count" class="bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-no-penalty" class="bg-blue-500 hover:bg-blue-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Prevents score loss when rolling 1 or 2</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-star text-yellow-500 text-xl mr-2"></i>
<span class="font-medium">Lucky Streak Card</span>
</div>
<div class="flex items-center">
<span id="lucky-streak-count" class="bg-yellow-100 text-yellow-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-lucky-streak" class="bg-yellow-500 hover:bg-yellow-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Next 3 rolls of 1 or 2 won't count towards your total roll count</p>
</div>
</div>
<!-- Free rolls indicator -->
<div id="free-rolls-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fa fa-star mr-1" style="color: #f59e0b;"></i>
<span>Free Rolls: <span id="free-rolls-count">0</span></span>
</span>
</div>
<!-- Active item indicator -->
<div id="active-item-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
<i id="active-item-icon" class="mr-1"></i>
<span id="active-item-text">Active Item: None</span>
</span>
</div>
</div>
<!-- Dice color selection -->
<div class="mb-8">
<label class="block text-sm font-medium text-gray-700 mb-2">Dice Rarity</label>
<div class="grid grid-cols-5 gap-4">
<!-- Empty -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #FFFFFF; border: 1px solid #dddddd;" data-color="#FFFFFF"></button>
<span class="text-xs text-center text-gray-600">Empty</span>
</div>
<!-- Common -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #7EEF6D;" data-color="#7EEF6D"></button>
<span class="text-xs text-center text-gray-600">Common</span>
</div>
<!-- Unusual -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #FFE65D;" data-color="#FFE65D"></button>
<span class="text-xs text-center text-gray-600">Unusual</span>
</div>
<!-- Rare -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #4d52e3;" data-color="#4d52e3"></button>
<span class="text-xs text-center text-gray-600">Rare</span>
</div>
<!-- Epic -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #861FDE;" data-color="#861FDE"></button>
<span class="text-xs text-center text-gray-600">Epic</span>
</div>
<!-- Legendary -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #DE1F1F;" data-color="#DE1F1F"></button>
<span class="text-xs text-center text-gray-600">Legendary</span>
</div>
<!-- Mythic -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #1fdbde;" data-color="#1fdbde"></button>
<span class="text-xs text-center text-gray-600">Mythic</span>
</div>
<!-- Ultra -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #ff2b75;" data-color="#ff2b75"></button>
<span class="text-xs text-center text-gray-600">Ultra</span>
</div>
<!-- Super -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #2bffa3;" data-color="#2bffa3"></button>
<span class="text-xs text-center text-gray-600">Super</span>
</div>
<!-- Unique -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #555555;" data-color="#555555"></button>
<span class="text-xs text-center text-gray-600">Unique</span>
</div>
</div>
</div>
<!-- History log -->
<div class="bg-white rounded-lg p-4 h-32 overflow-y-auto">
<h2 class="text-lg font-semibold text-center mb-2">Roll History</h2>
<ul id="history-list" class="text-sm">
<!-- History items will be inserted here by JavaScript -->
</ul>
</div>
</div>
<footer class="mt-8 text-center text-gray-600 text-sm">
<p>Click the button to roll the dice and see the result!</p>
</footer>
<script>
// Set up global error handler
window.addEventListener('error', function(event) {
console.error('Global error caught:', event.error);
// Try to get roll button element
const rollButton = document.getElementById('roll-button');
if (rollButton && rollButton.disabled) {
console.warn('Error during dice roll, re-enabling button');
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
// Try to show error message
const gameMessage = document.getElementById('game-message');
if (gameMessage) {
gameMessage.textContent = 'An error occurred. Please try again.';
gameMessage.className = 'mt-4 text-center font-semibold text-red-500';
gameMessage.classList.remove('hidden');
}
}
});
// DOM elements
const diceElement = document.getElementById('dice');
const rollButton = document.getElementById('roll-button');
const resultDisplay = document.getElementById('result-display');
const resultValue = document.getElementById('result-value');
const historyList = document.getElementById('history-list');
const colorOptions = document.querySelectorAll('.color-option');
const scoreDisplay = document.getElementById('score-display');
const scoreValue = document.getElementById('score-value');
const scoreChange = document.getElementById('score-change');
const upgradeButton = document.getElementById('upgrade-button');
const currentTierDisplay = document.getElementById('current-tier');
const gameMessageDisplay = document.getElementById('game-message');
const rollCountDisplay = document.getElementById('roll-count-display');
const rollCountValue = document.getElementById('roll-count-value');
const upgradeCost = document.getElementById('upgrade-cost');
// Item related DOM elements
const doublePointsCount = document.getElementById('double-points-count');
const noPenaltyCount = document.getElementById('no-penalty-count');
const luckyStreakCount = document.getElementById('lucky-streak-count');
const useDoublePointsButton = document.getElementById('use-double-points');
const useNoPenaltyButton = document.getElementById('use-no-penalty');
const useLuckyStreakButton = document.getElementById('use-lucky-streak');
const activeItemIndicator = document.getElementById('active-item-indicator');
const freeRollsIndicator = document.getElementById('free-rolls-indicator');
const freeRollsCountElement = document.getElementById('free-rolls-count');
// Game state
let currentScore = 10; // Initial score
let currentTier = 0; // Initial dice tier (0 = Empty)
let gameOver = false; // Game over flag
let rollCount = 0; // Number of dice rolls
let luckyRollsRemaining = 0; // Number of lucky rolls remaining
// Item system
let doublePointsCards = 0; // Number of double points cards
let noPenaltyCards = 0; // Number of no penalty cards
let luckyStreakCards = 0; // Number of lucky streak cards
let freeRolls = 0; // Number of free rolls available
let activeItems = { // Currently active items
double: 0, // Number of active double points cards
noPenalty: false // Whether no penalty card is active
};
// Dice tiers configuration
const diceTiers = [
{ name: 'Empty', color: '#FFFFFF' },
{ name: 'Common', color: '#7EEF6D' },
{ name: 'Unusual', color: '#FFE65D' },
{ name: 'Rare', color: '#4d52e3' },
{ name: 'Epic', color: '#861FDE' },
{ name: 'Legendary', color: '#DE1F1F' },
{ name: 'Mythic', color: '#1fdbde' },
{ name: 'Ultra', color: '#ff2b75' },
{ name: 'Super', color: '#2bffa3' },
{ name: 'Unique', color: '#555555' }
];
// Initialize 3D dice
function initializeDice() {
console.log('Initializing dice...');
// Create 6 faces for the dice
const faces = [1, 2, 3, 4, 5, 6];
faces.forEach(faceNumber => {
const face = document.createElement('div');
face.className = `dice-face face-${faceNumber}`;
// Add dots to the face based on the number
for (let i = 0; i < faceNumber; i++) {
const dot = document.createElement('div');
dot.className = 'dot';
face.appendChild(dot);
}
diceElement.appendChild(face);
console.log(`Added face ${faceNumber}`);
});
console.log('Dice initialized with faces:', diceElement.children.length);
// Set initial position to show face 1 clearly
diceElement.style.transform = 'rotateX(0deg) rotateY(0deg)';
}
// Get random rotation values for the dice
function getRandomRotation() {
// Determine which face we want to show
const targetFace = Math.floor(Math.random() * 6) + 1;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let currentX = 0, currentY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
currentX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
currentY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Set target rotation based on target face
// These values are precisely calibrated to show the correct face
let targetX = 0, targetY = 0;
switch(targetFace) {
case 1: // Front face (Z+) - visible when no rotation
targetX = 0;
targetY = 0;
break;
case 2: // Left face (X-) - visible when Y rotated 270 degrees
targetX = 0;
targetY = 270;
break;
case 3: // Back face (Z-) - visible when Y rotated 180 degrees
targetX = 0;
targetY = 180;
break;
case 4: // Right face (X+) - visible when Y rotated 90 degrees
targetX = 0;
targetY = 90;
break;
case 5: // Top face (Y-) - visible when X rotated -90 degrees
targetX = -90;
targetY = 0;
break;
case 6: // Bottom face (Y+) - visible when X rotated 90 degrees
targetX = 90;
targetY = 0;
break;
}
// Calculate the shortest path to the target rotation
// This prevents large rotation values from accumulating
let diffX = targetX - currentX;
let diffY = targetY - currentY;
// Normalize the difference to the range [-180, 180] to find the shortest path
diffX = ((diffX + 180) % 360) - 180;
diffY = ((diffY + 180) % 360) - 180;
// Add multiple full rotations for spinning effect (2-4 full rotations)
const fullRotations = 2 + Math.floor(Math.random() * 3);
// Calculate final rotation with full spins
// We add full rotations in the direction of the shortest path
const spinDirectionX = diffX >= 0 ? 1 : -1;
const spinDirectionY = diffY >= 0 ? 1 : -1;
const finalX = currentX + diffX + spinDirectionX * fullRotations * 360;
const finalY = currentY + diffY + spinDirectionY * fullRotations * 360;
// Add a tiny bit of randomness to make it look more natural
// But not enough to change which face is visible
const randomX = (Math.random() - 0.5) * 2;
const randomY = (Math.random() - 0.5) * 2;
return {
x: finalX + randomX,
y: finalY + randomY,
targetFace: targetFace // Return the target face so we don't have to recalculate it
};
}
// Roll the dice function
function rollDice() {
// Check if game is over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if there are free rolls available
const isFreeRoll = freeRolls > 0;
// Check if in lucky streak mode
const isLuckyRoll = luckyRollsRemaining > 0;
// Increment roll count only if not a free roll and not a lucky roll that will be reverted
if (!isFreeRoll && !isLuckyRoll) {
rollCount++;
console.log(`Roll count: ${rollCount}`);
} else if (isFreeRoll) {
// Decrement free rolls count
freeRolls--;
updateFreeRollsDisplay();
console.log(`Free roll used. Remaining free rolls: ${freeRolls}`);
}
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
console.log('Rolling dice...');
console.log('Button state before disable:', rollButton.disabled);
// Disable button during animation
rollButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
console.log('Button state after disable:', rollButton.disabled);
// Hide result display
resultDisplay.classList.add('hidden');
try {
// Set animation duration
const duration = 2000; // Fixed duration for consistent experience
// Get random rotation values for the final position
const rotationData = getRandomRotation();
console.log('Rotation data:', rotationData);
console.log('Target face:', rotationData.targetFace);
// Animate the dice using JavaScript for more control
animateDice(duration, rotationData);
// Set a safety timeout to ensure button is re-enabled even if something goes wrong
setTimeout(() => {
if (rollButton.disabled) {
console.warn('Safety timeout: Re-enabling roll button');
enableRollButton();
}
}, duration + 1000); // Add 1 second buffer
} catch (error) {
console.error('Error during dice roll:', error);
// Re-enable button if there's an error
enableRollButton();
showGameMessage('An error occurred during the dice roll. Please try again.', 'text-red-500');
}
}
// Animate the dice with spin animation
function animateDice(duration, rotationData) {
console.log('Animate dice called with rotationData:', rotationData);
const startTime = performance.now();
const finalRotation = rotationData;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let startX = 0, startY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
startX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
startY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Function to handle each animation frame
function animate(currentTime) {
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
// Apply easing function for smooth, natural animation
const easedProgress = easeOutCubic(progress);
// Calculate current rotation - smooth continuous rotation
const currentX = startX + (finalRotation.x - startX) * easedProgress;
const currentY = startY + (finalRotation.y - startY) * easedProgress;
// Spin animation: rotate in place
diceElement.style.transform = `rotateX(${currentX}deg) rotateY(${currentY}deg)`;
// Continue animation if not complete
if (progress < 1) {
requestAnimationFrame(animate);
} else {
// Animation complete - ensure we're at the exact target rotation
diceElement.style.transform = `rotateX(${finalRotation.x}deg) rotateY(${finalRotation.y}deg)`;
// Show result after a delay to ensure rotation is complete and CSS has applied
console.log('Animation complete, waiting before finalizing...');
setTimeout(() => {
// Double-check that the transform has been applied
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
console.log('Current transform after animation:', currentTransform);
console.log('Calling finalizeAnimation...');
finalizeAnimation(finalRotation);
}, 600);
}
}
// Start the animation
requestAnimationFrame(animate);
}
// Helper function to normalize angles to the range [-180, 180]
function normalizeAngle(angle) {
angle = angle % 360;
if (angle > 180) angle -= 360;
if (angle < -180) angle += 360;
return angle;
}
// Easing function for smooth, natural animation with gentle acceleration and deceleration
// Uses a cubic easing function that starts slow, accelerates, then slows down at the end
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// Calculate score change based on dice roll result
function calculateScoreChange(result) {
let baseChange = 0;
switch(result) {
case 1:
case 2:
baseChange = -1;
break;
case 3:
baseChange = 0;
break;
case 4:
baseChange = 1;
break;
case 5:
baseChange = 2;
break;
case 6:
baseChange = 3;
break;
default:
baseChange = 0;
}
// Apply current tier multiplier
// If losing points (baseChange < 0), use multiplier of 1 instead of tier multiplier
const tierMultiplier = 1 + currentTier / 10;
let finalChange;
if (baseChange < 0) {
// For point loss, use multiplier of 1 regardless of tier
finalChange = baseChange * 1;
} else {
// For point gain or neutral, use tier multiplier
finalChange = baseChange * tierMultiplier;
}
// Apply active item effects
const itemsUsed = {
double: activeItems.double,
noPenalty: activeItems.noPenalty
};
// Apply no penalty card first
if (activeItems.noPenalty && baseChange < 0) {
finalChange = 0;
}
// Apply double points cards
if (activeItems.double > 0) {
finalChange = finalChange * Math.pow(2, activeItems.double);
}
// Round to 2 decimal places to avoid floating point precision issues
finalChange = Math.round(finalChange * 100) / 100;
return {
baseChange: baseChange,
finalChange: finalChange,
itemsUsed: itemsUsed,
tierMultiplier: tierMultiplier
};
}
// Enable roll button
function enableRollButton() {
console.log('Button state before enable:', rollButton.disabled);
// Directly enable the button
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
console.log('Button state after enable:', rollButton.disabled);
// Double-check and force enable if needed
if (rollButton.disabled) {
console.warn('Forcing button enable');
setTimeout(() => {
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
console.log('Button state after forced enable:', rollButton.disabled);
}, 100);
}
}
// Finalize the animation and show result
function finalizeAnimation(rotationData) {
console.log('=== Finalize animation called ===');
console.log('Current time:', new Date().toISOString().split('T')[1]);
console.log('Rotation data:', rotationData);
const finalRotation = rotationData;
// Use the target face directly instead of recalculating
const result = finalRotation.targetFace;
console.log(`Final result: ${result} (should show face ${result})`);
// Verify dice is in the correct position
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
console.log('Current dice transform:', currentTransform);
// Show result display
console.log('Showing result display...');
resultValue.textContent = result;
resultDisplay.classList.remove('hidden');
// Add shine effect to result
resultDisplay.classList.add('result-shine');
setTimeout(() => {
resultDisplay.classList.remove('result-shine');
}, 500);
// Calculate and update score
console.log('Calculating score change...');
const scoreChangeData = calculateScoreChange(result);
console.log('Score change data:', scoreChangeData);
currentScore += scoreChangeData.finalChange;
console.log('Updated score:', currentScore);
// Update score display with animation
console.log('Updating score display...');
updateScoreDisplay(scoreChangeData);
// Enable button
console.log('Enabling roll button...');
enableRollButton();
// Add to history - include whether it was a free roll or lucky roll
addToHistory(result, scoreChangeData, isFreeRoll, isLuckyRoll);
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Handle lucky roll logic
if (luckyRollsRemaining > 0) {
// Decrement remaining lucky rolls
luckyRollsRemaining--;
// If rolled 1 or 2, decrement roll count
if (result === 1 || result === 2) {
rollCount--;
console.log(`Lucky roll: Reverted roll count to ${rollCount}`);
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
// Show message
showGameMessage('Lucky! This 1/2 roll doesn\'t count!', 'text-yellow-500');
}
// Update lucky rolls display
updateLuckyRollsDisplay();
// If no more lucky rolls, show message
if (luckyRollsRemaining === 0) {
setTimeout(() => {
showGameMessage('Lucky Streak ended!', 'text-yellow-500');
}, 1000);
}
}
// Create confetti effect if result is 6
if (result === 6) {
createConfetti();
}
// Debug: log final state
console.log(`Final precise rotation: X=${finalRotation.x}°, Y=${finalRotation.y}°`);
console.log(`Displayed result: ${result}`);
console.log(`Score change: ${scoreChangeData.finalChange}, Current score: ${currentScore}`);
console.log('=== finalizeAnimation completed ===');
}
// Update score display with animation
function updateScoreDisplay(scoreChangeData) {
console.log('=== updateScoreDisplay called ===');
console.log('Score change data:', scoreChangeData);
const change = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
const tierMultiplier = scoreChangeData.tierMultiplier;
console.log('Change:', change, 'Base change:', baseChange, 'Items used:', itemsUsed);
// Update the score value - round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
scoreValue.textContent = currentScore;
console.log('Score value updated to:', currentScore);
// Clear previous score change display
scoreChange.textContent = '';
scoreChange.className = 'ml-2 text-sm font-normal';
console.log('Cleared previous score change display');
// Show score change with appropriate styling
if (change > 0) {
scoreChange.textContent = `+${change}`;
scoreChange.classList.add('score-increase');
console.log('Score increase:', change);
} else if (change < 0) {
scoreChange.textContent = `${change}`;
scoreChange.classList.add('score-decrease');
console.log('Score decrease:', change);
} else {
scoreChange.textContent = `±0`;
scoreChange.classList.add('score-neutral');
console.log('Score neutral');
}
// Show item effect message if items were used
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
// Create container for item effects
const itemEffectsContainer = document.createElement('div');
itemEffectsContainer.className = 'flex flex-wrap gap-2 mt-1';
// Add tier multiplier effect if applicable
if (tierMultiplier !== 1) {
const tierEffect = document.createElement('div');
tierEffect.className = 'text-xs text-purple-500';
tierEffect.textContent = `Tier Multiplier ×${tierMultiplier.toFixed(1)}!`;
itemEffectsContainer.appendChild(tierEffect);
}
// Add no penalty effect
if (itemsUsed.noPenalty) {
const noPenaltyEffect = document.createElement('div');
noPenaltyEffect.className = 'text-xs text-blue-500';
noPenaltyEffect.textContent = `No Penalty! (Score protected from ${baseChange < 0 ? baseChange : 0} loss)`;
itemEffectsContainer.appendChild(noPenaltyEffect);
}
// Add double points effect
if (itemsUsed.double > 0) {
const doublePointsEffect = document.createElement('div');
doublePointsEffect.className = 'text-xs text-green-500';
// Calculate multiplier
const multiplier = Math.pow(2, itemsUsed.double);
let calculationText = `${baseChange}`;
// Apply tier multiplier for display
let displayChange = baseChange * tierMultiplier;
// Apply no penalty first for display
if (itemsUsed.noPenalty && baseChange < 0) {
displayChange = 0;
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} → 0)`;
} else if (tierMultiplier !== 1) {
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} = ${displayChange.toFixed(2)})`;
}
// Show multiplication steps if multiple double cards used
if (itemsUsed.double > 1) {
for (let i = 0; i < itemsUsed.double; i++) {
calculationText += ` × 2`;
}
calculationText += ` = ${(displayChange * multiplier).toFixed(2)}`;
} else {
calculationText += ` × 2 = ${(displayChange * multiplier).toFixed(2)}`;
}
doublePointsEffect.textContent = `Double Points ×${itemsUsed.double}! (${calculationText})`;
itemEffectsContainer.appendChild(doublePointsEffect);
}
// Add to score display
scoreDisplay.appendChild(itemEffectsContainer);
// Remove after animation
setTimeout(() => {
scoreDisplay.removeChild(itemEffectsContainer);
}, 1000);
}
// Reset score change display after animation completes
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
// Clear active items after score update
console.log('Checking if items need to be cleared...');
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
console.log('Clearing active items...');
clearActiveItems();
console.log('Updating items display...');
updateItemsDisplay();
}
// Check game state after score update
console.log('Checking game state...');
checkGameState();
// Update upgrade button state
updateUpgradeCostDisplay();
console.log('=== updateScoreDisplay completed ===');
}
// Handle dice upgrade
function upgradeDice() {
// Check if game is already over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if already at maximum tier
if (currentTier >= diceTiers.length - 1) {
// If not already game over, end the game with win condition
if (!gameOver) {
gameOver = true;
endGame(true);
} else {
showGameMessage('Congratulations! You already have the Unique dice!', 'text-green-500');
}
return;
}
// Calculate required score for upgrade (5 + currentTier)
const requiredScore = 5 + currentTier;
// Check if enough score to upgrade
if (currentScore < requiredScore) {
showGameMessage(`Not enough score to upgrade! Need ${requiredScore} points.`, 'text-orange-500');
// Add shake animation to score display
scoreDisplay.classList.add('animate-shake');
setTimeout(() => {
scoreDisplay.classList.remove('animate-shake');
}, 500);
return;
}
// Deduct score for upgrade
currentScore -= requiredScore;
// Round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
// Update score display
scoreValue.textContent = currentScore;
showScoreChange(-requiredScore);
// Increase tier
currentTier++;
// Update current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Update tier multiplier display
updateTierMultiplierDisplay();
// Unlock the new dice color
unlockDiceColor(currentTier);
// Update color selection UI
updateColorSelection(currentTier);
// Update upgrade cost display
updateUpgradeCostDisplay();
// Change to the new dice color
console.log(`Changing dice color to ${diceTiers[currentTier].color} (${diceTiers[currentTier].name})`);
changeDiceColor(diceTiers[currentTier].color);
// Check if this upgrade reached the maximum tier
checkGameState();
// Randomly get an item
const itemChance = Math.random();
if (itemChance < 0.30) { // 30% chance to get double points card
doublePointsCards++;
} else if (itemChance < 0.60) { // 30% chance to get no penalty card
noPenaltyCards++;
} else if (itemChance < 0.70) { // 10% chance to get lucky streak card
luckyStreakCards++;
} else { // 30% chance to get nothing
// Do nothing
}
// Update items display to show new counts
updateItemsDisplay();
// Update items display
updateItemsDisplay();
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Handle lucky roll logic
if (luckyRollsRemaining > 0) {
// Decrement remaining lucky rolls
luckyRollsRemaining--;
// If rolled 1 or 2, decrement roll count
if (result === 1 || result === 2) {
rollCount--;
console.log(`Lucky roll: Reverted roll count to ${rollCount}`);
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
// Show message
showGameMessage('Lucky! This 1/2 roll doesn\'t count!', 'text-yellow-500');
}
// Update lucky rolls display
updateLuckyRollsDisplay();
// If no more lucky rolls, show message
if (luckyRollsRemaining === 0) {
setTimeout(() => {
showGameMessage('Lucky Streak ended!', 'text-yellow-500');
}, 1000);
}
}
// Add to history
addToHistory('UPGRADE', -5);
// Add item to history if obtained
if (itemChance < 0.30) {
addToHistory('ITEM', 'Double Points Card');
} else if (itemChance < 0.60) {
addToHistory('ITEM', 'No Penalty Card');
} else if (itemChance < 0.70) {
addToHistory('ITEM', 'Lucky Streak Card');
}
// Check game state after upgrade
checkGameState();
}
// Show score change temporarily
function showScoreChange(change) {
scoreChange.textContent = change > 0 ? `+${change}` : change;
scoreChange.className = `ml-2 text-sm font-normal ${change > 0 ? 'score-increase' : 'score-decrease'}`;
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
}
// Unlock a dice color option
function unlockDiceColor(tierIndex) {
if (tierIndex >= 0 && tierIndex < colorOptions.length) {
const option = colorOptions[tierIndex];
// Show particle effect on the lock before unlocking
createLockParticles(option);
// Add a small delay to show the particles before unlocking
setTimeout(() => {
option.disabled = false;
option.classList.remove('locked');
}, 300);
}
}
// Show game message
function showGameMessage(message, className) {
gameMessageDisplay.textContent = message;
gameMessageDisplay.className = `mt-4 text-center font-semibold ${className}`;
gameMessageDisplay.classList.remove('hidden');
// Hide message after 3 seconds
setTimeout(() => {
if (!gameOver) {
gameMessageDisplay.classList.add('hidden');
}
}, 3000);
}
// Check game state (win/lose conditions)
function checkGameState() {
console.log('=== checkGameState called ===');
console.log('Current score:', currentScore, 'Game over:', gameOver);
// Check if score is negative (lose condition)
if (currentScore < 0 && !gameOver) {
console.log('Score is negative, ending game...');
gameOver = true;
endGame(false);
}
// Check if reached Unique dice (win condition)
if (currentTier === diceTiers.length - 1 && !gameOver) {
console.log('Reached Unique dice, ending game...');
gameOver = true;
endGame(true);
// Check if this is a new record
setTimeout(() => {
checkWinRecord();
}, 1000);
}
// Check if score is low (warning)
if (currentScore >= 0 && currentScore <= 5 && !gameOver) {
showLowScoreWarning();
}
console.log('=== checkGameState completed ===');
}
// Update items display
function updateItemsDisplay() {
// Update counts
doublePointsCount.textContent = doublePointsCards;
noPenaltyCount.textContent = noPenaltyCards;
luckyStreakCount.textContent = luckyStreakCards;
// Enable/disable buttons based on available items
useDoublePointsButton.disabled = doublePointsCards <= 0 || gameOver;
useNoPenaltyButton.disabled = noPenaltyCards <= 0 || activeItems.noPenalty || gameOver;
useLuckyStreakButton.disabled = luckyStreakCards <= 0 || gameOver;
// Update free rolls display
updateFreeRollsDisplay();
// Update lucky rolls display
updateLuckyRollsDisplay();
}
// Update free rolls display
function updateFreeRollsDisplay() {
if (freeRolls > 0) {
freeRollsCountElement.textContent = freeRolls;
freeRollsIndicator.classList.remove('hidden');
} else {
freeRollsIndicator.classList.add('hidden');
}
}
// Update upgrade cost display and button state
function updateUpgradeCostDisplay() {
if (currentTier >= diceTiers.length - 1) {
// Already at maximum tier
upgradeCost.textContent = '(Max Tier)';
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
} else {
const requiredScore = 5 + currentTier;
upgradeCost.textContent = `(Cost: ${requiredScore})`;
// Update button state based on available score
if (currentScore >= requiredScore && !gameOver) {
upgradeButton.disabled = false;
upgradeButton.classList.remove('opacity-70', 'cursor-not-allowed');
} else {
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
}
}
}
// Use double points card
function useDoublePointsCard() {
if (doublePointsCards > 0 && !gameOver) {
activeItems.double++;
doublePointsCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('Double Points Card activated! Next roll will double your score change.', 'text-green-500');
}
}
// Use no penalty card
function useNoPenaltyCard() {
if (noPenaltyCards > 0 && !activeItems.noPenalty && !gameOver) {
activeItems.noPenalty = true;
noPenaltyCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('No Penalty Card activated! Next roll won\'t lose points.', 'text-blue-500');
}
}
// Use lucky streak card
function useLuckyStreakCard() {
if (luckyStreakCards > 0 && !gameOver) {
luckyRollsRemaining = 3;
luckyStreakCards--;
// Update UI
updateItemsDisplay();
updateLuckyRollsDisplay();
// Show message
showGameMessage('Lucky Streak Card activated! Next 3 rolls of 1 or 2 won\'t count!', 'text-yellow-500');
// Add to history
addToHistory('ITEM USE', 'Lucky Streak Card');
}
}
// Update lucky rolls display
function updateLuckyRollsDisplay() {
if (luckyRollsRemaining > 0) {
// Check if indicator exists, if not create it
let luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (!luckyRollsIndicator) {
luckyRollsIndicator = document.createElement('div');
luckyRollsIndicator.id = 'lucky-rolls-indicator';
luckyRollsIndicator.className = 'mt-3 text-center';
// Insert after free rolls indicator
const freeRollsIndicator = document.getElementById('free-rolls-indicator');
if (freeRollsIndicator) {
freeRollsIndicator.parentNode.insertBefore(luckyRollsIndicator, freeRollsIndicator.nextSibling);
} else {
// Fallback: insert after active item indicator
const activeItemIndicator = document.getElementById('active-item-indicator');
if (activeItemIndicator) {
activeItemIndicator.parentNode.insertBefore(luckyRollsIndicator, activeItemIndicator.nextSibling);
} else {
// Fallback: insert after items display
const itemsDisplay = document.getElementById('items-display');
if (itemsDisplay) {
itemsDisplay.parentNode.insertBefore(luckyRollsIndicator, itemsDisplay.nextSibling);
}
}
}
}
// Update content
luckyRollsIndicator.innerHTML = `
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fa fa-star mr-1" style="color: #f59e0b;"></i>
<span>Lucky Rolls: <span id="lucky-rolls-count">${luckyRollsRemaining}</span> (1-2 won't count)</span>
</span>
`;
// Show indicator
luckyRollsIndicator.classList.remove('hidden');
} else {
// Remove indicator if exists
const luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (luckyRollsIndicator) {
luckyRollsIndicator.classList.add('hidden');
}
}
}
// Show active item indicator
function showActiveItemIndicator() {
// Clear previous content
activeItemIndicator.innerHTML = '';
// Check if any items are active
if (activeItems.double === 0 && !activeItems.noPenalty) {
activeItemIndicator.className = 'mt-3 text-center hidden';
return;
}
// Create container for active items
const itemsContainer = document.createElement('div');
itemsContainer.className = 'flex flex-wrap justify-center gap-2';
// Add double points cards indicator
if (activeItems.double > 0) {
const doublePointsIndicator = document.createElement('span');
doublePointsIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800';
doublePointsIndicator.innerHTML = `
<i class="fa fa-plus-circle mr-1" style="color: #10b981;"></i>
<span>Double Points ×${activeItems.double}</span>
`;
itemsContainer.appendChild(doublePointsIndicator);
}
// Add no penalty card indicator
if (activeItems.noPenalty) {
const noPenaltyIndicator = document.createElement('span');
noPenaltyIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800';
noPenaltyIndicator.innerHTML = `
<i class="fa fa-shield mr-1" style="color: #3b82f6;"></i>
<span>No Penalty</span>
`;
itemsContainer.appendChild(noPenaltyIndicator);
}
// Add indicators to container
activeItemIndicator.appendChild(itemsContainer);
activeItemIndicator.className = 'mt-3 text-center';
}
// Clear active items
function clearActiveItems() {
activeItems = {
double: 0,
noPenalty: false
};
showActiveItemIndicator();
}
// End the game and display appropriate message
function endGame(isWin) {
console.log('=== endGame called ===');
console.log('Is win:', isWin, 'Current score:', currentScore);
// Disable all interactive elements
rollButton.disabled = true;
upgradeButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
// Disable all interactive elements
colorOptions.forEach(option => {
option.disabled = true;
option.classList.add('opacity-50', 'cursor-not-allowed');
});
// Disable item buttons
useDoublePointsButton.disabled = true;
useNoPenaltyButton.disabled = true;
useLuckyStreakButton.disabled = true;
// Hide active indicators
activeItemIndicator.classList.add('hidden');
const luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (luckyRollsIndicator) {
luckyRollsIndicator.classList.add('hidden');
}
// Create game over message element
const gameOverMessage = document.createElement('div');
gameOverMessage.className = `mt-4 p-4 rounded-lg text-center font-bold ${isWin ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`;
// Format final score to 2 decimal places
const formattedScore = currentScore.toFixed(2);
if (isWin) {
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Congratulations!</div>
<p>You won the game!</p>
<p>You obtained the Unique dice!</p>
<p class="mt-2 text-sm">Final Score: ${formattedScore}</p>
<p class="mt-1 text-sm">Total Rolls: ${rollCount}</p>
<p class="mt-1 text-sm">Double Points Cards: ${doublePointsCards}</p>
<p class="mt-1 text-sm">No Penalty Cards: ${noPenaltyCards}</p>
<p class="mt-1 text-sm">Lucky Streak Cards: ${luckyStreakCards}</p>
<p class="mt-1 text-sm">Final Tier Multiplier: ×${(1 + currentTier / 10).toFixed(1)}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
createConfetti();
} else {
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Game Over!</div>
<p>Your score went negative.</p>
<p>Better luck next time!</p>
<p class="mt-2 text-sm">Final Score: ${formattedScore}</p>
<p class="mt-1 text-sm">Total Rolls: ${rollCount}</p>
<p class="mt-1 text-sm">Double Points Cards: ${doublePointsCards}</p>
<p class="mt-1 text-sm">No Penalty Cards: ${noPenaltyCards}</p>
<p class="mt-1 text-sm">Lucky Streak Cards: ${luckyStreakCards}</p>
<p class="mt-1 text-sm">Final Tier Multiplier: ×${(1 + currentTier / 10).toFixed(1)}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
}
// Add restart button event listener
gameOverMessage.querySelector('#play-again').addEventListener('click', () => {
window.location.reload();
});
// Replace game message display with game over message
const gameMessageContainer = gameMessageDisplay.parentElement;
gameMessageContainer.replaceChild(gameOverMessage, gameMessageDisplay);
gameMessageDisplay = gameOverMessage;
// Add game end to history
addToHistory(isWin ? 'GAME WIN' : 'GAME OVER', 0);
console.log('=== endGame completed ===');
}
// Show warning when score is low
function showLowScoreWarning() {
// Only show warning if not already showing
if (gameMessageDisplay.classList.contains('hidden')) {
showGameMessage('Warning: Low score! Risk of game over.', 'text-orange-500');
}
}
// Update color selection UI to show the currently selected color
function updateColorSelection(selectedIndex) {
// Remove active state from all color options
colorOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-offset-2', 'ring-primary'));
// Add active state to the selected color option
if (selectedIndex >= 0 && selectedIndex < colorOptions.length) {
colorOptions[selectedIndex].classList.add('ring-2', 'ring-offset-2', 'ring-primary');
}
}
// Add result to history
function addToHistory(result, scoreChangeData, isFreeRoll = false, isLuckyRoll = false) {
const now = new Date();
const timeString = now.toLocaleTimeString();
// Determine score change display and color
let scoreChangeText = '';
let scoreChangeClass = '';
let actionText = '';
if (result === 'UPGRADE') {
actionText = 'Upgraded dice';
scoreChangeText = `${scoreChangeData}`;
scoreChangeClass = 'text-red-500';
} else if (result === 'ITEM') {
actionText = `Received <span class="font-bold text-purple-500">${scoreChangeData}</span>`;
scoreChangeText = '+1';
scoreChangeClass = 'text-purple-500';
} else if (result === 'GAME WIN' || result === 'GAME OVER') {
actionText = result;
scoreChangeText = '';
scoreChangeClass = '';
} else {
const scoreChange = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
actionText = `Rolled a <span class="font-bold text-primary">${result}</span>`;
// Add free roll indicator if applicable
if (isFreeRoll) {
actionText += ` <span class="text-yellow-500">(Free Roll)</span>`;
}
// Add lucky roll indicator if applicable
if (isLuckyRoll) {
actionText += ` <span class="text-yellow-500">(Lucky Roll)</span>`;
}
// Add item effect indicators if applicable
const activeItemsText = [];
if (itemsUsed && itemsUsed.double > 0) {
activeItemsText.push(`<span class="text-green-500">(Double Points ×${itemsUsed.double})</span>`);
}
if (itemsUsed && itemsUsed.noPenalty) {
activeItemsText.push(`<span class="text-blue-500">(No Penalty)</span>`);
}
if (activeItemsText.length > 0) {
actionText += ` ${activeItemsText.join(' ')}`;
}
if (scoreChange > 0) {
scoreChangeText = `+${scoreChange}`;
scoreChangeClass = 'text-green-500';
} else if (scoreChange < 0) {
scoreChangeText = `${scoreChange}`;
scoreChangeClass = 'text-red-500';
} else {
scoreChangeText = '±0';
scoreChangeClass = 'text-gray-500';
}
// Show base change if different from final change (items were used)
if ((itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) && baseChange !== scoreChange) {
scoreChangeText += ` <span class="text-xs">(Base: ${baseChange > 0 ? '+' : ''}${baseChange})</span>`;
}
}
const listItem = document.createElement('li');
listItem.className = 'py-1 border-b border-gray-100 flex justify-between items-center';
listItem.innerHTML = `
<span><span class="text-gray-500">${timeString}</span>: ${actionText}</span>
<span class="font-medium ${scoreChangeClass}">${scoreChangeText}</span>
`;
historyList.prepend(listItem);
// Keep only last 5 history items
if (historyList.children.length > 5) {
historyList.removeChild(historyList.lastChild);
}
}
// Create confetti effect
function createConfetti() {
const confettiContainer = document.createElement('div');
confettiContainer.className = 'fixed inset-0 pointer-events-none overflow-hidden';
document.body.appendChild(confettiContainer);
// Create 50 confetti pieces
for (let i = 0; i < 50; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
// Random position
const posX = Math.random() * 100;
const delay = Math.random() * 3;
const duration = 3 + Math.random() * 2;
// Random colors
const colors = ['#3b82f6', '#f97316', '#10b981', '#ef4444', '#8b5cf6'];
const color = colors[Math.floor(Math.random() * colors.length)];
confetti.style.left = `${posX}%`;
confetti.style.backgroundColor = color;
confetti.style.animationDelay = `${delay}s`;
confetti.style.animationDuration = `${duration}s`;
confettiContainer.appendChild(confetti);
}
// Remove confetti container after animation completes
setTimeout(() => {
document.body.removeChild(confettiContainer);
}, 5000);
}
// Create particle effect on lock
function createLockParticles(lockElement) {
// Get lock element position
const rect = lockElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// Create particle container
const particleContainer = document.createElement('div');
particleContainer.className = 'absolute pointer-events-none';
particleContainer.style.left = `${centerX}px`;
particleContainer.style.top = `${centerY}px`;
particleContainer.style.transform = 'translate(-50%, -50%)';
document.body.appendChild(particleContainer);
// Create 15 particles
for (let i = 0; i < 15; i++) {
const particle = document.createElement('div');
particle.className = 'lock-particle';
// Random direction and distance
const angle = Math.random() * Math.PI * 2;
const distance = 10 + Math.random() * 20;
const tx = Math.cos(angle) * distance;
const ty = Math.sin(angle) * distance;
// Random color (use the color of the unlocked dice)
const color = lockElement.style.backgroundColor;
// Random animation duration
const duration = 0.5 + Math.random() * 0.5;
// Set particle styles
particle.style.backgroundColor = color;
particle.style.setProperty('--tx', `${tx}px`);
particle.style.setProperty('--ty', `${ty}px`);
particle.style.animation = `lock-particle ${duration}s ease-out forwards`;
particleContainer.appendChild(particle);
}
// Remove particle container after animation completes
setTimeout(() => {
document.body.removeChild(particleContainer);
}, 1000);
}
// Function to darken a color by a certain percentage
function darkenColor(color, percent) {
const hex = color.replace('#', '');
let r = parseInt(hex.substr(0, 2), 16);
let g = parseInt(hex.substr(2, 2), 16);
let b = parseInt(hex.substr(4, 2), 16);
// Darken each channel by the percentage
r = Math.max(0, Math.floor(r * (1 - percent / 100)));
g = Math.max(0, Math.floor(g * (1 - percent / 100)));
b = Math.max(0, Math.floor(b * (1 - percent / 100)));
// Convert back to hex
const darkenedHex = '#' +
r.toString(16).padStart(2, '0') +
g.toString(16).padStart(2, '0') +
b.toString(16).padStart(2, '0');
return darkenedHex;
}
// Change dice color function
function changeDiceColor(color) {
console.log(`changeDiceColor called with color: ${color}`);
const faces = document.querySelectorAll('.dice-face');
console.log(`Found ${faces.length} dice faces`);
let dotColor, borderColor;
// Special cases for high contrast
if (color.toUpperCase() === '#FFFFFF') {
// White dice - use black dots and light gray borders
dotColor = '#000000';
borderColor = '#CCCCCC';
} else if (color.toUpperCase() === '#555555') {
// Dark gray dice - use white dots and slightly lighter gray borders
dotColor = '#FFFFFF';
borderColor = '#777777';
} else {
// Calculate darker color for dots and borders (40% darker for more contrast)
dotColor = darkenColor(color, 40);
borderColor = darkenColor(color, 30); // Slightly lighter border than dots
}
faces.forEach(face => {
// Set face background color
face.style.backgroundColor = color;
// Set face border color
face.style.border = `3px solid ${borderColor}`;
// Set dots color
const dots = face.querySelectorAll('.dot');
dots.forEach(dot => {
dot.style.backgroundColor = dotColor;
// Add slight border to dots for better definition
dot.style.border = color.toUpperCase() === '#FFFFFF' ? '1px solid rgba(0, 0, 0, 0.2)' : '1px solid rgba(0, 0, 0, 0.1)';
});
});
}
// Update tier multiplier display
function updateTierMultiplierDisplay() {
const display = document.getElementById('tier-multiplier-display');
if (display) {
display.innerHTML = `Tier Multiplier: <span class="font-semibold text-purple-500">×${(1 + currentTier / 10).toFixed(1)}</span>`;
}
}
// Event listener for roll button
rollButton.addEventListener('click', rollDice);
// Event listener for upgrade button
upgradeButton.addEventListener('click', upgradeDice);
// Event listeners for item buttons
useDoublePointsButton.addEventListener('click', useDoublePointsCard);
useNoPenaltyButton.addEventListener('click', useNoPenaltyCard);
useLuckyStreakButton.addEventListener('click', useLuckyStreakCard);
// Event listeners for color options
colorOptions.forEach((option, index) => {
// Disable all color options except the first one initially
if (index !== 0) {
option.disabled = true;
option.classList.add('locked');
}
option.addEventListener('click', () => {
// Check if game is over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if color is unlocked
if (option.disabled) {
showGameMessage('You need to upgrade to unlock this dice color!', 'text-orange-500');
return;
}
const color = option.getAttribute('data-color');
changeDiceColor(color);
// Add active state to selected color
colorOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-offset-2', 'ring-primary'));
option.classList.add('ring-2', 'ring-offset-2', 'ring-primary');
// Update current tier to the selected color's tier
currentTier = index;
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
updateTierMultiplierDisplay();
});
});
// Initialize dice on page load
window.addEventListener('DOMContentLoaded', () => {
console.log('=== DOMContentLoaded event fired ===');
// Check if elements exist
console.log('Checking if elements exist before initialization:');
console.log('best-record-display exists:', !!document.getElementById('best-record-display'));
console.log('best-record-value exists:', !!document.getElementById('best-record-value'));
console.log('DOM fully loaded');
initializeDice();
// Set default color (first option)
if (colorOptions.length > 0) {
const defaultColor = colorOptions[0].getAttribute('data-color');
changeDiceColor(defaultColor);
colorOptions[0].classList.add('ring-2', 'ring-offset-2', 'ring-primary');
}
// Initialize current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Initialize tier multiplier display
updateTierMultiplierDisplay();
// Initialize items display
updateItemsDisplay();
// Initialize upgrade cost display
updateUpgradeCostDisplay();
// Show welcome message
setTimeout(() => {
showGameMessage('Welcome! Roll the dice to earn points and upgrade your dice!', 'text-blue-500');
}, 1000);
});
// Also try initializing on window load
window.addEventListener('load', () => {
console.log('Window loaded');
// If dice not already initialized, try again
if (diceElement.children.length === 0) {
console.log('Dice not initialized, trying again...');
initializeDice();
}
});
// Game Rules Modal Functionality
(function() {
// Get elements
const rulesButton = document.getElementById('game-rules-button');
const rulesModal = document.getElementById('game-rules-modal');
const closeButtons = rulesModal.querySelectorAll('.close-button');
const modalBackdrop = rulesModal.querySelector('.modal-backdrop');
const modalContent = rulesModal.querySelector('.modal-content');
// Function to prevent background scrolling
function preventBackgroundScroll(event) {
// Allow scrolling inside the modal content
if (modalContent.contains(event.target)) {
// Check if we're at the top or bottom of the modal content
const isAtTop = modalContent.scrollTop === 0;
const isAtBottom = modalContent.scrollTop + modalContent.clientHeight >= modalContent.scrollHeight;
// Prevent scrolling if at the top and scrolling up, or at the bottom and scrolling down
if ((isAtTop && event.deltaY < 0) || (isAtBottom && event.deltaY > 0)) {
event.preventDefault();
}
} else {
// Prevent scrolling outside the modal content
event.preventDefault();
}
}
// Function to open modal
function openModal() {
// Add event listeners to prevent background scrolling
document.addEventListener('wheel', preventBackgroundScroll, { passive: false });
document.addEventListener('touchmove', preventBackgroundScroll, { passive: false });
// Add open class to trigger animations
rulesModal.classList.add('modal-open');
// Show the modal
rulesModal.style.visibility = 'visible';
}
// Function to close modal
function closeModal() {
// Remove open class to trigger animations
rulesModal.classList.remove('modal-open');
// Remove event listeners that prevent background scrolling
document.removeEventListener('wheel', preventBackgroundScroll);
document.removeEventListener('touchmove', preventBackgroundScroll);
// Hide the modal after animation completes
setTimeout(() => {
rulesModal.style.visibility = 'hidden';
}, 300);
}
// Add event listeners
if (rulesButton) {
rulesButton.addEventListener('click', openModal);
}
// Close buttons
closeButtons.forEach(button => {
button.addEventListener('click', closeModal);
});
// Close when clicking outside the modal content
modalBackdrop.addEventListener('click', closeModal);
// Close when pressing Escape key
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && rulesModal.classList.contains('modal-open')) {
closeModal();
}
});
// Add hover effect to rules button
if (rulesButton) {
rulesButton.addEventListener('mouseenter', () => {
rulesButton.classList.add('scale-105');
});
rulesButton.addEventListener('mouseleave', () => {
rulesButton.classList.remove('scale-105');
});
}
// Add click effect to close buttons
closeButtons.forEach(button => {
button.addEventListener('mousedown', () => {
button.classList.add('scale-95');
});
button.addEventListener('mouseup', () => {
button.classList.remove('scale-95');
});
button.addEventListener('mouseleave', () => {
button.classList.remove('scale-95');
});
});
})();
</script>
</body>
</html>
v.1.2
HTML<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Dice Roll Animation</title>
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#f97316',
dark: '#1e293b',
light: '#f8fafc'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'shake': 'shake 0.5s ease-in-out',
},
keyframes: {
shake: {
'0%, 100%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(-5px)' },
'50%': { transform: 'translateX(5px)' },
'75%': { transform: 'translateX(-5px)' },
}
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.preserve-3d {
transform-style: preserve-3d;
}
.perspective {
perspective: 1000px;
}
.backface-hidden {
backface-visibility: hidden;
}
.dice-shadow {
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
}
.glass-effect {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.modal-content {
transform: translateY(-20px);
opacity: 0;
transition: all 0.3s ease-out;
}
.modal-open .modal-content {
transform: translateY(0);
opacity: 1;
}
.modal-open .modal-backdrop {
opacity: 1;
visibility: visible;
}
.trophy-animation {
animation: trophy-pulse 2s ease-in-out infinite;
}
.record-animation {
animation: record-shine 2s ease-in-out;
}
}
@keyframes trophy-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes record-shine {
0% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.7); }
70% { box-shadow: 0 0 0 15px rgba(251, 191, 36, 0); }
100% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0); }
}
/* Custom styles for 3D dice */
.dice-scene {
perspective: 1500px;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.dice-container {
position: relative;
width: 100px;
height: 100px;
transform-style: preserve-3d;
transition: transform 1s ease-out;
transform: rotateX(20deg) rotateY(20deg);
}
.dice-face {
position: absolute;
width: 100px;
height: 100px;
border-radius: 8px;
background-color: white;
border: 2px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
/* Dice face positions */
.face-1 { transform: translateZ(50px); }
.face-2 { transform: rotateY(90deg) translateZ(50px); }
.face-3 { transform: rotateY(180deg) translateZ(50px); }
.face-4 { transform: rotateY(-90deg) translateZ(50px); }
.face-5 { transform: rotateX(90deg) translateZ(50px); }
.face-6 { transform: rotateX(-90deg) translateZ(50px); }
/* Dots on dice faces */
.dot {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: black;
}
/* Dot positions for each face */
.face-1 .dot { top: 40px; left: 40px; }
.face-2 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-2 .dot:nth-child(2) { top: 60px; left: 60px; }
.face-3 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-3 .dot:nth-child(2) { top: 40px; left: 40px; }
.face-3 .dot:nth-child(3) { top: 60px; left: 60px; }
.face-4 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-4 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-4 .dot:nth-child(3) { top: 60px; left: 20px; }
.face-4 .dot:nth-child(4) { top: 60px; left: 60px; }
.face-5 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-5 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-5 .dot:nth-child(3) { top: 40px; left: 40px; }
.face-5 .dot:nth-child(4) { top: 60px; left: 20px; }
.face-5 .dot:nth-child(5) { top: 60px; left: 60px; }
.face-6 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-6 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-6 .dot:nth-child(3) { top: 40px; left: 20px; }
.face-6 .dot:nth-child(4) { top: 40px; left: 60px; }
.face-6 .dot:nth-child(5) { top: 60px; left: 20px; }
.face-6 .dot:nth-child(6) { top: 60px; left: 60px; }
.dice-result {
transition: all 0.5s ease-out;
}
/* Fixed height for result display to prevent page jumping */
#result-display {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.result-shine {
animation: shine 0.5s ease-out;
}
.score-increase {
color: #10b981;
animation: scorePopup 1s ease-out;
}
.score-decrease {
color: #ef4444;
animation: scorePopup 1s ease-out;
}
.score-neutral {
color: #6b7280;
animation: scorePopup 1s ease-out;
}
/* Locked color option styles */
.color-option.locked {
position: relative;
opacity: 0.5;
cursor: not-allowed;
}
.color-option.locked::after {
content: '\f023'; /* Lock icon from Font Awesome */
font-family: 'FontAwesome';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(0, 0, 0, 0.5);
font-size: 16px;
}
@keyframes shine {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
@keyframes scorePopup {
0% {
transform: translateY(0);
opacity: 0;
}
50% {
transform: translateY(-10px);
opacity: 1;
}
100% {
transform: translateY(-20px);
opacity: 0;
}
}
.confetti {
position: absolute;
width: 10px;
height: 10px;
background-color: #f97316;
animation: confetti-fall 3s ease-in-out infinite;
}
@keyframes confetti-fall {
0% {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
@keyframes lock-particle {
0% {
transform: translate(0, 0);
opacity: 1;
}
100% {
transform: translate(var(--tx), var(--ty));
opacity: 0;
}
}
/* Game Rules Modal Styles */
.game-rules-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
visibility: hidden;
box-sizing: border-box;
}
.game-rules-modal .modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease-out;
}
.game-rules-modal .modal-content {
position: relative;
background-color: white;
border-radius: 1rem;
max-width: 90%;
max-height: 80vh;
width: 500px;
overflow-y: auto;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.game-rules-modal .modal-header {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.game-rules-modal .modal-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
transition: color 0.2s ease-in-out;
}
.game-rules-modal .close-button:hover {
color: #1e293b;
}
.game-rules-modal .modal-body {
padding: 1.5rem;
line-height: 1.6;
color: #374151;
}
.game-rules-modal .modal-body h3 {
font-size: 1.1rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #1e293b;
}
.game-rules-modal .modal-body p {
margin-bottom: 1rem;
}
.game-rules-modal .modal-body ul {
margin-bottom: 1rem;
padding-left: 1.5rem;
list-style-type: disc;
}
.game-rules-modal .modal-body li {
margin-bottom: 0.5rem;
}
.game-rules-modal .modal-body strong {
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
}
.game-rules-modal .modal-footer button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-size: 1.25rem;
font-weight: 500;
transition: background-color 0.2s ease-in-out;
}
.game-rules-modal .modal-footer button:hover {
background-color: #2563eb;
}
/* Game Rules Button Styles */
.game-rules-button {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 40;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease-in-out;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.game-rules-button:hover {
background-color: #2563eb;
transform: translateY(-2px);
}
.game-rules-button i {
font-size: 1.1rem;
}
</style>
</head>
<body class="bg-gradient-to-br from-light to-gray-200 min-h-screen flex flex-col items-center justify-center p-4 m-0">
<!-- Game Rules Button -->
<button id="game-rules-button" class="game-rules-button">
<i class="fa fa-book"></i>
<span>Game Rules</span>
</button>
<!-- Game Rules Modal -->
<div id="game-rules-modal" class="game-rules-modal">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">3D Dice Game Rules</h2>
<button class="close-button">×</button>
</div>
<div class="modal-body">
<p>Welcome to the 3D Dice Game! This is an exciting game that combines luck, strategy, and upgrading mechanics. Below is the complete game rules explanation:</p>
<h3>Basic Gameplay</h3>
<ul>
<li>Click the "Roll Dice" button to roll the dice</li>
<li>The dice will randomly show a number between 1-6</li>
<li>Gain or lose points based on the number rolled</li>
<li>The goal is to obtain the highest tier "Unique" dice</li>
</ul>
<h3>Score System</h3>
<ul>
<li>Rolling 1 or 2: Lose 1 point (multiplier not applied)</li>
<li>Rolling 3: No points gained or lost</li>
<li>Rolling 4: Gain 1 point</li>
<li>Rolling 5: Gain 2 points</li>
<li>Rolling 6: Gain 3 points</li>
</ul>
<h3>Dice Upgrade System</h3>
<p>There are 10 different tiers of dice in the game, from common to rare:</p>
<ul>
<li><strong>Empty</strong> (Initial) - Score Multiplier ×1.0</li>
<li><strong>Common</strong> - Score Multiplier ×1.1</li>
<li><strong>Unusual</strong> - Score Multiplier ×1.2</li>
<li><strong>Rare</strong> - Score Multiplier ×1.3</li>
<li><strong>Epic</strong> - Score Multiplier ×1.4</li>
<li><strong>Legendary</strong> - Score Multiplier ×1.5</li>
<li><strong>Mythic</strong> - Score Multiplier ×1.6</li>
<li><strong>Ultra</strong> - Score Multiplier ×1.7</li>
<li><strong>Super</strong> - Score Multiplier ×1.8</li>
<li><strong>Unique</strong> - Score Multiplier ×1.9</li>
</ul>
<p>Each upgrade costs (5 + current tier) points. For example, upgrading from Empty (tier 0) costs 5 points, upgrading from Common (tier 1) costs 6 points, and so on. After upgrading, you can choose to use the new dice or continue using the old one.</p>
<h3>Item System</h3>
<p>When upgrading, you have a chance to obtain item cards that can be used at critical moments:</p>
<ul>
<li><strong>Double Points Card</strong>: Doubles the points from one roll</li>
<li><strong>No Penalty Card</strong>: Prevents point loss when rolling 1 or 2</li>
<li><strong>Lucky Streak Card</strong>: Next 3 rolls of 1 or 2 won't count towards your total roll count</li>
</ul>
<p>Items can be stockpiled and each card can only be used once. Double Points Cards and No Penalty Cards can be used simultaneously. Lucky Streak Cards make your next 3 rolls of 1 or 2 not count towards your total roll count, giving you a chance to recover from bad luck.</p>
<h3>Game Rules</h3>
<ul>
<li>Initial score is 10 points</li>
<li>Score multiplier changes based on the current dice tier being used</li>
<li>Game over when score is less than 0</li>
<li>Game victory when obtaining the Unique dice</li>
<li>Unlocked dice colors can be switched at any time</li>
</ul>
<h3>Operation Tips</h3>
<ul>
<li>Click on dice color options to switch the dice being used</li>
<li>Click the "Upgrade Dice" button to upgrade your dice (requires 5 points)</li>
<li>Click the "Use" button next to item cards to use them</li>
<li>Press the ESC key to close the rules window</li>
</ul>
<p>Good luck, and may you successfully obtain the highest tier Unique dice!</p>
</div>
<div class="modal-footer">
<button class="close-button">Got it</button>
</div>
</div>
</div>
<div class="w-full max-w-md mx-auto glass-effect rounded-2xl p-6 dice-shadow relative z-10">
<h1 class="text-3xl font-bold text-center text-dark mb-8">3D Dice Roll</h1>
<div class="flex flex-col items-center justify-center mb-8">
<!-- Dice display area -->
<div id="dice-display" class="w-48 h-48 flex items-center justify-center mb-4 bg-gray-100 rounded-lg">
<!-- Dice scene for 3D perspective -->
<div class="dice-scene">
<div id="dice" class="dice-container">
<!-- Dice faces will be inserted here by JavaScript -->
</div>
</div>
</div>
<!-- Result display -->
<div id="result-display" class="text-2xl font-bold text-center mb-4 hidden">
Result: <span id="result-value" class="text-primary">0</span>
</div>
<!-- Score display -->
<div id="score-display" class="text-xl font-bold text-center mb-2">
Score: <span id="score-value" class="text-secondary">10</span>
<span id="score-change" class="ml-2 text-sm font-normal"></span>
</div>
<!-- Roll count display -->
<div id="roll-count-display" class="text-lg font-medium text-center mb-4">
Rolls: <span id="roll-count-value" class="text-gray-700">0</span>
</div>
<!-- Roll button -->
<button id="roll-button" class="bg-primary hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-opacity-50">
<i class="fa fa-random mr-2"></i> Roll Dice
</button>
<!-- Upgrade button and current tier display -->
<div class="flex flex-col items-center mt-4">
<div id="current-tier" class="text-sm text-gray-600 mb-2">
Current Dice: <span class="font-semibold text-primary">Empty</span>
</div>
<div id="tier-multiplier-display" class="text-sm text-gray-600 mb-2">
Tier Multiplier: <span class="font-semibold text-purple-500">×1.0</span>
</div>
<button id="upgrade-button" class="bg-secondary hover:bg-orange-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-orange-300 focus:ring-opacity-50">
<i class="fa fa-arrow-up mr-1"></i> Upgrade Dice <span id="upgrade-cost">(Cost: 5)</span>
</button>
</div>
<!-- Game message display -->
<div id="game-message" class="mt-4 text-center font-semibold hidden"></div>
<!-- Items display -->
<div id="items-display" class="mt-6 grid grid-cols-2 gap-4">
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-plus-circle text-green-500 text-xl mr-2"></i>
<span class="font-medium">Double Points Card</span>
</div>
<div class="flex items-center">
<span id="double-points-count" class="bg-green-100 text-green-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-double-points" class="bg-green-500 hover:bg-green-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Doubles your score change for one roll</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-shield text-blue-500 text-xl mr-2"></i>
<span class="font-medium">No Penalty Card</span>
</div>
<div class="flex items-center">
<span id="no-penalty-count" class="bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-no-penalty" class="bg-blue-500 hover:bg-blue-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Prevents score loss when rolling 1 or 2</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-star text-yellow-500 text-xl mr-2"></i>
<span class="font-medium">Lucky Streak Card</span>
</div>
<div class="flex items-center">
<span id="lucky-streak-count" class="bg-yellow-100 text-yellow-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-lucky-streak" class="bg-yellow-500 hover:bg-yellow-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Next 3 rolls of 1 or 2 won't count towards your total roll count</p>
</div>
</div>
<!-- Free rolls indicator -->
<div id="free-rolls-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fa fa-star mr-1" style="color: #f59e0b;"></i>
<span>Free Rolls: <span id="free-rolls-count">0</span></span>
</span>
</div>
<!-- Active item indicator -->
<div id="active-item-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
<i id="active-item-icon" class="mr-1"></i>
<span id="active-item-text">Active Item: None</span>
</span>
</div>
</div>
<!-- Dice color selection -->
<div class="mb-8">
<label class="block text-sm font-medium text-gray-700 mb-2">Dice Rarity</label>
<div class="grid grid-cols-5 gap-4">
<!-- Empty -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #FFFFFF; border: 1px solid #dddddd;" data-color="#FFFFFF"></button>
<span class="text-xs text-center text-gray-600">Empty</span>
</div>
<!-- Common -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #7EEF6D;" data-color="#7EEF6D"></button>
<span class="text-xs text-center text-gray-600">Common</span>
</div>
<!-- Unusual -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #FFE65D;" data-color="#FFE65D"></button>
<span class="text-xs text-center text-gray-600">Unusual</span>
</div>
<!-- Rare -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #4d52e3;" data-color="#4d52e3"></button>
<span class="text-xs text-center text-gray-600">Rare</span>
</div>
<!-- Epic -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #861FDE;" data-color="#861FDE"></button>
<span class="text-xs text-center text-gray-600">Epic</span>
</div>
<!-- Legendary -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #DE1F1F;" data-color="#DE1F1F"></button>
<span class="text-xs text-center text-gray-600">Legendary</span>
</div>
<!-- Mythic -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #1fdbde;" data-color="#1fdbde"></button>
<span class="text-xs text-center text-gray-600">Mythic</span>
</div>
<!-- Ultra -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #ff2b75;" data-color="#ff2b75"></button>
<span class="text-xs text-center text-gray-600">Ultra</span>
</div>
<!-- Super -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #2bffa3;" data-color="#2bffa3"></button>
<span class="text-xs text-center text-gray-600">Super</span>
</div>
<!-- Unique -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #555555;" data-color="#555555"></button>
<span class="text-xs text-center text-gray-600">Unique</span>
</div>
</div>
</div>
<!-- History log -->
<div class="bg-white rounded-lg p-4">
<h2 class="text-lg font-semibold text-center mb-2">Roll History</h2>
<div id="history-container" class="h-64 overflow-y-auto">
<div class="grid grid-cols-3 gap-2 text-xs font-medium text-gray-500 border-b border-gray-200 sticky top-0 bg-white z-10">
<div>Time</div>
<div>Action</div>
<div class="text-right">Score</div>
</div>
<ul id="history-list" class="text-sm">
<!-- History items will be inserted here by JavaScript -->
</ul>
</div>
</div>
</div>
<footer class="mt-8 text-center text-gray-600 text-sm">
<p>Click the button to roll the dice and see the result!</p>
</footer>
<script>
// Set up global error handler
window.addEventListener('error', function(event) {
console.error('Global error caught:', event.error);
// Try to get roll button element
const rollButton = document.getElementById('roll-button');
if (rollButton && rollButton.disabled) {
console.warn('Error during dice roll, re-enabling button');
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
// Try to show error message
const gameMessage = document.getElementById('game-message');
if (gameMessage) {
gameMessage.textContent = 'An error occurred. Please try again.';
gameMessage.className = 'mt-4 text-center font-semibold text-red-500';
gameMessage.classList.remove('hidden');
}
}
});
// DOM elements
const diceElement = document.getElementById('dice');
const rollButton = document.getElementById('roll-button');
const resultDisplay = document.getElementById('result-display');
const resultValue = document.getElementById('result-value');
const historyList = document.getElementById('history-list');
const colorOptions = document.querySelectorAll('.color-option');
const scoreDisplay = document.getElementById('score-display');
const scoreValue = document.getElementById('score-value');
const scoreChange = document.getElementById('score-change');
const upgradeButton = document.getElementById('upgrade-button');
const currentTierDisplay = document.getElementById('current-tier');
const gameMessageDisplay = document.getElementById('game-message');
const rollCountDisplay = document.getElementById('roll-count-display');
const rollCountValue = document.getElementById('roll-count-value');
const upgradeCost = document.getElementById('upgrade-cost');
// Item related DOM elements
const doublePointsCount = document.getElementById('double-points-count');
const noPenaltyCount = document.getElementById('no-penalty-count');
const luckyStreakCount = document.getElementById('lucky-streak-count');
const useDoublePointsButton = document.getElementById('use-double-points');
const useNoPenaltyButton = document.getElementById('use-no-penalty');
const useLuckyStreakButton = document.getElementById('use-lucky-streak');
const activeItemIndicator = document.getElementById('active-item-indicator');
const freeRollsIndicator = document.getElementById('free-rolls-indicator');
const freeRollsCountElement = document.getElementById('free-rolls-count');
// Game state
let currentScore = 10; // Initial score
let currentTier = 0; // Initial dice tier (0 = Empty)
let gameOver = false; // Game over flag
let rollCount = 0; // Number of dice rolls
let luckyRollsRemaining = 0; // Number of lucky rolls remaining
let isRolling = false; // Whether dice is currently rolling
// Item system
let doublePointsCards = 0; // Number of double points cards
let noPenaltyCards = 0; // Number of no penalty cards
let luckyStreakCards = 0; // Number of lucky streak cards
let freeRolls = 0; // Number of free rolls available
let activeItems = { // Currently active items
double: 0, // Number of active double points cards
noPenalty: false // Whether no penalty card is active
};
// Dice tiers configuration
const diceTiers = [
{ name: 'Empty', color: '#FFFFFF' },
{ name: 'Common', color: '#7EEF6D' },
{ name: 'Unusual', color: '#FFE65D' },
{ name: 'Rare', color: '#4d52e3' },
{ name: 'Epic', color: '#861FDE' },
{ name: 'Legendary', color: '#DE1F1F' },
{ name: 'Mythic', color: '#1fdbde' },
{ name: 'Ultra', color: '#ff2b75' },
{ name: 'Super', color: '#2bffa3' },
{ name: 'Unique', color: '#555555' }
];
// Initialize 3D dice
function initializeDice() {
console.log('Initializing dice...');
// Create 6 faces for the dice
const faces = [1, 2, 3, 4, 5, 6];
faces.forEach(faceNumber => {
const face = document.createElement('div');
face.className = `dice-face face-${faceNumber}`;
// Add dots to the face based on the number
for (let i = 0; i < faceNumber; i++) {
const dot = document.createElement('div');
dot.className = 'dot';
face.appendChild(dot);
}
diceElement.appendChild(face);
console.log(`Added face ${faceNumber}`);
});
console.log('Dice initialized with faces:', diceElement.children.length);
// Set initial position to show face 1 clearly
diceElement.style.transform = 'rotateX(0deg) rotateY(0deg)';
}
// Get random rotation values for the dice
function getRandomRotation() {
// Determine which face we want to show
const targetFace = Math.floor(Math.random() * 6) + 1;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let currentX = 0, currentY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
currentX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
currentY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Set target rotation based on target face
// These values are precisely calibrated to show the correct face
let targetX = 0, targetY = 0;
switch(targetFace) {
case 1: // Front face (Z+) - visible when no rotation
targetX = 0;
targetY = 0;
break;
case 2: // Left face (X-) - visible when Y rotated 270 degrees
targetX = 0;
targetY = 270;
break;
case 3: // Back face (Z-) - visible when Y rotated 180 degrees
targetX = 0;
targetY = 180;
break;
case 4: // Right face (X+) - visible when Y rotated 90 degrees
targetX = 0;
targetY = 90;
break;
case 5: // Top face (Y-) - visible when X rotated -90 degrees
targetX = -90;
targetY = 0;
break;
case 6: // Bottom face (Y+) - visible when X rotated 90 degrees
targetX = 90;
targetY = 0;
break;
}
// Calculate the shortest path to the target rotation
// This prevents large rotation values from accumulating
let diffX = targetX - currentX;
let diffY = targetY - currentY;
// Normalize the difference to the range [-180, 180] to find the shortest path
diffX = ((diffX + 180) % 360) - 180;
diffY = ((diffY + 180) % 360) - 180;
// Add multiple full rotations for spinning effect (2-4 full rotations)
const fullRotations = 2 + Math.floor(Math.random() * 3);
// Calculate final rotation with full spins
// We add full rotations in the direction of the shortest path
const spinDirectionX = diffX >= 0 ? 1 : -1;
const spinDirectionY = diffY >= 0 ? 1 : -1;
const finalX = currentX + diffX + spinDirectionX * fullRotations * 360;
const finalY = currentY + diffY + spinDirectionY * fullRotations * 360;
// Add a tiny bit of randomness to make it look more natural
// But not enough to change which face is visible
const randomX = (Math.random() - 0.5) * 2;
const randomY = (Math.random() - 0.5) * 2;
return {
x: finalX + randomX,
y: finalY + randomY,
targetFace: targetFace // Return the target face so we don't have to recalculate it
};
}
// Roll the dice function
function rollDice() {
// Check if game is over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if there are free rolls available
const isFreeRoll = freeRolls > 0;
// Check if in lucky streak mode
const isLuckyRoll = luckyRollsRemaining > 0;
// Increment roll count only if not a free roll (lucky rolls will be handled based on result)
if (!isFreeRoll) {
rollCount++;
console.log(`Roll count: ${rollCount}`);
} else {
// Decrement free rolls count
freeRolls--;
updateFreeRollsDisplay();
console.log(`Free roll used. Remaining free rolls: ${freeRolls}`);
}
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
console.log('Rolling dice...');
console.log('Button state before disable:', rollButton.disabled);
// Set rolling state and disable button during animation
isRolling = true;
rollButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
// Disable other interactive elements during roll
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
useDoublePointsButton.disabled = true;
useNoPenaltyButton.disabled = true;
useLuckyStreakButton.disabled = true;
// Disable color options during roll
colorOptions.forEach(option => {
option.disabled = true;
});
console.log('Button state after disable:', rollButton.disabled);
// Hide result display
resultDisplay.classList.add('hidden');
// Reset dice display height - use Tailwind class instead of inline style
const diceDisplay = document.getElementById('dice-display');
if (diceDisplay) {
// Ensure the height class is applied to prevent layout shifts during animation
diceDisplay.classList.add('h-48');
}
try {
// Set animation duration
const duration = 2000; // Fixed duration for consistent experience
// Get random rotation values for the final position
const rotationData = getRandomRotation();
console.log('Rotation data:', rotationData);
console.log('Target face:', rotationData.targetFace);
// Animate the dice using JavaScript for more control
animateDice(duration, rotationData, { isFreeRoll, isLuckyRoll });
// Set a safety timeout to ensure button is re-enabled even if something goes wrong
setTimeout(() => {
if (rollButton.disabled) {
console.warn('Safety timeout: Re-enabling roll button');
enableRollButton();
}
}, duration + 1000); // Add 1 second buffer
} catch (error) {
console.error('Error during dice roll:', error);
// Re-enable button if there's an error
enableRollButton();
showGameMessage('An error occurred during the dice roll. Please try again.', 'text-red-500');
}
}
// Animate the dice with spin animation
function animateDice(duration, rotationData, rollContext) {
console.log('Animate dice called with rotationData:', rotationData);
console.log('Roll context:', rollContext);
const startTime = performance.now();
const finalRotation = rotationData;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let startX = 0, startY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
startX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
startY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Function to handle each animation frame
function animate(currentTime) {
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
// Apply easing function for smooth, natural animation
const easedProgress = easeOutCubic(progress);
// Calculate current rotation - smooth continuous rotation
const currentX = startX + (finalRotation.x - startX) * easedProgress;
const currentY = startY + (finalRotation.y - startY) * easedProgress;
// Spin animation: rotate in place
diceElement.style.transform = `rotateX(${currentX}deg) rotateY(${currentY}deg)`;
// Continue animation if not complete
if (progress < 1) {
requestAnimationFrame(animate);
} else {
// Animation complete - ensure we're at the exact target rotation
diceElement.style.transform = `rotateX(${finalRotation.x}deg) rotateY(${finalRotation.y}deg)`;
// Show result after a delay to ensure rotation is complete and CSS has applied
console.log('Animation complete, waiting before finalizing...');
setTimeout(() => {
// Double-check that the transform has been applied
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
console.log('Current transform after animation:', currentTransform);
console.log('Calling finalizeAnimation...');
finalizeAnimation(finalRotation, rollContext);
}, 600);
}
}
// Start the animation
requestAnimationFrame(animate);
}
// Helper function to normalize angles to the range [-180, 180]
function normalizeAngle(angle) {
angle = angle % 360;
if (angle > 180) angle -= 360;
if (angle < -180) angle += 360;
return angle;
}
// Easing function for smooth, natural animation with gentle acceleration and deceleration
// Uses a cubic easing function that starts slow, accelerates, then slows down at the end
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// Calculate score change based on dice roll result
function calculateScoreChange(result) {
let baseChange = 0;
switch(result) {
case 1:
case 2:
baseChange = -1;
break;
case 3:
baseChange = 0;
break;
case 4:
baseChange = 1;
break;
case 5:
baseChange = 2;
break;
case 6:
baseChange = 3;
break;
default:
baseChange = 0;
}
// Apply current tier multiplier
// If losing points (baseChange < 0), use multiplier of 1 instead of tier multiplier
const tierMultiplier = 1 + currentTier / 10;
let finalChange;
if (baseChange < 0) {
// For point loss, use multiplier of 1 regardless of tier
finalChange = baseChange * 1;
} else {
// For point gain or neutral, use tier multiplier
finalChange = baseChange * tierMultiplier;
}
// Apply active item effects
const itemsUsed = {
double: activeItems.double,
noPenalty: activeItems.noPenalty
};
// Apply no penalty card first
if (activeItems.noPenalty && baseChange < 0) {
finalChange = 0;
}
// Apply double points cards
if (activeItems.double > 0) {
finalChange = finalChange * Math.pow(2, activeItems.double);
}
// Round to 2 decimal places to avoid floating point precision issues
finalChange = Math.round(finalChange * 100) / 100;
return {
baseChange: baseChange,
finalChange: finalChange,
itemsUsed: itemsUsed,
tierMultiplier: tierMultiplier
};
}
// Enable roll button
function enableRollButton() {
console.log('Button state before enable:', rollButton.disabled);
// Directly enable the button
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
// Re-enable other interactive buttons based on their conditions
updateUpgradeCostDisplay(); // This will enable/disable upgrade button based on score
updateItemsDisplay(); // This will enable/disable item buttons based on availability
console.log('Button state after enable:', rollButton.disabled);
// Double-check and force enable if needed
if (rollButton.disabled) {
console.warn('Forcing button enable');
setTimeout(() => {
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
console.log('Button state after forced enable:', rollButton.disabled);
}, 100);
}
}
// Finalize the animation and show result
function finalizeAnimation(rotationData, rollContext) {
console.log('=== Finalize animation called ===');
console.log('Current time:', new Date().toISOString().split('T')[1]);
console.log('Rotation data:', rotationData);
console.log('Roll context:', rollContext);
const finalRotation = rotationData;
const isFreeRoll = rollContext && rollContext.isFreeRoll || false;
const isLuckyRoll = rollContext && rollContext.isLuckyRoll || false;
// Use the target face directly instead of recalculating
const result = finalRotation.targetFace;
console.log(`Final result: ${result} (should show face ${result})`);
// Verify dice is in the correct position
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
console.log('Current dice transform:', currentTransform);
// Show result display
console.log('Showing result display...');
resultValue.textContent = result;
resultDisplay.classList.remove('hidden');
// Add shine effect to result
resultDisplay.classList.add('result-shine');
setTimeout(() => {
resultDisplay.classList.remove('result-shine');
}, 500);
// Calculate and update score
console.log('Calculating score change...');
const scoreChangeData = calculateScoreChange(result);
console.log('Score change data:', scoreChangeData);
currentScore += scoreChangeData.finalChange;
console.log('Updated score:', currentScore);
// Update score display with animation
console.log('Updating score display...');
updateScoreDisplay(scoreChangeData);
// Enable button
console.log('Enabling roll button...');
enableRollButton();
// Add to history - include whether it was a free roll or lucky roll
addToHistory(result, scoreChangeData, isFreeRoll, isLuckyRoll);
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Handle lucky roll logic
if (luckyRollsRemaining > 0) {
// Decrement remaining lucky rolls
luckyRollsRemaining--;
// If rolled 1 or 2, decrement roll count
if (result === 1 || result === 2) {
rollCount--;
console.log(`Lucky roll: Reverted roll count to ${rollCount}`);
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
// Show message
showGameMessage('Lucky! This 1/2 roll doesn\'t count!', 'text-yellow-500');
}
// Update lucky rolls display
updateLuckyRollsDisplay();
// If no more lucky rolls, show message
if (luckyRollsRemaining === 0) {
setTimeout(() => {
showGameMessage('Lucky Streak ended!', 'text-yellow-500');
}, 1000);
}
}
// Create confetti effect if result is 6
if (result === 6) {
createConfetti();
}
// Debug: log final state
console.log(`Final precise rotation: X=${finalRotation.x}°, Y=${finalRotation.y}°`);
console.log(`Displayed result: ${result}`);
console.log(`Score change: ${scoreChangeData.finalChange}, Current score: ${currentScore}`);
console.log('=== finalizeAnimation completed ===');
}
// Update score display with animation
function updateScoreDisplay(scoreChangeData) {
console.log('=== updateScoreDisplay called ===');
console.log('Score change data:', scoreChangeData);
const change = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
const tierMultiplier = scoreChangeData.tierMultiplier;
console.log('Change:', change, 'Base change:', baseChange, 'Items used:', itemsUsed);
// Update the score value - round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
scoreValue.textContent = currentScore;
console.log('Score value updated to:', currentScore);
// Clear previous score change display
scoreChange.textContent = '';
scoreChange.className = 'ml-2 text-sm font-normal';
console.log('Cleared previous score change display');
// Show score change with appropriate styling
if (change > 0) {
scoreChange.textContent = `+${change}`;
scoreChange.classList.add('score-increase');
console.log('Score increase:', change);
} else if (change < 0) {
scoreChange.textContent = `${change}`;
scoreChange.classList.add('score-decrease');
console.log('Score decrease:', change);
} else {
scoreChange.textContent = `±0`;
scoreChange.classList.add('score-neutral');
console.log('Score neutral');
}
// Show item effect message if items were used
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
// Create container for item effects
const itemEffectsContainer = document.createElement('div');
itemEffectsContainer.className = 'flex flex-wrap gap-2 mt-1';
// Add tier multiplier effect if applicable
if (tierMultiplier !== 1) {
const tierEffect = document.createElement('div');
tierEffect.className = 'text-xs text-purple-500';
tierEffect.textContent = `Tier Multiplier ×${tierMultiplier.toFixed(1)}!`;
itemEffectsContainer.appendChild(tierEffect);
}
// Add no penalty effect
if (itemsUsed.noPenalty) {
const noPenaltyEffect = document.createElement('div');
noPenaltyEffect.className = 'text-xs text-blue-500';
noPenaltyEffect.textContent = `No Penalty! (Score protected from ${baseChange < 0 ? baseChange : 0} loss)`;
itemEffectsContainer.appendChild(noPenaltyEffect);
}
// Add double points effect
if (itemsUsed.double > 0) {
const doublePointsEffect = document.createElement('div');
doublePointsEffect.className = 'text-xs text-green-500';
// Calculate multiplier
const multiplier = Math.pow(2, itemsUsed.double);
let calculationText = `${baseChange}`;
// Apply tier multiplier for display
let displayChange = baseChange * tierMultiplier;
// Apply no penalty first for display
if (itemsUsed.noPenalty && baseChange < 0) {
displayChange = 0;
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} → 0)`;
} else if (tierMultiplier !== 1) {
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} = ${displayChange.toFixed(2)})`;
}
// Show multiplication steps if multiple double cards used
if (itemsUsed.double > 1) {
for (let i = 0; i < itemsUsed.double; i++) {
calculationText += ` × 2`;
}
calculationText += ` = ${(displayChange * multiplier).toFixed(2)}`;
} else {
calculationText += ` × 2 = ${(displayChange * multiplier).toFixed(2)}`;
}
doublePointsEffect.textContent = `Double Points ×${itemsUsed.double}! (${calculationText})`;
itemEffectsContainer.appendChild(doublePointsEffect);
}
// Add to score display
scoreDisplay.appendChild(itemEffectsContainer);
// Remove after animation
setTimeout(() => {
scoreDisplay.removeChild(itemEffectsContainer);
}, 1000);
}
// Reset score change display after animation completes
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
// Clear active items after score update
console.log('Checking if items need to be cleared...');
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
console.log('Clearing active items...');
clearActiveItems();
console.log('Updating items display...');
updateItemsDisplay();
}
// Check game state after score update
console.log('Checking game state...');
checkGameState();
// Update upgrade button state
updateUpgradeCostDisplay();
console.log('=== updateScoreDisplay completed ===');
}
// Handle dice upgrade
function upgradeDice() {
// Check if game is already over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if already at maximum tier
if (currentTier >= diceTiers.length - 1) {
// If not already game over, end the game with win condition
if (!gameOver) {
gameOver = true;
endGame(true);
} else {
showGameMessage('Congratulations! You already have the Unique dice!', 'text-green-500');
}
return;
}
// Calculate required score for upgrade (5 + currentTier)
const requiredScore = 5 + currentTier;
// Check if enough score to upgrade
if (currentScore < requiredScore) {
showGameMessage(`Not enough score to upgrade! Need ${requiredScore} points.`, 'text-orange-500');
// Add shake animation to score display
scoreDisplay.classList.add('animate-shake');
setTimeout(() => {
scoreDisplay.classList.remove('animate-shake');
}, 500);
return;
}
// Deduct score for upgrade
currentScore -= requiredScore;
// Round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
// Update score display
scoreValue.textContent = currentScore;
showScoreChange(-requiredScore);
// Increase tier
currentTier++;
// Update current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Update tier multiplier display
updateTierMultiplierDisplay();
// Unlock the new dice color
unlockDiceColor(currentTier);
// Update color selection UI
updateColorSelection(currentTier);
// Update upgrade cost display
updateUpgradeCostDisplay();
// Change to the new dice color
console.log(`Changing dice color to ${diceTiers[currentTier].color} (${diceTiers[currentTier].name})`);
changeDiceColor(diceTiers[currentTier].color);
// Check if this upgrade reached the maximum tier
checkGameState();
// Randomly get an item
const itemChance = Math.random();
if (itemChance < 0.30) { // 30% chance to get double points card
doublePointsCards++;
} else if (itemChance < 0.60) { // 30% chance to get no penalty card
noPenaltyCards++;
} else if (itemChance < 0.70) { // 10% chance to get lucky streak card
luckyStreakCards++;
} else { // 30% chance to get nothing
// Do nothing
}
// Update items display to show new counts
updateItemsDisplay();
// Update items display
updateItemsDisplay();
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Handle lucky roll logic
if (luckyRollsRemaining > 0) {
// Decrement remaining lucky rolls
luckyRollsRemaining--;
// If rolled 1 or 2, decrement roll count
if (result === 1 || result === 2) {
rollCount--;
console.log(`Lucky roll: Reverted roll count to ${rollCount}`);
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
// Show message
showGameMessage('Lucky! This 1/2 roll doesn\'t count!', 'text-yellow-500');
}
// Update lucky rolls display
updateLuckyRollsDisplay();
// If no more lucky rolls, show message
if (luckyRollsRemaining === 0) {
setTimeout(() => {
showGameMessage('Lucky Streak ended!', 'text-yellow-500');
}, 1000);
}
}
// Add to history
addToHistory('UPGRADE', -5);
// Add item to history if obtained
if (itemChance < 0.30) {
addToHistory('ITEM', 'Double Points Card');
} else if (itemChance < 0.60) {
addToHistory('ITEM', 'No Penalty Card');
} else if (itemChance < 0.70) {
addToHistory('ITEM', 'Lucky Streak Card');
}
// Check game state after upgrade
checkGameState();
}
// Show score change temporarily
function showScoreChange(change) {
scoreChange.textContent = change > 0 ? `+${change}` : change;
scoreChange.className = `ml-2 text-sm font-normal ${change > 0 ? 'score-increase' : 'score-decrease'}`;
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
}
// Unlock a dice color option
function unlockDiceColor(tierIndex) {
if (tierIndex >= 0 && tierIndex < colorOptions.length) {
const option = colorOptions[tierIndex];
// Show particle effect on the lock before unlocking
createLockParticles(option);
// Add a small delay to show the particles before unlocking
setTimeout(() => {
option.disabled = false;
option.classList.remove('locked');
}, 300);
}
}
// Show game message
function showGameMessage(message, className) {
gameMessageDisplay.textContent = message;
gameMessageDisplay.className = `mt-4 text-center font-semibold ${className}`;
gameMessageDisplay.classList.remove('hidden');
// Hide message after 3 seconds
setTimeout(() => {
if (!gameOver) {
gameMessageDisplay.classList.add('hidden');
}
}, 3000);
}
// Check game state (win/lose conditions)
function checkGameState() {
console.log('=== checkGameState called ===');
console.log('Current score:', currentScore, 'Game over:', gameOver);
// Check if score is negative (lose condition)
if (currentScore < 0 && !gameOver) {
console.log('Score is negative, ending game...');
gameOver = true;
endGame(false);
}
// Check if reached Unique dice (win condition)
if (currentTier === diceTiers.length - 1 && !gameOver) {
console.log('Reached Unique dice, ending game...');
gameOver = true;
endGame(true);
// Check if this is a new record
setTimeout(() => {
checkWinRecord();
}, 1000);
}
// Check if score is low (warning)
if (currentScore >= 0 && currentScore <= 5 && !gameOver) {
showLowScoreWarning();
}
console.log('=== checkGameState completed ===');
}
// Update items display
function updateItemsDisplay() {
// Update counts
doublePointsCount.textContent = doublePointsCards;
noPenaltyCount.textContent = noPenaltyCards;
luckyStreakCount.textContent = luckyStreakCards;
// Enable/disable buttons based on available items
useDoublePointsButton.disabled = doublePointsCards <= 0 || gameOver;
useNoPenaltyButton.disabled = noPenaltyCards <= 0 || activeItems.noPenalty || gameOver;
useLuckyStreakButton.disabled = luckyStreakCards <= 0 || gameOver;
// Update free rolls display
updateFreeRollsDisplay();
// Update lucky rolls display
updateLuckyRollsDisplay();
}
// Update free rolls display
function updateFreeRollsDisplay() {
if (freeRolls > 0) {
freeRollsCountElement.textContent = freeRolls;
freeRollsIndicator.classList.remove('hidden');
} else {
freeRollsIndicator.classList.add('hidden');
}
}
// Update upgrade cost display and button state
function updateUpgradeCostDisplay() {
if (currentTier >= diceTiers.length - 1) {
// Already at maximum tier
upgradeCost.textContent = '(Max Tier)';
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
} else {
const requiredScore = 5 + currentTier;
upgradeCost.textContent = `(Cost: ${requiredScore})`;
// Update button state based on available score
if (currentScore >= requiredScore && !gameOver) {
upgradeButton.disabled = false;
upgradeButton.classList.remove('opacity-70', 'cursor-not-allowed');
} else {
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
}
}
}
// Use double points card
function useDoublePointsCard() {
if (doublePointsCards > 0 && !gameOver) {
activeItems.double++;
doublePointsCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('Double Points Card activated! Next roll will double your score change.', 'text-green-500');
}
}
// Use no penalty card
function useNoPenaltyCard() {
if (noPenaltyCards > 0 && !activeItems.noPenalty && !gameOver) {
activeItems.noPenalty = true;
noPenaltyCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('No Penalty Card activated! Next roll won\'t lose points.', 'text-blue-500');
}
}
// Use lucky streak card
function useLuckyStreakCard() {
if (luckyStreakCards > 0 && !gameOver) {
luckyRollsRemaining = 3;
luckyStreakCards--;
// Update UI
updateItemsDisplay();
updateLuckyRollsDisplay();
// Show message
showGameMessage('Lucky Streak Card activated! Next 3 rolls of 1 or 2 won\'t count!', 'text-yellow-500');
// Add to history
addToHistory('ITEM USE', 'Lucky Streak Card');
}
}
// Update lucky rolls display
function updateLuckyRollsDisplay() {
if (luckyRollsRemaining > 0) {
// Check if indicator exists, if not create it
let luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (!luckyRollsIndicator) {
luckyRollsIndicator = document.createElement('div');
luckyRollsIndicator.id = 'lucky-rolls-indicator';
luckyRollsIndicator.className = 'mt-3 text-center';
// Insert after free rolls indicator
const freeRollsIndicator = document.getElementById('free-rolls-indicator');
if (freeRollsIndicator) {
freeRollsIndicator.parentNode.insertBefore(luckyRollsIndicator, freeRollsIndicator.nextSibling);
} else {
// Fallback: insert after active item indicator
const activeItemIndicator = document.getElementById('active-item-indicator');
if (activeItemIndicator) {
activeItemIndicator.parentNode.insertBefore(luckyRollsIndicator, activeItemIndicator.nextSibling);
} else {
// Fallback: insert after items display
const itemsDisplay = document.getElementById('items-display');
if (itemsDisplay) {
itemsDisplay.parentNode.insertBefore(luckyRollsIndicator, itemsDisplay.nextSibling);
}
}
}
}
// Update content
luckyRollsIndicator.innerHTML = `
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fa fa-star mr-1" style="color: #f59e0b;"></i>
<span>Lucky Rolls: <span id="lucky-rolls-count">${luckyRollsRemaining}</span> (1-2 won't count)</span>
</span>
`;
// Show indicator
luckyRollsIndicator.classList.remove('hidden');
} else {
// Remove indicator if exists
const luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (luckyRollsIndicator) {
luckyRollsIndicator.classList.add('hidden');
}
}
}
// Show active item indicator
function showActiveItemIndicator() {
// Clear previous content
activeItemIndicator.innerHTML = '';
// Check if any items are active
if (activeItems.double === 0 && !activeItems.noPenalty) {
activeItemIndicator.className = 'mt-3 text-center hidden';
return;
}
// Create container for active items
const itemsContainer = document.createElement('div');
itemsContainer.className = 'flex flex-wrap justify-center gap-2';
// Add double points cards indicator
if (activeItems.double > 0) {
const doublePointsIndicator = document.createElement('span');
doublePointsIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800';
doublePointsIndicator.innerHTML = `
<i class="fa fa-plus-circle mr-1" style="color: #10b981;"></i>
<span>Double Points ×${activeItems.double}</span>
`;
itemsContainer.appendChild(doublePointsIndicator);
}
// Add no penalty card indicator
if (activeItems.noPenalty) {
const noPenaltyIndicator = document.createElement('span');
noPenaltyIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800';
noPenaltyIndicator.innerHTML = `
<i class="fa fa-shield mr-1" style="color: #3b82f6;"></i>
<span>No Penalty</span>
`;
itemsContainer.appendChild(noPenaltyIndicator);
}
// Add indicators to container
activeItemIndicator.appendChild(itemsContainer);
activeItemIndicator.className = 'mt-3 text-center';
}
// Clear active items
function clearActiveItems() {
activeItems = {
double: 0,
noPenalty: false
};
showActiveItemIndicator();
}
// End the game and display appropriate message
function endGame(isWin) {
console.log('=== endGame called ===');
console.log('Is win:', isWin, 'Current score:', currentScore);
// Disable all interactive elements
rollButton.disabled = true;
upgradeButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
// Disable all interactive elements
colorOptions.forEach(option => {
option.disabled = true;
option.classList.add('opacity-50', 'cursor-not-allowed');
});
// Disable item buttons
useDoublePointsButton.disabled = true;
useNoPenaltyButton.disabled = true;
useLuckyStreakButton.disabled = true;
// Hide active indicators
activeItemIndicator.classList.add('hidden');
const luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (luckyRollsIndicator) {
luckyRollsIndicator.classList.add('hidden');
}
// Create game over message element
const gameOverMessage = document.createElement('div');
gameOverMessage.className = `mt-4 p-4 rounded-lg text-center font-bold ${isWin ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`;
// Format final score to 2 decimal places
const formattedScore = currentScore.toFixed(2);
if (isWin) {
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Congratulations!</div>
<p>You won the game!</p>
<p>You obtained the Unique dice!</p>
<p class="mt-2 text-sm">Final Score: ${formattedScore}</p>
<p class="mt-1 text-sm">Total Rolls: ${rollCount}</p>
<p class="mt-1 text-sm">Double Points Cards: ${doublePointsCards}</p>
<p class="mt-1 text-sm">No Penalty Cards: ${noPenaltyCards}</p>
<p class="mt-1 text-sm">Lucky Streak Cards: ${luckyStreakCards}</p>
<p class="mt-1 text-sm">Final Tier Multiplier: ×${(1 + currentTier / 10).toFixed(1)}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
createConfetti();
} else {
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Game Over!</div>
<p>Your score went negative.</p>
<p>Better luck next time!</p>
<p class="mt-2 text-sm">Final Score: ${formattedScore}</p>
<p class="mt-1 text-sm">Total Rolls: ${rollCount}</p>
<p class="mt-1 text-sm">Double Points Cards: ${doublePointsCards}</p>
<p class="mt-1 text-sm">No Penalty Cards: ${noPenaltyCards}</p>
<p class="mt-1 text-sm">Lucky Streak Cards: ${luckyStreakCards}</p>
<p class="mt-1 text-sm">Final Tier Multiplier: ×${(1 + currentTier / 10).toFixed(1)}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
}
// Add restart button event listener
gameOverMessage.querySelector('#play-again').addEventListener('click', () => {
window.location.reload();
});
// Replace game message display with game over message
const gameMessageContainer = gameMessageDisplay.parentElement;
gameMessageContainer.replaceChild(gameOverMessage, gameMessageDisplay);
gameMessageDisplay = gameOverMessage;
// Add game end to history
addToHistory(isWin ? 'GAME WIN' : 'GAME OVER', 0);
console.log('=== endGame completed ===');
}
// Show warning when score is low
function showLowScoreWarning() {
// Only show warning if not already showing
if (gameMessageDisplay.classList.contains('hidden')) {
showGameMessage('Warning: Low score! Risk of game over.', 'text-orange-500');
}
}
// Update color selection UI to show the currently selected color
function updateColorSelection(selectedIndex) {
// Remove active state from all color options
colorOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-offset-2', 'ring-primary'));
// Add active state to the selected color option
if (selectedIndex >= 0 && selectedIndex < colorOptions.length) {
colorOptions[selectedIndex].classList.add('ring-2', 'ring-offset-2', 'ring-primary');
}
}
// Add result to history
function addToHistory(result, scoreChangeData, isFreeRoll = false, isLuckyRoll = false) {
const now = new Date();
const timeString = now.toLocaleTimeString();
// Determine score change display and color
let scoreChangeText = '';
let scoreChangeClass = '';
let actionText = '';
if (result === 'UPGRADE') {
actionText = 'Upgraded dice';
scoreChangeText = `${scoreChangeData}`;
scoreChangeClass = 'text-red-500';
} else if (result === 'ITEM') {
actionText = `Received <span class="font-bold text-purple-500">${scoreChangeData}</span>`;
scoreChangeText = '+1';
scoreChangeClass = 'text-purple-500';
} else if (result === 'GAME WIN' || result === 'GAME OVER') {
actionText = result;
scoreChangeText = '';
scoreChangeClass = '';
} else {
const scoreChange = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
actionText = `Rolled a <span class="font-bold text-primary">${result}</span>`;
// Add free roll indicator if applicable
if (isFreeRoll) {
actionText += ` <span class="text-yellow-500">(Free Roll)</span>`;
}
// Add lucky roll indicator if applicable
if (isLuckyRoll) {
actionText += ` <span class="text-yellow-500">(Lucky Roll)</span>`;
}
// Add item effect indicators if applicable
const activeItemsText = [];
if (itemsUsed && itemsUsed.double > 0) {
activeItemsText.push(`<span class="text-green-500">(Double Points ×${itemsUsed.double})</span>`);
}
if (itemsUsed && itemsUsed.noPenalty) {
activeItemsText.push(`<span class="text-blue-500">(No Penalty)</span>`);
}
if (activeItemsText.length > 0) {
actionText += ` ${activeItemsText.join(' ')}`;
}
if (scoreChange > 0) {
scoreChangeText = `+${scoreChange}`;
scoreChangeClass = 'text-green-500';
} else if (scoreChange < 0) {
scoreChangeText = `${scoreChange}`;
scoreChangeClass = 'text-red-500';
} else {
scoreChangeText = '±0';
scoreChangeClass = 'text-gray-500';
}
// Show base change if different from final change (items were used)
if ((itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) && baseChange !== scoreChange) {
scoreChangeText += ` <span class="text-xs">(Base: ${baseChange > 0 ? '+' : ''}${baseChange})</span>`;
}
}
const listItem = document.createElement('li');
listItem.className = 'py-1 border-b border-gray-100 grid grid-cols-3 gap-2 items-center';
listItem.innerHTML = `
<span class="col-span-1 text-gray-500">${timeString}</span>
<span class="col-span-1">${actionText}</span>
<span class="col-span-1 text-right font-medium ${scoreChangeClass}">${scoreChangeText}</span>
`;
historyList.prepend(listItem);
// Keep only last 10 history items
if (historyList.children.length > 10) {
historyList.removeChild(historyList.lastChild);
}
// Ensure scroll is at the bottom
const historyContainer = document.getElementById('history-container');
if (historyContainer) {
historyContainer.scrollTop = historyContainer.scrollHeight;
}
}
// Create confetti effect
function createConfetti() {
const confettiContainer = document.createElement('div');
confettiContainer.className = 'fixed inset-0 pointer-events-none overflow-hidden';
document.body.appendChild(confettiContainer);
// Create 50 confetti pieces
for (let i = 0; i < 50; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
// Random position
const posX = Math.random() * 100;
const delay = Math.random() * 3;
const duration = 3 + Math.random() * 2;
// Random colors
const colors = ['#3b82f6', '#f97316', '#10b981', '#ef4444', '#8b5cf6'];
const color = colors[Math.floor(Math.random() * colors.length)];
confetti.style.left = `${posX}%`;
confetti.style.backgroundColor = color;
confetti.style.animationDelay = `${delay}s`;
confetti.style.animationDuration = `${duration}s`;
confettiContainer.appendChild(confetti);
}
// Remove confetti container after animation completes
setTimeout(() => {
document.body.removeChild(confettiContainer);
}, 5000);
}
// Create particle effect on lock
function createLockParticles(lockElement) {
// Get lock element position
const rect = lockElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// Create particle container
const particleContainer = document.createElement('div');
particleContainer.className = 'absolute pointer-events-none';
particleContainer.style.left = `${centerX}px`;
particleContainer.style.top = `${centerY}px`;
particleContainer.style.transform = 'translate(-50%, -50%)';
document.body.appendChild(particleContainer);
// Create 15 particles
for (let i = 0; i < 15; i++) {
const particle = document.createElement('div');
particle.className = 'lock-particle';
// Random direction and distance
const angle = Math.random() * Math.PI * 2;
const distance = 10 + Math.random() * 20;
const tx = Math.cos(angle) * distance;
const ty = Math.sin(angle) * distance;
// Random color (use the color of the unlocked dice)
const color = lockElement.style.backgroundColor;
// Random animation duration
const duration = 0.5 + Math.random() * 0.5;
// Set particle styles
particle.style.backgroundColor = color;
particle.style.setProperty('--tx', `${tx}px`);
particle.style.setProperty('--ty', `${ty}px`);
particle.style.animation = `lock-particle ${duration}s ease-out forwards`;
particleContainer.appendChild(particle);
}
// Remove particle container after animation completes
setTimeout(() => {
document.body.removeChild(particleContainer);
}, 1000);
}
// Function to darken a color by a certain percentage
function darkenColor(color, percent) {
const hex = color.replace('#', '');
let r = parseInt(hex.substr(0, 2), 16);
let g = parseInt(hex.substr(2, 2), 16);
let b = parseInt(hex.substr(4, 2), 16);
// Darken each channel by the percentage
r = Math.max(0, Math.floor(r * (1 - percent / 100)));
g = Math.max(0, Math.floor(g * (1 - percent / 100)));
b = Math.max(0, Math.floor(b * (1 - percent / 100)));
// Convert back to hex
const darkenedHex = '#' +
r.toString(16).padStart(2, '0') +
g.toString(16).padStart(2, '0') +
b.toString(16).padStart(2, '0');
return darkenedHex;
}
// Change dice color function
function changeDiceColor(color) {
console.log(`changeDiceColor called with color: ${color}`);
const faces = document.querySelectorAll('.dice-face');
console.log(`Found ${faces.length} dice faces`);
let dotColor, borderColor;
// Special cases for high contrast
if (color.toUpperCase() === '#FFFFFF') {
// White dice - use black dots and light gray borders
dotColor = '#000000';
borderColor = '#CCCCCC';
} else if (color.toUpperCase() === '#555555') {
// Dark gray dice - use white dots and slightly lighter gray borders
dotColor = '#FFFFFF';
borderColor = '#777777';
} else {
// Calculate darker color for dots and borders (40% darker for more contrast)
dotColor = darkenColor(color, 40);
borderColor = darkenColor(color, 30); // Slightly lighter border than dots
}
faces.forEach(face => {
// Set face background color
face.style.backgroundColor = color;
// Set face border color
face.style.border = `3px solid ${borderColor}`;
// Set dots color
const dots = face.querySelectorAll('.dot');
dots.forEach(dot => {
dot.style.backgroundColor = dotColor;
// Add slight border to dots for better definition
dot.style.border = color.toUpperCase() === '#FFFFFF' ? '1px solid rgba(0, 0, 0, 0.2)' : '1px solid rgba(0, 0, 0, 0.1)';
});
});
}
// Update tier multiplier display
function updateTierMultiplierDisplay() {
const display = document.getElementById('tier-multiplier-display');
if (display) {
display.innerHTML = `Tier Multiplier: <span class="font-semibold text-purple-500">×${(1 + currentTier / 10).toFixed(1)}</span>`;
}
}
// Event listener for roll button
rollButton.addEventListener('click', rollDice);
// Event listener for upgrade button
upgradeButton.addEventListener('click', upgradeDice);
// Event listeners for item buttons
useDoublePointsButton.addEventListener('click', useDoublePointsCard);
useNoPenaltyButton.addEventListener('click', useNoPenaltyCard);
useLuckyStreakButton.addEventListener('click', useLuckyStreakCard);
// Event listeners for color options
colorOptions.forEach((option, index) => {
// Disable all color options except the first one initially
if (index !== 0) {
option.disabled = true;
option.classList.add('locked');
}
option.addEventListener('click', () => {
// Check if game is over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if color is unlocked
if (option.disabled) {
showGameMessage('You need to upgrade to unlock this dice color!', 'text-orange-500');
return;
}
const color = option.getAttribute('data-color');
changeDiceColor(color);
// Add active state to selected color
colorOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-offset-2', 'ring-primary'));
option.classList.add('ring-2', 'ring-offset-2', 'ring-primary');
// Update current tier to the selected color's tier
currentTier = index;
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
updateTierMultiplierDisplay();
});
});
// Initialize dice on page load
window.addEventListener('DOMContentLoaded', () => {
console.log('=== DOMContentLoaded event fired ===');
// Check if elements exist
console.log('Checking if elements exist before initialization:');
console.log('best-record-display exists:', !!document.getElementById('best-record-display'));
console.log('best-record-value exists:', !!document.getElementById('best-record-value'));
console.log('DOM fully loaded');
initializeDice();
// Set default color (first option)
if (colorOptions.length > 0) {
const defaultColor = colorOptions[0].getAttribute('data-color');
changeDiceColor(defaultColor);
colorOptions[0].classList.add('ring-2', 'ring-offset-2', 'ring-primary');
}
// Initialize current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Initialize tier multiplier display
updateTierMultiplierDisplay();
// Initialize items display
updateItemsDisplay();
// Initialize upgrade cost display
updateUpgradeCostDisplay();
// Show welcome message
setTimeout(() => {
showGameMessage('Welcome! Roll the dice to earn points and upgrade your dice!', 'text-blue-500');
}, 1000);
});
// Also try initializing on window load
window.addEventListener('load', () => {
console.log('Window loaded');
// If dice not already initialized, try again
if (diceElement.children.length === 0) {
console.log('Dice not initialized, trying again...');
initializeDice();
}
});
// Game Rules Modal Functionality
(function() {
// Get elements
const rulesButton = document.getElementById('game-rules-button');
const rulesModal = document.getElementById('game-rules-modal');
const closeButtons = rulesModal.querySelectorAll('.close-button');
const modalBackdrop = rulesModal.querySelector('.modal-backdrop');
const modalContent = rulesModal.querySelector('.modal-content');
// Function to prevent background scrolling
function preventBackgroundScroll(event) {
// Allow scrolling inside the modal content
if (modalContent.contains(event.target)) {
// Check if we're at the top or bottom of the modal content
const isAtTop = modalContent.scrollTop === 0;
const isAtBottom = modalContent.scrollTop + modalContent.clientHeight >= modalContent.scrollHeight;
// Prevent scrolling if at the top and scrolling up, or at the bottom and scrolling down
if ((isAtTop && event.deltaY < 0) || (isAtBottom && event.deltaY > 0)) {
event.preventDefault();
}
} else {
// Prevent scrolling outside the modal content
event.preventDefault();
}
}
// Function to open modal
function openModal() {
// Add event listeners to prevent background scrolling
document.addEventListener('wheel', preventBackgroundScroll, { passive: false });
document.addEventListener('touchmove', preventBackgroundScroll, { passive: false });
// Add open class to trigger animations
rulesModal.classList.add('modal-open');
// Show the modal
rulesModal.style.visibility = 'visible';
}
// Function to close modal
function closeModal() {
// Remove open class to trigger animations
rulesModal.classList.remove('modal-open');
// Remove event listeners that prevent background scrolling
document.removeEventListener('wheel', preventBackgroundScroll);
document.removeEventListener('touchmove', preventBackgroundScroll);
// Hide the modal after animation completes
setTimeout(() => {
rulesModal.style.visibility = 'hidden';
}, 300);
}
// Add event listeners
if (rulesButton) {
rulesButton.addEventListener('click', openModal);
}
// Close buttons
closeButtons.forEach(button => {
button.addEventListener('click', closeModal);
});
// Close when clicking outside the modal content
modalBackdrop.addEventListener('click', closeModal);
// Close when pressing Escape key
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && rulesModal.classList.contains('modal-open')) {
closeModal();
}
});
// Add hover effect to rules button
if (rulesButton) {
rulesButton.addEventListener('mouseenter', () => {
rulesButton.classList.add('scale-105');
});
rulesButton.addEventListener('mouseleave', () => {
rulesButton.classList.remove('scale-105');
});
}
// Add click effect to close buttons
closeButtons.forEach(button => {
button.addEventListener('mousedown', () => {
button.classList.add('scale-95');
});
button.addEventListener('mouseup', () => {
button.classList.remove('scale-95');
});
button.addEventListener('mouseleave', () => {
button.classList.remove('scale-95');
});
});
})();
</script>
</body>
</html>
v.1.2.1
HTML<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Dice Roll Animation</title>
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#f97316',
dark: '#1e293b',
light: '#f8fafc'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'shake': 'shake 0.5s ease-in-out',
},
keyframes: {
shake: {
'0%, 100%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(-5px)' },
'50%': { transform: 'translateX(5px)' },
'75%': { transform: 'translateX(-5px)' },
}
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.preserve-3d {
transform-style: preserve-3d;
}
.perspective {
perspective: 1000px;
}
.backface-hidden {
backface-visibility: hidden;
}
.dice-shadow {
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
}
.glass-effect {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.modal-content {
transform: translateY(-20px);
opacity: 0;
transition: all 0.3s ease-out;
}
.modal-open .modal-content {
transform: translateY(0);
opacity: 1;
}
.modal-open .modal-backdrop {
opacity: 1;
visibility: visible;
}
.trophy-animation {
animation: trophy-pulse 2s ease-in-out infinite;
}
.record-animation {
animation: record-shine 2s ease-in-out;
}
}
@keyframes trophy-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes record-shine {
0% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.7); }
70% { box-shadow: 0 0 0 15px rgba(251, 191, 36, 0); }
100% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0); }
}
/* Custom styles for 3D dice */
.dice-scene {
perspective: 1500px;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.dice-container {
position: relative;
width: 100px;
height: 100px;
transform-style: preserve-3d;
transition: transform 1s ease-out;
transform: rotateX(20deg) rotateY(20deg);
}
.dice-face {
position: absolute;
width: 100px;
height: 100px;
border-radius: 8px;
background-color: white;
border: 2px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
/* Dice face positions */
.face-1 { transform: translateZ(50px); }
.face-2 { transform: rotateY(90deg) translateZ(50px); }
.face-3 { transform: rotateY(180deg) translateZ(50px); }
.face-4 { transform: rotateY(-90deg) translateZ(50px); }
.face-5 { transform: rotateX(90deg) translateZ(50px); }
.face-6 { transform: rotateX(-90deg) translateZ(50px); }
/* Dots on dice faces */
.dot {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: black;
}
/* Dot positions for each face */
.face-1 .dot { top: 40px; left: 40px; }
.face-2 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-2 .dot:nth-child(2) { top: 60px; left: 60px; }
.face-3 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-3 .dot:nth-child(2) { top: 40px; left: 40px; }
.face-3 .dot:nth-child(3) { top: 60px; left: 60px; }
.face-4 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-4 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-4 .dot:nth-child(3) { top: 60px; left: 20px; }
.face-4 .dot:nth-child(4) { top: 60px; left: 60px; }
.face-5 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-5 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-5 .dot:nth-child(3) { top: 40px; left: 40px; }
.face-5 .dot:nth-child(4) { top: 60px; left: 20px; }
.face-5 .dot:nth-child(5) { top: 60px; left: 60px; }
.face-6 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-6 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-6 .dot:nth-child(3) { top: 40px; left: 20px; }
.face-6 .dot:nth-child(4) { top: 40px; left: 60px; }
.face-6 .dot:nth-child(5) { top: 60px; left: 20px; }
.face-6 .dot:nth-child(6) { top: 60px; left: 60px; }
.dice-result {
transition: all 0.5s ease-out;
}
/* Fixed height for result display to prevent page jumping */
#result-display {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.result-shine {
animation: shine 0.5s ease-out;
}
.score-increase {
color: #10b981;
animation: scorePopup 1s ease-out;
}
.score-decrease {
color: #ef4444;
animation: scorePopup 1s ease-out;
}
.score-neutral {
color: #6b7280;
animation: scorePopup 1s ease-out;
}
/* Locked color option styles */
.color-option.locked {
position: relative;
opacity: 0.5;
cursor: not-allowed;
}
.color-option.locked::after {
content: '\f023'; /* Lock icon from Font Awesome */
font-family: 'FontAwesome';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(0, 0, 0, 0.5);
font-size: 16px;
}
@keyframes shine {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
@keyframes scorePopup {
0% {
transform: translateY(0);
opacity: 0;
}
50% {
transform: translateY(-10px);
opacity: 1;
}
100% {
transform: translateY(-20px);
opacity: 0;
}
}
.confetti {
position: absolute;
width: 10px;
height: 10px;
background-color: #f97316;
animation: confetti-fall 3s ease-in-out infinite;
}
@keyframes confetti-fall {
0% {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
@keyframes lock-particle {
0% {
transform: translate(0, 0);
opacity: 1;
}
100% {
transform: translate(var(--tx), var(--ty));
opacity: 0;
}
}
/* Game Rules Modal Styles */
.game-rules-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
visibility: hidden;
box-sizing: border-box;
}
.game-rules-modal .modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease-out;
}
.game-rules-modal .modal-content {
position: relative;
background-color: white;
border-radius: 1rem;
max-width: 90%;
max-height: 80vh;
width: 500px;
overflow-y: auto;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.game-rules-modal .modal-header {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.game-rules-modal .modal-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
transition: color 0.2s ease-in-out;
}
.game-rules-modal .close-button:hover {
color: #1e293b;
}
.game-rules-modal .modal-body {
padding: 1.5rem;
line-height: 1.6;
color: #374151;
}
.game-rules-modal .modal-body h3 {
font-size: 1.1rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #1e293b;
}
.game-rules-modal .modal-body p {
margin-bottom: 1rem;
}
.game-rules-modal .modal-body ul {
margin-bottom: 1rem;
padding-left: 1.5rem;
list-style-type: disc;
}
.game-rules-modal .modal-body li {
margin-bottom: 0.5rem;
}
.game-rules-modal .modal-body strong {
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
}
.game-rules-modal .modal-footer button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-size: 1.25rem;
font-weight: 500;
transition: background-color 0.2s ease-in-out;
}
.game-rules-modal .modal-footer button:hover {
background-color: #2563eb;
}
/* Game Rules Button Styles */
.game-rules-button {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 40;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease-in-out;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.game-rules-button:hover {
background-color: #2563eb;
transform: translateY(-2px);
}
.game-rules-button i {
font-size: 1.1rem;
}
</style>
</head>
<body class="bg-gradient-to-br from-light to-gray-200 min-h-screen flex flex-col items-center justify-center p-4 m-0">
<!-- Game Rules Button -->
<button id="game-rules-button" class="game-rules-button">
<i class="fa fa-book"></i>
<span>Game Rules</span>
</button>
<!-- Game Rules Modal -->
<div id="game-rules-modal" class="game-rules-modal">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">3D Dice Game Rules</h2>
<button class="close-button">×</button>
</div>
<div class="modal-body">
<p>Welcome to the 3D Dice Game! This is an exciting game that combines luck, strategy, and upgrading mechanics. Below is the complete game rules explanation:</p>
<h3>Basic Gameplay</h3>
<ul>
<li>Click the "Roll Dice" button to roll the dice</li>
<li>The dice will randomly show a number between 1-6</li>
<li>Gain or lose points based on the number rolled</li>
<li>The goal is to obtain the highest tier "Unique" dice</li>
</ul>
<h3>Score System</h3>
<ul>
<li>Rolling 1 or 2: Lose 1 point (multiplier not applied)</li>
<li>Rolling 3: No points gained or lost</li>
<li>Rolling 4: Gain 1 point</li>
<li>Rolling 5: Gain 2 points</li>
<li>Rolling 6: Gain 3 points</li>
</ul>
<h3>Dice Upgrade System</h3>
<p>There are 10 different tiers of dice in the game, from common to rare:</p>
<ul>
<li><strong>Empty</strong> (Initial) - Score Multiplier ×1.0</li>
<li><strong>Common</strong> - Score Multiplier ×1.1</li>
<li><strong>Unusual</strong> - Score Multiplier ×1.2</li>
<li><strong>Rare</strong> - Score Multiplier ×1.3</li>
<li><strong>Epic</strong> - Score Multiplier ×1.4</li>
<li><strong>Legendary</strong> - Score Multiplier ×1.5</li>
<li><strong>Mythic</strong> - Score Multiplier ×1.6</li>
<li><strong>Ultra</strong> - Score Multiplier ×1.7</li>
<li><strong>Super</strong> - Score Multiplier ×1.8</li>
<li><strong>Unique</strong> - Score Multiplier ×1.9</li>
</ul>
<p>Each upgrade costs (5 + current tier) points. For example, upgrading from Empty (tier 0) costs 5 points, upgrading from Common (tier 1) costs 6 points, and so on. After upgrading, you can choose to use the new dice or continue using the old one.</p>
<h3>Item System</h3>
<p>When upgrading, you have a chance to obtain item cards that can be used at critical moments:</p>
<ul>
<li><strong>Double Points Card</strong>: Doubles the points from one roll</li>
<li><strong>No Penalty Card</strong>: Prevents point loss when rolling 1 or 2</li>
<li><strong>Lucky Streak Card</strong>: Next 3 rolls of 1 or 2 won't count towards your total roll count</li>
</ul>
<p>Items can be stockpiled and each card can only be used once. Double Points Cards and No Penalty Cards can be used simultaneously. Lucky Streak Cards make your next 3 rolls of 1 or 2 not count towards your total roll count, giving you a chance to recover from bad luck.</p>
<h3>Game Rules</h3>
<ul>
<li>Initial score is 10 points</li>
<li>Score multiplier changes based on the current dice tier being used</li>
<li>Game over when score is less than 0</li>
<li>Game victory when obtaining the Unique dice</li>
<li>Unlocked dice colors can be switched at any time</li>
</ul>
<h3>Operation Tips</h3>
<ul>
<li>Click on dice color options to switch the dice being used</li>
<li>Click the "Upgrade Dice" button to upgrade your dice (requires 5 points)</li>
<li>Click the "Use" button next to item cards to use them</li>
<li>Press the ESC key to close the rules window</li>
</ul>
<p>Good luck, and may you successfully obtain the highest tier Unique dice!</p>
</div>
<div class="modal-footer">
<button class="close-button">Got it</button>
</div>
</div>
</div>
<div class="w-full max-w-md mx-auto glass-effect rounded-2xl p-6 dice-shadow relative z-10">
<h1 class="text-3xl font-bold text-center text-dark mb-8">3D Dice Roll</h1>
<div class="flex flex-col items-center justify-center mb-8">
<!-- Dice display area -->
<div id="dice-display" class="w-48 h-48 flex items-center justify-center mb-4 bg-gray-100 rounded-lg">
<!-- Dice scene for 3D perspective -->
<div class="dice-scene">
<div id="dice" class="dice-container">
<!-- Dice faces will be inserted here by JavaScript -->
</div>
</div>
</div>
<!-- Result display -->
<div id="result-display" class="text-2xl font-bold text-center mb-4 hidden">
Result: <span id="result-value" class="text-primary">0</span>
</div>
<!-- Score display -->
<div id="score-display" class="text-xl font-bold text-center mb-2">
Score: <span id="score-value" class="text-secondary">10</span>
<span id="score-change" class="ml-2 text-sm font-normal"></span>
</div>
<!-- Roll count display -->
<div id="roll-count-display" class="text-lg font-medium text-center mb-4">
Rolls: <span id="roll-count-value" class="text-gray-700">0</span>
</div>
<!-- Roll button -->
<button id="roll-button" class="bg-primary hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-opacity-50">
<i class="fa fa-random mr-2"></i> Roll Dice
</button>
<!-- Upgrade button and current tier display -->
<div class="flex flex-col items-center mt-4">
<div id="current-tier" class="text-sm text-gray-600 mb-2">
Current Dice: <span class="font-semibold text-primary">Empty</span>
</div>
<div id="tier-multiplier-display" class="text-sm text-gray-600 mb-2">
Tier Multiplier: <span class="font-semibold text-purple-500">×1.0</span>
</div>
<button id="upgrade-button" class="bg-secondary hover:bg-orange-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-orange-300 focus:ring-opacity-50">
<i class="fa fa-arrow-up mr-1"></i> Upgrade Dice <span id="upgrade-cost">(Cost: 5)</span>
</button>
</div>
<!-- Game message display -->
<div id="game-message" class="mt-4 text-center font-semibold hidden"></div>
<!-- Items display -->
<div id="items-display" class="mt-6 grid grid-cols-2 gap-4">
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-plus-circle text-green-500 text-xl mr-2"></i>
<span class="font-medium">Double Points Card</span>
</div>
<div class="flex items-center">
<span id="double-points-count" class="bg-green-100 text-green-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-double-points" class="bg-green-500 hover:bg-green-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Doubles your score change for one roll</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-shield text-blue-500 text-xl mr-2"></i>
<span class="font-medium">No Penalty Card</span>
</div>
<div class="flex items-center">
<span id="no-penalty-count" class="bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-no-penalty" class="bg-blue-500 hover:bg-blue-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Prevents score loss when rolling 1 or 2</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa fa-star text-yellow-500 text-xl mr-2"></i>
<span class="font-medium">Lucky Streak Card</span>
</div>
<div class="flex items-center">
<span id="lucky-streak-count" class="bg-yellow-100 text-yellow-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">0</span>
<button id="use-lucky-streak" class="bg-yellow-500 hover:bg-yellow-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Next 3 rolls of 1 or 2 won't count towards your total roll count</p>
</div>
</div>
<!-- Free rolls indicator -->
<div id="free-rolls-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fa fa-star mr-1" style="color: #f59e0b;"></i>
<span>Free Rolls: <span id="free-rolls-count">0</span></span>
</span>
</div>
<!-- Active item indicator -->
<div id="active-item-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
<i id="active-item-icon" class="mr-1"></i>
<span id="active-item-text">Active Item: None</span>
</span>
</div>
</div>
<!-- Dice color selection -->
<div class="mb-8">
<label class="block text-sm font-medium text-gray-700 mb-2">Dice Rarity</label>
<div class="grid grid-cols-5 gap-4">
<!-- Empty -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #FFFFFF; border: 1px solid #dddddd;" data-color="#FFFFFF"></button>
<span class="text-xs text-center text-gray-600">Empty</span>
</div>
<!-- Common -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #7EEF6D;" data-color="#7EEF6D"></button>
<span class="text-xs text-center text-gray-600">Common</span>
</div>
<!-- Unusual -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #FFE65D;" data-color="#FFE65D"></button>
<span class="text-xs text-center text-gray-600">Unusual</span>
</div>
<!-- Rare -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #4d52e3;" data-color="#4d52e3"></button>
<span class="text-xs text-center text-gray-600">Rare</span>
</div>
<!-- Epic -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #861FDE;" data-color="#861FDE"></button>
<span class="text-xs text-center text-gray-600">Epic</span>
</div>
<!-- Legendary -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #DE1F1F;" data-color="#DE1F1F"></button>
<span class="text-xs text-center text-gray-600">Legendary</span>
</div>
<!-- Mythic -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #1fdbde;" data-color="#1fdbde"></button>
<span class="text-xs text-center text-gray-600">Mythic</span>
</div>
<!-- Ultra -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #ff2b75;" data-color="#ff2b75"></button>
<span class="text-xs text-center text-gray-600">Ultra</span>
</div>
<!-- Super -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #2bffa3;" data-color="#2bffa3"></button>
<span class="text-xs text-center text-gray-600">Super</span>
</div>
<!-- Unique -->
<div class="flex flex-col items-center">
<button class="color-option w-full h-8 rounded-md mb-1" style="background-color: #555555;" data-color="#555555"></button>
<span class="text-xs text-center text-gray-600">Unique</span>
</div>
</div>
</div>
<!-- History log -->
<div class="bg-white rounded-lg p-4">
<h2 class="text-lg font-semibold text-center mb-2">Roll History</h2>
<div id="history-container" class="h-64 overflow-y-auto">
<div class="grid grid-cols-3 gap-2 text-xs font-medium text-gray-500 border-b border-gray-200 sticky top-0 bg-white z-10">
<div>Time</div>
<div>Action</div>
<div class="text-right">Score</div>
</div>
<ul id="history-list" class="text-sm">
<!-- History items will be inserted here by JavaScript -->
</ul>
</div>
</div>
</div>
<footer class="mt-8 text-center text-gray-600 text-sm">
<p>Click the button to roll the dice and see the result!</p>
</footer>
<script>
// Set up global error handler
window.addEventListener('error', function(event) {
console.error('Global error caught:', event.error);
// Try to get roll button element
const rollButton = document.getElementById('roll-button');
if (rollButton && rollButton.disabled) {
console.warn('Error during dice roll, re-enabling button');
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
// Try to show error message
const gameMessage = document.getElementById('game-message');
if (gameMessage) {
gameMessage.textContent = 'An error occurred. Please try again.';
gameMessage.className = 'mt-4 text-center font-semibold text-red-500';
gameMessage.classList.remove('hidden');
}
}
});
// DOM elements
const diceElement = document.getElementById('dice');
const rollButton = document.getElementById('roll-button');
const resultDisplay = document.getElementById('result-display');
const resultValue = document.getElementById('result-value');
const historyList = document.getElementById('history-list');
const colorOptions = document.querySelectorAll('.color-option');
const scoreDisplay = document.getElementById('score-display');
const scoreValue = document.getElementById('score-value');
const scoreChange = document.getElementById('score-change');
const upgradeButton = document.getElementById('upgrade-button');
const currentTierDisplay = document.getElementById('current-tier');
const gameMessageDisplay = document.getElementById('game-message');
const rollCountDisplay = document.getElementById('roll-count-display');
const rollCountValue = document.getElementById('roll-count-value');
const upgradeCost = document.getElementById('upgrade-cost');
// Item related DOM elements
const doublePointsCount = document.getElementById('double-points-count');
const noPenaltyCount = document.getElementById('no-penalty-count');
const luckyStreakCount = document.getElementById('lucky-streak-count');
const useDoublePointsButton = document.getElementById('use-double-points');
const useNoPenaltyButton = document.getElementById('use-no-penalty');
const useLuckyStreakButton = document.getElementById('use-lucky-streak');
const activeItemIndicator = document.getElementById('active-item-indicator');
const freeRollsIndicator = document.getElementById('free-rolls-indicator');
const freeRollsCountElement = document.getElementById('free-rolls-count');
// Game state
let currentScore = 10; // Initial score
let currentTier = 0; // Initial dice tier (0 = Empty)
let gameOver = false; // Game over flag
let rollCount = 0; // Number of dice rolls
let luckyRollsRemaining = 0; // Number of lucky rolls remaining
let isRolling = false; // Whether dice is currently rolling
// Item system
let doublePointsCards = 0; // Number of double points cards
let noPenaltyCards = 0; // Number of no penalty cards
let luckyStreakCards = 0; // Number of lucky streak cards
let freeRolls = 0; // Number of free rolls available
let activeItems = { // Currently active items
double: 0, // Number of active double points cards
noPenalty: false // Whether no penalty card is active
};
// Dice tiers configuration
const diceTiers = [
{ name: 'Empty', color: '#FFFFFF' },
{ name: 'Common', color: '#7EEF6D' },
{ name: 'Unusual', color: '#FFE65D' },
{ name: 'Rare', color: '#4d52e3' },
{ name: 'Epic', color: '#861FDE' },
{ name: 'Legendary', color: '#DE1F1F' },
{ name: 'Mythic', color: '#1fdbde' },
{ name: 'Ultra', color: '#ff2b75' },
{ name: 'Super', color: '#2bffa3' },
{ name: 'Unique', color: '#555555' }
];
// Initialize 3D dice
function initializeDice() {
console.log('Initializing dice...');
// Create 6 faces for the dice
const faces = [1, 2, 3, 4, 5, 6];
faces.forEach(faceNumber => {
const face = document.createElement('div');
face.className = `dice-face face-${faceNumber}`;
// Add dots to the face based on the number
for (let i = 0; i < faceNumber; i++) {
const dot = document.createElement('div');
dot.className = 'dot';
face.appendChild(dot);
}
diceElement.appendChild(face);
console.log(`Added face ${faceNumber}`);
});
console.log('Dice initialized with faces:', diceElement.children.length);
// Set initial position to show face 1 clearly
diceElement.style.transform = 'rotateX(0deg) rotateY(0deg)';
}
// Get random rotation values for the dice
function getRandomRotation() {
// Determine which face we want to show
const targetFace = Math.floor(Math.random() * 6) + 1;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let currentX = 0, currentY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
currentX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
currentY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Set target rotation based on target face
// These values are precisely calibrated to show the correct face
let targetX = 0, targetY = 0;
switch(targetFace) {
case 1: // Front face (Z+) - visible when no rotation
targetX = 0;
targetY = 0;
break;
case 2: // Left face (X-) - visible when Y rotated 270 degrees
targetX = 0;
targetY = 270;
break;
case 3: // Back face (Z-) - visible when Y rotated 180 degrees
targetX = 0;
targetY = 180;
break;
case 4: // Right face (X+) - visible when Y rotated 90 degrees
targetX = 0;
targetY = 90;
break;
case 5: // Top face (Y-) - visible when X rotated -90 degrees
targetX = -90;
targetY = 0;
break;
case 6: // Bottom face (Y+) - visible when X rotated 90 degrees
targetX = 90;
targetY = 0;
break;
}
// Calculate the shortest path to the target rotation
// This prevents large rotation values from accumulating
let diffX = targetX - currentX;
let diffY = targetY - currentY;
// Normalize the difference to the range [-180, 180] to find the shortest path
diffX = ((diffX + 180) % 360) - 180;
diffY = ((diffY + 180) % 360) - 180;
// Add multiple full rotations for spinning effect (2-4 full rotations)
const fullRotations = 2 + Math.floor(Math.random() * 3);
// Calculate final rotation with full spins
// We add full rotations in the direction of the shortest path
const spinDirectionX = diffX >= 0 ? 1 : -1;
const spinDirectionY = diffY >= 0 ? 1 : -1;
const finalX = currentX + diffX + spinDirectionX * fullRotations * 360;
const finalY = currentY + diffY + spinDirectionY * fullRotations * 360;
// Add a tiny bit of randomness to make it look more natural
// But not enough to change which face is visible
const randomX = (Math.random() - 0.5) * 2;
const randomY = (Math.random() - 0.5) * 2;
return {
x: finalX + randomX,
y: finalY + randomY,
targetFace: targetFace // Return the target face so we don't have to recalculate it
};
}
// Roll the dice function
function rollDice() {
// Check if game is over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if there are free rolls available
const isFreeRoll = freeRolls > 0;
// Check if in lucky streak mode
const isLuckyRoll = luckyRollsRemaining > 0;
// Increment roll count only if not a free roll (lucky rolls will be handled based on result)
if (!isFreeRoll) {
rollCount++;
console.log(`Roll count: ${rollCount}`);
} else {
// Decrement free rolls count
freeRolls--;
updateFreeRollsDisplay();
console.log(`Free roll used. Remaining free rolls: ${freeRolls}`);
}
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
console.log('Rolling dice...');
console.log('Button state before disable:', rollButton.disabled);
// Set rolling state and disable button during animation
isRolling = true;
rollButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
// Disable other interactive elements during roll
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
useDoublePointsButton.disabled = true;
useNoPenaltyButton.disabled = true;
useLuckyStreakButton.disabled = true;
// Disable color options during roll
colorOptions.forEach(option => {
option.disabled = true;
});
console.log('Button state after disable:', rollButton.disabled);
// Hide result display
resultDisplay.classList.add('hidden');
// Reset dice display height - use Tailwind class instead of inline style
const diceDisplay = document.getElementById('dice-display');
if (diceDisplay) {
// Ensure the height class is applied to prevent layout shifts during animation
diceDisplay.classList.add('h-48');
}
try {
// Set animation duration
const duration = 2000; // Fixed duration for consistent experience
// Get random rotation values for the final position
const rotationData = getRandomRotation();
console.log('Rotation data:', rotationData);
console.log('Target face:', rotationData.targetFace);
// Animate the dice using JavaScript for more control
animateDice(duration, rotationData, { isFreeRoll, isLuckyRoll });
// Set a safety timeout to ensure button is re-enabled even if something goes wrong
setTimeout(() => {
if (rollButton.disabled) {
console.warn('Safety timeout: Re-enabling roll button');
enableRollButton();
}
}, duration + 1000); // Add 1 second buffer
} catch (error) {
console.error('Error during dice roll:', error);
// Re-enable button if there's an error
enableRollButton();
showGameMessage('An error occurred during the dice roll. Please try again.', 'text-red-500');
}
}
// Animate the dice with spin animation
function animateDice(duration, rotationData, rollContext) {
console.log('Animate dice called with rotationData:', rotationData);
console.log('Roll context:', rollContext);
const startTime = performance.now();
const finalRotation = rotationData;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let startX = 0, startY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
startX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
startY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Function to handle each animation frame
function animate(currentTime) {
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
// Apply easing function for smooth, natural animation
const easedProgress = easeOutCubic(progress);
// Calculate current rotation - smooth continuous rotation
const currentX = startX + (finalRotation.x - startX) * easedProgress;
const currentY = startY + (finalRotation.y - startY) * easedProgress;
// Spin animation: rotate in place
diceElement.style.transform = `rotateX(${currentX}deg) rotateY(${currentY}deg)`;
// Continue animation if not complete
if (progress < 1) {
requestAnimationFrame(animate);
} else {
// Animation complete - ensure we're at the exact target rotation
diceElement.style.transform = `rotateX(${finalRotation.x}deg) rotateY(${finalRotation.y}deg)`;
// Show result after a delay to ensure rotation is complete and CSS has applied
console.log('Animation complete, waiting before finalizing...');
setTimeout(() => {
// Double-check that the transform has been applied
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
console.log('Current transform after animation:', currentTransform);
console.log('Calling finalizeAnimation...');
finalizeAnimation(finalRotation, rollContext);
}, 600);
}
}
// Start the animation
requestAnimationFrame(animate);
}
// Helper function to normalize angles to the range [-180, 180]
function normalizeAngle(angle) {
angle = angle % 360;
if (angle > 180) angle -= 360;
if (angle < -180) angle += 360;
return angle;
}
// Easing function for smooth, natural animation with gentle acceleration and deceleration
// Uses a cubic easing function that starts slow, accelerates, then slows down at the end
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// Calculate score change based on dice roll result
function calculateScoreChange(result) {
let baseChange = 0;
switch(result) {
case 1:
case 2:
baseChange = -1;
break;
case 3:
baseChange = 0;
break;
case 4:
baseChange = 1;
break;
case 5:
baseChange = 2;
break;
case 6:
baseChange = 3;
break;
default:
baseChange = 0;
}
// Apply current tier multiplier
// If losing points (baseChange < 0), use multiplier of 1 instead of tier multiplier
const tierMultiplier = 1 + currentTier / 10;
let finalChange;
if (baseChange < 0) {
// For point loss, use multiplier of 1 regardless of tier
finalChange = baseChange * 1;
} else {
// For point gain or neutral, use tier multiplier
finalChange = baseChange * tierMultiplier;
}
// Apply active item effects
const itemsUsed = {
double: activeItems.double,
noPenalty: activeItems.noPenalty
};
// Apply no penalty card first
if (activeItems.noPenalty && baseChange < 0) {
finalChange = 0;
}
// Apply double points cards
if (activeItems.double > 0) {
finalChange = finalChange * Math.pow(2, activeItems.double);
}
// Round to 2 decimal places to avoid floating point precision issues
finalChange = Math.round(finalChange * 100) / 100;
return {
baseChange: baseChange,
finalChange: finalChange,
itemsUsed: itemsUsed,
tierMultiplier: tierMultiplier
};
}
// Enable roll button
function enableRollButton() {
console.log('Button state before enable:', rollButton.disabled);
// Directly enable the button
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
// Re-enable other interactive buttons based on their conditions
updateUpgradeCostDisplay(); // This will enable/disable upgrade button based on score
updateItemsDisplay(); // This will enable/disable item buttons based on availability
console.log('Button state after enable:', rollButton.disabled);
// Double-check and force enable if needed
if (rollButton.disabled) {
console.warn('Forcing button enable');
setTimeout(() => {
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
console.log('Button state after forced enable:', rollButton.disabled);
}, 100);
}
}
// Finalize the animation and show result
function finalizeAnimation(rotationData, rollContext) {
console.log('=== Finalize animation called ===');
console.log('Current time:', new Date().toISOString().split('T')[1]);
console.log('Rotation data:', rotationData);
console.log('Roll context:', rollContext);
const finalRotation = rotationData;
const isFreeRoll = rollContext && rollContext.isFreeRoll || false;
const isLuckyRoll = rollContext && rollContext.isLuckyRoll || false;
// Use the target face directly instead of recalculating
const result = finalRotation.targetFace;
console.log(`Final result: ${result} (should show face ${result})`);
// Verify dice is in the correct position
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
console.log('Current dice transform:', currentTransform);
// Show result display
console.log('Showing result display...');
resultValue.textContent = result;
resultDisplay.classList.remove('hidden');
// Add shine effect to result
resultDisplay.classList.add('result-shine');
setTimeout(() => {
resultDisplay.classList.remove('result-shine');
}, 500);
// Calculate and update score
console.log('Calculating score change...');
const scoreChangeData = calculateScoreChange(result);
console.log('Score change data:', scoreChangeData);
currentScore += scoreChangeData.finalChange;
console.log('Updated score:', currentScore);
// Update score display with animation
console.log('Updating score display...');
updateScoreDisplay(scoreChangeData);
// Enable button
console.log('Enabling roll button...');
enableRollButton();
// Add to history - include whether it was a free roll or lucky roll
addToHistory(result, scoreChangeData, isFreeRoll, isLuckyRoll);
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Handle lucky roll logic
if (luckyRollsRemaining > 0) {
// Decrement remaining lucky rolls
luckyRollsRemaining--;
// If rolled 1 or 2, decrement roll count
if (result === 1 || result === 2) {
rollCount--;
console.log(`Lucky roll: Reverted roll count to ${rollCount}`);
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
// Show message
showGameMessage('Lucky! This 1/2 roll doesn\'t count!', 'text-yellow-500');
}
// Update lucky rolls display
updateLuckyRollsDisplay();
// If no more lucky rolls, show message
if (luckyRollsRemaining === 0) {
setTimeout(() => {
showGameMessage('Lucky Streak ended!', 'text-yellow-500');
}, 1000);
}
}
// Create confetti effect if result is 6
if (result === 6) {
createConfetti();
}
// Debug: log final state
console.log(`Final precise rotation: X=${finalRotation.x}°, Y=${finalRotation.y}°`);
console.log(`Displayed result: ${result}`);
console.log(`Score change: ${scoreChangeData.finalChange}, Current score: ${currentScore}`);
console.log('=== finalizeAnimation completed ===');
}
// Update score display with animation
function updateScoreDisplay(scoreChangeData) {
console.log('=== updateScoreDisplay called ===');
console.log('Score change data:', scoreChangeData);
const change = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
const tierMultiplier = scoreChangeData.tierMultiplier;
console.log('Change:', change, 'Base change:', baseChange, 'Items used:', itemsUsed);
// Update the score value - round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
scoreValue.textContent = currentScore;
console.log('Score value updated to:', currentScore);
// Clear previous score change display
scoreChange.textContent = '';
scoreChange.className = 'ml-2 text-sm font-normal';
console.log('Cleared previous score change display');
// Show score change with appropriate styling
if (change > 0) {
scoreChange.textContent = `+${change}`;
scoreChange.classList.add('score-increase');
console.log('Score increase:', change);
} else if (change < 0) {
scoreChange.textContent = `${change}`;
scoreChange.classList.add('score-decrease');
console.log('Score decrease:', change);
} else {
scoreChange.textContent = `±0`;
scoreChange.classList.add('score-neutral');
console.log('Score neutral');
}
// Show item effect message if items were used
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
// Create container for item effects
const itemEffectsContainer = document.createElement('div');
itemEffectsContainer.className = 'flex flex-wrap gap-2 mt-1';
// Add tier multiplier effect if applicable
if (tierMultiplier !== 1) {
const tierEffect = document.createElement('div');
tierEffect.className = 'text-xs text-purple-500';
tierEffect.textContent = `Tier Multiplier ×${tierMultiplier.toFixed(1)}!`;
itemEffectsContainer.appendChild(tierEffect);
}
// Add no penalty effect
if (itemsUsed.noPenalty) {
const noPenaltyEffect = document.createElement('div');
noPenaltyEffect.className = 'text-xs text-blue-500';
noPenaltyEffect.textContent = `No Penalty! (Score protected from ${baseChange < 0 ? baseChange : 0} loss)`;
itemEffectsContainer.appendChild(noPenaltyEffect);
}
// Add double points effect
if (itemsUsed.double > 0) {
const doublePointsEffect = document.createElement('div');
doublePointsEffect.className = 'text-xs text-green-500';
// Calculate multiplier
const multiplier = Math.pow(2, itemsUsed.double);
let calculationText = `${baseChange}`;
// Apply tier multiplier for display
let displayChange = baseChange * tierMultiplier;
// Apply no penalty first for display
if (itemsUsed.noPenalty && baseChange < 0) {
displayChange = 0;
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} → 0)`;
} else if (tierMultiplier !== 1) {
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} = ${displayChange.toFixed(2)})`;
}
// Show multiplication steps if multiple double cards used
if (itemsUsed.double > 1) {
for (let i = 0; i < itemsUsed.double; i++) {
calculationText += ` × 2`;
}
calculationText += ` = ${(displayChange * multiplier).toFixed(2)}`;
} else {
calculationText += ` × 2 = ${(displayChange * multiplier).toFixed(2)}`;
}
doublePointsEffect.textContent = `Double Points ×${itemsUsed.double}! (${calculationText})`;
itemEffectsContainer.appendChild(doublePointsEffect);
}
// Add to score display
scoreDisplay.appendChild(itemEffectsContainer);
// Remove after animation
setTimeout(() => {
scoreDisplay.removeChild(itemEffectsContainer);
}, 1000);
}
// Reset score change display after animation completes
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
// Clear active items after score update
console.log('Checking if items need to be cleared...');
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
console.log('Clearing active items...');
clearActiveItems();
console.log('Updating items display...');
updateItemsDisplay();
}
// Check game state after score update
console.log('Checking game state...');
checkGameState();
// Update upgrade button state
updateUpgradeCostDisplay();
console.log('=== updateScoreDisplay completed ===');
}
// Handle dice upgrade
function upgradeDice() {
// Check if game is already over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if already at maximum tier
if (currentTier >= diceTiers.length - 1) {
// If not already game over, end the game with win condition
if (!gameOver) {
gameOver = true;
endGame(true);
} else {
showGameMessage('Congratulations! You already have the Unique dice!', 'text-green-500');
}
return;
}
// Calculate required score for upgrade (5 + currentTier)
const requiredScore = 5 + currentTier;
// Check if enough score to upgrade
if (currentScore < requiredScore) {
showGameMessage(`Not enough score to upgrade! Need ${requiredScore} points.`, 'text-orange-500');
// Add shake animation to score display
scoreDisplay.classList.add('animate-shake');
setTimeout(() => {
scoreDisplay.classList.remove('animate-shake');
}, 500);
return;
}
// Deduct score for upgrade
currentScore -= requiredScore;
// Round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
// Update score display
scoreValue.textContent = currentScore;
showScoreChange(-requiredScore);
// Increase tier
currentTier++;
// Update current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Update tier multiplier display
updateTierMultiplierDisplay();
// Unlock the new dice color
unlockDiceColor(currentTier);
// Update color selection UI
updateColorSelection(currentTier);
// Update upgrade cost display
updateUpgradeCostDisplay();
// Change to the new dice color
console.log(`Changing dice color to ${diceTiers[currentTier].color} (${diceTiers[currentTier].name})`);
changeDiceColor(diceTiers[currentTier].color);
// Check if this upgrade reached the maximum tier
checkGameState();
// Randomly get an item
const itemChance = Math.random();
if (itemChance < 0.30) { // 30% chance to get double points card
doublePointsCards++;
} else if (itemChance < 0.60) { // 30% chance to get no penalty card
noPenaltyCards++;
} else if (itemChance < 0.70) { // 10% chance to get lucky streak card
luckyStreakCards++;
} else { // 30% chance to get nothing
// Do nothing
}
// Update items display to show new counts
updateItemsDisplay();
// Update items display
updateItemsDisplay();
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Handle lucky roll logic
if (luckyRollsRemaining > 0) {
// Decrement remaining lucky rolls
luckyRollsRemaining--;
// If rolled 1 or 2, decrement roll count
if (result === 1 || result === 2) {
rollCount--;
console.log(`Lucky roll: Reverted roll count to ${rollCount}`);
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
// Show message
showGameMessage('Lucky! This 1/2 roll doesn\'t count!', 'text-yellow-500');
}
// Update lucky rolls display
updateLuckyRollsDisplay();
// If no more lucky rolls, show message
if (luckyRollsRemaining === 0) {
setTimeout(() => {
showGameMessage('Lucky Streak ended!', 'text-yellow-500');
}, 1000);
}
}
// Add to history
addToHistory('UPGRADE', -requiredScore);
// Add item to history if obtained
if (itemChance < 0.30) {
addToHistory('ITEM', 'Double Points Card');
} else if (itemChance < 0.60) {
addToHistory('ITEM', 'No Penalty Card');
} else if (itemChance < 0.70) {
addToHistory('ITEM', 'Lucky Streak Card');
}
// Check game state after upgrade
checkGameState();
}
// Show score change temporarily
function showScoreChange(change) {
scoreChange.textContent = change > 0 ? `+${change}` : change;
scoreChange.className = `ml-2 text-sm font-normal ${change > 0 ? 'score-increase' : 'score-decrease'}`;
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
}
// Unlock a dice color option
function unlockDiceColor(tierIndex) {
if (tierIndex >= 0 && tierIndex < colorOptions.length) {
const option = colorOptions[tierIndex];
// Show particle effect on the lock before unlocking
createLockParticles(option);
// Add a small delay to show the particles before unlocking
setTimeout(() => {
option.disabled = false;
option.classList.remove('locked');
}, 300);
}
}
// Show game message
function showGameMessage(message, className) {
gameMessageDisplay.textContent = message;
gameMessageDisplay.className = `mt-4 text-center font-semibold ${className}`;
gameMessageDisplay.classList.remove('hidden');
// Hide message after 3 seconds
setTimeout(() => {
if (!gameOver) {
gameMessageDisplay.classList.add('hidden');
}
}, 3000);
}
// Check game state (win/lose conditions)
function checkGameState() {
console.log('=== checkGameState called ===');
console.log('Current score:', currentScore, 'Game over:', gameOver);
// Check if score is negative (lose condition)
if (currentScore < 0 && !gameOver) {
console.log('Score is negative, ending game...');
gameOver = true;
endGame(false);
}
// Check if reached Unique dice (win condition)
if (currentTier === diceTiers.length - 1 && !gameOver) {
console.log('Reached Unique dice, ending game...');
gameOver = true;
endGame(true);
// Check if this is a new record
setTimeout(() => {
checkWinRecord();
}, 1000);
}
// Check if score is low (warning)
if (currentScore >= 0 && currentScore <= 5 && !gameOver) {
showLowScoreWarning();
}
console.log('=== checkGameState completed ===');
}
// Update items display
function updateItemsDisplay() {
// Update counts
doublePointsCount.textContent = doublePointsCards;
noPenaltyCount.textContent = noPenaltyCards;
luckyStreakCount.textContent = luckyStreakCards;
// Enable/disable buttons based on available items
useDoublePointsButton.disabled = doublePointsCards <= 0 || gameOver;
useNoPenaltyButton.disabled = noPenaltyCards <= 0 || activeItems.noPenalty || gameOver;
useLuckyStreakButton.disabled = luckyStreakCards <= 0 || gameOver;
// Update free rolls display
updateFreeRollsDisplay();
// Update lucky rolls display
updateLuckyRollsDisplay();
}
// Update free rolls display
function updateFreeRollsDisplay() {
if (freeRolls > 0) {
freeRollsCountElement.textContent = freeRolls;
freeRollsIndicator.classList.remove('hidden');
} else {
freeRollsIndicator.classList.add('hidden');
}
}
// Update upgrade cost display and button state
function updateUpgradeCostDisplay() {
if (currentTier >= diceTiers.length - 1) {
// Already at maximum tier
upgradeCost.textContent = '(Max Tier)';
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
} else {
const requiredScore = 5 + currentTier;
upgradeCost.textContent = `(Cost: ${requiredScore})`;
// Update button state based on available score
if (currentScore >= requiredScore && !gameOver) {
upgradeButton.disabled = false;
upgradeButton.classList.remove('opacity-70', 'cursor-not-allowed');
} else {
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
}
}
}
// Use double points card
function useDoublePointsCard() {
if (doublePointsCards > 0 && !gameOver) {
activeItems.double++;
doublePointsCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('Double Points Card activated! Next roll will double your score change.', 'text-green-500');
}
}
// Use no penalty card
function useNoPenaltyCard() {
if (noPenaltyCards > 0 && !activeItems.noPenalty && !gameOver) {
activeItems.noPenalty = true;
noPenaltyCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('No Penalty Card activated! Next roll won\'t lose points.', 'text-blue-500');
}
}
// Use lucky streak card
function useLuckyStreakCard() {
if (luckyStreakCards > 0 && !gameOver) {
luckyRollsRemaining = 3;
luckyStreakCards--;
// Update UI
updateItemsDisplay();
updateLuckyRollsDisplay();
// Show message
showGameMessage('Lucky Streak Card activated! Next 3 rolls of 1 or 2 won\'t count!', 'text-yellow-500');
// Add to history
addToHistory('ITEM USE', 'Lucky Streak Card');
}
}
// Update lucky rolls display
function updateLuckyRollsDisplay() {
if (luckyRollsRemaining > 0) {
// Check if indicator exists, if not create it
let luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (!luckyRollsIndicator) {
luckyRollsIndicator = document.createElement('div');
luckyRollsIndicator.id = 'lucky-rolls-indicator';
luckyRollsIndicator.className = 'mt-3 text-center';
// Insert after free rolls indicator
const freeRollsIndicator = document.getElementById('free-rolls-indicator');
if (freeRollsIndicator) {
freeRollsIndicator.parentNode.insertBefore(luckyRollsIndicator, freeRollsIndicator.nextSibling);
} else {
// Fallback: insert after active item indicator
const activeItemIndicator = document.getElementById('active-item-indicator');
if (activeItemIndicator) {
activeItemIndicator.parentNode.insertBefore(luckyRollsIndicator, activeItemIndicator.nextSibling);
} else {
// Fallback: insert after items display
const itemsDisplay = document.getElementById('items-display');
if (itemsDisplay) {
itemsDisplay.parentNode.insertBefore(luckyRollsIndicator, itemsDisplay.nextSibling);
}
}
}
}
// Update content
luckyRollsIndicator.innerHTML = `
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fa fa-star mr-1" style="color: #f59e0b;"></i>
<span>Lucky Rolls: <span id="lucky-rolls-count">${luckyRollsRemaining}</span> (1-2 won't count)</span>
</span>
`;
// Show indicator
luckyRollsIndicator.classList.remove('hidden');
} else {
// Remove indicator if exists
const luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (luckyRollsIndicator) {
luckyRollsIndicator.classList.add('hidden');
}
}
}
// Show active item indicator
function showActiveItemIndicator() {
// Clear previous content
activeItemIndicator.innerHTML = '';
// Check if any items are active
if (activeItems.double === 0 && !activeItems.noPenalty) {
activeItemIndicator.className = 'mt-3 text-center hidden';
return;
}
// Create container for active items
const itemsContainer = document.createElement('div');
itemsContainer.className = 'flex flex-wrap justify-center gap-2';
// Add double points cards indicator
if (activeItems.double > 0) {
const doublePointsIndicator = document.createElement('span');
doublePointsIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800';
doublePointsIndicator.innerHTML = `
<i class="fa fa-plus-circle mr-1" style="color: #10b981;"></i>
<span>Double Points ×${activeItems.double}</span>
`;
itemsContainer.appendChild(doublePointsIndicator);
}
// Add no penalty card indicator
if (activeItems.noPenalty) {
const noPenaltyIndicator = document.createElement('span');
noPenaltyIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800';
noPenaltyIndicator.innerHTML = `
<i class="fa fa-shield mr-1" style="color: #3b82f6;"></i>
<span>No Penalty</span>
`;
itemsContainer.appendChild(noPenaltyIndicator);
}
// Add indicators to container
activeItemIndicator.appendChild(itemsContainer);
activeItemIndicator.className = 'mt-3 text-center';
}
// Clear active items
function clearActiveItems() {
activeItems = {
double: 0,
noPenalty: false
};
showActiveItemIndicator();
}
// End the game and display appropriate message
function endGame(isWin) {
console.log('=== endGame called ===');
console.log('Is win:', isWin, 'Current score:', currentScore);
// Disable all interactive elements
rollButton.disabled = true;
upgradeButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
// Disable all interactive elements
colorOptions.forEach(option => {
option.disabled = true;
option.classList.add('opacity-50', 'cursor-not-allowed');
});
// Disable item buttons
useDoublePointsButton.disabled = true;
useNoPenaltyButton.disabled = true;
useLuckyStreakButton.disabled = true;
// Hide active indicators
activeItemIndicator.classList.add('hidden');
const luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (luckyRollsIndicator) {
luckyRollsIndicator.classList.add('hidden');
}
// Create game over message element
const gameOverMessage = document.createElement('div');
gameOverMessage.className = `mt-4 p-4 rounded-lg text-center font-bold ${isWin ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`;
// Format final score to 2 decimal places
const formattedScore = currentScore.toFixed(2);
if (isWin) {
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Congratulations!</div>
<p>You won the game!</p>
<p>You obtained the Unique dice!</p>
<p class="mt-2 text-sm">Final Score: ${formattedScore}</p>
<p class="mt-1 text-sm">Total Rolls: ${rollCount}</p>
<p class="mt-1 text-sm">Double Points Cards: ${doublePointsCards}</p>
<p class="mt-1 text-sm">No Penalty Cards: ${noPenaltyCards}</p>
<p class="mt-1 text-sm">Lucky Streak Cards: ${luckyStreakCards}</p>
<p class="mt-1 text-sm">Final Tier Multiplier: ×${(1 + currentTier / 10).toFixed(1)}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
createConfetti();
} else {
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Game Over!</div>
<p>Your score went negative.</p>
<p>Better luck next time!</p>
<p class="mt-2 text-sm">Final Score: ${formattedScore}</p>
<p class="mt-1 text-sm">Total Rolls: ${rollCount}</p>
<p class="mt-1 text-sm">Double Points Cards: ${doublePointsCards}</p>
<p class="mt-1 text-sm">No Penalty Cards: ${noPenaltyCards}</p>
<p class="mt-1 text-sm">Lucky Streak Cards: ${luckyStreakCards}</p>
<p class="mt-1 text-sm">Final Tier Multiplier: ×${(1 + currentTier / 10).toFixed(1)}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
}
// Add restart button event listener
gameOverMessage.querySelector('#play-again').addEventListener('click', () => {
window.location.reload();
});
// Replace game message display with game over message
const gameMessageContainer = gameMessageDisplay.parentElement;
gameMessageContainer.replaceChild(gameOverMessage, gameMessageDisplay);
gameMessageDisplay = gameOverMessage;
// Add game end to history
addToHistory(isWin ? 'GAME WIN' : 'GAME OVER', 0);
console.log('=== endGame completed ===');
}
// Show warning when score is low
function showLowScoreWarning() {
// Only show warning if not already showing
if (gameMessageDisplay.classList.contains('hidden')) {
showGameMessage('Warning: Low score! Risk of game over.', 'text-orange-500');
}
}
// Update color selection UI to show the currently selected color
function updateColorSelection(selectedIndex) {
// Remove active state from all color options
colorOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-offset-2', 'ring-primary'));
// Add active state to the selected color option
if (selectedIndex >= 0 && selectedIndex < colorOptions.length) {
colorOptions[selectedIndex].classList.add('ring-2', 'ring-offset-2', 'ring-primary');
}
}
// Add result to history
function addToHistory(result, scoreChangeData, isFreeRoll = false, isLuckyRoll = false) {
const now = new Date();
const timeString = now.toLocaleTimeString();
// Determine score change display and color
let scoreChangeText = '';
let scoreChangeClass = '';
let actionText = '';
if (result === 'UPGRADE') {
actionText = 'Upgraded dice';
scoreChangeText = `${scoreChangeData}`;
scoreChangeClass = 'text-red-500';
} else if (result === 'ITEM') {
actionText = `Received <span class="font-bold text-purple-500">${scoreChangeData}</span>`;
scoreChangeText = '+1';
scoreChangeClass = 'text-purple-500';
} else if (result === 'GAME WIN' || result === 'GAME OVER') {
actionText = result;
scoreChangeText = '';
scoreChangeClass = '';
} else {
const scoreChange = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
actionText = `Rolled a <span class="font-bold text-primary">${result}</span>`;
// Add free roll indicator if applicable
if (isFreeRoll) {
actionText += ` <span class="text-yellow-500">(Free Roll)</span>`;
}
// Add lucky roll indicator if applicable
if (isLuckyRoll) {
actionText += ` <span class="text-yellow-500">(Lucky Roll)</span>`;
}
// Add item effect indicators if applicable
const activeItemsText = [];
if (itemsUsed && itemsUsed.double > 0) {
activeItemsText.push(`<span class="text-green-500">(Double Points ×${itemsUsed.double})</span>`);
}
if (itemsUsed && itemsUsed.noPenalty) {
activeItemsText.push(`<span class="text-blue-500">(No Penalty)</span>`);
}
if (activeItemsText.length > 0) {
actionText += ` ${activeItemsText.join(' ')}`;
}
if (scoreChange > 0) {
scoreChangeText = `+${scoreChange}`;
scoreChangeClass = 'text-green-500';
} else if (scoreChange < 0) {
scoreChangeText = `${scoreChange}`;
scoreChangeClass = 'text-red-500';
} else {
scoreChangeText = '±0';
scoreChangeClass = 'text-gray-500';
}
// Show base change if different from final change (items were used)
if ((itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) && baseChange !== scoreChange) {
scoreChangeText += ` <span class="text-xs">(Base: ${baseChange > 0 ? '+' : ''}${baseChange})</span>`;
}
}
const listItem = document.createElement('li');
listItem.className = 'py-1 border-b border-gray-100 grid grid-cols-3 gap-2 items-center';
listItem.innerHTML = `
<span class="col-span-1 text-gray-500">${timeString}</span>
<span class="col-span-1">${actionText}</span>
<span class="col-span-1 text-right font-medium ${scoreChangeClass}">${scoreChangeText}</span>
`;
historyList.prepend(listItem);
// Keep only last 10 history items
if (historyList.children.length > 10) {
historyList.removeChild(historyList.lastChild);
}
// Ensure scroll is at the bottom
const historyContainer = document.getElementById('history-container');
if (historyContainer) {
historyContainer.scrollTop = historyContainer.scrollHeight;
}
}
// Create confetti effect
function createConfetti() {
const confettiContainer = document.createElement('div');
confettiContainer.className = 'fixed inset-0 pointer-events-none overflow-hidden';
document.body.appendChild(confettiContainer);
// Create 50 confetti pieces
for (let i = 0; i < 50; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
// Random position
const posX = Math.random() * 100;
const delay = Math.random() * 3;
const duration = 3 + Math.random() * 2;
// Random colors
const colors = ['#3b82f6', '#f97316', '#10b981', '#ef4444', '#8b5cf6'];
const color = colors[Math.floor(Math.random() * colors.length)];
confetti.style.left = `${posX}%`;
confetti.style.backgroundColor = color;
confetti.style.animationDelay = `${delay}s`;
confetti.style.animationDuration = `${duration}s`;
confettiContainer.appendChild(confetti);
}
// Remove confetti container after animation completes
setTimeout(() => {
document.body.removeChild(confettiContainer);
}, 5000);
}
// Create particle effect on lock
function createLockParticles(lockElement) {
// Get lock element position
const rect = lockElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// Create particle container
const particleContainer = document.createElement('div');
particleContainer.className = 'absolute pointer-events-none';
particleContainer.style.left = `${centerX}px`;
particleContainer.style.top = `${centerY}px`;
particleContainer.style.transform = 'translate(-50%, -50%)';
document.body.appendChild(particleContainer);
// Create 15 particles
for (let i = 0; i < 15; i++) {
const particle = document.createElement('div');
particle.className = 'lock-particle';
// Random direction and distance
const angle = Math.random() * Math.PI * 2;
const distance = 10 + Math.random() * 20;
const tx = Math.cos(angle) * distance;
const ty = Math.sin(angle) * distance;
// Random color (use the color of the unlocked dice)
const color = lockElement.style.backgroundColor;
// Random animation duration
const duration = 0.5 + Math.random() * 0.5;
// Set particle styles
particle.style.backgroundColor = color;
particle.style.setProperty('--tx', `${tx}px`);
particle.style.setProperty('--ty', `${ty}px`);
particle.style.animation = `lock-particle ${duration}s ease-out forwards`;
particleContainer.appendChild(particle);
}
// Remove particle container after animation completes
setTimeout(() => {
document.body.removeChild(particleContainer);
}, 1000);
}
// Function to darken a color by a certain percentage
function darkenColor(color, percent) {
const hex = color.replace('#', '');
let r = parseInt(hex.substr(0, 2), 16);
let g = parseInt(hex.substr(2, 2), 16);
let b = parseInt(hex.substr(4, 2), 16);
// Darken each channel by the percentage
r = Math.max(0, Math.floor(r * (1 - percent / 100)));
g = Math.max(0, Math.floor(g * (1 - percent / 100)));
b = Math.max(0, Math.floor(b * (1 - percent / 100)));
// Convert back to hex
const darkenedHex = '#' +
r.toString(16).padStart(2, '0') +
g.toString(16).padStart(2, '0') +
b.toString(16).padStart(2, '0');
return darkenedHex;
}
// Change dice color function
function changeDiceColor(color) {
console.log(`changeDiceColor called with color: ${color}`);
const faces = document.querySelectorAll('.dice-face');
console.log(`Found ${faces.length} dice faces`);
let dotColor, borderColor;
// Special cases for high contrast
if (color.toUpperCase() === '#FFFFFF') {
// White dice - use black dots and light gray borders
dotColor = '#000000';
borderColor = '#CCCCCC';
} else if (color.toUpperCase() === '#555555') {
// Dark gray dice - use white dots and slightly lighter gray borders
dotColor = '#FFFFFF';
borderColor = '#777777';
} else {
// Calculate darker color for dots and borders (40% darker for more contrast)
dotColor = darkenColor(color, 40);
borderColor = darkenColor(color, 30); // Slightly lighter border than dots
}
faces.forEach(face => {
// Set face background color
face.style.backgroundColor = color;
// Set face border color
face.style.border = `3px solid ${borderColor}`;
// Set dots color
const dots = face.querySelectorAll('.dot');
dots.forEach(dot => {
dot.style.backgroundColor = dotColor;
// Add slight border to dots for better definition
dot.style.border = color.toUpperCase() === '#FFFFFF' ? '1px solid rgba(0, 0, 0, 0.2)' : '1px solid rgba(0, 0, 0, 0.1)';
});
});
}
// Update tier multiplier display
function updateTierMultiplierDisplay() {
const display = document.getElementById('tier-multiplier-display');
if (display) {
display.innerHTML = `Tier Multiplier: <span class="font-semibold text-purple-500">×${(1 + currentTier / 10).toFixed(1)}</span>`;
}
}
// Event listener for roll button
rollButton.addEventListener('click', rollDice);
// Event listener for upgrade button
upgradeButton.addEventListener('click', upgradeDice);
// Event listeners for item buttons
useDoublePointsButton.addEventListener('click', useDoublePointsCard);
useNoPenaltyButton.addEventListener('click', useNoPenaltyCard);
useLuckyStreakButton.addEventListener('click', useLuckyStreakCard);
// Event listeners for color options
colorOptions.forEach((option, index) => {
// Disable all color options except the first one initially
if (index !== 0) {
option.disabled = true;
option.classList.add('locked');
}
option.addEventListener('click', () => {
// Check if game is over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if color is unlocked
if (option.disabled) {
showGameMessage('You need to upgrade to unlock this dice color!', 'text-orange-500');
return;
}
const color = option.getAttribute('data-color');
changeDiceColor(color);
// Add active state to selected color
colorOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-offset-2', 'ring-primary'));
option.classList.add('ring-2', 'ring-offset-2', 'ring-primary');
// Update current tier to the selected color's tier
currentTier = index;
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
updateTierMultiplierDisplay();
});
});
// Initialize dice on page load
window.addEventListener('DOMContentLoaded', () => {
console.log('=== DOMContentLoaded event fired ===');
// Check if elements exist
console.log('Checking if elements exist before initialization:');
console.log('best-record-display exists:', !!document.getElementById('best-record-display'));
console.log('best-record-value exists:', !!document.getElementById('best-record-value'));
console.log('DOM fully loaded');
initializeDice();
// Set default color (first option)
if (colorOptions.length > 0) {
const defaultColor = colorOptions[0].getAttribute('data-color');
changeDiceColor(defaultColor);
colorOptions[0].classList.add('ring-2', 'ring-offset-2', 'ring-primary');
}
// Initialize current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Initialize tier multiplier display
updateTierMultiplierDisplay();
// Initialize items display
updateItemsDisplay();
// Initialize upgrade cost display
updateUpgradeCostDisplay();
// Show welcome message
setTimeout(() => {
showGameMessage('Welcome! Roll the dice to earn points and upgrade your dice!', 'text-blue-500');
}, 1000);
});
// Also try initializing on window load
window.addEventListener('load', () => {
console.log('Window loaded');
// If dice not already initialized, try again
if (diceElement.children.length === 0) {
console.log('Dice not initialized, trying again...');
initializeDice();
}
});
// Game Rules Modal Functionality
(function() {
// Get elements
const rulesButton = document.getElementById('game-rules-button');
const rulesModal = document.getElementById('game-rules-modal');
const closeButtons = rulesModal.querySelectorAll('.close-button');
const modalBackdrop = rulesModal.querySelector('.modal-backdrop');
const modalContent = rulesModal.querySelector('.modal-content');
// Function to prevent background scrolling
function preventBackgroundScroll(event) {
// Allow scrolling inside the modal content
if (modalContent.contains(event.target)) {
// Check if we're at the top or bottom of the modal content
const isAtTop = modalContent.scrollTop === 0;
const isAtBottom = modalContent.scrollTop + modalContent.clientHeight >= modalContent.scrollHeight;
// Prevent scrolling if at the top and scrolling up, or at the bottom and scrolling down
if ((isAtTop && event.deltaY < 0) || (isAtBottom && event.deltaY > 0)) {
event.preventDefault();
}
} else {
// Prevent scrolling outside the modal content
event.preventDefault();
}
}
// Function to open modal
function openModal() {
// Add event listeners to prevent background scrolling
document.addEventListener('wheel', preventBackgroundScroll, { passive: false });
document.addEventListener('touchmove', preventBackgroundScroll, { passive: false });
// Add open class to trigger animations
rulesModal.classList.add('modal-open');
// Show the modal
rulesModal.style.visibility = 'visible';
}
// Function to close modal
function closeModal() {
// Remove open class to trigger animations
rulesModal.classList.remove('modal-open');
// Remove event listeners that prevent background scrolling
document.removeEventListener('wheel', preventBackgroundScroll);
document.removeEventListener('touchmove', preventBackgroundScroll);
// Hide the modal after animation completes
setTimeout(() => {
rulesModal.style.visibility = 'hidden';
}, 300);
}
// Add event listeners
if (rulesButton) {
rulesButton.addEventListener('click', openModal);
}
// Close buttons
closeButtons.forEach(button => {
button.addEventListener('click', closeModal);
});
// Close when clicking outside the modal content
modalBackdrop.addEventListener('click', closeModal);
// Close when pressing Escape key
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && rulesModal.classList.contains('modal-open')) {
closeModal();
}
});
// Add hover effect to rules button
if (rulesButton) {
rulesButton.addEventListener('mouseenter', () => {
rulesButton.classList.add('scale-105');
});
rulesButton.addEventListener('mouseleave', () => {
rulesButton.classList.remove('scale-105');
});
}
// Add click effect to close buttons
closeButtons.forEach(button => {
button.addEventListener('mousedown', () => {
button.classList.add('scale-95');
});
button.addEventListener('mouseup', () => {
button.classList.remove('scale-95');
});
button.addEventListener('mouseleave', () => {
button.classList.remove('scale-95');
});
});
})();
</script>
</body>
</html>
v.1.3(bug 极其严重)
HTML<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Dice Roll Animation</title>
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#f97316',
dark: '#1e293b',
light: '#f8fafc'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'shake': 'shake 0.5s ease-in-out',
},
keyframes: {
shake: {
'0%, 100%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(-5px)' },
'50%': { transform: 'translateX(5px)' },
'75%': { transform: 'translateX(-5px)' },
}
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.preserve-3d {
transform-style: preserve-3d;
}
.perspective {
perspective: 1000px;
}
.backface-hidden {
backface-visibility: hidden;
}
.dice-shadow {
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
}
.glass-effect {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.modal-content {
transform: translateY(-20px);
opacity: 0;
transition: all 0.3s ease-out;
}
.modal-open .modal-content {
transform: translateY(0);
opacity: 1;
}
.modal-open .modal-backdrop {
opacity: 1;
visibility: visible;
}
.trophy-animation {
animation: trophy-pulse 2s ease-in-out infinite;
}
.record-animation {
animation: record-shine 2s ease-in-out;
}
}
@keyframes trophy-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes record-shine {
0% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.7); }
70% { box-shadow: 0 0 0 15px rgba(251, 191, 36, 0); }
100% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0); }
}
/* Custom styles for 3D dice */
.dice-scene {
perspective: 1500px;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.dice-container {
position: relative;
width: 100px;
height: 100px;
transform-style: preserve-3d;
transition: transform 1s ease-out;
transform: rotateX(20deg) rotateY(20deg);
}
.dice-face {
position: absolute;
width: 100px;
height: 100px;
border-radius: 8px;
background-color: white;
border: 2px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
/* Dice face positions */
.face-1 { transform: translateZ(50px); }
.face-2 { transform: rotateY(90deg) translateZ(50px); }
.face-3 { transform: rotateY(180deg) translateZ(50px); }
.face-4 { transform: rotateY(-90deg) translateZ(50px); }
.face-5 { transform: rotateX(90deg) translateZ(50px); }
.face-6 { transform: rotateX(-90deg) translateZ(50px); }
/* Dots on dice faces */
.dot {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: black;
}
/* Dot positions for each face */
.face-1 .dot { top: 40px; left: 40px; }
.face-2 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-2 .dot:nth-child(2) { top: 60px; left: 60px; }
.face-3 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-3 .dot:nth-child(2) { top: 40px; left: 40px; }
.face-3 .dot:nth-child(3) { top: 60px; left: 60px; }
.face-4 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-4 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-4 .dot:nth-child(3) { top: 60px; left: 20px; }
.face-4 .dot:nth-child(4) { top: 60px; left: 60px; }
.face-5 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-5 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-5 .dot:nth-child(3) { top: 40px; left: 40px; }
.face-5 .dot:nth-child(4) { top: 60px; left: 20px; }
.face-5 .dot:nth-child(5) { top: 60px; left: 60px; }
.face-6 .dot:nth-child(1) { top: 20px; left: 20px; }
.face-6 .dot:nth-child(2) { top: 20px; left: 60px; }
.face-6 .dot:nth-child(3) { top: 40px; left: 20px; }
.face-6 .dot:nth-child(4) { top: 40px; left: 60px; }
.face-6 .dot:nth-child(5) { top: 60px; left: 20px; }
.face-6 .dot:nth-child(6) { top: 60px; left: 60px; }
.dice-result {
transition: all 0.5s ease-out;
}
/* Fixed height for result display to prevent page jumping */
#result-display {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.result-shine {
animation: shine 0.5s ease-out;
}
.score-increase {
color: #10b981;
animation: scorePopup 1s ease-out;
}
.score-decrease {
color: #ef4444;
animation: scorePopup 1s ease-out;
}
.score-neutral {
color: #6b7280;
animation: scorePopup 1s ease-out;
}
/* Locked color option styles */
.color-option.locked {
position: relative !important;
opacity: 0.5;
cursor: not-allowed;
}
.color-option.locked::after {
content: var(--content, '\f023'); /* Lock icon from Font Awesome */
font-family: 'FontAwesome';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(0, 0, 0, 0.5);
font-size: 16px;
z-index: 10; /* Ensure lock icon is above other content */
display: block; /* Ensure the pseudo-element is displayed */
}
@keyframes shine {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
@keyframes scorePopup {
0% {
transform: translateY(0);
opacity: 0;
}
50% {
transform: translateY(-10px);
opacity: 1;
}
100% {
transform: translateY(-20px);
opacity: 0;
}
}
.confetti {
position: absolute;
width: 10px;
height: 10px;
background-color: #f97316;
animation: confetti-fall 3s ease-in-out infinite;
}
@keyframes confetti-fall {
0% {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
@keyframes lock-particle {
0% {
transform: translate(0, 0);
opacity: 1;
}
100% {
transform: translate(var(--tx), var(--ty));
opacity: 0;
}
}
/* Game Rules Modal Styles */
.game-rules-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
visibility: hidden;
box-sizing: border-box;
}
.game-rules-modal .modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease-out;
}
.game-rules-modal .modal-content {
position: relative;
background-color: white;
border-radius: 1rem;
max-width: 90%;
max-height: 80vh;
width: 500px;
overflow-y: auto;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.game-rules-modal .modal-header {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.game-rules-modal .modal-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
transition: color 0.2s ease-in-out;
}
.game-rules-modal .close-button:hover {
color: #1e293b;
}
.game-rules-modal .modal-body {
padding: 1.5rem;
line-height: 1.6;
color: #374151;
}
.game-rules-modal .modal-body h3 {
font-size: 1.1rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #1e293b;
}
.game-rules-modal .modal-body p {
margin-bottom: 1rem;
}
.game-rules-modal .modal-body ul {
margin-bottom: 1rem;
padding-left: 1.5rem;
list-style-type: disc;
}
.game-rules-modal .modal-body li {
margin-bottom: 0.5rem;
}
.game-rules-modal .modal-body strong {
font-weight: 600;
color: #1e293b;
}
.game-rules-modal .modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
}
.game-rules-modal .modal-footer button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-size: 1.25rem;
font-weight: 500;
transition: background-color 0.2s ease-in-out;
}
.game-rules-modal .modal-footer button:hover {
background-color: #2563eb;
}
/* Game Rules Button Styles */
.game-rules-button, .win-records-button {
position: fixed;
right: 1rem;
z-index: 40;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: white;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease-in-out;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.game-rules-button {
top: 1rem;
}
.win-records-button {
top: 4.5rem;
}
.game-rules-button:hover {
background-color: #2563eb;
transform: translateY(-2px);
}
.game-rules-button i {
font-size: 1.1rem;
}
</style>
</head>
<body class="bg-gradient-to-br from-light to-gray-200 min-h-screen flex flex-col items-center justify-center p-4 m-0">
<!-- Game Rules Button -->
<button id="game-rules-button" class="game-rules-button">
<i class="fa fa-book"></i>
<span>Game Rules</span>
</button>
<!-- Win Records Button -->
<button id="win-records-button" class="win-records-button">
<i class="fa fa-trophy"></i>
<span>Win Records</span>
</button>
<!-- Game Rules Modal -->
<div id="game-rules-modal" class="game-rules-modal">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">3D Dice Game Rules</h2>
<button class="close-button">×</button>
</div>
<div class="modal-body">
<p>Welcome to the 3D Dice Game! This is an exciting game that combines luck, strategy, and upgrading mechanics. Below is the complete game rules explanation:</p>
<h3>Basic Gameplay</h3>
<ul>
<li>Click the "Roll Dice" button to roll the dice</li>
<li>The dice will randomly show a number between 1-6</li>
<li>Gain or lose points based on the number rolled</li>
<li>The goal is to obtain the highest tier "Unique" dice</li>
</ul>
<h3>Score System</h3>
<ul>
<li>Rolling 1 or 2: Lose 1 point (multiplier not applied)</li>
<li>Rolling 3: No points gained or lost</li>
<li>Rolling 4: Gain 1 point</li>
<li>Rolling 5: Gain 2 points</li>
<li>Rolling 6: Gain 3 points</li>
</ul>
<h3>Dice Upgrade System</h3>
<p>There are 10 different tiers of dice in the game, from common to rare:</p>
<ul>
<li><strong>Empty</strong> (Initial) - Score Multiplier ×1.0</li>
<li><strong>Common</strong> - Score Multiplier ×1.1</li>
<li><strong>Unusual</strong> - Score Multiplier ×1.2</li>
<li><strong>Rare</strong> - Score Multiplier ×1.3</li>
<li><strong>Epic</strong> - Score Multiplier ×1.4</li>
<li><strong>Legendary</strong> - Score Multiplier ×1.5</li>
<li><strong>Mythic</strong> - Score Multiplier ×1.6</li>
<li><strong>Ultra</strong> - Score Multiplier ×1.7</li>
<li><strong>Super</strong> - Score Multiplier ×1.8</li>
<li><strong>Unique</strong> - Score Multiplier ×1.9</li>
</ul>
<p>Each upgrade costs (5 + current tier) points. For example, upgrading from Empty (tier 0) costs 5 points, upgrading from Common (tier 1) costs 6 points, and so on. After upgrading, you can choose to use the new dice or continue using the old one.</p>
<h3>Item System</h3>
<p>When upgrading, you have a chance to obtain item cards that can be used at critical moments:</p>
<ul>
<li><strong>Double Points Card</strong>: Doubles the points from one roll</li>
<li><strong>No Penalty Card</strong>: Prevents point loss when rolling 1 or 2</li>
<li><strong>Lucky Streak Card</strong>: Next 3 rolls of 1 or 2 won't count towards your total roll count</li>
</ul>
<p>Items can be stockpiled and each card can only be used once. Double Points Cards and No Penalty Cards can be used simultaneously. Lucky Streak Cards make your next 3 rolls of 1 or 2 not count towards your total roll count, giving you a chance to recover from bad luck.</p>
<h3>Game Rules</h3>
<ul>
<li>Initial score is 10 points</li>
<li>Score multiplier changes based on the current dice tier being used</li>
<li>Game over when score is less than 0</li>
<li>Game victory when obtaining the Unique dice</li>
<li>Unlocked dice colors can be switched at any time</li>
</ul>
<h3>Operation Tips</h3>
<ul>
<li>Click on dice color options to switch the dice being used</li>
<li>Click the "Upgrade Dice" button to upgrade your dice (requires 5 points)</li>
<li>Click the "Use" button next to item cards to use them</li>
<li>Press the ESC key to close the rules window</li>
</ul>
<p>Good luck, and may you successfully obtain the highest tier Unique dice!</p>
</div>
<div class="modal-footer">
<button class="close-button bg-primary hover:bg-blue-600 text-white font-medium py-1.5 px-3 rounded text-sm">Got it</button>
</div>
</div>
</div>
<!-- Win Records Modal -->
<div id="win-records-modal" class="game-rules-modal">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Win Records</h2>
<button class="close-button">×</button>
</div>
<div class="modal-body">
<p>Here are your records for completing the game (obtaining the Unique dice):</p>
<div id="no-records-message" class="text-center py-8">
<i class="fa fa-trophy text-4xl text-gray-300 mb-4"></i>
<p class="text-gray-500">No win records yet. Play and win to see your records!</p>
</div>
<div id="win-records-container" class="hidden">
<div class="overflow-x-auto">
<table class="min-w-full bg-white rounded-lg overflow-hidden">
<thead class="bg-gray-100">
<tr>
<th class="py-2 px-4 text-left text-sm font-semibold text-gray-700">Game #</th>
<th class="py-2 px-4 text-left text-sm font-semibold text-gray-700">Date & Time</th>
<th class="py-2 px-4 text-left text-sm font-semibold text-gray-700">Rolls</th>
<th class="py-2 px-4 text-left text-sm font-semibold text-gray-700">Time</th>
</tr>
</thead>
<tbody id="win-records-list" class="divide-y divide-gray-200">
<!-- Records will be added here by JavaScript -->
</tbody>
</table>
</div>
<div class="mt-6 p-4 bg-blue-50 rounded-lg">
<h3 class="font-semibold text-blue-800 mb-2">Statistics:</h3>
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<p class="text-sm text-gray-600">Total Wins</p>
<p id="total-wins" class="text-2xl font-bold text-primary">0</p>
</div>
<div>
<p class="text-sm text-gray-600">Best (Fewest Rolls)</p>
<p id="best-rolls" class="text-2xl font-bold text-primary">-</p>
</div>
<div>
<p class="text-sm text-gray-600">Average Rolls</p>
<p id="average-rolls" class="text-2xl font-bold text-primary">-</p>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="close-button bg-primary hover:bg-blue-600 text-white font-medium py-1.5 px-3 rounded text-sm">
Close
</button>
</div>
</div>
</div>
<div class="w-full max-w-md mx-auto glass-effect rounded-2xl p-6 dice-shadow relative z-10">
<h1 class="text-3xl font-bold text-center text-dark mb-8">3D Dice Roll</h1>
<div class="flex flex-col items-center justify-center mb-8">
<!-- Dice display area -->
<div id="dice-display" class="w-48 h-48 flex items-center justify-center mb-4 bg-gray-100 rounded-lg">
<!-- Dice scene for 3D perspective -->
<div class="dice-scene">
<div id="dice" class="dice-container">
<!-- Dice faces will be inserted here by JavaScript -->
</div>
</div>
</div>
<!-- Result display -->
<div id="result-display" class="text-2xl font-bold text-center mb-4 hidden">
Result: <span id="result-value" class="text-primary">0</span>
</div>
<!-- Score display -->
<div id="score-display" class="text-xl font-bold text-center mb-2">
Score: <span id="score-value" class="text-secondary">10</span>
<span id="score-change" class="ml-2 text-sm font-normal"></span>
</div>
<!-- Roll count display -->
<div id="roll-count-display" class="text-lg font-medium text-center mb-4">
Rolls: <span id="roll-count-value" class="text-gray-700">0</span>
</div>
<!-- Roll button -->
<button id="roll-button" class="bg-primary hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-opacity-50">
<i class="fa fa-random mr-2"></i> Roll Dice
</button>
<!-- Upgrade button and current tier display -->
<div class="flex flex-col items-center mt-4">
<div id="current-tier" class="text-sm text-gray-600 mb-2">
Current Dice: <span class="font-semibold text-primary">Empty</span>
</div>
<div id="tier-multiplier-display" class="text-sm text-gray-600 mb-2">
Tier Multiplier: <span class="font-semibold text-purple-500">×1.0</span>
</div>
<button id="upgrade-button" class="bg-secondary hover:bg-orange-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-orange-300 focus:ring-opacity-50">
<i class="fa fa-arrow-up mr-1"></i> Upgrade Dice <span id="upgrade-cost">(Cost: 5)</span>
</button>
</div>
<!-- Game message display -->
<div id="game-message" class="mt-4 text-center font-semibold hidden"></div>
<!-- Items display -->
<div id="items-display" class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-white rounded-lg p-3 dice-shadow flex flex-col">
<div class="flex items-center justify-between flex-shrink-0">
<div class="flex items-center">
<i class="fa fa-plus-circle text-green-500 text-xl mr-2"></i>
<span class="font-medium">Double Points Card</span>
</div>
<div class="flex items-center ml-2">
<span id="double-points-count" class="bg-green-100 text-green-800 text-xs font-semibold mr-2 px-3 py-0.5 rounded min-w-[40px] text-center whitespace-nowrap inline-block">0</span>
<button id="use-double-points" class="bg-green-500 hover:bg-green-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1 flex-shrink-0">Doubles your score change for one roll</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow flex flex-col">
<div class="flex items-center justify-between flex-shrink-0">
<div class="flex items-center">
<i class="fa fa-shield text-blue-500 text-xl mr-2"></i>
<span class="font-medium">No Penalty Card</span>
</div>
<div class="flex items-center ml-2">
<span id="no-penalty-count" class="bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-3 py-0.5 rounded min-w-[40px] text-center whitespace-nowrap inline-block">0</span>
<button id="use-no-penalty" class="bg-blue-500 hover:bg-blue-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1 flex-shrink-0">Prevents score loss when rolling 1 or 2</p>
</div>
<div class="bg-white rounded-lg p-3 dice-shadow flex flex-col">
<div class="flex items-center justify-between flex-shrink-0">
<div class="flex items-center">
<i class="fa fa-star text-yellow-500 text-xl mr-2"></i>
<span class="font-medium">Lucky Streak Card</span>
</div>
<div class="flex items-center ml-2">
<span id="lucky-streak-count" class="bg-yellow-100 text-yellow-800 text-xs font-semibold mr-2 px-3 py-0.5 rounded min-w-[30px] text-center whitespace-nowrap inline-block">0</span>
<button id="use-lucky-streak" class="bg-yellow-500 hover:bg-yellow-600 text-white text-xs py-1 px-2 rounded disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" disabled>
Use
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-1 flex-shrink-0">Next 3 rolls of 1 or 2 won't count towards your total roll count</p>
</div>
</div>
<!-- Free rolls indicator -->
<div id="free-rolls-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fa fa-star mr-1" style="color: #f59e0b;"></i>
<span>Free Rolls: <span id="free-rolls-count">0</span></span>
</span>
</div>
<!-- Active item indicator -->
<div id="active-item-indicator" class="mt-3 text-center hidden">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
<i id="active-item-icon" class="mr-1"></i>
<span id="active-item-text">Active Item: None</span>
</span>
</div>
</div>
<!-- Dice Rarity Display -->
<div class="mb-8">
<label class="block text-sm font-medium text-gray-700 mb-2">Current Dice Rarity</label>
<div class="flex items-center justify-center">
<div id="current-rarity-display" class="flex items-center bg-white rounded-lg p-3 shadow-md">
<div id="current-rarity-color" class="w-8 h-8 rounded-md mr-3" style="background-color: #FFFFFF; border: 1px solid #dddddd;"></div>
<div>
<div id="current-rarity-name" class="font-semibold">Empty</div>
<div class="text-xs text-gray-500">Automatically upgraded to highest obtained rarity</div>
</div>
</div>
</div>
</div>
<!-- History log -->
<div class="bg-white rounded-lg p-4">
<h2 class="text-lg font-semibold text-center mb-2">Roll History</h2>
<div id="history-container" class="h-64 overflow-y-auto">
<div class="grid grid-cols-3 gap-2 text-xs font-medium text-gray-500 border-b border-gray-200 sticky top-0 bg-white z-10">
<div>Time</div>
<div>Action</div>
<div class="text-right">Score</div>
</div>
<ul id="history-list" class="text-sm">
<!-- History items will be inserted here by JavaScript -->
</ul>
</div>
</div>
</div>
<footer class="mt-8 text-center text-gray-600 text-sm">
<p>Click the button to roll the dice and see the result!</p>
</footer>
<script>
// Set up global error handler
window.addEventListener('error', function(event) {
console.error('Global error caught:', event.error);
// Try to get roll button element
const rollButton = document.getElementById('roll-button');
if (rollButton && rollButton.disabled) {
console.warn('Error during dice roll, re-enabling button');
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
// Try to show error message
const gameMessage = document.getElementById('game-message');
if (gameMessage) {
gameMessage.textContent = 'An error occurred. Please try again.';
gameMessage.className = 'mt-4 text-center font-semibold text-red-500';
gameMessage.classList.remove('hidden');
}
}
});
// DOM elements
const diceElement = document.getElementById('dice');
const rollButton = document.getElementById('roll-button');
const resultDisplay = document.getElementById('result-display');
const resultValue = document.getElementById('result-value');
const historyList = document.getElementById('history-list');
const colorOptions = document.querySelectorAll('.color-option');
const scoreDisplay = document.getElementById('score-display');
const scoreValue = document.getElementById('score-value');
const scoreChange = document.getElementById('score-change');
const upgradeButton = document.getElementById('upgrade-button');
const currentTierDisplay = document.getElementById('current-tier');
const gameMessageDisplay = document.getElementById('game-message');
const rollCountDisplay = document.getElementById('roll-count-display');
const rollCountValue = document.getElementById('roll-count-value');
const upgradeCost = document.getElementById('upgrade-cost');
// Item related DOM elements
const doublePointsCount = document.getElementById('double-points-count');
const noPenaltyCount = document.getElementById('no-penalty-count');
const luckyStreakCount = document.getElementById('lucky-streak-count');
const useDoublePointsButton = document.getElementById('use-double-points');
const useNoPenaltyButton = document.getElementById('use-no-penalty');
const useLuckyStreakButton = document.getElementById('use-lucky-streak');
const activeItemIndicator = document.getElementById('active-item-indicator');
const freeRollsIndicator = document.getElementById('free-rolls-indicator');
const freeRollsCountElement = document.getElementById('free-rolls-count');
// Game state
let currentScore = 10; // Initial score
let currentTier = 0; // Initial dice tier (0 = Empty)
let gameOver = false; // Game over flag
let rollCount = 0; // Number of dice rolls
let luckyRollsRemaining = 0; // Number of lucky rolls remaining
let isRolling = false; // Whether dice is currently rolling
let currentSafetyTimeout = null; // Stores the current safety timeout ID
// Item system
let doublePointsCards = 0; // Number of double points cards
let noPenaltyCards = 0; // Number of no penalty cards
let luckyStreakCards = 0; // Number of lucky streak cards
let freeRolls = 0; // Number of free rolls available
let activeItems = { // Currently active items
double: 0, // Number of active double points cards
noPenalty: false // Whether no penalty card is active
};
// Dice tiers configuration
const diceTiers = [
{ name: 'Empty', color: '#FFFFFF' },
{ name: 'Common', color: '#7EEF6D' },
{ name: 'Unusual', color: '#FFE65D' },
{ name: 'Rare', color: '#4d52e3' },
{ name: 'Epic', color: '#861FDE' },
{ name: 'Legendary', color: '#DE1F1F' },
{ name: 'Mythic', color: '#1fdbde' },
{ name: 'Ultra', color: '#ff2b75' },
{ name: 'Super', color: '#2bffa3' },
{ name: 'Unique', color: '#555555' }
];
// Initialize 3D dice
function initializeDice() {
console.log('Initializing dice...');
// Create 6 faces for the dice
const faces = [1, 2, 3, 4, 5, 6];
faces.forEach(faceNumber => {
const face = document.createElement('div');
face.className = `dice-face face-${faceNumber}`;
// Add dots to the face based on the number
for (let i = 0; i < faceNumber; i++) {
const dot = document.createElement('div');
dot.className = 'dot';
face.appendChild(dot);
}
diceElement.appendChild(face);
console.log(`Added face ${faceNumber}`);
});
console.log('Dice initialized with faces:', diceElement.children.length);
// Set initial position to show face 1 clearly
diceElement.style.transform = 'rotateX(0deg) rotateY(0deg)';
}
// Get random rotation values for the dice
function getRandomRotation() {
// Determine which face we want to show
const targetFace = Math.floor(Math.random() * 6) + 1;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let currentX = 0, currentY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
currentX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
currentY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Set target rotation based on target face
// These values are precisely calibrated to show the correct face
let targetX = 0, targetY = 0;
switch(targetFace) {
case 1: // Front face (Z+) - visible when no rotation
targetX = 0;
targetY = 0;
break;
case 2: // Left face (X-) - visible when Y rotated 270 degrees
targetX = 0;
targetY = 270;
break;
case 3: // Back face (Z-) - visible when Y rotated 180 degrees
targetX = 0;
targetY = 180;
break;
case 4: // Right face (X+) - visible when Y rotated 90 degrees
targetX = 0;
targetY = 90;
break;
case 5: // Top face (Y-) - visible when X rotated -90 degrees
targetX = -90;
targetY = 0;
break;
case 6: // Bottom face (Y+) - visible when X rotated 90 degrees
targetX = 90;
targetY = 0;
break;
}
// Calculate the shortest path to the target rotation
// This prevents large rotation values from accumulating
let diffX = targetX - currentX;
let diffY = targetY - currentY;
// Normalize the difference to the range [-180, 180] to find the shortest path
diffX = ((diffX + 180) % 360) - 180;
diffY = ((diffY + 180) % 360) - 180;
// Add multiple full rotations for spinning effect (2-4 full rotations)
const fullRotations = 2 + Math.floor(Math.random() * 3);
// Calculate final rotation with full spins
// We add full rotations in the direction of the shortest path
const spinDirectionX = diffX >= 0 ? 1 : -1;
const spinDirectionY = diffY >= 0 ? 1 : -1;
const finalX = currentX + diffX + spinDirectionX * fullRotations * 360;
const finalY = currentY + diffY + spinDirectionY * fullRotations * 360;
// Add a tiny bit of randomness to make it look more natural
// But not enough to change which face is visible
const randomX = (Math.random() - 0.5) * 2;
const randomY = (Math.random() - 0.5) * 2;
return {
x: finalX + randomX,
y: finalY + randomY,
targetFace: targetFace // Return the target face so we don't have to recalculate it
};
}
// Roll the dice function
function rollDice() {
// Check if already rolling
if (isRolling) {
console.log('Ignoring roll request - already rolling');
return;
}
// Check if game is over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if there are free rolls available
const isFreeRoll = freeRolls > 0;
// Check if in lucky streak mode
const isLuckyRoll = luckyRollsRemaining > 0;
// Increment roll count only if not a free roll (lucky rolls will be handled based on result)
if (!isFreeRoll) {
rollCount++;
console.log(`Roll count: ${rollCount}`);
} else {
// Decrement free rolls count
freeRolls--;
updateFreeRollsDisplay();
console.log(`Free roll used. Remaining free rolls: ${freeRolls}`);
}
// Update roll count display
if (rollCountValue) {
rollCountValue.textContent = rollCount;
}
console.log('Rolling dice...');
console.log('Button state before disable:', rollButton.disabled);
// Set rolling state and disable button during animation
isRolling = true;
rollButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
// Disable other interactive elements during roll
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
useDoublePointsButton.disabled = true;
useNoPenaltyButton.disabled = true;
useLuckyStreakButton.disabled = true;
// Disable color options during roll
colorOptions.forEach(option => {
option.disabled = true;
});
console.log('Button state after disable:', rollButton.disabled);
// Hide result display
resultDisplay.classList.add('hidden');
// Reset dice display height - use Tailwind class instead of inline style
const diceDisplay = document.getElementById('dice-display');
if (diceDisplay) {
// Ensure the height class is applied to prevent layout shifts during animation
diceDisplay.classList.add('h-48');
}
try {
// Set animation duration
const duration = 2000; // Fixed duration for consistent experience
// Get random rotation values for the final position
const rotationData = getRandomRotation();
console.log('Rotation data:', rotationData);
console.log('Target face:', rotationData.targetFace);
// Animate the dice using JavaScript for more control
animateDice(duration, rotationData, { isFreeRoll, isLuckyRoll });
// Set a safety timeout to ensure button is re-enabled even if something goes wrong
const safetyTimeoutId = setTimeout(() => {
if (rollButton.disabled && isRolling) {
console.warn('Safety timeout: Re-enabling roll button');
enableRollButton();
}
}, duration + 1000); // Add 1 second buffer
// Store timeout ID to clear it later if animation completes normally
currentSafetyTimeout = safetyTimeoutId;
} catch (error) {
console.error('Error during dice roll:', error);
// Re-enable button if there's an error
enableRollButton();
showGameMessage('An error occurred during the dice roll. Please try again.', 'text-red-500');
}
}
// Animate the dice with spin animation
function animateDice(duration, rotationData, rollContext) {
console.log('Animate dice called with rotationData:', rotationData);
console.log('Roll context:', rollContext);
const startTime = performance.now();
const finalRotation = rotationData;
// Get current rotation from dice element
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
const matrix = new DOMMatrix(currentTransform);
// Extract current rotation angles
let startX = 0, startY = 0;
// If transform is not identity matrix, extract rotation
if (currentTransform !== 'none') {
// Calculate rotation from matrix
startX = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
startY = Math.atan2(-matrix.c, matrix.f) * (180 / Math.PI);
}
// Function to handle each animation frame
function animate(currentTime) {
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
// Apply easing function for smooth, natural animation
const easedProgress = easeOutCubic(progress);
// Calculate current rotation - smooth continuous rotation
const currentX = startX + (finalRotation.x - startX) * easedProgress;
const currentY = startY + (finalRotation.y - startY) * easedProgress;
// Spin animation: rotate in place
diceElement.style.transform = `rotateX(${currentX}deg) rotateY(${currentY}deg)`;
// Continue animation if not complete
if (progress < 1) {
requestAnimationFrame(animate);
} else {
// Animation complete - ensure we're at the exact target rotation
diceElement.style.transform = `rotateX(${finalRotation.x}deg) rotateY(${finalRotation.y}deg)`;
// Finalize immediately after animation completes
console.log('Animation complete, finalizing...');
// Double-check that the transform has been applied
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
console.log('Current transform after animation:', currentTransform);
console.log('Calling finalizeAnimation...');
finalizeAnimation(finalRotation, rollContext);
}
}
// Start the animation
requestAnimationFrame(animate);
}
// Helper function to normalize angles to the range [-180, 180]
function normalizeAngle(angle) {
angle = angle % 360;
if (angle > 180) angle -= 360;
if (angle < -180) angle += 360;
return angle;
}
// Easing function for smooth, natural animation with gentle acceleration and deceleration
// Uses a cubic easing function that starts slow, accelerates, then slows down at the end
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// Calculate score change based on dice roll result
function calculateScoreChange(result) {
let baseChange = 0;
switch(result) {
case 1:
case 2:
baseChange = -1;
break;
case 3:
baseChange = 0;
break;
case 4:
baseChange = 1;
break;
case 5:
baseChange = 2;
break;
case 6:
baseChange = 3;
break;
default:
baseChange = 0;
}
// Apply current tier multiplier
// If losing points (baseChange < 0), use multiplier of 1 instead of tier multiplier
const tierMultiplier = 1 + currentTier / 10;
let finalChange;
if (baseChange < 0) {
// For point loss, use multiplier of 1 regardless of tier
finalChange = baseChange * 1;
} else {
// For point gain or neutral, use tier multiplier
finalChange = baseChange * tierMultiplier;
}
// Apply active item effects
const itemsUsed = {
double: activeItems.double,
noPenalty: activeItems.noPenalty
};
// Apply no penalty card first
if (activeItems.noPenalty && baseChange < 0) {
finalChange = 0;
}
// Apply double points cards
if (activeItems.double > 0) {
finalChange = finalChange * Math.pow(2, activeItems.double);
}
// Round to 2 decimal places to avoid floating point precision issues
finalChange = Math.round(finalChange * 100) / 100;
return {
baseChange: baseChange,
finalChange: finalChange,
itemsUsed: itemsUsed,
tierMultiplier: tierMultiplier
};
}
// Enable roll button
function enableRollButton() {
console.log('Button state before enable:', rollButton.disabled);
// Clear any existing safety timeout
if (currentSafetyTimeout !== null) {
clearTimeout(currentSafetyTimeout);
currentSafetyTimeout = null;
console.log('Cleared existing safety timeout');
}
// Directly enable the button
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
// Re-enable other interactive buttons based on their conditions
updateUpgradeCostDisplay(); // This will enable/disable upgrade button based on score
updateItemsDisplay(); // This will enable/disable item buttons based on availability
// Re-enable color options
colorOptions.forEach((option, index) => {
// Only enable unlocked color options (up to currentTier)
if (index <= currentTier) {
option.disabled = false;
option.classList.remove('locked', 'opacity-50', 'cursor-not-allowed');
} else {
option.disabled = true;
option.classList.add('locked', 'opacity-50', 'cursor-not-allowed');
}
});
console.log('Button state after enable:', rollButton.disabled);
// Double-check and force enable if needed
if (rollButton.disabled) {
console.warn('Forcing button enable');
setTimeout(() => {
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
console.log('Button state after forced enable:', rollButton.disabled);
}, 100);
}
// Update rolling state
isRolling = false;
}
// Finalize the animation and show result
function finalizeAnimation(rotationData, rollContext) {
console.log('=== Finalize animation called ===');
console.log('Current time:', new Date().toISOString().split('T')[1]);
console.log('Rotation data:', rotationData);
console.log('Roll context:', rollContext);
const finalRotation = rotationData;
const isFreeRoll = rollContext && rollContext.isFreeRoll || false;
const isLuckyRoll = rollContext && rollContext.isLuckyRoll || false;
// Use the target face directly instead of recalculating
const result = finalRotation.targetFace;
console.log(`Final result: ${result} (should show face ${result})`);
// Verify dice is in the correct position
const currentTransform = window.getComputedStyle(diceElement).getPropertyValue('transform');
console.log('Current dice transform:', currentTransform);
// Show result display
console.log('Showing result display...');
resultValue.textContent = result;
resultDisplay.classList.remove('hidden');
// Add shine effect to result
resultDisplay.classList.add('result-shine');
setTimeout(() => {
resultDisplay.classList.remove('result-shine');
}, 500);
// Calculate and update score
console.log('Calculating score change...');
const scoreChangeData = calculateScoreChange(result);
console.log('Score change data:', scoreChangeData);
currentScore += scoreChangeData.finalChange;
console.log('Updated score:', currentScore);
// Update score display with animation
console.log('Updating score display...');
updateScoreDisplay(scoreChangeData);
// Enable button
console.log('Enabling roll button...');
enableRollButton();
// Add to history - include whether it was a free roll or lucky roll
addToHistory(result, scoreChangeData, isFreeRoll, isLuckyRoll);
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Create confetti effect if result is 6
if (result === 6) {
createConfetti();
}
// Debug: log final state
console.log(`Final precise rotation: X=${finalRotation.x}°, Y=${finalRotation.y}°`);
console.log(`Displayed result: ${result}`);
console.log(`Score change: ${scoreChangeData.finalChange}, Current score: ${currentScore}`);
console.log('=== finalizeAnimation completed ===');
}
// Update score display with animation
function updateScoreDisplay(scoreChangeData) {
console.log('=== updateScoreDisplay called ===');
console.log('Score change data:', scoreChangeData);
const change = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
const tierMultiplier = scoreChangeData.tierMultiplier;
console.log('Change:', change, 'Base change:', baseChange, 'Items used:', itemsUsed);
// Update the score value - round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
scoreValue.textContent = currentScore;
console.log('Score value updated to:', currentScore);
// Clear previous score change display
scoreChange.textContent = '';
scoreChange.className = 'ml-2 text-sm font-normal';
console.log('Cleared previous score change display');
// Show score change with appropriate styling
if (change > 0) {
scoreChange.textContent = `+${change}`;
scoreChange.classList.add('score-increase');
console.log('Score increase:', change);
} else if (change < 0) {
scoreChange.textContent = `${change}`;
scoreChange.classList.add('score-decrease');
console.log('Score decrease:', change);
} else {
scoreChange.textContent = `±0`;
scoreChange.classList.add('score-neutral');
console.log('Score neutral');
}
// Show item effect message if items were used
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
// Create container for item effects
const itemEffectsContainer = document.createElement('div');
itemEffectsContainer.className = 'flex flex-wrap gap-2 mt-1';
// Add tier multiplier effect if applicable
if (tierMultiplier !== 1) {
const tierEffect = document.createElement('div');
tierEffect.className = 'text-xs text-purple-500';
tierEffect.textContent = `Tier Multiplier ×${tierMultiplier.toFixed(1)}!`;
itemEffectsContainer.appendChild(tierEffect);
}
// Add no penalty effect
if (itemsUsed.noPenalty) {
const noPenaltyEffect = document.createElement('div');
noPenaltyEffect.className = 'text-xs text-blue-500';
noPenaltyEffect.textContent = `No Penalty! (Score protected from ${baseChange < 0 ? baseChange : 0} loss)`;
itemEffectsContainer.appendChild(noPenaltyEffect);
}
// Add double points effect
if (itemsUsed.double > 0) {
const doublePointsEffect = document.createElement('div');
doublePointsEffect.className = 'text-xs text-green-500';
// Calculate multiplier
const multiplier = Math.pow(2, itemsUsed.double);
let calculationText = `${baseChange}`;
// Apply tier multiplier for display
let displayChange = baseChange * tierMultiplier;
// Apply no penalty first for display
if (itemsUsed.noPenalty && baseChange < 0) {
displayChange = 0;
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} → 0)`;
} else if (tierMultiplier !== 1) {
calculationText = `(${baseChange} × ${tierMultiplier.toFixed(1)} = ${displayChange.toFixed(2)})`;
}
// Show multiplication steps if multiple double cards used
if (itemsUsed.double > 1) {
for (let i = 0; i < itemsUsed.double; i++) {
calculationText += ` × 2`;
}
calculationText += ` = ${(displayChange * multiplier).toFixed(2)}`;
} else {
calculationText += ` × 2 = ${(displayChange * multiplier).toFixed(2)}`;
}
doublePointsEffect.textContent = `Double Points ×${itemsUsed.double}! (${calculationText})`;
itemEffectsContainer.appendChild(doublePointsEffect);
}
// Add to score display
scoreDisplay.appendChild(itemEffectsContainer);
// Remove after animation
setTimeout(() => {
scoreDisplay.removeChild(itemEffectsContainer);
}, 1000);
}
// Reset score change display after animation completes
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
// Clear active items after score update
console.log('Checking if items need to be cleared...');
if (itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) {
console.log('Clearing active items...');
clearActiveItems();
console.log('Updating items display...');
updateItemsDisplay();
}
// Check game state after score update
console.log('Checking game state...');
checkGameState();
// Update upgrade button state
updateUpgradeCostDisplay();
console.log('=== updateScoreDisplay completed ===');
}
// Handle dice upgrade
function upgradeDice() {
// Check if game is already over
if (gameOver) {
showGameMessage('Game over! Click "Play Again" to restart.', 'text-red-500');
return;
}
// Check if already at maximum tier
if (currentTier >= diceTiers.length - 1) {
// If not already game over, end the game with win condition
if (!gameOver) {
gameOver = true;
endGame(true);
} else {
showGameMessage('Congratulations! You already have the Unique dice!', 'text-green-500');
}
return;
}
// Calculate required score for upgrade (5 + currentTier)
const requiredScore = 5 + currentTier;
// Check if enough score to upgrade
if (currentScore < requiredScore) {
showGameMessage(`Not enough score to upgrade! Need ${requiredScore} points.`, 'text-orange-500');
// Add shake animation to score display
scoreDisplay.classList.add('animate-shake');
setTimeout(() => {
scoreDisplay.classList.remove('animate-shake');
}, 500);
return;
}
// Deduct score for upgrade
currentScore -= requiredScore;
// Round to 2 decimal places
currentScore = Math.round(currentScore * 100) / 100;
// Update score display
scoreValue.textContent = currentScore;
showScoreChange(-requiredScore);
// Increase tier
currentTier++;
// Update highest unlocked tier
if (currentTier > highestUnlockedTier) {
highestUnlockedTier = currentTier;
}
// Update current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Update tier multiplier display
updateTierMultiplierDisplay();
// Update current rarity display
updateCurrentRarityDisplay();
// Unlock the new dice color
unlockDiceColor(currentTier);
// Update color selection UI
updateColorSelection(currentTier);
// Update upgrade cost display
updateUpgradeCostDisplay();
// Change to the new dice color
console.log(`Changing dice color to ${diceTiers[currentTier].color} (${diceTiers[currentTier].name})`);
changeDiceColor(diceTiers[currentTier].color);
// Check if this upgrade reached the maximum tier
checkGameState();
// Randomly get an item
const itemChance = Math.random();
if (itemChance < 0.30) { // 30% chance to get double points card
doublePointsCards++;
} else if (itemChance < 0.60) { // 30% chance to get no penalty card
noPenaltyCards++;
} else if (itemChance < 0.70) { // 10% chance to get lucky streak card
luckyStreakCards++;
} else { // 30% chance to get nothing
// Do nothing
}
// Update items display to show new counts
updateItemsDisplay();
// Update items display
updateItemsDisplay();
// Check if reached the Unique dice
if (currentTier === diceTiers.length - 1) {
showGameMessage('Congratulations! You obtained the Unique dice!', 'text-green-500');
gameOver = true;
createConfetti();
}
// Add to history
addToHistory('UPGRADE', -requiredScore);
// Add item to history if obtained
if (itemChance < 0.30) {
addToHistory('ITEM', 'Double Points Card');
} else if (itemChance < 0.60) {
addToHistory('ITEM', 'No Penalty Card');
} else if (itemChance < 0.70) {
addToHistory('ITEM', 'Lucky Streak Card');
}
// Check game state after upgrade
checkGameState();
}
// Show score change temporarily
function showScoreChange(change) {
scoreChange.textContent = change > 0 ? `+${change}` : change;
scoreChange.className = `ml-2 text-sm font-normal ${change > 0 ? 'score-increase' : 'score-decrease'}`;
setTimeout(() => {
scoreChange.textContent = '';
}, 1000);
}
// Unlock a dice color option
function unlockDiceColor(tierIndex) {
if (tierIndex >= 0 && tierIndex < colorOptions.length) {
const option = colorOptions[tierIndex];
// Show particle effect on the lock before unlocking
createLockParticles(option);
// Add a small delay to show the particles before unlocking
setTimeout(() => {
option.disabled = false;
option.classList.remove('locked', 'opacity-50', 'cursor-not-allowed');
// Update highest unlocked tier if this is higher
if (tierIndex > highestUnlockedTier) {
highestUnlockedTier = tierIndex;
// Automatically switch to the highest unlocked tier
currentTier = highestUnlockedTier;
// Change dice color to match the new tier
const color = option.getAttribute('data-color');
changeDiceColor(color);
// Update displays
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
updateCurrentRarityDisplay();
updateTierMultiplierDisplay();
// Show message about the new tier
showGameMessage(`Unlocked ${diceTiers[tierIndex].name} Dice!`, 'text-green-500');
}
}, 300);
}
}
// Show game message
function showGameMessage(message, className) {
gameMessageDisplay.textContent = message;
gameMessageDisplay.className = `mt-4 text-center font-semibold ${className}`;
gameMessageDisplay.classList.remove('hidden');
// Hide message after 3 seconds
setTimeout(() => {
if (!gameOver) {
gameMessageDisplay.classList.add('hidden');
}
}, 3000);
}
// Check game state (win/lose conditions)
function checkGameState() {
console.log('=== checkGameState called ===');
console.log('Current score:', currentScore, 'Game over:', gameOver);
// Check if score is negative (lose condition)
if (currentScore < 0 && !gameOver) {
console.log('Score is negative, ending game...');
gameOver = true;
endGame(false);
}
// Check if reached Unique dice (win condition)
if (currentTier === diceTiers.length - 1 && !gameOver) {
console.log('Reached Unique dice, ending game...');
gameOver = true;
endGame(true);
// Check if this is a new record
setTimeout(() => {
checkWinRecord();
}, 1000);
}
// Check if score is low (warning)
if (currentScore >= 0 && currentScore <= 5 && !gameOver) {
showLowScoreWarning();
}
console.log('=== checkGameState completed ===');
}
// Update items display
function updateItemsDisplay() {
console.log('=== updateItemsDisplay called ===');
console.log('Current item counts:');
console.log('doublePointsCards:', doublePointsCards);
console.log('noPenaltyCards:', noPenaltyCards);
console.log('luckyStreakCards:', luckyStreakCards);
console.log('gameOver:', gameOver);
// Update counts
if (doublePointsCount) doublePointsCount.textContent = doublePointsCards;
if (noPenaltyCount) noPenaltyCount.textContent = noPenaltyCards;
if (luckyStreakCount) luckyStreakCount.textContent = luckyStreakCards;
// Enable/disable buttons based on available items
useDoublePointsButton.disabled = doublePointsCards <= 0 || gameOver;
useNoPenaltyButton.disabled = noPenaltyCards <= 0 || activeItems.noPenalty || gameOver;
useLuckyStreakButton.disabled = luckyStreakCards <= 0 || luckyRollsRemaining > 0 || gameOver;
console.log('Button states after update:');
console.log('useDoublePointsButton.disabled:', useDoublePointsButton.disabled);
console.log('useNoPenaltyButton.disabled:', useNoPenaltyButton.disabled);
console.log('useLuckyStreakButton.disabled:', useLuckyStreakButton.disabled);
// Update free rolls display
updateFreeRollsDisplay();
// Update lucky rolls display
updateLuckyRollsDisplay();
}
// Update free rolls display
function updateFreeRollsDisplay() {
if (freeRolls > 0) {
freeRollsCountElement.textContent = freeRolls;
freeRollsIndicator.classList.remove('hidden');
} else {
freeRollsIndicator.classList.add('hidden');
}
}
// Update upgrade cost display and button state
function updateUpgradeCostDisplay() {
if (currentTier >= diceTiers.length - 1) {
// Already at maximum tier
upgradeCost.textContent = '(Max Tier)';
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
} else {
const requiredScore = 5 + currentTier;
upgradeCost.textContent = `(Cost: ${requiredScore})`;
// Update button state based on available score
if (currentScore >= requiredScore && !gameOver) {
upgradeButton.disabled = false;
upgradeButton.classList.remove('opacity-70', 'cursor-not-allowed');
} else {
upgradeButton.disabled = true;
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
}
}
}
// Use double points card
function useDoublePointsCard() {
if (doublePointsCards > 0 && !gameOver) {
activeItems.double++;
doublePointsCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('Double Points Card activated! Next roll will double your score change.', 'text-green-500');
}
}
// Use no penalty card
function useNoPenaltyCard() {
if (noPenaltyCards > 0 && !activeItems.noPenalty && !gameOver) {
activeItems.noPenalty = true;
noPenaltyCards--;
// Update UI
updateItemsDisplay();
showActiveItemIndicator();
// Show message
showGameMessage('No Penalty Card activated! Next roll won\'t lose points.', 'text-blue-500');
}
}
// Use lucky streak card
function useLuckyStreakCard() {
if (luckyStreakCards > 0 && !gameOver && luckyRollsRemaining === 0) {
luckyRollsRemaining = 3;
luckyStreakCards--;
// Update UI
updateItemsDisplay();
updateLuckyRollsDisplay();
// Show message
showGameMessage('Lucky Streak Card activated! Next 3 rolls of 1 or 2 won\'t count!', 'text-yellow-500');
// Add to history
addToHistory('ITEM USE', 'Lucky Streak Card');
} else if (luckyRollsRemaining > 0) {
// Show message if already has active lucky streak
showGameMessage('You already have an active Lucky Streak!', 'text-yellow-500');
}
}
// Update lucky rolls display
function updateLuckyRollsDisplay() {
if (luckyRollsRemaining > 0) {
// Check if indicator exists, if not create it
let luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (!luckyRollsIndicator) {
luckyRollsIndicator = document.createElement('div');
luckyRollsIndicator.id = 'lucky-rolls-indicator';
luckyRollsIndicator.className = 'mt-3 text-center';
// Insert after free rolls indicator
const freeRollsIndicator = document.getElementById('free-rolls-indicator');
if (freeRollsIndicator) {
freeRollsIndicator.parentNode.insertBefore(luckyRollsIndicator, freeRollsIndicator.nextSibling);
} else {
// Fallback: insert after active item indicator
const activeItemIndicator = document.getElementById('active-item-indicator');
if (activeItemIndicator) {
activeItemIndicator.parentNode.insertBefore(luckyRollsIndicator, activeItemIndicator.nextSibling);
} else {
// Fallback: insert after items display
const itemsDisplay = document.getElementById('items-display');
if (itemsDisplay) {
itemsDisplay.parentNode.insertBefore(luckyRollsIndicator, itemsDisplay.nextSibling);
}
}
}
}
// Update content
luckyRollsIndicator.innerHTML = `
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fa fa-star mr-1" style="color: #f59e0b;"></i>
<span>Lucky Rolls: <span id="lucky-rolls-count">${luckyRollsRemaining}</span> (1-2 won't count)</span>
</span>
`;
// Show indicator
luckyRollsIndicator.classList.remove('hidden');
} else {
// Remove indicator if exists
const luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (luckyRollsIndicator) {
luckyRollsIndicator.classList.add('hidden');
}
}
}
// Show active item indicator
function showActiveItemIndicator() {
// Clear previous content
activeItemIndicator.innerHTML = '';
// Check if any items are active
if (activeItems.double === 0 && !activeItems.noPenalty) {
activeItemIndicator.className = 'mt-3 text-center hidden';
return;
}
// Create container for active items
const itemsContainer = document.createElement('div');
itemsContainer.className = 'flex flex-wrap justify-center gap-2';
// Add double points cards indicator
if (activeItems.double > 0) {
const doublePointsIndicator = document.createElement('span');
doublePointsIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800';
doublePointsIndicator.innerHTML = `
<i class="fa fa-plus-circle mr-1" style="color: #10b981;"></i>
<span>Double Points ×${activeItems.double}</span>
`;
itemsContainer.appendChild(doublePointsIndicator);
}
// Add no penalty card indicator
if (activeItems.noPenalty) {
const noPenaltyIndicator = document.createElement('span');
noPenaltyIndicator.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800';
noPenaltyIndicator.innerHTML = `
<i class="fa fa-shield mr-1" style="color: #3b82f6;"></i>
<span>No Penalty</span>
`;
itemsContainer.appendChild(noPenaltyIndicator);
}
// Add indicators to container
activeItemIndicator.appendChild(itemsContainer);
activeItemIndicator.className = 'mt-3 text-center';
}
// Clear active items
function clearActiveItems() {
activeItems = {
double: 0,
noPenalty: false
};
showActiveItemIndicator();
}
// End the game and display appropriate message
function endGame(isWin) {
console.log('=== endGame called ===');
console.log('Is win:', isWin, 'Current score:', currentScore);
// Disable all interactive elements
rollButton.disabled = true;
upgradeButton.disabled = true;
rollButton.classList.add('opacity-70', 'cursor-not-allowed');
upgradeButton.classList.add('opacity-70', 'cursor-not-allowed');
// Disable all interactive elements but preserve their locked/unlocked state
colorOptions.forEach(option => {
option.disabled = true;
// For unlocked colors, we still want to show them as disabled but not locked
// So we'll add a semi-transparent overlay effect
if (!option.classList.contains('locked')) {
option.classList.add('opacity-50', 'cursor-not-allowed');
option.style.opacity = '0.7';
}
});
// Debug: Log color options state at game end
console.log('Color options state at game end:');
colorOptions.forEach((option, index) => {
console.log(`Option ${index}:`, {
locked: option.classList.contains('locked'),
disabled: option.disabled,
opacity: option.style.opacity,
position: option.style.position
});
});
// Debug: Log color options state at game end
console.log('Color options state at game end:');
colorOptions.forEach((option, index) => {
console.log(`Option ${index}:`, {
locked: option.classList.contains('locked'),
disabled: option.disabled,
opacity: option.style.opacity,
position: option.style.position
});
});
// Disable item buttons
useDoublePointsButton.disabled = true;
useNoPenaltyButton.disabled = true;
useLuckyStreakButton.disabled = true;
// Hide active indicators
activeItemIndicator.classList.add('hidden');
const luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (luckyRollsIndicator) {
luckyRollsIndicator.classList.add('hidden');
}
// Create game over message element
const gameOverMessage = document.createElement('div');
gameOverMessage.className = `mt-4 p-4 rounded-lg text-center font-bold ${isWin ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`;
// Format final score to 2 decimal places
const formattedScore = currentScore.toFixed(2);
if (isWin) {
// Save win record immediately when game is won
saveWinRecord(rollCount);
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Congratulations!</div>
<p>You won the game!</p>
<p>You obtained the Unique dice!</p>
<p class="mt-2 text-sm">Total Rolls: ${rollCount}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
createConfetti();
} else {
gameOverMessage.innerHTML = `
<div class="text-2xl mb-2">Game Over!</div>
<p>Your score went negative.</p>
<p>Better luck next time!</p>
<p class="mt-2 text-sm">Final Score: ${formattedScore}</p>
<p class="mt-1 text-sm">Total Rolls: ${rollCount}</p>
<button id="play-again" class="mt-4 bg-primary hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition-all duration-300 transform hover:scale-105">
<i class="fa fa-refresh mr-1"></i> Play Again
</button>
`;
}
// Add restart button event listener
gameOverMessage.querySelector('#play-again').addEventListener('click', () => {
// If this was a win (obtained Unique dice), save the record
if (isWin) {
saveWinRecord(rollCount);
}
// Debug: Log color options state before reset
console.log('Color options state before reset:');
colorOptions.forEach((option, index) => {
console.log(`Option ${index}:`, {
locked: option.classList.contains('locked'),
disabled: option.disabled,
opacity: option.style.opacity,
position: option.style.position
});
});
// Reset game without reloading page
resetGame();
});
// Replace game message display with game over message
const gameMessageContainer = gameMessageDisplay.parentElement;
gameMessageContainer.replaceChild(gameOverMessage, gameMessageDisplay);
gameMessageDisplay = gameOverMessage;
// Add game end to history
addToHistory(isWin ? 'GAME WIN' : 'GAME OVER', 0);
console.log('=== endGame completed ===');
}
// Show warning when score is low
function showLowScoreWarning() {
// Only show warning if not already showing
if (gameMessageDisplay.classList.contains('hidden')) {
showGameMessage('Warning: Low score! Risk of game over.', 'text-orange-500');
}
}
// Update color selection UI to show the currently selected color
function updateColorSelection(selectedIndex) {
// Remove active state from all color options
colorOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-offset-2', 'ring-primary'));
// Add active state to the selected color option
if (selectedIndex >= 0 && selectedIndex < colorOptions.length) {
colorOptions[selectedIndex].classList.add('ring-2', 'ring-offset-2', 'ring-primary');
}
}
// Add result to history
function addToHistory(result, scoreChangeData, isFreeRoll = false, isLuckyRoll = false) {
const now = new Date();
const timeString = now.toLocaleTimeString();
// Determine score change display and color
let scoreChangeText = '';
let scoreChangeClass = '';
let actionText = '';
if (result === 'UPGRADE') {
actionText = 'Upgraded dice';
scoreChangeText = `${scoreChangeData}`;
scoreChangeClass = 'text-red-500';
} else if (result === 'ITEM') {
actionText = `Received <span class="font-bold text-purple-500">${scoreChangeData}</span>`;
scoreChangeText = '+1';
scoreChangeClass = 'text-purple-500';
} else if (result === 'GAME WIN' || result === 'GAME OVER') {
actionText = result;
scoreChangeText = '';
scoreChangeClass = '';
} else {
const scoreChange = scoreChangeData.finalChange;
const baseChange = scoreChangeData.baseChange;
const itemsUsed = scoreChangeData.itemsUsed;
actionText = `Rolled a <span class="font-bold text-primary">${result}</span>`;
// Add free roll indicator if applicable
if (isFreeRoll) {
actionText += ` <span class="text-yellow-500">(Free Roll)</span>`;
}
// Add lucky roll indicator if applicable
if (isLuckyRoll) {
actionText += ` <span class="text-yellow-500">(Lucky Roll)</span>`;
}
// Add item effect indicators if applicable
const activeItemsText = [];
if (itemsUsed && itemsUsed.double > 0) {
activeItemsText.push(`<span class="text-green-500">(Double Points ×${itemsUsed.double})</span>`);
}
if (itemsUsed && itemsUsed.noPenalty) {
activeItemsText.push(`<span class="text-blue-500">(No Penalty)</span>`);
}
if (activeItemsText.length > 0) {
actionText += ` ${activeItemsText.join(' ')}`;
}
if (scoreChange > 0) {
scoreChangeText = `+${scoreChange}`;
scoreChangeClass = 'text-green-500';
} else if (scoreChange < 0) {
scoreChangeText = `${scoreChange}`;
scoreChangeClass = 'text-red-500';
} else {
scoreChangeText = '±0';
scoreChangeClass = 'text-gray-500';
}
// Show base change if different from final change (items were used)
if ((itemsUsed && (itemsUsed.double > 0 || itemsUsed.noPenalty)) && baseChange !== scoreChange) {
scoreChangeText += ` <span class="text-xs">(Base: ${baseChange > 0 ? '+' : ''}${baseChange})</span>`;
}
}
const listItem = document.createElement('li');
listItem.className = 'py-1 border-b border-gray-100 grid grid-cols-3 gap-2 items-center';
listItem.innerHTML = `
<span class="col-span-1 text-gray-500">${timeString}</span>
<span class="col-span-1">${actionText}</span>
<span class="col-span-1 text-right font-medium ${scoreChangeClass}">${scoreChangeText}</span>
`;
historyList.prepend(listItem);
// Keep only last 10 history items
if (historyList.children.length > 10) {
historyList.removeChild(historyList.lastChild);
}
// Ensure scroll is at the bottom
const historyContainer = document.getElementById('history-container');
if (historyContainer) {
historyContainer.scrollTop = historyContainer.scrollHeight;
}
}
// Create confetti effect
function createConfetti() {
const confettiContainer = document.createElement('div');
confettiContainer.className = 'fixed inset-0 pointer-events-none overflow-hidden';
document.body.appendChild(confettiContainer);
// Create 50 confetti pieces
for (let i = 0; i < 50; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
// Random position
const posX = Math.random() * 100;
const delay = Math.random() * 3;
const duration = 3 + Math.random() * 2;
// Random colors
const colors = ['#3b82f6', '#f97316', '#10b981', '#ef4444', '#8b5cf6'];
const color = colors[Math.floor(Math.random() * colors.length)];
confetti.style.left = `${posX}%`;
confetti.style.backgroundColor = color;
confetti.style.animationDelay = `${delay}s`;
confetti.style.animationDuration = `${duration}s`;
confettiContainer.appendChild(confetti);
}
// Remove confetti container after animation completes
setTimeout(() => {
document.body.removeChild(confettiContainer);
}, 5000);
}
// Create particle effect on lock
function createLockParticles(lockElement) {
// Get lock element position
const rect = lockElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// Create particle container
const particleContainer = document.createElement('div');
particleContainer.className = 'absolute pointer-events-none';
particleContainer.style.left = `${centerX}px`;
particleContainer.style.top = `${centerY}px`;
particleContainer.style.transform = 'translate(-50%, -50%)';
document.body.appendChild(particleContainer);
// Create 15 particles
for (let i = 0; i < 15; i++) {
const particle = document.createElement('div');
particle.className = 'lock-particle';
// Random direction and distance
const angle = Math.random() * Math.PI * 2;
const distance = 10 + Math.random() * 20;
const tx = Math.cos(angle) * distance;
const ty = Math.sin(angle) * distance;
// Random color (use the color of the unlocked dice)
const color = lockElement.style.backgroundColor;
// Random animation duration
const duration = 0.5 + Math.random() * 0.5;
// Set particle styles
particle.style.backgroundColor = color;
particle.style.setProperty('--tx', `${tx}px`);
particle.style.setProperty('--ty', `${ty}px`);
particle.style.animation = `lock-particle ${duration}s ease-out forwards`;
particleContainer.appendChild(particle);
}
// Remove particle container after animation completes
setTimeout(() => {
document.body.removeChild(particleContainer);
}, 1000);
}
// Function to darken a color by a certain percentage
function darkenColor(color, percent) {
const hex = color.replace('#', '');
let r = parseInt(hex.substr(0, 2), 16);
let g = parseInt(hex.substr(2, 2), 16);
let b = parseInt(hex.substr(4, 2), 16);
// Darken each channel by the percentage
r = Math.max(0, Math.floor(r * (1 - percent / 100)));
g = Math.max(0, Math.floor(g * (1 - percent / 100)));
b = Math.max(0, Math.floor(b * (1 - percent / 100)));
// Convert back to hex
const darkenedHex = '#' +
r.toString(16).padStart(2, '0') +
g.toString(16).padStart(2, '0') +
b.toString(16).padStart(2, '0');
return darkenedHex;
}
// Change dice color function
function changeDiceColor(color) {
console.log(`changeDiceColor called with color: ${color}`);
const faces = document.querySelectorAll('.dice-face');
console.log(`Found ${faces.length} dice faces`);
let dotColor, borderColor;
// Special cases for high contrast
if (color.toUpperCase() === '#FFFFFF') {
// White dice - use black dots and light gray borders
dotColor = '#000000';
borderColor = '#CCCCCC';
} else if (color.toUpperCase() === '#555555') {
// Dark gray dice - use white dots and slightly lighter gray borders
dotColor = '#FFFFFF';
borderColor = '#777777';
} else {
// Calculate darker color for dots and borders (40% darker for more contrast)
dotColor = darkenColor(color, 40);
borderColor = darkenColor(color, 30); // Slightly lighter border than dots
}
faces.forEach(face => {
// Set face background color
face.style.backgroundColor = color;
// Set face border color
face.style.border = `3px solid ${borderColor}`;
// Set dots color
const dots = face.querySelectorAll('.dot');
dots.forEach(dot => {
dot.style.backgroundColor = dotColor;
// Add slight border to dots for better definition
dot.style.border = color.toUpperCase() === '#FFFFFF' ? '1px solid rgba(0, 0, 0, 0.2)' : '1px solid rgba(0, 0, 0, 0.1)';
});
});
}
// Update tier multiplier display
function updateTierMultiplierDisplay() {
const display = document.getElementById('tier-multiplier-display');
if (display) {
display.innerHTML = `Tier Multiplier: <span class="font-semibold text-purple-500">×${(1 + currentTier / 10).toFixed(1)}</span>`;
}
}
// Event listener for roll button
rollButton.addEventListener('click', rollDice);
// Event listener for upgrade button
upgradeButton.addEventListener('click', upgradeDice);
// Event listeners for item buttons
useDoublePointsButton.addEventListener('click', useDoublePointsCard);
useNoPenaltyButton.addEventListener('click', useNoPenaltyCard);
useLuckyStreakButton.addEventListener('click', useLuckyStreakCard);
// Track the highest unlocked tier
let highestUnlockedTier = 0;
// Function to update current rarity display
function updateCurrentRarityDisplay() {
const rarityColorElement = document.getElementById('current-rarity-color');
const rarityNameElement = document.getElementById('current-rarity-name');
if (rarityColorElement && rarityNameElement) {
// Get the dice tier data for the current tier
if (currentTier >= 0 && currentTier < diceTiers.length) {
const tierData = diceTiers[currentTier];
rarityColorElement.style.backgroundColor = tierData.color;
// Special case for white color to ensure border visibility
if (tierData.color.toUpperCase() === '#FFFFFF') {
rarityColorElement.style.border = '1px solid #dddddd';
} else {
rarityColorElement.style.border = 'none';
}
// Update the rarity name
rarityNameElement.textContent = tierData.name;
}
}
}
// Initialize dice on page load
window.addEventListener('DOMContentLoaded', () => {
console.log('=== DOMContentLoaded event fired ===');
// Check if elements exist
console.log('Checking if elements exist before initialization:');
console.log('best-record-display exists:', !!document.getElementById('best-record-display'));
console.log('best-record-value exists:', !!document.getElementById('best-record-value'));
console.log('DOM fully loaded');
initializeDice();
// Set default color (first option)
if (colorOptions.length > 0) {
const defaultColor = colorOptions[0].getAttribute('data-color');
changeDiceColor(defaultColor);
}
// Initialize highest unlocked tier
highestUnlockedTier = 0;
// Initialize current tier to highest unlocked tier
currentTier = highestUnlockedTier;
// Initialize current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Update current rarity display
updateCurrentRarityDisplay();
// Initialize tier multiplier display
updateTierMultiplierDisplay();
// Initialize items display
updateItemsDisplay();
// Initialize upgrade cost display
updateUpgradeCostDisplay();
// Show welcome message
setTimeout(() => {
showGameMessage('Welcome! Roll the dice to earn points and upgrade your dice!', 'text-blue-500');
}, 1000);
});
// Also try initializing on window load
window.addEventListener('load', () => {
console.log('Window loaded');
// If dice not already initialized, try again
if (diceElement.children.length === 0) {
console.log('Dice not initialized, trying again...');
initializeDice();
}
});
// Game Rules Modal Functionality
(function() {
// Get elements
const rulesButton = document.getElementById('game-rules-button');
const rulesModal = document.getElementById('game-rules-modal');
const closeButtons = rulesModal.querySelectorAll('.close-button');
const modalBackdrop = rulesModal.querySelector('.modal-backdrop');
const modalContent = rulesModal.querySelector('.modal-content');
// Function to prevent background scrolling
function preventBackgroundScroll(event) {
// Allow scrolling inside the modal content
if (modalContent.contains(event.target)) {
// Check if we're at the top or bottom of the modal content
const isAtTop = modalContent.scrollTop === 0;
const isAtBottom = modalContent.scrollTop + modalContent.clientHeight >= modalContent.scrollHeight;
// Prevent scrolling if at the top and scrolling up, or at the bottom and scrolling down
if ((isAtTop && event.deltaY < 0) || (isAtBottom && event.deltaY > 0)) {
event.preventDefault();
}
} else {
// Prevent scrolling outside the modal content
event.preventDefault();
}
}
// Function to open modal
function openModal() {
// Add event listeners to prevent background scrolling
document.addEventListener('wheel', preventBackgroundScroll, { passive: false });
document.addEventListener('touchmove', preventBackgroundScroll, { passive: false });
// Add open class to trigger animations
rulesModal.classList.add('modal-open');
// Show the modal
rulesModal.style.visibility = 'visible';
}
// Function to close modal
function closeModal() {
// Remove open class to trigger animations
rulesModal.classList.remove('modal-open');
// Remove event listeners that prevent background scrolling
document.removeEventListener('wheel', preventBackgroundScroll);
document.removeEventListener('touchmove', preventBackgroundScroll);
// Hide the modal after animation completes
setTimeout(() => {
rulesModal.style.visibility = 'hidden';
}, 300);
}
// Add event listeners
if (rulesButton) {
rulesButton.addEventListener('click', openModal);
}
// Close buttons
closeButtons.forEach(button => {
button.addEventListener('click', closeModal);
});
// Close when clicking outside the modal content
modalBackdrop.addEventListener('click', closeModal);
// Close when pressing Escape key
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && rulesModal.classList.contains('modal-open')) {
closeModal();
}
});
// Add hover effect to rules button
if (rulesButton) {
rulesButton.addEventListener('mouseenter', () => {
rulesButton.classList.add('scale-105');
});
rulesButton.addEventListener('mouseleave', () => {
rulesButton.classList.remove('scale-105');
});
}
// Add click effect to close buttons
closeButtons.forEach(button => {
button.addEventListener('mousedown', () => {
button.classList.add('scale-95');
});
button.addEventListener('mouseup', () => {
button.classList.remove('scale-95');
});
button.addEventListener('mouseleave', () => {
button.classList.remove('scale-95');
});
});
})();
// Reset Game Functionality
function resetGame() {
console.log('=== resetGame called ===');
// Reset game state variables
currentScore = 10;
currentTier = 0;
gameOver = false;
rollCount = 0;
luckyRollsRemaining = 0;
isRolling = false;
// Debug: Log game state after reset
console.log('Game state after reset:');
console.log('currentScore:', currentScore);
console.log('currentTier:', currentTier);
console.log('gameOver:', gameOver);
console.log('rollCount:', rollCount);
console.log('luckyRollsRemaining:', luckyRollsRemaining);
// Reset item system
doublePointsCards = 0;
noPenaltyCards = 0;
luckyStreakCards = 0;
freeRolls = 0;
activeItems = {
double: 0,
noPenalty: false
};
// Debug: Log item counts after reset
console.log('=== Item counts after reset ===');
console.log('doublePointsCards:', doublePointsCards);
console.log('noPenaltyCards:', noPenaltyCards);
console.log('luckyStreakCards:', luckyStreakCards);
console.log('freeRolls:', freeRolls);
console.log('activeItems:', activeItems);
console.log('gameOver:', gameOver);
// Immediately update UI to show zero counts
if (doublePointsCount) doublePointsCount.textContent = '0';
if (noPenaltyCount) noPenaltyCount.textContent = '0';
if (luckyStreakCount) luckyStreakCount.textContent = '0';
// Reset highest unlocked tier
highestUnlockedTier = 0;
// Update UI
scoreValue.textContent = currentScore;
rollCountValue.textContent = rollCount;
// Reset current tier display
currentTierDisplay.innerHTML = `Current Dice: <span class="font-semibold text-primary">${diceTiers[currentTier].name}</span>`;
// Explicitly update upgrade cost display to ensure it's correct
updateUpgradeCostDisplay();
// Reset tier multiplier display
updateTierMultiplierDisplay();
// Reset current rarity display
updateCurrentRarityDisplay();
// Reset dice color to default (Empty)
changeDiceColor(diceTiers[0].color);
// Reset color selection UI
updateColorSelection(0);
// Reset color options
colorOptions.forEach((option, index) => {
// Reset all color options to locked state first
option.disabled = true;
option.classList.add('locked', 'opacity-50', 'cursor-not-allowed');
option.style.position = 'relative'; // Ensure position is set for lock icon
});
// Only the first color option (index 0) should be unlocked
if (colorOptions.length > 0) {
colorOptions[0].disabled = false;
colorOptions[0].classList.remove('locked', 'opacity-50', 'cursor-not-allowed');
colorOptions[0].style.position = ''; // Reset position for unlocked option
}
// Reset history
historyList.innerHTML = '';
// Hide active indicators
activeItemIndicator.classList.add('hidden');
const luckyRollsIndicator = document.getElementById('lucky-rolls-indicator');
if (luckyRollsIndicator) {
luckyRollsIndicator.classList.add('hidden');
}
freeRollsIndicator.classList.add('hidden');
// Hide result display
resultDisplay.classList.add('hidden');
// Hide score change
scoreChange.textContent = '';
// Remove game over message and restore original game message display
// First, try to find and remove any game over message
const gameOverMessages = document.querySelectorAll('.bg-green-100, .bg-red-100');
gameOverMessages.forEach(message => {
message.remove();
});
// Now, ensure the original game message element exists
let gameMessageContainer = null;
if (gameMessageDisplay) {
gameMessageContainer = gameMessageDisplay.parentElement;
// Remove the current game message display (which might be the game over message)
gameMessageContainer.removeChild(gameMessageDisplay);
} else {
// If gameMessageDisplay is not available, find the container by looking for the items display
const itemsDisplay = document.getElementById('items-display');
if (itemsDisplay) {
gameMessageContainer = itemsDisplay.previousElementSibling;
}
}
// If we found the container, create and add the original game message element
if (gameMessageContainer) {
// Create a new game message element
const newGameMessage = document.createElement('div');
newGameMessage.id = 'game-message';
newGameMessage.className = 'mt-4 text-center font-semibold hidden';
// Add the new game message element to the container
gameMessageContainer.appendChild(newGameMessage);
// Update the reference to the new game message element
gameMessageDisplay = newGameMessage;
} else {
console.error('Game message container not found!');
// Fallback: try to create the game message element and add it to the DOM
const newGameMessage = document.createElement('div');
newGameMessage.id = 'game-message';
newGameMessage.className = 'mt-4 text-center font-semibold hidden';
// Try to insert it after the upgrade button area
const upgradeButtonArea = document.getElementById('upgrade-button')?.parentElement;
if (upgradeButtonArea) {
upgradeButtonArea.parentNode.insertBefore(newGameMessage, upgradeButtonArea.nextSibling);
gameMessageDisplay = newGameMessage;
}
}
// Ensure the lock icon is displayed for locked options
colorOptions.forEach(option => {
if (option.classList.contains('locked')) {
option.style.position = 'relative';
// Explicitly set the pseudo-element content to ensure it's displayed
option.style.setProperty('--content', '\\f023');
} else {
option.style.position = '';
option.style.removeProperty('--content');
}
});
// Enable all interactive elements
rollButton.disabled = false;
rollButton.classList.remove('opacity-70', 'cursor-not-allowed');
// Update items display
updateItemsDisplay();
// Update upgrade button state
updateUpgradeCostDisplay();
// Ensure skill card counts are properly displayed
if (doublePointsCount) doublePointsCount.textContent = doublePointsCards;
if (noPenaltyCount) noPenaltyCount.textContent = noPenaltyCards;
if (luckyStreakCount) luckyStreakCount.textContent = luckyStreakCards;
// Debug: Log UI state after items display update
console.log('UI state after items display update:');
console.log('doublePointsCount.textContent:', doublePointsCount.textContent);
console.log('noPenaltyCount.textContent:', noPenaltyCount.textContent);
console.log('luckyStreakCount.textContent:', luckyStreakCount.textContent);
console.log('useDoublePointsButton.disabled:', useDoublePointsButton.disabled);
console.log('useNoPenaltyButton.disabled:', useNoPenaltyButton.disabled);
console.log('useLuckyStreakButton.disabled:', useLuckyStreakButton.disabled);
// Show welcome message
setTimeout(() => {
showGameMessage('Welcome! Roll the dice to earn points and upgrade your dice!', 'text-blue-500');
}, 500);
console.log('=== resetGame completed ===');
console.log('Current tier after reset:', currentTier);
console.log('Highest unlocked tier after reset:', highestUnlockedTier);
}
// Save win record to localStorage
function saveWinRecord(rolls) {
try {
// Get current records or initialize empty array
let records = JSON.parse(localStorage.getItem('diceGameWinRecords')) || [];
// Create new record
const newRecord = {
id: records.length + 1,
date: new Date().toISOString(),
rolls: rolls
};
// Add to records
records.push(newRecord);
// Save back to localStorage
localStorage.setItem('diceGameWinRecords', JSON.stringify(records));
console.log('Win record saved:', newRecord);
} catch (error) {
console.error('Error saving win record:', error);
}
}
// Load win records from localStorage
function loadWinRecords() {
try {
const records = JSON.parse(localStorage.getItem('diceGameWinRecords')) || [];
const recordsList = document.getElementById('win-records-list');
const noRecordsMessage = document.getElementById('no-records-message');
const recordsContainer = document.getElementById('win-records-container');
// Clear previous records
recordsList.innerHTML = '';
if (records.length === 0) {
// Show no records message
if (noRecordsMessage) noRecordsMessage.classList.remove('hidden');
if (recordsContainer) recordsContainer.classList.add('hidden');
return;
}
// Hide no records message
if (noRecordsMessage) noRecordsMessage.classList.add('hidden');
if (recordsContainer) recordsContainer.classList.remove('hidden');
// Add records to list
records.forEach(record => {
const date = new Date(record.date);
const formattedDate = date.toLocaleDateString();
const formattedTime = date.toLocaleTimeString();
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
row.innerHTML = `
<td class="py-2 px-4">${record.id}</td>
<td class="py-2 px-4">${formattedDate} ${formattedTime}</td>
<td class="py-2 px-4">${record.rolls}</td>
<td class="py-2 px-4">-</td>
`;
recordsList.appendChild(row);
});
// Update statistics
updateWinStatistics(records);
} catch (error) {
console.error('Error loading win records:', error);
}
}
// Update win statistics
function updateWinStatistics(records) {
try {
const totalWinsElement = document.getElementById('total-wins');
const bestRollsElement = document.getElementById('best-rolls');
const averageRollsElement = document.getElementById('average-rolls');
if (!totalWinsElement || !bestRollsElement || !averageRollsElement) {
console.error('Win statistics elements not found');
return;
}
// Total wins
totalWinsElement.textContent = records.length;
// Best rolls (minimum)
const rolls = records.map(record => record.rolls);
const bestRolls = Math.min(...rolls);
bestRollsElement.textContent = bestRolls;
// Average rolls
const averageRolls = rolls.reduce((sum, val) => sum + val, 0) / rolls.length;
averageRollsElement.textContent = averageRolls.toFixed(1);
} catch (error) {
console.error('Error updating win statistics:', error);
}
}
// Clear win records
function clearWinRecords() {
if (confirm('Are you sure you want to clear all win records? This cannot be undone.')) {
try {
localStorage.removeItem('diceGameWinRecords');
loadWinRecords(); // Refresh display
alert('All win records have been cleared.');
} catch (error) {
console.error('Error clearing win records:', error);
alert('An error occurred while clearing records.');
}
}
}
// Win Records Modal Functionality
(function() {
// Get elements
const recordsButton = document.getElementById('win-records-button');
const recordsModal = document.getElementById('win-records-modal');
const closeButtons = recordsModal.querySelectorAll('.close-button');
const modalBackdrop = recordsModal.querySelector('.modal-backdrop');
const modalContent = recordsModal.querySelector('.modal-content');
// const clearRecordsButton = document.getElementById('clear-records-button');
// Function to prevent background scrolling
function preventBackgroundScroll(event) {
// Allow scrolling inside the modal content
if (modalContent.contains(event.target)) {
// Check if we're at the top or bottom of the modal content
const isAtTop = modalContent.scrollTop === 0;
const isAtBottom = modalContent.scrollTop + modalContent.clientHeight >= modalContent.scrollHeight;
// Prevent scrolling if at the top and scrolling up, or at the bottom and scrolling down
if ((isAtTop && event.deltaY < 0) || (isAtBottom && event.deltaY > 0)) {
event.preventDefault();
}
} else {
// Prevent scrolling outside the modal content
event.preventDefault();
}
}
// Function to open modal
function openModal() {
// Load win records
loadWinRecords();
// Add event listeners to prevent background scrolling
document.addEventListener('wheel', preventBackgroundScroll, { passive: false });
document.addEventListener('touchmove', preventBackgroundScroll, { passive: false });
// Add open class to trigger animations
recordsModal.classList.add('modal-open');
// Show the modal
recordsModal.style.visibility = 'visible';
}
// Function to close modal
function closeModal() {
// Remove open class to trigger animations
recordsModal.classList.remove('modal-open');
// Remove event listeners that prevent background scrolling
document.removeEventListener('wheel', preventBackgroundScroll);
document.removeEventListener('touchmove', preventBackgroundScroll);
// Hide the modal after animation completes
setTimeout(() => {
recordsModal.style.visibility = 'hidden';
}, 300);
}
// Add event listeners
if (recordsButton) {
recordsButton.addEventListener('click', openModal);
}
// Close buttons
closeButtons.forEach(button => {
button.addEventListener('click', closeModal);
});
// Close when clicking outside the modal content
modalBackdrop.addEventListener('click', closeModal);
// Close when pressing Escape key
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && recordsModal.classList.contains('modal-open')) {
closeModal();
}
});
// // Clear records button
// if (clearRecordsButton) {
// clearRecordsButton.addEventListener('click', clearWinRecords);
// }
// Add hover effect to records button
if (recordsButton) {
recordsButton.addEventListener('mouseenter', () => {
recordsButton.classList.add('scale-105');
});
recordsButton.addEventListener('mouseleave', () => {
recordsButton.classList.remove('scale-105');
});
}
// Add click effect to close buttons
closeButtons.forEach(button => {
button.addEventListener('mousedown', () => {
button.classList.add('scale-95');
});
button.addEventListener('mouseup', () => {
button.classList.remove('scale-95');
});
button.addEventListener('mouseleave', () => {
button.classList.remove('scale-95');
});
});
})();
</script>
</body>
</html>
相关推荐
评论
共 4 条评论,欢迎与作者交流。
正在加载评论...