Spaces:
Running
Running
| <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> |