deepcoralreef / index.html
offerpk3's picture
Update index.html
ac41aac verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deep Sea Breakout</title>
<!-- Tailwind CSS (as per your HTML) - can be removed if not actively used for new additions -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Orbitron:wght@400;700&display=swap');
:root {
--game-width: 800px; /* Default, will be set by JS */
--game-height: 600px; /* Default, will be set by JS */
}
body {
margin: 0;
padding: 0;
background: radial-gradient(ellipse at bottom, #1B2735 0%, #090A0F 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Orbitron', sans-serif;
overflow: hidden;
color: #e0f7ff;
}
.stars-bg { /* Changed from ::before to allow multiple layers */
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
z-index: -1;
}
.stars-bg::before, .stars-bg::after {
content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background-image:
radial-gradient(1px 1px at 20% 30%, white, rgba(255,255,255,0)),
radial-gradient(1px 1px at 50% 70%, white, rgba(255,255,255,0)),
radial-gradient(2px 2px at 80% 40%, white, rgba(255,255,255,0)),
radial-gradient(1px 1px at 10% 90%, white, rgba(255,255,255,0)),
radial-gradient(2px 2px at 90% 10%, white, rgba(255,255,255,0));
background-size: 200px 200px; animation: twinkle 15s linear infinite alternate; opacity: 0;
}
.stars-bg::after { background-size: 300px 300px; animation-delay: -7.5s; }
@keyframes twinkle {
0% { opacity: 0.1; transform: translateY(0px); }
50% { opacity: 0.7; transform: translateY(-5px); }
100% { opacity: 0.1; transform: translateY(0px); }
}
.game-container {
position: relative;
width: var(--game-width); /* Set by JS */
height: var(--game-height); /* Set by JS */
border: 3px solid #0ff;
border-radius: 12px;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.5), 0 0 10px rgba(0,255,255,0.7) inset;
background: rgba(0, 10, 20, 0.85);
overflow: hidden;
}
canvas#gameCanvas { /* Ensure it targets the right canvas */
display: block;
width: 100%; height: 100%;
/* Background gradient is now dynamically applied by JS */
}
.ui {
position: absolute;
top: 10px; left: 10px; right: 10px;
display: flex; justify-content: space-between;
color: #0ff; font-size: calc(var(--game-height) * 0.03); /* Responsive font */
font-weight: bold; text-shadow: 0 0 10px rgba(0, 255, 255, 0.8);
pointer-events: none; font-family: 'Orbitron', sans-serif; z-index: 3;
}
.ui > div { /* Target direct children of .ui */
background: rgba(0, 40, 80, 0.6);
padding: calc(var(--game-height) * 0.008) calc(var(--game-height) * 0.025);
border-radius: 20px; border: 1px solid rgba(0, 255, 255, 0.3);
margin: 0 3px; /* Reduced margin for more items */
white-space: nowrap;
}
.dialog-box, .menu, .settings-panel, .leaderboard, .shop, .daily-challenge,
.achievement-system, .tutorial-screen, .stats-screen, .credits, .customization,
.profile, .multiplayer-info, .matchmaking, .matchmaking-invite, .matchmaking-scoreboard {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 5, 15, 0.97); color: #0ff;
padding: clamp(20px, 4vh, 30px); border-radius: 15px;
text-align: center; border: 2px solid #0ff;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.8), 0 0 20px #0af inset;
z-index: 20; width: 85%; max-width: 500px;
backdrop-filter: blur(4px); display: none; /* Hidden by default */
max-height: 80vh; overflow-y: auto; /* Scroll for long content */
}
/* Titles for all dialogs */
.dialog-box h1, .dialog-box h2, .menu-title, .settings-header, .leaderboard-title,
.shop-title, .daily-challenge-title, .achievement-system-title, .tutorial-screen-title,
.stats-screen-title, .credits-title, .customization-title, .profile-title,
.multiplayer-title, .matchmaking-title, .matchmaking-scoreboard-title {
font-family: 'Press Start 2P', cursive;
font-size: clamp(20px, 3.5vw, 28px);
margin-bottom: 20px; text-shadow: 0 0 10px #0ff; color: #7ff;
}
.dialog-box p {
font-size: clamp(14px, 2.2vw, 16px); margin-bottom: 20px; line-height: 1.5;
}
.start-screen { display: flex; flex-direction: column; align-items: center; } /* Initial state for start */
.pause-message {
/* Style from previous version - looks good */
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%); color: #f0f;
font-size: clamp(40px, 8vw, 60px); font-weight: bold;
text-shadow: 0 0 15px #f0f, 0 0 30px #f0f; pointer-events: none;
display: none; font-family: 'Press Start 2P', cursive; z-index: 100;
animation: pulse 1.5s infinite alternate;
}
@keyframes pulse {
from { opacity: 0.6; transform: translate(-50%, -50%) scale(1); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1.05); }
}
.powerup-indicator {
position: absolute; bottom: calc(var(--game-height) * 0.08); /* Above controls */
right: 10px; display: flex; flex-direction: column; gap: 8px; z-index: 5;
}
.powerup-icon {
width: clamp(30px, 4vw, 40px); height: clamp(30px, 4vw, 40px);
background: rgba(0, 40, 80, 0.8); border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-weight: bold; font-size: clamp(12px, 2vw, 16px);
border: 1px solid rgba(0, 255, 255, 0.6); position: relative; color: #fff;
box-shadow: 0 0 8px rgba(0,255,255,0.4);
}
.powerup-icon::after { /* Timer bar */
content: ''; position: absolute; bottom: -4px; left: 50%; transform: translateX(-50%);
height: 5px; background: #0f0; border-radius: 2px;
transition: width 0.1s linear; width: var(--time-left, 100%);
}
.powerup-icon.rare::before {
content: ''; position: absolute; inset: -3px; border-radius: 50%;
border: 2px solid gold; animation: rareGlow 1s infinite alternate; opacity: 0.9;
}
@keyframes rareGlow {
from { box-shadow: 0 0 6px gold, 0 0 12px yellow; }
to { box-shadow: 0 0 12px gold, 0 0 18px yellow; }
}
.button, .menu-button, .sound-button, .save-button, .item-button, .challenge-button,
.profile-button, .matchmaking-button, .matchmaking-control-button, .matchmaking-invite-button,
.theme-button { /* Consolidated button styles */
background: linear-gradient(45deg, #0077aa, #00aadd);
border: 1px solid #00ccff; color: white;
padding: clamp(10px, 1.8vh, 12px) clamp(18px, 2.5vw, 24px);
margin: 8px 5px; border-radius: 8px; cursor: pointer;
font-size: clamp(13px, 2.2vw, 16px);
transition: all 0.25s ease-out;
box-shadow: 0 4px 12px rgba(0, 180, 220, 0.3), 0 0 4px rgba(0,255,255,0.4) inset;
font-family: 'Orbitron', sans-serif; text-transform: uppercase;
letter-spacing: 0.5px; position: relative; overflow: hidden;
}
.button::before, .menu-button::before, .sound-button::before, .save-button::before,
.item-button::before, .challenge-button::before, .profile-button::before,
.matchmaking-button::before, .matchmaking-control-button::before, .matchmaking-invite-button::before,
.theme-button::before {
content: ''; position: absolute; top: -50%; left: -150%; width: 50%; height: 200%;
background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0) 100%);
transform: skewX(-25deg); transition: left 0.5s ease-in-out;
}
.button:hover, .menu-button:hover, .sound-button:hover, .save-button:hover,
.item-button:hover, .challenge-button:hover, .profile-button:hover,
.matchmaking-button:hover, .matchmaking-control-button:hover, .matchmaking-invite-button:hover,
.theme-button:hover {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 6px 18px rgba(0, 255, 255, 0.6), 0 0 8px rgba(128,255,255,0.6) inset;
background: linear-gradient(45deg, #0088bb, #00bbff); /* Brighter on hover */
}
.button:hover::before, .menu-button:hover::before, .sound-button:hover::before,
.save-button:hover::before, .item-button:hover::before, .challenge-button:hover::before,
.profile-button:hover::before, .matchmaking-button:hover::before, .matchmaking-control-button:hover::before,
.matchmaking-invite-button:hover::before, .theme-button:hover::before { left: 150%; }
.button:active, .menu-button:active, .sound-button:active, .save-button:active,
.item-button:active, .challenge-button:active, .profile-button:active,
.matchmaking-button:active, .matchmaking-control-button:active, .matchmaking-invite-button:active,
.theme-button:active { transform: translateY(0px) scale(0.98); }
.instructions {
background: rgba(0, 20, 40, 0.7); padding: 15px; border-radius: 10px;
margin-top: 20px; border: 1px solid rgba(0, 255, 255, 0.3);
font-size: clamp(12px, 2vw, 14px);
}
.controls {
position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%);
color: #7ff; font-size: clamp(10px, 1.8vw, 12px); text-align: center;
width: 90%; max-width: 450px; background: rgba(0, 20, 40, 0.8);
padding: 6px; border-radius: 20px; border: 1px solid rgba(0, 255, 255, 0.4);
z-index: 5;
}
.combo-meter {
position: absolute; top: 45%; left: 50%;
transform: translate(-50%, -50%) scale(0.8);
font-size: clamp(30px, 7vw, 45px); font-weight: bold; color: #ff0;
text-shadow: 0 0 10px #ff0, 0 0 20px #f90, 3px 3px 0px #a50;
opacity: 0; pointer-events: none; z-index: 4;
font-family: 'Press Start 2P', cursive;
transition: opacity 0.3s ease-out, transform 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
/* New UI Elements - Basic Styling */
.achievement-popup, .notification {
position: fixed; top: 20px; right: 20px;
background: rgba(10, 30, 50, 0.9); color: #fff;
padding: 15px 20px; border-radius: 10px; border: 2px solid #0ff;
box-shadow: 0 0 15px rgba(0, 255, 255, 0.5); font-family: 'Orbitron', sans-serif;
z-index: 100; opacity: 0; transform: translateX(100%);
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
width: auto; max-width: 300px;
}
.achievement-popup.show, .notification.show { opacity: 1; transform: translateX(0); }
.achievement-icon, .notification-icon { /* Shared icon style */
width: 30px; height: 30px; background: #0ff; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
float: left; margin-right: 10px; font-size: 16px; color: #036;
}
.achievement-title, .notification-title { font-weight: bold; font-size: 16px; margin-bottom: 5px; color: #0ff; }
.achievement-description, .notification-description { font-size: 12px; color: #ccc; }
.level-transition, .boss-intro, .boss-defeated, .matchmaking-result, .loading-screen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 5, 10, 0.9); display: flex; flex-direction: column; /* For text + bar */
align-items: center; justify-content: center;
font-family: 'Press Start 2P', cursive; font-size: clamp(30px, 6vw, 48px);
color: #0ff; z-index: 1000; opacity: 0; visibility: hidden;
transition: opacity 0.4s ease, visibility 0.4s;
}
.level-transition.active, .boss-intro.active, .boss-defeated.active,
.matchmaking-result.active, .loading-screen:not(.hidden) { opacity: 1; visibility: visible; }
.level-number, .boss-name-large, .boss-defeated-text, .matchmaking-result-text { animation: popIn 0.8s ease-in-out; }
@keyframes popIn {
0% { transform: scale(0.3) rotate(-15deg); opacity: 0; }
60% { transform: scale(1.1) rotate(5deg); opacity: 1; }
100% { transform: scale(1) rotate(0deg); opacity: 1; }
}
.health-bar { /* Player lives as health bar */
position: absolute; top: calc(var(--game-height) * 0.03); left: calc(var(--game-height) * 0.025);
width: clamp(100px, 20vw, 150px); height: calc(var(--game-height) * 0.02);
background: rgba(100, 0, 0, 0.5); border: 1px solid #f00;
border-radius: 5px; overflow: hidden; z-index: 3;
}
.health-fill {
height: 100%; width: 100%;
background: linear-gradient(to right, #ff3333, #ff9933);
border-radius: 4px 0 0 4px; /* Keep left radius */
transition: width 0.3s ease-in-out;
}
/* Particle Canvas - placeholder, assuming primary particles are on gameCanvas */
.particle-canvas { display: none; } /* Hide if not used for DOM particles */
.glow-border::before { /* Animated border glow */
content: ""; position: absolute; top: -3px; left: -3px; right: -3px; bottom: -3px;
border-radius: 14px; /* Slightly larger than game-container border-radius */
background: repeating-linear-gradient( 45deg,
rgba(0,255,255,0) 0%, rgba(0,255,255,0) 5%,
rgba(0,255,255,0.2) 10%, rgba(0,255,255,0.2) 15%,
rgba(0,255,255,0) 20%, rgba(0,255,255,0) 100%
);
background-size: 200% 200%;
animation: glowBorderMove 4s linear infinite;
pointer-events: none; z-index: 0;
}
@keyframes glowBorderMove {
0% { background-position: 0% 0%; }
100% { background-position: -200% -200%; }
}
.vignette {
position: absolute; top:0; left:0; width:100%; height:100%;
pointer-events:none; box-shadow: inset 0 0 100px rgba(0,0,0,0.6);
z-index:1;
}
.radial-glow { /* Subtle central glow */
position: absolute; top:0; left:0; width:100%; height:100%;
background: radial-gradient(ellipse at center, rgba(0,128,255,0.08) 0%, rgba(0,0,0,0) 70%);
pointer-events:none; z-index:0;
}
.water-effect { /* Subtle water surface reflection at bottom */
position: absolute; bottom:0; left:0; width:100%; height:15%;
background: linear-gradient(to top, rgba(0,100,150,0.15), transparent);
pointer-events:none; z-index:0; overflow: hidden;
}
.water-effect::after {
content:""; position: absolute; top: 0; left: -50%; width: 200%; height: 100%;
background: repeating-linear-gradient(0deg,
rgba(100,200,255,0.05) 0px, rgba(100,200,255,0.05) 1px,
transparent 1px, transparent 15px);
animation: waterRipple 8s linear infinite;
}
@keyframes waterRipple {
0% { transform: translateY(0%) translateX(0%); }
50% { transform: translateY(-10%) translateX(2%); }
100% { transform: translateY(0%) translateX(0%); }
}
.depth-meter {
position: absolute; top: calc(var(--game-height) * 0.1); right: 15px;
width: calc(var(--game-height) * 0.03); height: calc(var(--game-height) * 0.3);
background: rgba(0,20,40,0.7); border: 2px solid #0ff; border-radius: 10px;
overflow: hidden; z-index:3;
}
.depth-label { /* Remove from here, will add via JS if needed */ }
.depth-indicator {
position:absolute; bottom:0; left:0; width:100%; height:0%;
background: linear-gradient(to top, #00BFFF, #00FFFF);
transition: height 0.5s ease-out;
}
/* Ticks can be added via JS for precision */
.level-progress {
position: absolute; bottom: calc(var(--game-height) * 0.08); /* Match powerup indicator height */
left: 10px; width: clamp(100px, 18vw, 150px); height: calc(var(--game-height) * 0.025);
background: rgba(0,20,40,0.7); border: 1px solid #0ff; border-radius: 10px;
overflow: hidden; z-index:3;
}
.level-progress-bar {
height:100%; width:0%;
background: linear-gradient(to right, #00FFFF, #00BFFF);
border-radius: 9px 0 0 9px;
transition: width 0.3s ease-out;
}
.boss-health-container {
position: absolute; top: calc(var(--game-height) * 0.03); left: 50%;
transform: translateX(-50%); width: 60%; max-width: 400px;
height: calc(var(--game-height) * 0.025);
background: rgba(100,0,0,0.5); border: 2px solid #f00;
border-radius: 10px; z-index:3; display: none;
}
.boss-health-bar {
height:100%; width:100%;
background: linear-gradient(to right, #ff3333, #ff9933);
border-radius: 8px 0 0 8px;
transition: width 0.3s ease-out;
}
.boss-name {
position: absolute; top: calc(var(--game-height) * 0.03 + var(--game-height) * 0.025 + 5px); /* Below health bar */
left: 50%; transform: translateX(-50%);
color: #f33; font-size: clamp(12px, 2vw, 14px); font-weight: bold;
text-shadow: 0 0 5px #f00; z-index:3; display:none;
background-color: rgba(0,0,0,0.5); padding: 2px 8px; border-radius: 5px;
}
.tutorial { /* Popup tutorial message */
position: absolute; bottom: calc(var(--game-height) * 0.15); left: 50%;
transform: translateX(-50%); background: rgba(0,10,20,0.85);
padding: 10px 15px; border-radius: 8px; border: 1px solid #0ff;
font-size: clamp(12px, 2vw, 14px); color: #9ff; z-index:10;
opacity:0; transition: opacity 0.5s, transform 0.5s; pointer-events: none;
}
.tutorial.show { opacity:1; transform: translateX(-50%) translateY(-10px); }
.slider-container {
width: 60%; background: rgba(0,40,80,0.6); border-radius: 10px;
padding: 5px; border: 1px solid rgba(0,255,255,0.3);
}
input[type="range"].volume-slider { /* More specific selector */
width: 100%; height: 8px; background: rgba(0,255,255,0.2);
border-radius: 5px; outline: none; -webkit-appearance: none; appearance: none;
}
input[type="range"].volume-slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 18px; height: 18px; border-radius: 50%;
background: #0ff; cursor: pointer;
box-shadow: 0 0 5px #0ff;
}
input[type="range"].volume-slider::-moz-range-thumb {
width: 18px; height: 18px; border-radius: 50%;
background: #0ff; cursor: pointer; border: none;
box-shadow: 0 0 5px #0ff;
}
.loading-bar {
width: 60%; max-width: 300px; height: 15px;
background: rgba(0,40,80,0.7); border-radius: 10px;
margin-top: 20px; overflow:hidden; border: 1px solid #08a;
}
.loading-progress {
height:100%; width:0%;
background: linear-gradient(to right, #0ff, #0aa);
transition: width 0.2s ease-out; border-radius: 9px 0 0 9px;
}
/* Leaderboard Table styling */
.leaderboard-table { width: 100%; border-collapse: collapse; }
.leaderboard-table th, .leaderboard-table td {
padding: 8px; border-bottom: 1px solid rgba(0,255,255,0.2);
font-size: clamp(12px, 2vw, 14px);
}
.leaderboard-table th { background:rgba(0,40,80,0.6); text-transform: uppercase; }
.leaderboard-table tr:last-child td { border-bottom:none; }
.leaderboard-table tr:nth-child(even) { background-color: rgba(0,30,60,0.3); }
.leaderboard-table td:first-child { width: 10%; text-align: center; } /* Rank */
.leaderboard-table td:last-child { text-align: right; } /* Score */
/* Customization skin selector */
.skin-selector { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; justify-content: center;}
.skin {
width: 50px; height: 25px; /* represent paddle */
border-radius: 5px; display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.2s; border: 2px solid transparent;
}
.skin.selected { transform: scale(1.1); border-color: #f0f; box-shadow: 0 0 10px #f0f; }
/* Ensure buttons inside dialogs are also styled if not covered by general .button */
.dialog-box .button-group { display: flex; justify-content: center; gap: 10px; margin-top: 15px;}
/* Placeholder for features not implemented */
.placeholder-content p { margin-top: 15px; color: #789; font-style: italic; }
</style>
</head>
<body>
<div class="stars-bg"></div>
<div class="game-container">
<div class="glow-border"></div>
<div class="radial-glow"></div>
<canvas id="gameCanvas"></canvas> <!-- Main game canvas -->
<!-- <canvas id="particleCanvas" class="particle-canvas"></canvas> --> <!-- Optional: Separate particle canvas, if needed -->
<div class="ui">
<div id="playerHealthUI" style="display:none;">Health: <span id="healthDisplay"></span></div> <!-- Alternative lives display -->
<div class="health-bar" id="healthBarContainer"><div class="health-fill" id="healthFill"></div></div>
<div>Score: <span id="score">0</span></div>
<div style="display:none;">Lives: <span id="livesLegacy">3</span></div> <!-- Hidden legacy lives display -->
<div>Level: <span id="level">1</span>/<span id="maxLevels">10</span></div>
<div>Combo: <span id="combo">0x</span></div>
</div>
<div class="depth-meter"><div class="depth-indicator" id="depthIndicator"></div></div>
<div class="level-progress"><div class="level-progress-bar" id="levelProgressBar"></div></div>
<div class="boss-health-container" id="bossHealthContainer">
<div class="boss-health-bar" id="bossHealthBar"></div>
</div>
<div class="boss-name" id="bossName">BOSS NAME</div>
<div class="boss-warning" id="bossWarning">!!! BOSS APPROACHING !!!</div>
<div class="powerup-indicator" id="powerupIndicator"></div>
<!-- MAIN MENU / START SCREEN -->
<div class="dialog-box start-screen" id="mainMenu">
<h1>DEEP SEA BREAKOUT</h1>
<p>Navigate the treacherous depths, clear coral, and face menacing bosses. Collect power-ups to survive!</p>
<div class="button-group">
<button class="button" onclick="showDifficultySelect()">Start Game</button>
<button class="button" onclick="showScreen('settingsPanel')">Settings</button>
<button class="button" onclick="showScreen('leaderboardScreen')">Leaderboard</button>
</div>
<div class="button-group">
<button class="button" onclick="showScreen('tutorialScreen')">How to Play</button>
<button class="button" onclick="showScreen('statsScreen')">Player Stats</button>
<button class="button" onclick="showScreen('customizationScreen')">Customize</button>
</div>
<div class="button-group">
<button class="button" onclick="showScreen('achievementsScreen')">Achievements</button>
<button class="button" onclick="showScreen('creditsScreen')">Credits</button>
</div>
<div class="instructions" style="margin-top:15px; font-size: 12px;">
<p><strong>Quick Controls:</strong> Mouse: Move | Click: Launch/Laser | P: Pause | M: Mute</p>
</div>
</div>
<div class="dialog-box" id="difficultySelectScreen" style="display: none;">
<h2>Select Difficulty</h2>
<div class="button-group">
<button class="button" onclick="initGame('normal')">Normal</button>
<button class="button" onclick="initGame('hardcore')">Hardcore</button>
</div>
<button class="button" onclick="showScreen('mainMenu')" style="margin-top: 15px;">Back</button>
</div>
<div class="pause-message" id="pauseMessage">PAUSED</div>
<div class="combo-meter" id="comboMeter">0x COMBO!</div>
<div class="dialog-box" id="gameOver" style="display: none;">
<h2>MISSION FAILED</h2>
<p>Your score: <span id="finalScore">0</span></p>
<p id="highScoreText" style="color: #facc15;"></p>
<div class="button-group">
<button class="button" onclick="initGame(game.difficulty)">Try Again</button>
<button class="button" onclick="showScreen('mainMenu')">Main Menu</button>
</div>
</div>
<div class="dialog-box" id="gameWon" style="display: none;">
<h2>MISSION COMPLETE!</h2>
<p>You conquered all <span id="totalLevelsWon">10</span> ocean levels!</p>
<p>Final score: <span id="winScore">0</span></p>
<p id="winHighScoreText" style="color: #facc15;"></p>
<div class="button-group">
<button class="button" onclick="initGame(game.difficulty)">Play Again</button>
<button class="button" onclick="showScreen('mainMenu')">Main Menu</button>
</div>
</div>
<div class="controls" id="gameControls">
Mouse: Move โ€ข Click: Launch/Laser โ€ข P: Pause โ€ข M: Mute
</div>
<!-- New UI Popups/Screens -->
<div class="achievement-popup" id="achievementPopup">
<div class="achievement-icon" id="achievementPopupIcon">๐Ÿ†</div>
<div>
<div class="achievement-title" id="achievementPopupTitle">Achievement Unlocked!</div>
<div class="achievement-description" id="achievementPopupDesc">You did something awesome!</div>
</div>
</div>
<div class="notification" id="notificationPopup">
<div class="notification-icon" id="notificationPopupIcon">โ„น๏ธ</div>
<div>
<div class="notification-title" id="notificationPopupTitle">Notification</div>
<div class="notification-description" id="notificationPopupDesc">Something happened.</div>
</div>
</div>
<div class="level-transition" id="levelTransitionScreen">
<div class="level-number" id="levelTransitionNumber">LEVEL 1</div>
</div>
<div class="boss-intro" id="bossIntroScreen"><div class="boss-name-large" id="bossIntroName">BOSS NAME</div></div>
<div class="boss-defeated" id="bossDefeatedScreen"><div class="boss-defeated-text" id="bossDefeatedName">BOSS DEFEATED!</div></div>
<div class="tutorial" id="tutorialPopup"><span id="tutorialText">Move mouse to control paddle!</span></div>
<div class="tooltip" id="tooltip">Tooltip text</div>
<!-- Settings Panel -->
<div class="settings-panel" id="settingsPanel">
<h2 class="settings-header">Settings</h2>
<div class="setting-row">
<span class="setting-label">Master Volume:</span>
<div class="slider-container">
<input type="range" min="0" max="1" step="0.01" value="0.4" class="volume-slider" id="masterVolumeSlider">
</div>
</div>
<div class="setting-row">
<span class="setting-label">SFX Volume:</span>
<div class="slider-container">
<input type="range" min="0" max="1" step="0.01" value="0.5" class="volume-slider" id="sfxVolumeSlider">
</div>
</div>
<div class="setting-row">
<span class="setting-label">Visual Theme:</span>
<select id="themeSelector" class="button" style="padding: 5px 10px; text-transform: none;">
<option value="deepSea">Deep Sea</option>
<option value="volcanic">Volcanic Depths</option>
<option value="crystalCaverns">Crystal Caverns</option>
</select>
</div>
<div class="sound-test">
<button class="sound-button" onclick="audioManager.playSound(sounds.definitions.blockHit)">Test Hit</button>
<button class="sound-button" onclick="audioManager.playSound(sounds.definitions.powerUpCollect)">Test PU</button>
</div>
<button class="button" onclick="showScreen('mainMenu')" style="margin-top: 20px;">Back to Menu</button>
</div>
<!-- Leaderboard Screen -->
<div class="leaderboard" id="leaderboardScreen">
<h2 class="leaderboard-title">High Scores</h2>
<table class="leaderboard-table" id="leaderboardTable">
<thead><tr><th>Rank</th><th>Name</th><th>Score</th></tr></thead>
<tbody> <!-- Populated by JS --> </tbody>
</table>
<button class="button" onclick="showScreen('mainMenu')" style="margin-top: 20px;">Back to Menu</button>
</div>
<!-- Other Placeholder Screens -->
<div class="shop dialog-box" id="shopScreen">
<h2 class="shop-title">Item Shop</h2>
<div class="placeholder-content"><p>The shop is currently under construction. Check back later for awesome upgrades!</p></div>
<button class="button" onclick="showScreen('mainMenu')">Back</button>
</div>
<div class="daily-challenge dialog-box" id="dailyChallengeScreen">
<h2 class="daily-challenge-title">Daily Challenges</h2>
<div class="placeholder-content"><p>Today's challenges are being prepared by the sea creatures. Try again tomorrow!</p></div>
<button class="button" onclick="showScreen('mainMenu')">Back</button>
</div>
<div class="achievement-system dialog-box" id="achievementsScreen">
<h2 class="achievement-system-title">Achievements</h2>
<div id="achievementsListContainer" style="max-height: 300px; overflow-y: auto; text-align: left;">
<!-- Achievements will be populated here by JS -->
</div>
<button class="button" onclick="showScreen('mainMenu')" style="margin-top: 20px;">Back</button>
</div>
<div class="tutorial-screen dialog-box" id="tutorialScreenContent"> <!-- Renamed to avoid conflict -->
<h2 class="tutorial-screen-title">How To Play</h2>
<div style="text-align: left; font-size: clamp(12px, 2vw, 14px);">
<p><strong>Objective:</strong> Clear all blocks (coral creatures) on each level by hitting them with the pearl.</p>
<p><strong>Controls:</strong></p>
<ul>
<li>- Move your mouse to control the paddle at the bottom.</li>
<li>- Click the left mouse button to launch the pearl from the paddle.</li>
<li>- Press 'P' to pause or resume the game.</li>
<li>- Press 'M' to mute or unmute all game sounds.</li>
</ul>
<p><strong>Gameplay:</strong></p>
<ul>
<li>- Don't let the primary pearl fall below your paddle, or you'll lose a life!</li>
<li>- Some blocks require multiple hits. Some creatures are dangerous!</li>
<li>- Collect falling power-ups for special abilities. Rare power-ups are extra potent!</li>
<li>- Build combos by hitting blocks consecutively for bonus points.</li>
<li>- Defeat bosses on special levels to progress further into the abyss.</li>
</ul>
<p>Good luck, diver!</p>
</div>
<button class="button" onclick="showScreen('mainMenu')" style="margin-top: 20px;">Back</button>
</div>
<div class="stats-screen dialog-box" id="statsScreen">
<h2 class="stats-screen-title">Player Statistics</h2>
<div id="statsContainer" style="text-align: left;">
<!-- Stats populated by JS -->
</div>
<button class="button" onclick="showScreen('mainMenu')" style="margin-top: 20px;">Back</button>
</div>
<div class="credits dialog-box" id="creditsScreen">
<h2 class="credits-title">Credits</h2>
<p>Game Design & Development: You & AI</p>
<p>Inspiration: Classic Breakout Games</p>
<p>Fonts: Orbitron, Press Start 2P (via Google Fonts)</p>
<div class="placeholder-content"><p>Thanks for playing!</p></div>
<button class="button" onclick="showScreen('mainMenu')">Back</button>
</div>
<div class="customization dialog-box" id="customizationScreen">
<h2 class="customization-title">Customize Paddle</h2>
<p>Select your paddle's appearance:</p>
<div class="skin-selector" id="paddleSkinSelector">
<!-- Skins populated by JS -->
</div>
<button class="button" onclick="showScreen('mainMenu')" style="margin-top: 20px;">Back</button>
</div>
<!-- Complex features - placeholders -->
<div class="profile dialog-box" id="profileScreen">
<h2 class="profile-title">Player Profile</h2>
<div class="placeholder-content"><p>Profile features coming soon! Track your overall progress and customize your diver identity.</p></div>
<button class="button" onclick="showScreen('mainMenu')">Back</button>
</div>
<div class="multiplayer-info dialog-box" id="multiplayerScreen">
<h2 class="multiplayer-title">Multiplayer</h2>
<div class="placeholder-content"><p>Multiplayer modes are planned for a future update requiring server support. Stay tuned!</p></div>
<button class="button" onclick="showScreen('mainMenu')">Back</button>
</div>
<div class="matchmaking dialog-box" id="matchmakingScreen">
<h2 class="matchmaking-title">Matchmaking</h2>
<div class="placeholder-content"><p>Online matchmaking is a future goal! This feature will require server infrastructure.</p></div>
<button class="button" onclick="showScreen('mainMenu')">Back</button>
</div>
<div class="loading-screen" id="loadingScreen">
<div id="loadingText">LOADING...</div>
<div class="loading-bar"><div class="loading-progress" id="loadingProgress"></div></div>
</div>
<div class="vignette"></div>
<div class="water-effect"></div>
</div>
<script>
// --- Game Setup ---
const gameContainer = document.querySelector('.game-container');
const canvas = document.getElementById('gameCanvas');
// Dynamically set canvas size from CSS variables or defaults
const GAME_WIDTH = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--game-width')) || 800;
const GAME_HEIGHT = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--game-height')) || 600;
gameContainer.style.width = `${GAME_WIDTH}px`;
gameContainer.style.height = `${GAME_HEIGHT}px`;
canvas.width = GAME_WIDTH;
canvas.height = GAME_HEIGHT;
const ctx = canvas.getContext('2d');
// --- Chroma.js Basic Fallback ---
const chroma = window.chroma || {
contrast: (c1, c2) => Math.abs( (c1.r + c1.g + c1.b) - (c2.r + c2.g + c2.b) ) > 128 ? 21 : 1, // Simplified contrast
(colorStr) => {
let r=0,g=0,b=0;
if (colorStr.startsWith('#')) {
const hex = colorStr.slice(1);
r = parseInt(hex.substring(0,2),16);
g = parseInt(hex.substring(2,4),16);
b = parseInt(hex.substring(4,6),16);
} // Rudimentary parsing
return {
r,g,b,
darken: (amount = 1) => `rgb(${Math.max(0,r-50*amount)}, ${Math.max(0,g-50*amount)}, ${Math.max(0,b-50*amount)})`,
brighten: (amount = 1) => `rgb(${Math.min(255,r+50*amount)}, ${Math.min(255,g+50*amount)}, ${Math.min(255,b+50*amount)})`,
alpha: (a) => `rgba(${r},${g},${b},${a})`,
hex: () => colorStr, // Assuming it was a hex
css: () => colorStr
}
}
};
// --- Audio Manager ---
class AudioManager {
constructor() {
this.audioContext = null;
this.masterGain = null;
this.sfxGain = null;
this.isMuted = false;
this.sfxDefinitions = { // Store sound parameters
blockHit: { freq1: 600, freq2: 300, duration: 0.05, volume: 0.15, type: 'triangle' },
paddleHit: { freq1: 440, freq2: 440, duration: 0.05, volume: 0.2, type: 'square' },
wallHit: { freq1: 200, freq2: 150, duration: 0.05, volume: 0.1, type: 'sawtooth' }, // Pan will be added
loseLife: { freq1: 300, freq2: 50, duration: 0.5, volume: 0.3, type: 'sawtooth' },
levelUp: [ // Array for multiple sounds
{ freq1: 500, freq2: 1000, duration: 0.3, volume: 0.25, type: 'sine', attack: 0.05, decay: 0.3 },
{ freq1: 300, freq2: 600, duration: 0.3, volume: 0.15, type: 'square', attack: 0.05, decay: 0.3, delay: 0.05 }
],
powerUpSpawn: { freq1: 700, freq2: 900, duration: 0.1, volume: 0.2, type: 'sine' },
powerUpCollect: { freq1: 800, freq2: 1200, duration: 0.2, volume: 0.3, type: 'triangle' },
rarePowerUpCollect: [
{ freq1: 1000, freq2: 2000, duration: 0.4, volume: 0.4, type: 'sine', decay: 0.4 },
{ freq1: 800, freq2: 1500, duration: 0.3, volume: 0.3, type: 'square', decay: 0.3, delay: 0.1 }
],
combo: (comboCount) => ({ // Dynamic sound definition
freq1: Math.min(1200, 300 + (comboCount * 25)),
freq2: Math.min(1350, 450 + (comboCount * 25)),
duration: 0.15,
volume: Math.min(0.4, 0.15 + (comboCount * 0.02)),
type: 'sine'
}),
gameOver: [
{ freq1: 300, freq2: 100, duration: 1.5, volume: 0.4, type: 'sine' },
{ freq1: 200, freq2: 50, duration: 1.0, volume: 0.3, type: 'square', delay: 0.2 }
],
gameWon: [
{ freq1: 500, freq2: 1200, duration: 1.0, volume: 0.4, type: 'sine' },
{ freq1: 800, freq2: 1000, duration: 0.2, volume: 0.25, type: 'triangle', delay: 0.2 },
{ freq1: 900, freq2: 1100, duration: 0.2, volume: 0.25, type: 'triangle', delay: 0.4 },
{ freq1: 1000, freq2: 1200, duration: 0.2, volume: 0.25, type: 'triangle', delay: 0.6 }
],
laserFire: { freq1: 1800, freq2: 1000, duration: 0.08, volume: 0.08, type: 'sawtooth', attack:0.005, decay: 0.04 },
blackHoleOpen: { freq1: 100, freq2: 30, duration: 1.5, volume: 0.5, type: 'sawtooth', decay: 1.5 },
blackHoleAbsorb: { freq1: 200, freq2: 400, duration: 0.05, volume: 0.2, type: 'noise', decay: 0.1 }, // Noise type needs browser support
bossHit: { freq1: 250, freq2: 150, duration: 0.2, volume: 0.35, type: 'square' },
bossDefeat: [
{ freq1: 100, freq2: 50, duration: 1.0, volume: 0.5, type: 'sawtooth' }, // Rumble
{ freq1: 1000, freq2: 400, duration: 0.8, volume: 0.4, type: 'sine', delay: 0.2 } // Falling tone
],
achievement: { freq1: 1200, freq2: 1800, duration: 0.3, volume: 0.3, type: 'triangle', attack: 0.02, decay: 0.2 }
};
}
async init() {
if (!this.audioContext) {
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.audioContext.createGain();
this.sfxGain = this.audioContext.createGain();
this.sfxGain.connect(this.masterGain);
this.masterGain.connect(this.audioContext.destination);
this.setVolume('master', parseFloat(localStorage.getItem('masterVolume') || '0.4'));
this.setVolume('sfx', parseFloat(localStorage.getItem('sfxVolume') || '0.5'));
} catch (e) { console.error("Web Audio API init failed.", e); return; }
}
if (this.audioContext.state === 'suspended') await this.audioContext.resume();
}
setVolume(type, value) {
const gainNode = type === 'master' ? this.masterGain : this.sfxGain;
if (gainNode) {
gainNode.gain.setValueAtTime(this.isMuted && type === 'master' ? 0 : value, this.audioContext.currentTime);
localStorage.setItem(`${type}Volume`, value);
}
if (type === 'master' && document.getElementById('masterVolumeSlider')) document.getElementById('masterVolumeSlider').value = value;
if (type === 'sfx' && document.getElementById('sfxVolumeSlider')) document.getElementById('sfxVolumeSlider').value = value;
}
playSound(soundDef, dynamicParams = {}) { // soundDef can be an object or a function returning an object
if (this.isMuted || !this.audioContext || this.audioContext.state !== 'running' || !soundDef) return;
const playSingleSound = (params) => {
const { freq1, freq2, duration, volume = 0.3, type = 'sine', attack = 0.01, decay = 0.1, pan = 0, delay = 0 } = params;
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
const panner = this.audioContext.createStereoPanner();
oscillator.connect(gainNode); gainNode.connect(panner); panner.connect(this.sfxGain); // Connect to SFX gain
oscillator.type = type;
panner.pan.setValueAtTime(pan, this.audioContext.currentTime + delay);
oscillator.frequency.setValueAtTime(freq1, this.audioContext.currentTime + delay);
if (freq2 && freq1 !== freq2) {
oscillator.frequency.exponentialRampToValueAtTime(freq2, this.audioContext.currentTime + delay + duration);
}
gainNode.gain.setValueAtTime(0, this.audioContext.currentTime + delay);
gainNode.gain.linearRampToValueAtTime(volume, this.audioContext.currentTime + delay + attack);
gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + delay + duration + decay);
oscillator.start(this.audioContext.currentTime + delay);
oscillator.stop(this.audioContext.currentTime + delay + duration + decay + 0.05);
};
let effectiveSoundDef = typeof soundDef === 'function' ? soundDef(dynamicParams) : soundDef;
if (Array.isArray(effectiveSoundDef)) {
effectiveSoundDef.forEach(params => playSingleSound({...params, ...dynamicParams}));
} else {
playSingleSound({...effectiveSoundDef, ...dynamicParams});
}
}
toggleMute() {
if (!this.masterGain) return this.isMuted;
this.isMuted = !this.isMuted;
const currentMasterVolume = parseFloat(localStorage.getItem('masterVolume') || '0.4');
this.masterGain.gain.setValueAtTime(this.isMuted ? 0 : currentMasterVolume, this.audioContext.currentTime);
return this.isMuted;
}
get definitions() { return this.sfxDefinitions; }
}
const audioManager = new AudioManager();
// --- Game Constants ---
const PADDLE_DEFAULT_WIDTH = GAME_WIDTH * 0.13;
const PADDLE_HEIGHT = GAME_HEIGHT * 0.025;
const BALL_RADIUS = GAME_WIDTH * 0.01;
const BALL_INITIAL_SPEED = GAME_HEIGHT * 0.0085;
const POWERUP_DURATION = 9000;
const RARE_POWERUP_DURATION_MULTIPLIER = 1.4;
const POWERUP_DROP_CHANCE = 0.20;
const RARE_POWERUP_CHANCE = 0.10;
const COMBO_TIMEOUT = 2200;
const MAX_LEVELS = 10; // Normal game max levels
const BOSS_LEVELS = [5, MAX_LEVELS]; // Levels where bosses appear
const POWERUP_TYPES = {
WIDE_PADDLE: 'WIDE_PADDLE', STICKY_PADDLE: 'STICKY_PADDLE', PIERCING_BALL: 'PIERCING_BALL',
LASER_PADDLE: 'LASER_PADDLE', MULTI_BALL: 'MULTI_BALL', SLOW_MO: 'SLOW_MO', SHIELD: 'SHIELD' // New: Shield
};
const RARE_POWERUP_TYPES = {
INVINCIBILITY: 'INVINCIBILITY', TIME_STOP: 'TIME_STOP', BLACK_HOLE: 'BLACK_HOLE', PENTA_BALL: 'PENTA_BALL',
MEGA_LASER: 'MEGA_LASER', BLOCK_BOMB: 'BLOCK_BOMB' // New: Mega Laser, Block Bomb
};
const ALL_POWERUP_TYPES = {...POWERUP_TYPES, ...RARE_POWERUP_TYPES};
const BLOCK_CREATURES = ['๐Ÿ ', '๐ŸŸ', '๐Ÿก', '๐Ÿข', '๐Ÿฆ€', '๐Ÿฆž', '๐Ÿฆ', '๐Ÿš', 'โญ', '๐ŸŒŠ', '๐Ÿซง'];
const DANGEROUS_CREATURES = ['๐Ÿฆˆ', '๐Ÿ™', '๐Ÿฆ‘', '๐ŸŠ', '๐Ÿก']; // Pufferfish can be dangerous
const BOSS_CREATURES = {5: '๐Ÿ™๐Ÿ‘‘', 10: '๐Ÿฒ๐ŸŒŒ'}; // Kraken King, Cosmic Dragon
const PADDLE_SKINS = [
{ id: 'default', name: 'Classic Blue', color1: '#30A8F8', color2: '#0077cc', outline: '#60C8FF' },
{ id: 'ember', name: 'Ember Glow', color1: '#ff7e5f', color2: '#feb47b', outline: '#ffcf9f' },
{ id: 'forest', name: 'Forest Gem', color1: '#00b09b', color2: '#96c93d', outline: '#c0ff7d' },
{ id: 'void', name: 'Void Walker', color1: '#4A00E0', color2: '#8E2DE2', outline: '#C675FF' },
{ id: 'gold', name: 'Golden Pearl', color1: '#FFD700', color2: '#FFA500', outline: '#FFEEAA' },
];
// Game state
let game = {
score: 0, highScore: parseInt(localStorage.getItem('deepSeaBreakoutHighScore_v2') || '0'),
lives: 3, maxLives: 3, level: 1, running: false, paused: false, difficulty: 'normal',
initialBlockCount: 0,
particles: [], backgroundFish: [], backgroundBubbles: [],
powerUpsOnScreen: [], activePowerUps: {},
combo: 0, lastHitTime: 0,
balls: [], lasers: [], bubbleTexts: [], blackHole: null,
currentBackgroundHue: 180, currentTheme: 'deepSea',
currentBoss: null,
achievements: {}, playerStats: {},
paddleSkin: PADDLE_SKINS[0], // Default skin
levelTransitioning: false,
gameTime: 0, // total time played this session
currentScreen: 'loadingScreen' // Tracks the currently visible dialog/screen
};
let paddle = {
x: GAME_WIDTH / 2 - PADDLE_DEFAULT_WIDTH / 2,
y: GAME_HEIGHT - PADDLE_HEIGHT * 2.5,
width: PADDLE_DEFAULT_WIDTH, height: PADDLE_HEIGHT,
targetX: GAME_WIDTH / 2 - PADDLE_DEFAULT_WIDTH / 2
};
let blocks = [];
let mouseX = GAME_WIDTH / 2;
const blockColors = [ /* More vibrant and distinct */
'#FF6347', '#FFD700', '#ADFF2F', '#00CED1', '#1E90FF', '#9370DB',
'#FF69B4', '#FFA07A', '#20B2AA', '#7FFF00', '#BA55D3', '#DA70D6',
'#6495ED', '#FF4500', '#32CD32', '#8A2BE2'
];
// --- UI Management ---
function showScreen(screenId) {
// Hide all dialogs first
document.querySelectorAll('.dialog-box, .menu, .settings-panel, .leaderboard, .shop, .daily-challenge, .achievement-system, .tutorial-screen, .stats-screen, .credits, .customization, .profile, .multiplayer-info, .matchmaking, .difficultySelectScreen').forEach(el => el.style.display = 'none');
const screenElement = document.getElementById(screenId);
if (screenElement) {
screenElement.style.display = 'flex'; // Use flex for dialog-box centering
game.currentScreen = screenId;
if (screenId === 'leaderboardScreen') updateLeaderboardDisplay();
if (screenId === 'statsScreen') updateStatsDisplay();
if (screenId === 'achievementsScreen') updateAchievementsDisplay();
} else {
console.warn(`Screen with ID ${screenId} not found.`);
document.getElementById('mainMenu').style.display = 'flex'; // Fallback to main menu
game.currentScreen = 'mainMenu';
}
}
function showNotification(title, description, icon = 'โ„น๏ธ', duration = 3000) {
const popup = document.getElementById('notificationPopup');
document.getElementById('notificationPopupTitle').textContent = title;
document.getElementById('notificationPopupDesc').textContent = description;
document.getElementById('notificationPopupIcon').textContent = icon;
popup.classList.add('show');
setTimeout(() => popup.classList.remove('show'), duration);
}
function showAchievementPopup(achievement) {
const popup = document.getElementById('achievementPopup');
document.getElementById('achievementPopupTitle').textContent = achievement.name;
document.getElementById('achievementPopupDesc').textContent = achievement.description;
document.getElementById('achievementPopupIcon').textContent = achievement.icon;
popup.classList.add('show');
audioManager.playSound(audioManager.definitions.achievement);
setTimeout(() => popup.classList.remove('show'), 4000);
}
// --- Initialization ---
function initPrimaryBall() {
const speedMultiplier = game.difficulty === 'hardcore' ? 1.3 : 1;
const baseSpeed = BALL_INITIAL_SPEED * speedMultiplier;
game.balls = [{
x: paddle.x + paddle.width / 2, y: paddle.y - BALL_RADIUS * 1.5,
dx: 0, dy: 0, radius: BALL_RADIUS,
speed: baseSpeed + (game.level - 1) * (GAME_HEIGHT * 0.0004) * speedMultiplier,
attached: true, piercing: false, isPrimary: true, trail: [],
color1: '#ffffff', color2: '#99ffff', color3: '#0099ff' // Default ball colors
}];
}
function createBackgroundElements() { /* (Content largely same, ensure scaling) */
if (game.backgroundFish.length === 0) {
for (let i = 0; i < 15; i++) game.backgroundFish.push({
x: Math.random() * GAME_WIDTH, y: Math.random() * GAME_HEIGHT,
dx: (Math.random() - 0.5) * (GAME_WIDTH * 0.001), dy: (Math.random() - 0.5) * (GAME_HEIGHT * 0.0005),
size: Math.random() * (GAME_WIDTH * 0.02) + (GAME_WIDTH * 0.015),
creature: ['๐Ÿ ','๐ŸŸ','๐Ÿก'][Math.floor(Math.random()*3)], alpha: Math.random()*0.2+0.05, flipX: Math.random()>0.5
});
}
if (game.backgroundBubbles.length === 0) {
for (let i = 0; i < 40; i++) {
let b = {
r: Math.random()*(GAME_WIDTH*0.0035)+(GAME_WIDTH*0.001), dy:-(Math.random()*(GAME_HEIGHT*0.0008)+(GAME_HEIGHT*0.0002)),
a: Math.random()*0.3+0.1, amp: Math.random()*(GAME_WIDTH*0.015)+(GAME_WIDTH*0.004), freq: Math.random()*0.05+0.01
};
b.x = Math.random()*GAME_WIDTH; b.y = GAME_HEIGHT+Math.random()*GAME_HEIGHT+b.r; b.ix = b.x; game.backgroundBubbles.push(b);
}
}
}
function createBlocks() {
blocks = [];
if (game.currentBoss) return; // No normal blocks during boss fight
const baseRows = game.difficulty === 'hardcore' ? 4 : 3;
const rows = Math.min(10, baseRows + Math.floor(game.level * (game.difficulty === 'hardcore' ? 0.9 : 0.7)));
const cols = game.difficulty === 'hardcore' ? 14 : 12;
const totalBlockAreaWidth = GAME_WIDTH * 0.95;
const sideMargin = (GAME_WIDTH - totalBlockAreaWidth) / 2;
const blockWidth = (totalBlockAreaWidth / cols) * 0.92; // Smaller gap
const blockGap = (totalBlockAreaWidth / cols) * 0.08;
const blockHeight = GAME_HEIGHT * 0.04;
const topMargin = GAME_HEIGHT * 0.12;
const dangerousBlockChance = Math.min(0.25, game.level * 0.025 * (game.difficulty === 'hardcore' ? 1.6 : 1.1));
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (Math.random() < 0.08 && game.level > 2) continue;
const isDangerous = Math.random() < dangerousBlockChance && game.level > 1;
let maxHits = 1;
if (game.difficulty === 'hardcore') maxHits = r < 2 ? 3 : (r < 5 ? 2 : 1);
else maxHits = r < 1 ? 2 : 1;
if (isDangerous) maxHits = Math.min(4, maxHits + 1);
maxHits = Math.min(5, maxHits + Math.floor(game.level / 2.5));
blocks.push({
x: sideMargin + c * (blockWidth + blockGap), y: topMargin + r * (blockHeight + blockGap * 0.7),
width: blockWidth, height: blockHeight,
color: blockColors[(r * cols + c + game.level) % blockColors.length], // Vary color with level
hits: maxHits, maxHits: maxHits,
creature: isDangerous ? DANGEROUS_CREATURES[Math.floor(Math.random() * DANGEROUS_CREATURES.length)] : BLOCK_CREATURES[Math.floor(Math.random() * BLOCK_CREATURES.length)],
isDangerous: isDangerous, animationOffset: Math.random() * Math.PI * 2,
id: `block-${r}-${c}` // Unique ID
});
}
}
game.initialBlockCount = blocks.length;
updateLevelProgressUI();
}
function createBoss(level) {
const bossCreature = BOSS_CREATURES[level] || '๐Ÿ˜ˆ';
let bossHP, bossWidth, bossHeight, bossSpeed, bossAttackPattern;
if (level === BOSS_LEVELS[0]) { // Example: Kraken Boss
bossHP = 50 + 20 * (game.difficulty === 'hardcore' ? 2 : 1);
bossWidth = GAME_WIDTH * 0.3;
bossHeight = GAME_HEIGHT * 0.15;
bossSpeed = GAME_WIDTH * 0.0005;
bossAttackPattern = 'tentacleSwipe'; // Placeholder for attack type
} else if (level === BOSS_LEVELS[1]) { // Example: Cosmic Dragon
bossHP = 80 + 30 * (game.difficulty === 'hardcore' ? 2 : 1);
bossWidth = GAME_WIDTH * 0.4;
bossHeight = GAME_HEIGHT * 0.2;
bossSpeed = GAME_WIDTH * 0.0003;
bossAttackPattern = 'energyBeam';
} else { return null; } // No boss for this level
game.currentBoss = {
x: GAME_WIDTH / 2 - bossWidth / 2, y: GAME_HEIGHT * 0.1,
width: bossWidth, height: bossHeight,
hp: bossHP, maxHp: bossHP,
creature: bossCreature, color: '#FF0055',
dx: bossSpeed, vulnerableTime: 0, lastAttackTime: 0,
attackPattern: bossAttackPattern,
animationOffset: Math.random() * Math.PI * 2
};
blocks = []; // Clear normal blocks
document.getElementById('bossHealthContainer').style.display = 'block';
document.getElementById('bossName').textContent = bossCreature;
document.getElementById('bossName').style.display = 'block';
updateBossHealthUI();
}
// --- Achievements ---
function defineAchievements() {
game.achievements = {
FIRST_BREAK: { name: "Icebreaker", description: "Destroy your first block.", unlocked: false, icon: '๐Ÿ’ฅ' },
LEVEL_3: { name: "Deep Diver", description: "Reach Level 3.", unlocked: false, icon: '๐ŸŒŠ' },
LEVEL_5_BOSS: { name: "Kraken Guard Defeated", description: "Defeat the boss of Level 5.", unlocked: false, icon: '๐Ÿ™' },
GAME_WON: { name: "Abyssal Conqueror", description: `Clear all ${MAX_LEVELS} levels.`, unlocked: false, icon: '๐Ÿ‘‘' },
HIGH_SCORE_10K: { name: "Coral Collector", description: "Achieve a score of 10,000.", unlocked: false, icon: '๐Ÿ’ฐ' },
POWERUP_MASTER: { name: "Power Overwhelming", description: "Collect 5 power-ups in a single game.", unlocked: false, icon: 'โšก' },
COMBO_15X: { name: "Combo King", description: "Achieve a 15x combo.", unlocked: false, icon: 'โœจ' },
NO_LIVES_LOST_LEVEL: { name: "Flawless Victory (Level)", description: "Clear a level without losing a life.", unlocked: false, icon: '๐Ÿ›ก๏ธ' },
};
// Load unlocked achievements
const savedAchievements = JSON.parse(localStorage.getItem('deepSeaBreakoutAchievements_v2') || '{}');
for (const key in game.achievements) {
if (savedAchievements[key]) game.achievements[key].unlocked = true;
}
}
function unlockAchievement(id) {
if (game.achievements[id] && !game.achievements[id].unlocked) {
game.achievements[id].unlocked = true;
showAchievementPopup(game.achievements[id]);
localStorage.setItem('deepSeaBreakoutAchievements_v2', JSON.stringify(game.achievements));
// Potentially give a small score bonus or other reward
game.score += 500;
addBubbleText(`+500 (Achievement!)`, GAME_WIDTH/2, GAME_HEIGHT/2, '#FFD700', 18);
}
}
// --- Player Stats ---
function initPlayerStats() {
game.playerStats = JSON.parse(localStorage.getItem('deepSeaBreakoutStats_v2') || JSON.stringify({
gamesPlayed: 0, totalScore: 0, totalBlocksBroken: 0, totalPowerupsCollected: 0,
totalGameTime: 0, highestLevelReached: 0, bossesDefeated: 0
}));
}
function updatePlayerStat(stat, value) {
if (game.playerStats.hasOwnProperty(stat)) {
game.playerStats[stat] = (game.playerStats[stat] || 0) + value;
} else {
game.playerStats[stat] = value;
}
}
function savePlayerStats() {
localStorage.setItem('deepSeaBreakoutStats_v2', JSON.stringify(game.playerStats));
}
// --- Particle & Effect Updaters / Drawers (simplified for brevity, assume previous implementation largely ok) ---
function createEffectParticle(x, y, color, count = 8, options = {}) {
const { sizeRange = [1.5,3.5], speedRange=[3,6], lifeRange=[30,40], gravity=0.08, glow=false, upwardBias=-2 } = options;
for (let i = 0; i < count; i++) {
const angle = Math.random()*Math.PI*2; const speed = Math.random()*(speedRange[1]-speedRange[0])+speedRange[0];
game.particles.push({
x,y, dx:Math.cos(angle)*speed*(GAME_WIDTH/800), dy:Math.sin(angle)*speed*(GAME_HEIGHT/600)+upwardBias*(GAME_HEIGHT/600),
life:Math.random()*(lifeRange[1]-lifeRange[0])+lifeRange[0],maxLife:lifeRange[1],color,
size:(Math.random()*(sizeRange[1]-sizeRange[0])+sizeRange[0])*(GAME_WIDTH/800), glow,gravity
});
}
}
function updateParticles() { /* As before */
for(let i=game.particles.length-1;i>=0;i--){let p=game.particles[i];p.x+=p.dx;p.y+=p.dy;p.life--;p.dy+=p.gravity;p.dx*=0.98;if(p.life<=0)game.particles.splice(i,1);}
}
function drawParticles() { /* As before */
game.particles.forEach(p=>{let a=Math.pow(p.life/p.maxLife,1.5);ctx.save();ctx.globalAlpha=a;if(p.glow){let g=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,p.size*a);g.addColorStop(0,p.color);g.addColorStop(1,`${p.color}00`);ctx.fillStyle=g;}else{ctx.fillStyle=p.color;}ctx.beginPath();ctx.arc(p.x,p.y,p.size*a,0,Math.PI*2);ctx.fill();ctx.restore();});
}
function createBallTrailParticles() { /* As before */
if(!game.paused&&game.running)game.balls.forEach(b=>{if(!b.attached){b.trail.push({x:b.x,y:b.y,r:b.radius*(game.activePowerUps[ALL_POWERUP_TYPES.PIERCING_BALL]||b.piercing?0.7:0.5)});if(b.trail.length>10)b.trail.shift();}else{b.trail=[];}});
}
function drawBallTrails() { /* As before */
game.balls.forEach(b=>{if(b.trail.length>1){ctx.save();let p=game.activePowerUps[ALL_POWERUP_TYPES.PIERCING_BALL]||b.piercing;let c=p?[255,0,255]:(b.isPrimary?[0,200,255]:[200,200,200]);for(let i=0;i<b.trail.length;i++){let p=b.trail[i],a=(i/b.trail.length)*0.4,r=p.r*(i/b.trail.length);ctx.fillStyle=`rgba(${c[0]},${c[1]},${c[2]},${a})`;ctx.beginPath();ctx.arc(p.x,p.y,r,0,Math.PI*2);ctx.fill();}ctx.restore();}});
}
function updateBackgroundElements() { /* As before, ensure scaling */
game.backgroundFish.forEach(f=>{f.x+=f.dx;f.y+=f.dy;if(f.x>GAME_WIDTH+f.size)f.x=-f.size;if(f.x<-f.size)f.x=GAME_WIDTH+f.size;if(f.y>GAME_HEIGHT+f.size)f.y=-f.size;if(f.y<-f.size)f.y=GAME_HEIGHT+f.size;if(Math.random()<0.005){f.dx=(Math.random()-0.5)*(GAME_WIDTH*0.001);f.dy=(Math.random()-0.5)*(GAME_HEIGHT*0.0005);f.flipX=f.dx>0;}});
game.backgroundBubbles.forEach(b=>{b.y+=b.dy;b.x=b.ix+Math.sin(b.y*b.freq)*b.amp;if(b.y+b.r<0){b.y=GAME_HEIGHT+b.r+Math.random()*GAME_HEIGHT*0.5;b.ix=Math.random()*GAME_WIDTH;b.x=b.ix;b.a=Math.random()*0.3+0.1;}});
}
function drawBackgroundElements() { /* As before with dynamic background */
const targetHue=game.currentTheme==='volcanic'?20:(game.currentTheme==='crystalCaverns'?260:180+(game.level*4));game.currentBackgroundHue+=(targetHue-game.currentBackgroundHue)*0.01;
const grad=ctx.createLinearGradient(0,0,0,GAME_HEIGHT);
grad.addColorStop(0,`hsla(${game.currentBackgroundHue},70%,${game.currentTheme==='volcanic'?15:20}%,0.9)`);
grad.addColorStop(0.7,`hsla(${game.currentBackgroundHue+10},70%,${game.currentTheme==='volcanic'?8:10}%,1)`);
grad.addColorStop(1,`hsla(${game.currentBackgroundHue+20},70%,${game.currentTheme==='volcanic'?3:5}%,1)`);
ctx.fillStyle=grad;ctx.fillRect(0,0,GAME_WIDTH,GAME_HEIGHT);
game.backgroundFish.forEach(f=>{ctx.save();ctx.globalAlpha=f.alpha;ctx.font=`${f.size}px Arial`;ctx.textAlign='center';if(f.flipX){ctx.scale(-1,1);ctx.fillText(f.creature,-f.x,f.y);}else{ctx.fillText(f.creature,f.x,f.y);}ctx.restore();});
game.backgroundBubbles.forEach(b=>{ctx.save();ctx.globalAlpha=b.a;ctx.fillStyle='rgba(173,216,230,0.4)';ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,Math.PI*2);ctx.fill();ctx.fillStyle='rgba(255,255,255,0.6)';ctx.beginPath();ctx.arc(b.x-b.r*0.3,b.y-b.r*0.3,b.r*0.4,0,Math.PI*2);ctx.fill();ctx.restore();});
}
function drawBubbleTexts() { /* As before */
for(let i=game.bubbleTexts.length-1;i>=0;i--){let b=game.bubbleTexts[i];ctx.save();ctx.globalAlpha=b.alpha;ctx.font=`bold ${b.size}px 'Orbitron',sans-serif`;ctx.textAlign='center';ctx.fillStyle=b.color;ctx.shadowColor='rgba(0,0,0,0.7)';ctx.shadowBlur=3;ctx.shadowOffsetX=1;ctx.shadowOffsetY=1;ctx.fillText(b.text,b.x,b.y);ctx.restore();b.y-=1*(GAME_HEIGHT/600);b.alpha=Math.max(0,b.life/60);b.size*=0.995;b.life--;if(b.life<=0)game.bubbleTexts.splice(i,1);}
}
// --- Main Drawing Functions ---
function drawPaddle() {
const { x, y, width, height } = paddle;
const skin = game.paddleSkin;
const cornerRadius = height / 2;
ctx.save();
ctx.shadowColor = skin.outline; ctx.shadowBlur = 15 + Math.sin(Date.now()/200)*3; // Pulsing glow
ctx.fillStyle = chroma(skin.outline).alpha(0.15).css();
ctx.beginPath(); ctx.roundRect(x - 5, y - 5, width + 10, height + 10, cornerRadius + 5); ctx.fill();
ctx.restore();
const gradient = ctx.createLinearGradient(x, y, x, y + height);
gradient.addColorStop(0, skin.color1); gradient.addColorStop(1, skin.color2);
ctx.fillStyle = gradient;
ctx.beginPath(); ctx.roundRect(x, y, width, height, cornerRadius); ctx.fill();
ctx.strokeStyle = skin.outline; ctx.lineWidth = 2;
ctx.beginPath(); ctx.roundRect(x, y, width, height, cornerRadius); ctx.stroke();
if (game.activePowerUps[POWERUP_TYPES.LASER_PADDLE] || game.activePowerUps[RARE_POWERUP_TYPES.MEGA_LASER]) {
const isMega = game.activePowerUps[RARE_POWERUP_TYPES.MEGA_LASER];
ctx.fillStyle = isMega ? '#FF8C00' : '#ff3333'; // Orange for mega
const cannonWidth = width * (isMega ? 0.15 : 0.1);
const cannonHeight = height * (isMega ? 1.2 : 0.8);
ctx.beginPath(); ctx.roundRect(x+width*0.15-cannonWidth/2,y-cannonHeight,cannonWidth,cannonHeight,2); ctx.fill();
ctx.beginPath(); ctx.roundRect(x+width*0.85-cannonWidth/2,y-cannonHeight,cannonWidth,cannonHeight,2); ctx.fill();
if (isMega) { // Central mega cannon
ctx.beginPath(); ctx.roundRect(x+width*0.5-cannonWidth*0.8,y-cannonHeight*1.2,cannonWidth*1.6,cannonHeight*1.2,3); ctx.fill();
}
}
if (game.activePowerUps[POWERUP_TYPES.SHIELD]) {
ctx.save();
ctx.strokeStyle = '#00FFFF'; ctx.lineWidth = 3;
ctx.globalAlpha = 0.5 + Math.sin(Date.now()/150)*0.2;
ctx.beginPath(); ctx.arc(x + width/2, y + height/2, width/1.8, 0, Math.PI*2); ctx.stroke();
ctx.restore();
}
}
function drawBalls() { /* As before with skin support if needed */
game.balls.forEach(ball => {
const { x, y, radius, attached, isPrimary } = ball;
const isPiercing = ball.piercing || game.activePowerUps[ALL_POWERUP_TYPES.PIERCING_BALL] || game.activePowerUps[RARE_POWERUP_TYPES.INVINCIBILITY];
if (!attached) {
ctx.save();
const glowRadius = radius * (isPiercing ? 2.2 : 1.8);
const grad = ctx.createRadialGradient(x, y, 0, x, y, glowRadius);
const color = isPiercing ? '255,50,255' : (isPrimary ? ball.color2.substring(1) : '220,220,220'); // Use ball's own colors or defaults
const rgb = isPiercing ? [255,50,255] : (isPrimary ? [parseInt(ball.color2.substring(1,3),16), parseInt(ball.color2.substring(3,5),16), parseInt(ball.color2.substring(5,7),16)] : [220,220,220]);
grad.addColorStop(0, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.4)`);
grad.addColorStop(0.5, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.15)`);
grad.addColorStop(1, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0)`);
ctx.fillStyle = grad;
ctx.beginPath(); ctx.arc(x, y, glowRadius, 0, Math.PI * 2); ctx.fill();
ctx.restore();
}
const bodyGrad = ctx.createRadialGradient(x - radius * 0.3, y - radius * 0.3, 0, x, y, radius);
const c1 = isPiercing ? '#ffffff' : ball.color1;
const c2 = isPiercing ? '#ffccff' : ball.color2;
const c3 = isPiercing ? '#ff33ff' : ball.color3;
bodyGrad.addColorStop(0, c1); bodyGrad.addColorStop(0.5, c2); bodyGrad.addColorStop(1, c3);
ctx.fillStyle = bodyGrad;
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.beginPath(); ctx.arc(x - radius*0.35, y - radius*0.35, radius*0.3, 0, Math.PI*2); ctx.fill();
if (game.activePowerUps[POWERUP_TYPES.STICKY_PADDLE] && attached && isPrimary) {
ctx.save();
ctx.strokeStyle = 'rgba(0,255,255,0.4)'; ctx.setLineDash([BALL_RADIUS*0.5,BALL_RADIUS*0.3]); ctx.lineWidth=1.5;
ctx.beginPath(); ctx.moveTo(x,y-radius); ctx.lineTo(x,0); ctx.stroke();
ctx.restore();
}
});
}
function drawBlocks() { /* As before with minor tweaks */
blocks.forEach(block => {
const { x,y,width,height,color,hits,maxHits,creature,isDangerous,animationOffset } = block;
const healthRatio = hits/maxHits; const blockRadius = Math.min(width,height)*0.15;
const ay = isDangerous ? Math.sin(Date.now()/250+animationOffset)*(height*0.05) : 0;
if(isDangerous){ctx.save();ctx.shadowColor=`rgba(255,${60+Math.sin(Date.now()/180+animationOffset)*40},0,0.7)`;ctx.shadowBlur=12+Math.sin(Date.now()/180+animationOffset)*4;ctx.fillStyle=`rgba(180,0,0,${0.08+Math.sin(Date.now()/180+animationOffset)*0.04})`;ctx.beginPath();ctx.roundRect(x-2,y-2+ay,width+4,height+4,blockRadius+2);ctx.fill();ctx.restore();}
const grad=ctx.createLinearGradient(x,y,x,y+height);
if(hits>1){grad.addColorStop(0,color);grad.addColorStop(1,chroma(color).darken(1.8).hex());}
else{grad.addColorStop(0,color);grad.addColorStop(1,chroma(color).darken(0.8).hex()+'E0');}
ctx.fillStyle=grad; ctx.beginPath(); ctx.roundRect(x,y+ay,width,height,blockRadius);ctx.fill();
ctx.strokeStyle='rgba(255,255,255,0.15)';ctx.lineWidth=1;
ctx.beginPath();ctx.roundRect(x,y+ay,width,height,blockRadius);ctx.stroke();
ctx.font=`${Math.min(width,height)*0.55}px Arial`;ctx.textAlign='center';ctx.textBaseline='middle';
const cY=y+height/2+ay;
ctx.fillStyle='rgba(0,0,0,0.3)';ctx.fillText(creature,x+width/2+1,cY+1);
ctx.fillStyle=healthRatio===1?'#fff':`rgba(255,255,255,${0.4+healthRatio*0.6})`;ctx.fillText(creature,x+width/2,cY);
if(hits<maxHits&&hits>0){ctx.save();const bY=y-(height*0.18)+ay;const bH=height*0.08;ctx.fillStyle='rgba(0,0,0,0.3)';ctx.beginPath();ctx.roundRect(x,bY,width,bH,bH/2);ctx.fill();ctx.fillStyle=healthRatio>0.5?'#5f5':(healthRatio>0.2?'#ff5':'#f55');ctx.beginPath();ctx.roundRect(x,bY,width*healthRatio,bH,bH/2);ctx.fill();ctx.restore();}
});
}
function drawLasers() { /* As before */
game.lasers.forEach(l=>{ctx.save();let g=ctx.createLinearGradient(l.x,l.y,l.x,l.y-l.h);g.addColorStop(0,'rgba(255,0,0,0)');g.addColorStop(0.3,l.isMega?'rgba(255,100,0,1)':'rgba(255,50,50,1)');g.addColorStop(1,l.isMega?'rgba(255,255,0,1)':'rgba(255,200,0,1)');ctx.fillStyle=g;ctx.beginPath();ctx.moveTo(l.x-l.w/2,l.y);ctx.lineTo(l.x+l.w/2,l.y);ctx.lineTo(l.x,l.y-l.h);ctx.closePath();ctx.fill();ctx.fillStyle='rgba(255,255,255,0.8)';ctx.beginPath();ctx.moveTo(l.x-l.w/4,l.y);ctx.lineTo(l.x+l.w/4,l.y);ctx.lineTo(l.x,l.y-l.h);ctx.closePath();ctx.fill();ctx.restore();});
}
function drawBlackHole() { /* As before */
if(!game.blackHole)return;const{x,y,radius,timeLeft,maxTimeLeft}=game.blackHole;const effScale=Math.min(1,(maxTimeLeft-timeLeft)/(maxTimeLeft*0.2));const curRad=radius*effScale;ctx.save();
const ogRad=curRad*(2.5+Math.sin(Date.now()/150)*0.3);let grad=ctx.createRadialGradient(x,y,curRad*0.8,x,y,ogRad);grad.addColorStop(0,'rgba(100,0,150,0)');grad.addColorStop(0.5,`rgba(100,0,150,${0.4*effScale})`);grad.addColorStop(1,'rgba(100,0,150,0)');ctx.fillStyle=grad;ctx.beginPath();ctx.arc(x,y,ogRad,0,Math.PI*2);ctx.fill();
ctx.strokeStyle=`rgba(150,50,255,${0.6*effScale})`;ctx.lineWidth=2;const numT=5;for(let i=0;i<numT;i++){ctx.beginPath();const rot=Date.now()/500+(i*Math.PI*2/numT);for(let ang=0;ang<Math.PI*1.5;ang+=0.1){const r=curRad*0.3+ang*curRad*0.2+Math.sin(ang*5+Date.now()/200)*curRad*0.1;const sx=x+Math.cos(ang+rot)*r;const sy=y+Math.sin(ang+rot)*r;if(ang===0)ctx.moveTo(sx,sy);else ctx.lineTo(sx,sy);}ctx.stroke();}
ctx.fillStyle=`rgba(0,0,0,${0.9*effScale})`;ctx.beginPath();ctx.arc(x,y,curRad,0,Math.PI*2);ctx.fill();ctx.restore();
}
function drawPowerUpsOnScreen() { /* As before with pulse */
game.powerUpsOnScreen.forEach(pu=>{const{x,y,radius,color,text,isRare}=pu;const eR=radius*(1+Math.sin(Date.now()/180+x*0.1)*(isRare?0.15:0.08));if(isRare){ctx.save();const gG=ctx.createRadialGradient(x,y,0,x,y,eR*1.8);gG.addColorStop(0,chroma(color).alpha(0.5).css());gG.addColorStop(1,chroma(color).alpha(0).css());ctx.fillStyle=gG;ctx.beginPath();ctx.arc(x,y,eR*1.8,0,Math.PI*2);ctx.fill();ctx.restore();}
ctx.fillStyle=color;ctx.beginPath();ctx.arc(x,y,eR,0,Math.PI*2);ctx.fill();ctx.fillStyle=chroma.contrast(color,'black')>chroma.contrast(color,'white')?'black':'white';ctx.font=`bold ${eR*(text.length>1?0.75:0.95)}px 'Orbitron'`;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(text,x,y+eR*0.05);});
}
function drawBoss() {
if (!game.currentBoss) return;
const boss = game.currentBoss;
const { x, y, width, height, creature, color, animationOffset } = boss;
const animatedY = y + Math.sin(Date.now()/400 + animationOffset) * (height * 0.05); // Bobbing animation
ctx.save();
// Boss shadow/glow
ctx.shadowColor = chroma(color).darken(1).hex();
ctx.shadowBlur = 20 + Math.sin(Date.now()/200)*5;
ctx.fillStyle = chroma(color).alpha(0.2).css();
ctx.beginPath();
ctx.ellipse(x + width/2, animatedY + height/2 + 10, width/1.8, height/3, 0, 0, Math.PI * 2);
ctx.fill();
ctx.shadowColor = 'transparent'; // Reset for main drawing
// Main boss body
const grad = ctx.createRadialGradient(x+width/2, animatedY+height/2, 0, x+width/2, animatedY+height/2, Math.max(width,height)/2);
grad.addColorStop(0, chroma(color).brighten(1).hex());
grad.addColorStop(0.7, color);
grad.addColorStop(1, chroma(color).darken(1).hex());
ctx.fillStyle = grad;
// Placeholder: simple rectangle for boss, ideally emoji or sprite
ctx.fillRect(x, animatedY, width, height);
ctx.font = `${Math.min(width,height)*0.6}px Arial`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = '#FFF';
ctx.fillText(creature, x+width/2, animatedY+height/2);
ctx.restore();
}
// --- UI Update Functions ---
function updateHealthBarUI() {
const healthPercentage = (game.lives / game.maxLives) * 100;
document.getElementById('healthFill').style.width = `${healthPercentage}%`;
}
function updateDepthMeterUI() {
const depthPercentage = (game.level / MAX_LEVELS) * 100;
document.getElementById('depthIndicator').style.height = `${Math.min(100, depthPercentage)}%`;
}
function updateLevelProgressUI() {
const progress = game.initialBlockCount > 0 ? ((game.initialBlockCount - blocks.length) / game.initialBlockCount) * 100 : (game.currentBoss ? 0 : 100);
document.getElementById('levelProgressBar').style.width = `${progress}%`;
}
function updateBossHealthUI() {
if (game.currentBoss) {
const healthPercentage = (game.currentBoss.hp / game.currentBoss.maxHp) * 100;
document.getElementById('bossHealthBar').style.width = `${healthPercentage}%`;
}
}
function showTutorialMessage(text, duration = 4000) {
const tutPopup = document.getElementById('tutorialPopup');
document.getElementById('tutorialText').textContent = text;
tutPopup.classList.add('show');
setTimeout(() => tutPopup.classList.remove('show'), duration);
}
function updateLeaderboardDisplay() {
const scores = JSON.parse(localStorage.getItem('deepSeaBreakoutLeaderboard_v2') || '[]');
scores.sort((a, b) => b.score - a.score); // Sort descending
const tableBody = document.getElementById('leaderboardTable').getElementsByTagName('tbody')[0];
tableBody.innerHTML = ''; // Clear old scores
scores.slice(0, 10).forEach((entry, index) => { // Show top 10
const row = tableBody.insertRow();
row.insertCell().textContent = index + 1;
row.insertCell().textContent = entry.name || 'Player';
row.insertCell().textContent = entry.score;
});
if (scores.length === 0) {
const row = tableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 3;
cell.textContent = "No high scores yet. Be the first!";
cell.style.textAlign = "center";
}
}
function updateStatsDisplay() {
const container = document.getElementById('statsContainer');
container.innerHTML = `
<p><strong>Games Played:</strong> ${game.playerStats.gamesPlayed || 0}</p>
<p><strong>Total Score Accumulated:</strong> ${game.playerStats.totalScore || 0}</p>
<p><strong>Highest Level Reached:</strong> ${game.playerStats.highestLevelReached || 0}</p>
<p><strong>Total Blocks Broken:</strong> ${game.playerStats.totalBlocksBroken || 0}</p>
<p><strong>Power-ups Collected:</strong> ${game.playerStats.totalPowerupsCollected || 0}</p>
<p><strong>Bosses Defeated:</strong> ${game.playerStats.bossesDefeated || 0}</p>
<p><strong>Total Playtime:</strong> ${formatTime(game.playerStats.totalGameTime || 0)}</p>
`;
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h > 0 ? h + 'h ' : ''}${m > 0 ? m + 'm ' : ''}${s}s`;
}
function updateAchievementsDisplay() {
const container = document.getElementById('achievementsListContainer');
container.innerHTML = '';
Object.values(game.achievements).forEach(ach => {
const div = document.createElement('div');
div.className = 'achievement';
div.innerHTML = `
<div class="achievement-icon">${ach.icon}</div>
<div class="achievement-details">
<div class="achievement-title">${ach.name}</div>
<div class="achievement-description">${ach.description}</div>
</div>
<div class="achievement-status ${ach.unlocked ? 'completed' : ''}">${ach.unlocked ? 'Unlocked' : 'Locked'}</div>
`;
container.appendChild(div);
});
}
function populatePaddleSkins() {
const selector = document.getElementById('paddleSkinSelector');
selector.innerHTML = '';
PADDLE_SKINS.forEach(skin => {
const div = document.createElement('div');
div.className = 'skin';
div.style.background = `linear-gradient(45deg, ${skin.color1}, ${skin.color2})`;
div.style.borderColor = skin.outline;
if (skin.id === game.paddleSkin.id) div.classList.add('selected');
div.title = skin.name;
div.onclick = () => {
game.paddleSkin = skin;
localStorage.setItem('deepSeaBreakoutPaddleSkin_v2', skin.id);
populatePaddleSkins(); // Re-render to show selection
};
selector.appendChild(div);
});
}
// --- Update Logic ---
function updatePaddle() { /* As before, targetX based */
if(game.activePowerUps[RARE_POWERUP_TYPES.TIME_STOP])return;
paddle.x+=(paddle.targetX-paddle.x)*0.28;paddle.x=Math.max(0,Math.min(paddle.x,GAME_WIDTH-paddle.width));
game.balls.forEach(b=>{if(b.attached){b.x=paddle.x+paddle.width/2;b.y=paddle.y-b.radius-1;}});
}
function updateBalls() { /* As before, with boss collision */
for(let i=game.balls.length-1;i>=0;i--){
const ball=game.balls[i];if(ball.attached)continue;
let sF=1;if(game.activePowerUps[POWERUP_TYPES.SLOW_MO])sF=0.6;if(game.activePowerUps[RARE_POWERUP_TYPES.TIME_STOP]&&!ball.isPrimary)sF=0.05;
ball.x+=ball.dx*sF;ball.y+=ball.dy*sF;
if(ball.x<=ball.radius||ball.x>=GAME_WIDTH-ball.radius){ball.dx*=-1;audioManager.playSound(audioManager.definitions.wallHit,{pan:(ball.x/GAME_WIDTH)*2-1});createEffectParticle(ball.x,ball.y,'#87ceeb',5,{sizeRange:[1,2.5]});ball.x=Math.max(ball.radius,Math.min(ball.x,GAME_WIDTH-ball.radius));}
if(ball.y<=ball.radius){ball.dy*=-1;audioManager.playSound(audioManager.definitions.wallHit,{pan:(ball.x/GAME_WIDTH)*2-1});createEffectParticle(ball.x,ball.y,'#87ceeb',5,{sizeRange:[1,2.5]});ball.y=ball.radius;}
if(ball.dy>0&&ball.y+ball.radius>=paddle.y&&ball.y-ball.radius<=paddle.y+paddle.height&&ball.x+ball.radius>=paddle.x&&ball.x-ball.radius<=paddle.x+paddle.width){
if(game.activePowerUps[POWERUP_TYPES.STICKY_PADDLE]&&ball.isPrimary){ball.attached=true;ball.dx=0;ball.dy=0;}
else{
const hpN=(ball.x-(paddle.x+paddle.width/2))/(paddle.width/2);const bA=hpN*(Math.PI/2.7);
const tS=ball.speed*(1+game.level*0.025); // Speed can increase slightly with impacts
ball.dx=tS*Math.sin(bA);ball.dy=-tS*Math.cos(bA);ball.dx+=(Math.random()-0.5)*0.15*tS;
}
audioManager.playSound(audioManager.definitions.paddleHit);createEffectParticle(ball.x,paddle.y,'#00ffff',8,{sizeRange:[1.5,3],upwardBias:-1});ball.y=paddle.y-ball.radius;
if(ball.isPrimary && game.activePowerUps[POWERUP_TYPES.SHIELD]){ // Shield absorbs one hit
delete game.activePowerUps[POWERUP_TYPES.SHIELD]; // Consume shield
addBubbleText("SHIELD BROKEN!", paddle.x + paddle.width/2, paddle.y - 20, "#FF8C00", 16);
}
if(!ball.isPrimary && game.combo > 0 && !game.activePowerUps[RARE_POWERUP_TYPES.PENTA_BALL]) { // Penta-ball doesn't reset combo easily
game.combo = Math.floor(game.combo / 2); // Reduce combo significantly if non-primary hits paddle
document.getElementById('combo').textContent = `${game.combo}x`;
}
}
const isInv=game.activePowerUps[RARE_POWERUP_TYPES.INVINCIBILITY];
ball.piercing=game.activePowerUps[POWERUP_TYPES.PIERCING_BALL]||isInv;
// Boss collision
if (game.currentBoss) {
const boss = game.currentBoss;
if (ball.x + ball.radius > boss.x && ball.x - ball.radius < boss.x + boss.width &&
ball.y + ball.radius > boss.y && ball.y - ball.radius < boss.y + boss.height) {
if (boss.vulnerableTime > 0 || isInv) {
boss.hp--;
game.score += 50 * game.level;
audioManager.playSound(audioManager.definitions.bossHit);
createEffectParticle(ball.x, ball.y, boss.color, 15, {glow: true, sizeRange:[2,5]});
updateBossHealthUI();
if (boss.hp <= 0) {
bossDefeated();
}
} else {
audioManager.playSound(audioManager.definitions.wallHit, {pan:(ball.x/GAME_WIDTH)*2-1}); // Boss deflects
createEffectParticle(ball.x, ball.y, '#AAAAFF', 8, {sizeRange:[1,3]});
}
// Ball reflects off boss
const dX=ball.x-(boss.x+boss.width/2),dY=ball.y-(boss.y+boss.height/2);
if(Math.abs(dX)/(boss.width/2)>Math.abs(dY)/(boss.height/2))ball.dx*=-1;else ball.dy*=-1;
}
} else { // Normal block collision
for(let j=blocks.length-1;j>=0;j--){
const block=blocks[j];
if(ball.x+ball.radius>block.x&&ball.x-ball.radius<block.x+block.width&&ball.y+ball.radius>block.y&&ball.y-ball.radius<block.y+block.height){
if(block.isDangerous&&ball.isPrimary&&!isInv&&!game.activePowerUps[POWERUP_TYPES.SHIELD]){game.lives--;audioManager.playSound(audioManager.definitions.loseLife);addBubbleText("-1 LIFE!",ball.x,ball.y,"#ff4444",20);createEffectParticle(ball.x,ball.y,"#ff0000",20,{glow:true});updateHealthBarUI();if(game.lives<=0){gameOver();return;}else{resetPrimaryBallAndPaddleState();continue;}}
else if (block.isDangerous && ball.isPrimary && game.activePowerUps[POWERUP_TYPES.SHIELD]) {
delete game.activePowerUps[POWERUP_TYPES.SHIELD];
addBubbleText("SHIELD PROTECTED!", paddle.x + paddle.width/2, paddle.y - 20, "#00FFFF", 16);
}
audioManager.playSound(audioManager.definitions.blockHit);
if (blocks.length === game.initialBlockCount) unlockAchievement('FIRST_BREAK'); // First break of level
updatePlayerStat('totalBlocksBroken', 1);
createEffectParticle(block.x+block.width/2,block.y+block.height/2,block.color,10,{sizeRange:[1,3]});
if(!ball.piercing){const bX=ball.x,bY=ball.y;const blX=block.x+block.width/2,blY=block.y+block.height/2;const dX=bX-blX,dY=bY-blY;const wTH=(ball.radius+block.width/2),hTH=(ball.radius+block.height/2);if(Math.abs(dX)/wTH>Math.abs(dY)/hTH){ball.dx*=-1;ball.x+=ball.dx>0?1:-1;}else{ball.dy*=-1;ball.y+=ball.dy>0?1:-1;}}
block.hits--;game.score+=5*game.level*(block.isDangerous?1.5:1);
if(block.hits<=0){game.score+=(10+(ball.piercing?5:0))*game.level*(block.isDangerous?1.5:1);blocks.splice(j,1);updateLevelProgressUI();if(Math.random()<POWERUP_DROP_CHANCE){spawnPowerUpItem(block.x+block.width/2,block.y+block.height/2);}}
updateCombo();if(!ball.piercing)break;
}
}
}
if(ball.y>GAME_HEIGHT+ball.radius*3){
if(ball.isPrimary){
if(!isInv && !game.activePowerUps[POWERUP_TYPES.SHIELD]){game.lives--;}
else if (game.activePowerUps[POWERUP_TYPES.SHIELD]) {
delete game.activePowerUps[POWERUP_TYPES.SHIELD];
addBubbleText("SHIELD SAVED BALL!", paddle.x + paddle.width/2, paddle.y - 20, "#00FFFF", 16);
}
audioManager.playSound(audioManager.definitions.loseLife);updateHealthBarUI();
if(game.lives<=0 && !isInv){gameOver();return;}
else{resetPrimaryBallAndPaddleState();}
}else{game.balls.splice(i,1);}
}
}
}
function updateCombo() { /* As before */
const now=Date.now();if(now-game.lastHitTime>COMBO_TIMEOUT)game.combo=0;
game.lastHitTime=now;game.combo++;document.getElementById('combo').textContent=`${game.combo}x`;
if(game.combo>=3){const pts=Math.min(25,game.combo)*game.level;game.score+=pts;addBubbleText(`+${pts} COMBO!`,GAME_WIDTH/2,GAME_HEIGHT*0.4,'#ffff00',16+Math.min(10,game.combo));}
if(game.combo>=5&&game.combo%2===(game.combo<10?1:0)){audioManager.playSound(audioManager.definitions.combo(game.combo));if(game.combo>=5){const cm=document.getElementById('comboMeter');cm.textContent=`${game.combo}X COMBO!`;cm.style.opacity='1';cm.style.transform='translate(-50%,-50%)scale(1.1)';cm.style.color=game.combo>=15?'#ff69b4':(game.combo>=10?'#ffaa00':'#ffff00');setTimeout(()=>{cm.style.opacity='0';cm.style.transform='translate(-50%,-50%)scale(0.8)';},800);}}
if(game.combo >= 15) unlockAchievement('COMBO_15X');
}
function updateLasers() { /* As before */
for(let i=game.lasers.length-1;i>=0;i--){let l=game.lasers[i];l.y-=l.speed;if(l.y+l.h<0){game.lasers.splice(i,1);continue;}
for(let j=blocks.length-1;j>=0;j--){let b=blocks[j];if(l.x>=b.x&&l.x<=b.x+b.width&&l.y<=b.y+b.height&&l.y+l.h>=b.y){createEffectParticle(l.x,b.y+b.height/2,l.isMega?'#FFA500':'#ffff00',10,{glow:true});if(l.isMega)b.hits=0;else b.hits-=2; if(b.hits<=0){blocks.splice(j,1);updateLevelProgressUI();}game.score+= (l.isMega?25:15)*game.level;updateCombo();game.lasers.splice(i,1);break;}}
// Laser vs Boss
if(game.currentBoss && game.lasers[i]) { // Check if laser still exists
const boss = game.currentBoss;
const laser = game.lasers[i];
if (laser.x >= boss.x && laser.x <= boss.x + boss.width && laser.y <= boss.y + boss.height && laser.y + laser.height >= boss.y) {
if (boss.vulnerableTime > 0) {
boss.hp -= (laser.isMega ? 3 : 1);
game.score += (laser.isMega ? 100 : 75) * game.level;
audioManager.playSound(audioManager.definitions.bossHit);
createEffectParticle(laser.x, laser.y, boss.color, 10, {glow:true, sizeRange:[1.5,4]});
updateBossHealthUI();
if (boss.hp <= 0) bossDefeated();
}
game.lasers.splice(i,1);
}
}}
}
function updateBoss() {
if (!game.currentBoss) return;
const boss = game.currentBoss;
// Movement
boss.x += boss.dx;
if (boss.x <= 0 || boss.x + boss.width >= GAME_WIDTH) {
boss.dx *= -1;
boss.vulnerableTime = 120; // Boss becomes vulnerable for 2s after hitting wall
}
if (boss.vulnerableTime > 0) boss.vulnerableTime--;
// Basic attack
if (Date.now() - boss.lastAttackTime > 3000 && boss.vulnerableTime <= 0) { // Attack every 3s if not vulnerable
// Placeholder: Simple projectile attack
addBubbleText("BOSS ATTACK!", boss.x + boss.width/2, boss.y + boss.height, "#FF5555", 18);
// (Actual projectile logic would be added here)
boss.lastAttackTime = Date.now();
}
}
function updateBlackHole() { /* As before */
if(!game.blackHole)return;const bh=game.blackHole;bh.timeLeft--;if(bh.timeLeft<=0){game.blackHole=null;return;}
game.balls.forEach(b=>{if(b.attached)return;const dx=bh.x-b.x,dy=bh.y-b.y;const dSq=dx*dx+dy*dy;if(dSq<Math.pow(bh.radius*5,2)&&dSq>10){const d=Math.sqrt(dSq);const f=bh.strength/dSq;b.dx+=dx/d*f;b.dy+=dy/d*f;const s=Math.sqrt(b.dx*b.dx+b.dy*b.dy);if(s>b.speed*1.5){b.dx=(b.dx/s)*b.speed*1.5;b.dy=(b.dy/s)*b.speed*1.5;}}});
for(let i=blocks.length-1;i>=0;i--){const bl=blocks[i];const bX=bl.x+bl.width/2,bY=bl.y+bl.height/2;const dx=bh.x-bX,dy=bh.y-bY;const dSq=dx*dx+dy*dy;if(dSq<Math.pow(bh.radius*3,2)){if(dSq<Math.pow(bh.radius*0.8,2)){audioManager.playSound(audioManager.definitions.blackHoleAbsorb);createEffectParticle(bX,bY,chroma(bl.color).brighten(1).hex(),15,{upwardBias:0,speedRange:[1,3]});blocks.splice(i,1);updateLevelProgressUI();game.score+=20*game.level;updateCombo();}}}
}
// --- Power-up Logic ---
function spawnPowerUpItem(x, y) { /* As before with new powerups */
const isRare = Math.random() < RARE_POWERUP_CHANCE;
let type, text, color;
const puRadius = GAME_WIDTH * (isRare ? 0.02 : 0.015);
const standardTypes = Object.values(POWERUP_TYPES);
const rareTypes = Object.values(RARE_POWERUP_TYPES);
if (isRare) { type = rareTypes[Math.floor(Math.random() * rareTypes.length)]; audioManager.playSound(audioManager.definitions.rarePowerUpCollect); }
else { type = standardTypes[Math.floor(Math.random() * standardTypes.length)]; audioManager.playSound(audioManager.definitions.powerUpSpawn); }
// Get icon/color from a central place
const puInfo = getPowerupInfo(type);
text = puInfo.icon; color = puInfo.color;
game.powerUpsOnScreen.push({ x, y, type, text, color, radius: puRadius, dy: GAME_HEIGHT*(isRare?0.0015:0.002), isRare });
}
function getPowerupInfo(type) { // Centralized info for drawing and UI
switch(type) {
case POWERUP_TYPES.WIDE_PADDLE: return { icon: 'Wโ†”', color: 'rgba(0,200,0,0.8)'};
case POWERUP_TYPES.STICKY_PADDLE: return { icon: 'S๐Ÿงฒ', color: 'rgba(255,200,0,0.8)'};
case POWERUP_TYPES.PIERCING_BALL: return { icon: 'P๐Ÿ’ฅ', color: 'rgba(255,0,200,0.8)'};
case POWERUP_TYPES.LASER_PADDLE: return { icon: 'Lโšก', color: 'rgba(255,50,50,0.8)'};
case POWERUP_TYPES.MULTI_BALL: return { icon: 'M๐ŸฅŽ', color: 'rgba(0,200,255,0.8)'};
case POWERUP_TYPES.SLOW_MO: return { icon: 'โŒ›', color: 'rgba(180,180,255,0.8)'};
case POWERUP_TYPES.SHIELD: return { icon: '๐Ÿ›ก๏ธ', color: 'rgba(0,255,255,0.8)'};
case RARE_POWERUP_TYPES.INVINCIBILITY: return { icon: '๐ŸŒŸ', color: 'rgba(255,215,0,0.9)'};
case RARE_POWERUP_TYPES.TIME_STOP: return { icon: '๐Ÿ›‘', color: 'rgba(100,220,255,0.9)'};
case RARE_POWERUP_TYPES.BLACK_HOLE: return { icon: '๐ŸŒ€', color: 'rgba(50,0,80,0.9)'};
case RARE_POWERUP_TYPES.PENTA_BALL: return { icon: '5๏ธโƒฃ', color: 'rgba(255,100,255,0.9)'};
case RARE_POWERUP_TYPES.MEGA_LASER: return { icon: 'L๐Ÿ’ฅ', color: 'rgba(255,120,0,0.9)'};
case RARE_POWERUP_TYPES.BLOCK_BOMB: return { icon: '๐Ÿ’ฃ', color: 'rgba(100,100,100,0.9)'};
default: return {icon: '?', color: 'grey'};
}
}
function updatePowerUpsOnScreen() { /* As before */
for(let i=game.powerUpsOnScreen.length-1;i>=0;i--){let pu=game.powerUpsOnScreen[i];pu.y+=pu.dy;if(pu.y-pu.radius>GAME_HEIGHT){game.powerUpsOnScreen.splice(i,1);continue;}if(pu.y+pu.radius>=paddle.y&&pu.y-pu.radius<=paddle.y+paddle.height&&pu.x+pu.radius>=paddle.x&&pu.x-pu.radius<=paddle.x+paddle.width){activatePowerUp(pu.type,pu.isRare);updatePlayerStat('totalPowerupsCollected',1);if(game.playerStats.totalPowerupsCollected >= 5) unlockAchievement('POWERUP_MASTER');addBubbleText((pu.isRare?"RARE: ":"")+pu.type.replace(/_/g,' ')+"!",pu.x,pu.y,pu.isRare?"gold":"#00dddd",16);game.powerUpsOnScreen.splice(i,1);}}
}
function activatePowerUp(type, isRare) { /* As before with new powerups */
isRare ? audioManager.playSound(audioManager.definitions.rarePowerUpCollect) : audioManager.playSound(audioManager.definitions.powerUpCollect);
const duration = (isRare ? POWERUP_DURATION * RARE_POWERUP_DURATION_MULTIPLIER : POWERUP_DURATION);
if(game.activePowerUps[type]) game.activePowerUps[type].endTime = Math.max(game.activePowerUps[type].endTime, Date.now()+duration);
else game.activePowerUps[type]={endTime:Date.now()+duration,isRare,data:{}};
switch(type){
case POWERUP_TYPES.WIDE_PADDLE:paddle.width=PADDLE_DEFAULT_WIDTH*1.6;break;
case POWERUP_TYPES.MULTI_BALL:addNewBall();break;
case POWERUP_TYPES.SHIELD: break; // Active effect is checked elsewhere
case RARE_POWERUP_TYPES.PENTA_BALL:for(let k=0;k<4;k++)addNewBall();break;
case RARE_POWERUP_TYPES.INVINCIBILITY:game.lives=Math.min(game.lives+1,game.maxLives);addBubbleText("+1 LIFE!",paddle.x+paddle.width/2,paddle.y,"#ffff00",20);updateHealthBarUI();break;
case RARE_POWERUP_TYPES.BLACK_HOLE:audioManager.playSound(audioManager.definitions.blackHoleOpen);game.blackHole={x:GAME_WIDTH/2,y:GAME_HEIGHT/3,radius:GAME_WIDTH*0.06,strength:GAME_WIDTH*0.12,timeLeft:360,maxTimeLeft:360};break; // 6 seconds
case RARE_POWERUP_TYPES.BLOCK_BOMB:
const bombX = paddle.x + paddle.width/2; const bombY = paddle.y - 20;
createEffectParticle(bombX, bombY, '#FF8C00', 30, {sizeRange:[3,8], glow:true, speedRange:[5,10]});
const bombRadius = GAME_WIDTH * 0.2;
for(let i=blocks.length-1;i>=0;i--){
const b=blocks[i]; const distSq = Math.pow(b.x+b.width/2 - bombX,2) + Math.pow(b.y+b.height/2 - bombY,2);
if (distSq < Math.pow(bombRadius,2)) {
game.score+=10*game.level; blocks.splice(i,1);updateLevelProgressUI();
createEffectParticle(b.x+b.width/2, b.y+b.height/2, b.color, 5);
}
}
updateCombo(); // Give combo for bomb
break;
}
}
function updateActivePowerUps() { /* As before */
const now=Date.now();Object.entries(game.activePowerUps).forEach(([t,d])=>{if(now>d.endTime){deactivatePowerUp(t);delete game.activePowerUps[t];}});drawPowerUpIndicatorsUI();
}
function deactivatePowerUp(type) { /* As before with new powerups */
switch(type){
case POWERUP_TYPES.WIDE_PADDLE:paddle.width=PADDLE_DEFAULT_WIDTH;break;
case POWERUP_TYPES.PIERCING_BALL:game.balls.forEach(b=>b.piercing=false);break;
// SHIELD, MEGA_LASER, LASER_PADDLE, etc. handled by time expiry or specific consumption
}
}
function fireLaser() { /* As before with mega laser */
const isMega = game.activePowerUps[RARE_POWERUP_TYPES.MEGA_LASER];
if(game.lasers.length>=(isMega?2:4)||!(game.activePowerUps[POWERUP_TYPES.LASER_PADDLE]||isMega))return;
audioManager.playSound(audioManager.definitions.laserFire);
const lW=paddle.width*(isMega?0.1:0.05);const lS=GAME_HEIGHT*(isMega?0.025:0.02);const lH=PADDLE_HEIGHT*(isMega?2.5:1.8);
if(isMega){game.lasers.push({x:paddle.x+paddle.width/2,y:paddle.y,width:lW*1.5,height:lH,speed:lS,isMega});}
else{game.lasers.push({x:paddle.x+paddle.width*0.2,y:paddle.y,width:lW,height:lH,speed:lS,isMega:false});game.lasers.push({x:paddle.x+paddle.width*0.8,y:paddle.y,width:lW,height:lH,speed:lS,isMega:false});}
}
function addNewBall() { /* As before */
const pB=game.balls.find(b=>b.isPrimary);if(!pB||game.balls.length>=6)return;
const nB={...pB};nB.isPrimary=false;nB.attached=false;nB.x=pB.x+(Math.random()-0.5)*pB.radius;nB.y=pB.y-pB.radius;nB.dx=(Math.random()-0.5)*pB.speed*0.6;nB.dy=-pB.speed*(0.7+Math.random()*0.3);nB.trail=[];game.balls.push(nB);
}
// --- Game State Management ---
function resetPrimaryBallAndPaddleState() { /* As before */
const pB=game.balls.find(b=>b.isPrimary);if(pB){pB.x=paddle.x+paddle.width/2;pB.y=paddle.y-pB.radius*1.5;pB.dx=0;pB.dy=0;pB.attached=true;pB.piercing=false;pB.trail=[];}
if(game.balls.some(b=>b.isPrimary&&b.attached))game.balls=game.balls.filter(b=>b.isPrimary);
if(!game.activePowerUps[POWERUP_TYPES.WIDE_PADDLE])paddle.width=PADDLE_DEFAULT_WIDTH;
game.combo=0;document.getElementById('combo').textContent='0x';
}
async function showLevelTransition(levelNumber) {
const screen = document.getElementById('levelTransitionScreen');
document.getElementById('levelTransitionNumber').textContent = `LEVEL ${levelNumber}`;
screen.classList.add('active');
await new Promise(resolve => setTimeout(resolve, 1500)); // Display for 1.5s
screen.classList.remove('active');
await new Promise(resolve => setTimeout(resolve, 400)); // Fade out time
}
async function showBossIntroScreen(bossName) {
const screen = document.getElementById('bossIntroScreen');
document.getElementById('bossIntroName').textContent = bossName;
screen.classList.add('active');
document.getElementById('bossWarning').style.display = 'block';
await new Promise(resolve => setTimeout(resolve, 2500));
screen.classList.remove('active');
document.getElementById('bossWarning').style.display = 'none';
await new Promise(resolve => setTimeout(resolve, 400));
}
async function showBossDefeatedScreen(bossName) {
const screen = document.getElementById('bossDefeatedScreen');
document.getElementById('bossDefeatedName').textContent = `${bossName} DEFEATED!`;
screen.classList.add('active');
await new Promise(resolve => setTimeout(resolve, 2500));
screen.classList.remove('active');
await new Promise(resolve => setTimeout(resolve, 400));
}
async function nextLevel() {
game.levelTransitioning = true;
if (game.lives === game.maxLives && game.level > 1) unlockAchievement('NO_LIVES_LOST_LEVEL'); // Check before lives reset for next level
audioManager.playSound(audioManager.definitions.levelUp);
addBubbleText(`LEVEL ${game.level + 1} INCOMING!`, GAME_WIDTH/2, GAME_HEIGHT/2, "#66ff66", 28, 120);
await showLevelTransition(game.level + 1);
game.level++;
updatePlayerStat('highestLevelReached', game.level);
if (game.level > MAX_LEVELS) { gameWon(); game.levelTransitioning = false; return; }
if (game.level === 3) unlockAchievement('LEVEL_3');
game.balls.forEach(b => b.speed += (GAME_HEIGHT * 0.0005) * (game.difficulty === 'hardcore' ? 1.8 : 1.2));
Object.entries(game.activePowerUps).forEach(([t,d])=>{if(!d.isRare || t === RARE_POWERUP_TYPES.BLACK_HOLE){deactivatePowerUp(t);delete game.activePowerUps[t];}}); // Clear non-rare & blackhole
game.powerUpsOnScreen=[];game.lasers=[]; if(game.blackHole) game.blackHole = null;
paddle.width = PADDLE_DEFAULT_WIDTH;
initPrimaryBall();
if (BOSS_LEVELS.includes(game.level)) {
await showBossIntroScreen(BOSS_CREATURES[game.level]);
createBoss(game.level);
game.initialBlockCount = 0; // No normal blocks
} else {
game.currentBoss = null;
document.getElementById('bossHealthContainer').style.display = 'none';
document.getElementById('bossName').style.display = 'none';
createBlocks();
}
updateDepthMeterUI();
updateLevelProgressUI(); // Reset progress for new level
if (game.level === 1) showTutorialMessage("Click to launch the Pearl!");
game.levelTransitioning = false;
}
function bossDefeated() {
const bossName = game.currentBoss.creature;
audioManager.playSound(audioManager.definitions.bossDefeat);
addBubbleText(`${bossName} Vanquished!`, GAME_WIDTH/2, GAME_HEIGHT*0.4, '#FFD700', 30, 150);
game.score += 2000 * game.level; // Big score bonus
updatePlayerStat('bossesDefeated', 1);
if (game.level === BOSS_LEVELS[0]) unlockAchievement('LEVEL_5_BOSS');
game.currentBoss = null;
document.getElementById('bossHealthContainer').style.display = 'none';
document.getElementById('bossName').style.display = 'none';
// Create some "loot" powerups
for(let i=0; i<3; i++) spawnPowerUpItem(GAME_WIDTH/2 + (i-1)*50, GAME_HEIGHT/3);
// This will trigger nextLevel logic after a delay in the gameLoop
// No blocks to clear, so blocks.length will be 0.
}
function updateHighScore() { /* As before */
if(game.score>game.highScore){game.highScore=game.score;localStorage.setItem('deepSeaBreakoutHighScore_v2',game.highScore);unlockAchievement('HIGH_SCORE_10K');return true;}return false;
}
function gameOver() { /* As before */
game.running=false;savePlayerStats();const nH=updateHighScore();document.getElementById('finalScore').textContent=game.score;document.getElementById('highScoreText').textContent=nH?`NEW HIGH SCORE: ${game.highScore}`:`High Score: ${game.highScore}`;showScreen('gameOver');audioManager.playSound(audioManager.definitions.gameOver);
}
function gameWon() { /* As before */
game.running=false;savePlayerStats();unlockAchievement('GAME_WON');const nH=updateHighScore();document.getElementById('winScore').textContent=game.score;document.getElementById('totalLevelsWon').textContent = MAX_LEVELS; document.getElementById('winHighScoreText').textContent=nH?`NEW HIGH SCORE: ${game.highScore}!`:`High Score: ${game.highScore}`;showScreen('gameWon');audioManager.playSound(audioManager.definitions.gameWon);
}
async function initGame(difficulty = 'normal') {
const loadingScreen = document.getElementById('loadingScreen');
const loadingText = document.getElementById('loadingText');
const loadingProgress = document.getElementById('loadingProgress');
loadingScreen.classList.remove('hidden');
loadingText.textContent = "INITIALIZING AUDIO...";
loadingProgress.style.width = '10%';
await audioManager.init();
loadingText.textContent = "PREPARING GAME STATE...";
loadingProgress.style.width = '30%';
await new Promise(r => setTimeout(r, 200)); // Simulate work
game.score=0; game.difficulty=difficulty; game.maxLives = game.difficulty==='hardcore'?2:3; game.lives=game.maxLives;
game.level=1; game.paused=false; game.running=true; game.levelTransitioning=false;
game.particles=[]; game.powerUpsOnScreen=[]; game.activePowerUps={};
game.combo=0; game.balls=[]; game.lasers=[]; game.bubbleTexts=[]; game.blackHole=null; game.currentBoss=null;
game.currentBackgroundHue=180; game.gameTime = 0;
if (!game.paddleSkin || PADDLE_SKINS.findIndex(s => s.id === game.paddleSkin.id) === -1) {
game.paddleSkin = PADDLE_SKINS.find(s => s.id === localStorage.getItem('deepSeaBreakoutPaddleSkin_v2')) || PADDLE_SKINS[0];
}
updatePlayerStat('gamesPlayed', 1);
defineAchievements(); // Define/load achievements
initPlayerStats(); // Load player stats
paddle.width=PADDLE_DEFAULT_WIDTH;paddle.x=GAME_WIDTH/2-paddle.width/2;paddle.targetX=paddle.x;
initPrimaryBall();
loadingText.textContent = "BUILDING LEVEL...";
loadingProgress.style.width = '60%';
await new Promise(r => setTimeout(r, 200));
createBlocks();
loadingText.textContent = "SUMMONING SEA LIFE...";
loadingProgress.style.width = '80%';
await new Promise(r => setTimeout(r, 200));
createBackgroundElements();
document.querySelectorAll('.dialog-box, .menu, .settings-panel, .leaderboard, .shop, .daily-challenge, .achievement-system, .tutorial-screen, .stats-screen, .credits, .customization, .profile, .multiplayer-info, .matchmaking, .difficultySelectScreen').forEach(el => el.style.display = 'none');
document.getElementById('bossHealthContainer').style.display = 'none';
document.getElementById('bossName').style.display = 'none';
updateHealthBarUI(); updateDepthMeterUI(); updateLevelProgressUI();
['score','livesLegacy','level','combo'].forEach(id=>document.getElementById(id).textContent=game[id]||'0');
document.getElementById('maxLevels').textContent = MAX_LEVELS;
drawPowerUpIndicatorsUI();
loadingText.textContent = "GAME READY!";
loadingProgress.style.width = '100%';
await new Promise(r => setTimeout(r, 300));
loadingScreen.classList.add('hidden');
game.currentScreen = 'game'; // Signifies game is active
if (game.playerStats.gamesPlayed <= 1) showTutorialMessage("Move mouse to control paddle!", 5000);
if(animationFrameId) cancelAnimationFrame(animationFrameId);
lastTime = performance.now(); // Initialize lastTime for gameLoop
gameLoop(lastTime);
}
// --- Game Loop ---
let lastTime = 0; let animationFrameId;
let frameCount = 0; let sessionStartTime = 0;
function gameLoop(timestamp) {
animationFrameId = requestAnimationFrame(gameLoop);
if (!game.running) return;
const deltaTime = (timestamp - lastTime) / 1000; // Delta time in seconds
lastTime = timestamp;
game.gameTime += deltaTime; // Accumulate game time
if (game.paused) { return; } // Skip updates if paused
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
updateBackgroundElements(); updatePaddle(); updateBalls(); updateBoss();
updateLasers(); updateParticles(); createBallTrailParticles();
updatePowerUpsOnScreen(); updateActivePowerUps(); updateBlackHole();
drawBackgroundElements(); drawBlocks(); drawBoss(); drawPowerUpsOnScreen();
drawLasers(); drawBlackHole(); drawBallTrails(); drawBalls();
drawPaddle(); drawParticles(); drawBubbleTexts();
['score','livesLegacy','level'].forEach(id=>document.getElementById(id).textContent=game[id]);
// Combo updated in its own function
if (!game.currentBoss && blocks.length === 0 && game.running && !game.levelTransitioning) {
nextLevel(); // Async, sets game.levelTransitioning
}
frameCount++;
}
// --- Event Listeners ---
let eventController = new AbortController();
function setupEventListeners() {
eventController.abort(); eventController = new AbortController(); const {signal} = eventController;
canvas.addEventListener('mousemove',e=>{if(!game.paused){const r=canvas.getBoundingClientRect();paddle.targetX=e.clientX-r.left-paddle.width/2;mouseX=e.clientX-r.left;}},{signal}); // Store general mouseX
canvas.addEventListener('click',async ()=>{
if(!game.running||game.paused)return;
await audioManager.init(); // Ensure audio on first interaction
let bL=false;game.balls.forEach(b=>{if(b.attached){const aO=(Math.random()-0.5)*Math.PI*0.1;const bA=-Math.PI/2;b.dx=b.speed*Math.sin(bA+aO);b.dy=b.speed*Math.cos(bA+aO);b.attached=false;bL=true;}});
if(!bL&&(game.activePowerUps[POWERUP_TYPES.LASER_PADDLE]||game.activePowerUps[RARE_POWERUP_TYPES.MEGA_LASER]))fireLaser();
},{signal});
window.addEventListener('keydown',e=>{const k=e.key.toLowerCase();
if(game.currentScreen !== 'game' && !['p','m'].includes(k)) return; // Only allow game controls if game is active screen
if(k==='p'&&game.running){game.paused=!game.paused;document.getElementById('pauseMessage').style.display=game.paused?'block':'none';}
if(k==='m'){const iM=audioManager.toggleMute();showNotification(iM?"Audio Muted":"Audio Unmuted", "", iM?"๐Ÿ”‡":"๐Ÿ”Š", 1500);}
if(k==='n'&&e.shiftKey&&game.running){if(game.currentBoss)bossDefeated();else blocks=[];console.log("Debug: Level skip.");}
},{signal});
// Settings Listeners
document.getElementById('masterVolumeSlider').addEventListener('input', (e) => audioManager.setVolume('master', parseFloat(e.target.value)), {signal});
document.getElementById('sfxVolumeSlider').addEventListener('input', (e) => audioManager.setVolume('sfx', parseFloat(e.target.value)), {signal});
document.getElementById('themeSelector').addEventListener('change', (e) => {
game.currentTheme = e.target.value;
localStorage.setItem('deepSeaTheme_v2', game.currentTheme);
// Visual update will happen in drawBackgroundElements
}, {signal});
// Tooltip example (can be expanded)
const comboUI = document.querySelector('.ui div:nth-child(4)'); // Assuming Combo is 4th
if(comboUI) {
comboUI.addEventListener('mouseenter', (e) => {
const tooltip = document.getElementById('tooltip');
tooltip.textContent = "Consecutive hits score bonus points!";
tooltip.style.left = `${e.clientX + 10}px`;
tooltip.style.top = `${e.clientY - 30}px`;
tooltip.classList.add('show');
}, {signal});
comboUI.addEventListener('mouseleave', () => document.getElementById('tooltip').classList.remove('show'), {signal});
}
}
// --- Initial Setup ---
document.addEventListener('DOMContentLoaded', () => {
sessionStartTime = Date.now();
populatePaddleSkins();
// Load saved theme
game.currentTheme = localStorage.getItem('deepSeaTheme_v2') || 'deepSea';
document.getElementById('themeSelector').value = game.currentTheme;
showScreen('mainMenu'); // Show main menu first, not loading screen directly
setupEventListeners();
// Placeholder for functions that open specific dialogs (if not handled by showScreen)
window.showDifficultySelect = () => showScreen('difficultySelectScreen');
});
</script>
</body>
</html>