Spaces:
Sleeping
Sleeping
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Mighty Cartoon Defender</title> | |
| <style> | |
| body { | |
| background: #0a0a12; | |
| color: #e94560; | |
| text-align: center; | |
| font-family: 'Segoe UI', sans-serif; | |
| overflow-y: auto; | |
| margin: 0; | |
| } | |
| #game-container { | |
| position: relative; | |
| display: inline-block; | |
| margin-top: 20px; | |
| } | |
| .btn-row { | |
| display: flex; | |
| gap: 10px; | |
| width: 100%; | |
| margin-top: 10px; | |
| } | |
| .btn-row button { | |
| flex: 1; | |
| padding: 10px; | |
| font-size: 0.85rem; | |
| } | |
| .btn-quit { | |
| background: #1a1a2e ; | |
| border: 2px solid #e94560 ; | |
| box-shadow: none ; | |
| } | |
| .btn-quit:hover { | |
| background: #e94560 ; | |
| color: white; | |
| } | |
| canvas { | |
| background: #0f0f1b; | |
| border: 3px solid #16213e; | |
| border-radius: 12px; | |
| box-shadow: 0 0 50px rgba(233, 69, 96, 0.3); | |
| cursor: crosshair; | |
| /* Making it fill the container but maintaining the aspect ratio */ | |
| max-width: 100%; | |
| max-height: 70vh; | |
| width: auto; | |
| height: auto; | |
| touch-action: none; /* Prevents scrolling while playing on mobile */ | |
| } | |
| .chart-box { | |
| width: 100%; | |
| height: 220px; | |
| margin: 15px 0; | |
| background: rgba(22, 33, 62, 0.4); | |
| border-radius: 12px; | |
| padding: 10px; | |
| border: 1px solid #4e4e6a; | |
| } | |
| #auth-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: radial-gradient(circle, #1a1a2e 0%, #0a0a12 100%); | |
| z-index: 1000; | |
| display: flex; | |
| /* CHANGE: Stack elements vertically */ | |
| flex-direction: column; | |
| /* CHANGE: Perfect vertical and horizontal centering */ | |
| justify-content: center; | |
| align-items: center; | |
| overflow-y: auto; | |
| padding: 20px; | |
| box-sizing: border-box; | |
| } | |
| .auth-form { | |
| background: rgba(22, 33, 62, 0.8); | |
| backdrop-filter: blur(10px); | |
| padding: 30px; | |
| border-radius: 20px; | |
| border: 2px solid rgba(233, 69, 96, 0.5); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| width: 100%; | |
| /* RESTORE: Slimmer width for balance */ | |
| max-width: 340px; | |
| box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); | |
| align-items: center; | |
| /* Adding space between the header and the box */ | |
| margin-top: 20px; | |
| } | |
| #auth-header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| /* Space between text and login box */ | |
| animation: fadeInDown 1s ease-out; | |
| } | |
| #auth-header p { | |
| font-size: 1.1rem; | |
| color: #00ffcc; | |
| font-style: italic; | |
| margin-top: 10px; | |
| letter-spacing: 1px; | |
| opacity: 0.9; | |
| } | |
| #auth-header h1 { | |
| /* RESTORE: Original font size and white color with glow */ | |
| font-size: clamp(2rem, 8vw, 3.5rem); | |
| margin: 0; | |
| letter-spacing: 8px; | |
| color: #fff; | |
| text-shadow: 0 0 20px rgba(233, 69, 96, 0.8), 0 0 40px rgba(0, 255, 204, 0.3); | |
| text-align: center; | |
| } | |
| .auth-input { | |
| background: #1a1a2e; | |
| border: 1px solid #4e4e6a; | |
| color: white; | |
| padding: 12px; | |
| border-radius: 8px; | |
| width: 100%; /* Makes fields fill the slimmer box */ | |
| box-sizing: border-box; | |
| text-align: center; /* Centers the typed text/placeholder */ | |
| transition: 0.3s; | |
| } | |
| .auth-input:focus { | |
| border-color: #e94560; | |
| outline: none; | |
| box-shadow: 0 0 10px rgba(233, 69, 96, 0.3); | |
| } | |
| #auth-buttons { | |
| width: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| #auth-buttons button { | |
| width: 100%; | |
| padding: 12px; | |
| font-size: 1.1rem; | |
| border-radius: 10px; | |
| } | |
| /* Start Overlay */ | |
| #overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(10, 10, 18, 0.9); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| border-radius: 12px; | |
| z-index: 100; | |
| } | |
| button { | |
| background: #e94560; | |
| color: white; | |
| border: none; | |
| padding: 15px 50px; | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| border-radius: 50px; | |
| cursor: pointer; | |
| transition: 0.3s; | |
| box-shadow: 0 0 20px rgba(233, 69, 96, 0.5); | |
| text-transform: uppercase; | |
| } | |
| button:hover { | |
| transform: scale(1.1); | |
| background: #ff5e7e; | |
| } | |
| #ui-layer { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 10px; | |
| margin-top: 10px; | |
| } | |
| .stat-box { | |
| background: rgba(22, 33, 62, 0.8); | |
| padding: 10px 20px; | |
| border-radius: 12px; | |
| border: 1px solid #4e4e6a; | |
| min-width: 140px; | |
| } | |
| #end-mission-btn { | |
| position: relative; | |
| /* Remove absolute positioning */ | |
| padding: 12px 20px; | |
| font-size: 0.75rem; | |
| height: 55px; | |
| /* Match height of stat-boxes */ | |
| background: #e94560; | |
| border-radius: 12px; | |
| border: none; | |
| color: white; | |
| font-weight: 800; | |
| cursor: pointer; | |
| box-shadow: 0 0 15px rgba(233, 69, 96, 0.4); | |
| transition: 0.3s; | |
| letter-spacing: 1px; | |
| } | |
| #end-mission-btn:hover { | |
| background: #ff5e7e; | |
| transform: scale(1.05); | |
| } | |
| #powerup-msg { | |
| position: absolute; | |
| top: 100px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| font-size: 1.8rem; | |
| font-weight: bold; | |
| color: #00ffcc; | |
| text-shadow: 0 0 15px #000; | |
| z-index: 50; | |
| pointer-events: none; | |
| } | |
| .label { | |
| font-size: 0.7rem; | |
| color: #888; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .val { | |
| font-size: 1.3rem; | |
| font-weight: bold; | |
| display: block; | |
| margin-top: 5px; | |
| } | |
| /* Witty Commentary Style */ | |
| #witty-comment { | |
| position: absolute; | |
| bottom: 25px; | |
| right: 25px; | |
| background: rgba(0, 255, 204, 0.15); | |
| padding: 12px; | |
| border-radius: 10px; | |
| border-right: 5px solid #00ffcc; | |
| max-width: 250px; | |
| font-style: italic; | |
| color: #fff; | |
| text-shadow: 1px 1px 2px #000; | |
| z-index: 60; | |
| transition: 0.5s; | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| #weapon-ui { | |
| display: flex; | |
| flex-direction: column; | |
| /* Stacks buttons vertically */ | |
| gap: 15px; | |
| background: rgba(22, 33, 62, 0.9); | |
| padding: 20px; | |
| border-radius: 15px; | |
| border: 2px solid #4e4e6a; | |
| box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); | |
| min-width: 160px; | |
| } | |
| /* Weapon Selector UI */ | |
| #weapon-ui h3 { | |
| margin: 0 0 10px 0; | |
| font-size: 0.9rem; | |
| color: #888; | |
| letter-spacing: 2px; | |
| } | |
| .weapon-btn { | |
| background: #1a1a2e; | |
| color: #fff; | |
| border: 1px solid #4e4e6a; | |
| padding: 12px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 0.9rem; | |
| font-weight: bold; | |
| text-align: left; | |
| transition: 0.2s; | |
| } | |
| .weapon-btn.active { | |
| border-color: #00ffcc; | |
| color: #00ffcc; | |
| background: rgba(0, 255, 204, 0.1); | |
| box-shadow: 0 0 15px rgba(0, 255, 204, 0.3); | |
| } | |
| /* End Game Overlay */ | |
| #summary-screen { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 90%; | |
| max-width: 480px; | |
| /* Slightly slimmer */ | |
| max-height: 85vh; | |
| /* Prevents it from going off the screen */ | |
| background: rgba(10, 10, 18, 0.98); | |
| display: none; | |
| flex-direction: column; | |
| justify-content: flex-start; | |
| align-items: center; | |
| z-index: 9999; | |
| padding: 20px; | |
| border-radius: 20px; | |
| border: 2px solid #e94560; | |
| box-shadow: 0 0 50px rgba(0, 0, 0, 1), 0 0 20px rgba(233, 69, 96, 0.4); | |
| color: white; | |
| box-sizing: border-box; | |
| overflow-y: auto; | |
| /* Adds scrollbar if height exceeds 85vh */ | |
| } | |
| .failed-text { | |
| color: #ff4b2b ; | |
| text-shadow: 0 0 15px rgba(255, 75, 43, 0.7); | |
| animation: shake 0.5s ease-in-out; | |
| } | |
| @keyframes shake { | |
| 0%, | |
| 100% { | |
| transform: translateX(0); | |
| } | |
| 25% { | |
| transform: translateX(-5px); | |
| } | |
| 75% { | |
| transform: translateX(5px); | |
| } | |
| } | |
| /* Custom scrollbar for a techy look */ | |
| #summary-screen::-webkit-scrollbar { | |
| width: 5px; | |
| } | |
| #summary-screen::-webkit-scrollbar-thumb { | |
| background: #e94560; | |
| border-radius: 10px; | |
| } | |
| #summary-screen h1 { | |
| font-size: 1.5rem; | |
| margin: 0 0 10px 0; | |
| letter-spacing: 4px; | |
| } | |
| .chart-container { | |
| width: 100%; | |
| height: 180px; | |
| margin: 5px 0; | |
| background: rgba(22, 33, 62, 0.5); | |
| border-radius: 12px; | |
| padding: 10px; | |
| border: 1px solid #4e4e6a; | |
| box-sizing: border-box; | |
| position: relative; | |
| } | |
| #high-score-flash { | |
| color: #00ffcc; | |
| font-size: 1.2rem; | |
| margin-top: 10px; | |
| animation: blink 1s infinite; | |
| } | |
| /* Container to hold Sidebar and Canvas side-by-side */ | |
| #main-layout { | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| gap: 15px; | |
| margin-top: 15px; | |
| flex-wrap: wrap; | |
| } | |
| /* Suit Sidebar (Right Side) */ | |
| #suit-ui { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| background: rgba(22, 33, 62, 0.9); | |
| padding: 15px; | |
| border-radius: 15px; | |
| border: 1px solid #4e4e6a; | |
| min-width: 150px; | |
| } | |
| #suit-ui h3 { | |
| margin: 0 0 5px 0; | |
| font-size: 0.8rem; | |
| color: #888; | |
| letter-spacing: 2px; | |
| } | |
| #weapon-ui, #suit-ui { | |
| flex: 1; | |
| min-width: 150px; | |
| max-width: 200px; | |
| } | |
| .suit-btn { | |
| background: #1a1a2e; | |
| color: #fff; | |
| border: 1px solid #4e4e6a; | |
| padding: 10px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| font-weight: bold; | |
| transition: 0.3s; | |
| text-transform: uppercase; | |
| } | |
| .suit-btn:hover { | |
| border-color: #e94560; | |
| background: rgba(233, 69, 96, 0.1); | |
| } | |
| .suit-btn.active { | |
| border-color: #ffcc00; | |
| color: #ffcc00; | |
| background: rgba(255, 204, 0, 0.1); | |
| box-shadow: 0 0 15px rgba(255, 204, 0, 0.3); | |
| } | |
| #final-stats h2 { | |
| animation: pulseText 1.5s infinite ease-in-out; | |
| } | |
| #status-text { | |
| animation: statusPulse 2s ease-in-out infinite; | |
| } | |
| @media (max-width: 768px) { | |
| #main-layout { | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| #weapon-ui, #suit-ui { | |
| flex-direction: row; | |
| overflow-x: auto; | |
| width: 95%; | |
| min-width: unset; | |
| padding: 10px; | |
| } | |
| #weapon-ui h3, #suit-ui h3 { | |
| display: none; /* Hide titles to save space on mobile */ | |
| } | |
| } | |
| @keyframes statusPulse { | |
| 0% { | |
| transform: scale(1); | |
| opacity: 0.8; | |
| } | |
| 50% { | |
| transform: scale(1.1); | |
| opacity: 1; | |
| text-shadow: 0 0 30px currentColor; | |
| } | |
| 100% { | |
| transform: scale(1); | |
| opacity: 0.8; | |
| } | |
| } | |
| @keyframes pulseText { | |
| 0% { | |
| transform: scale(1); | |
| opacity: 0.8; | |
| } | |
| 50% { | |
| transform: scale(1.05); | |
| opacity: 1; | |
| } | |
| 100% { | |
| transform: scale(1); | |
| opacity: 0.8; | |
| } | |
| } | |
| @keyframes fadeInDown { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes blink { | |
| 50% { | |
| opacity: 0; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h2 style="letter-spacing: 5px; margin-bottom: 5px;">MIGHTY<span style="color:#00ffcc">BATTLE ENGINE</span></h2> | |
| <div id="auth-overlay"> | |
| <div id="auth-header"> | |
| <h1>MIGHTY CARTOON DEFENDER</h1> | |
| <p id="motivational-quote">"The galaxy is waiting for you… it’s calling for your raw energy, your pulse, | |
| your fire. The mission is alive, and only you can ignite it. | |
| "</p> | |
| </div> | |
| <div class="auth-form" id="auth-box"> | |
| <h2 id="auth-title" style="color: #fff; margin-bottom: 20px;">PILOT LOGIN</h2> | |
| <input type="text" id="user-in" class="auth-input" placeholder="Username"> | |
| <input type="email" id="email-in" class="auth-input" placeholder="Email Address" style="display:none;"> | |
| <input type="password" id="pass-in" class="auth-input" placeholder="Password"> | |
| <input type="password" id="confirm-pass-in" class="auth-input" placeholder="Confirm New Password" | |
| style="display:none;"> | |
| <input type="text" id="otp-in" class="auth-input" placeholder="Enter 6-Digit OTP" | |
| style="display:none; border-color: #00ffcc;"> | |
| <div id="auth-buttons" style="display:flex; gap: 10px; flex-direction: column; width: 100%;"> | |
| <button id="primary-auth-btn" onclick="processAuth()">LOGIN</button> | |
| <button id="otp-req-btn" onclick="requestOTP()" style="display:none; background:#4db8ff;">SEND | |
| OTP</button> | |
| <button id="reset-req-btn" onclick="requestReset()" | |
| style="display:none; background:#ffcc00; color:black;">REQUEST RESET</button> | |
| <p id="toggle-text" style="color: #888; font-size: 0.8rem; cursor: pointer; margin-top: 10px;"> | |
| New Pilot? <span onclick="toggleAuthMode(true)" | |
| style="color: #e94560; text-decoration: underline;">Create Account</span> | |
| </p> | |
| </div> | |
| <p id="auth-msg" style="color: #e94560; font-size: 0.8rem; margin-top: 10px;"></p> | |
| <p onclick="toggleAuthMode('forgot')" style="color: #555; font-size: 0.7rem; cursor: pointer;">Forgot | |
| Password?</p> | |
| </div> | |
| </div> | |
| <div id="ui-layer"> | |
| <div class="stat-box"><span class="label">Score</span><span id="score" class="val">0</span></div> | |
| <div class="stat-box" style="border-color: #ffcc00;"><span class="label">Level</span><span id="level-val" | |
| class="val" style="color: #ffcc00;">1</span></div> | |
| <div class="stat-box"><span class="label">Threat Level</span><span id="danger-lvl" class="val">1.0x</span></div> | |
| <button id="end-mission-btn" onclick="endGame('MISSION ABORTED')" style="display: none; margin-left: 10px;">END | |
| MISSION</button> | |
| </div> | |
| <div id="main-layout"> | |
| <div id="weapon-ui"> | |
| <h3>ARSENAL</h3> | |
| <button class="weapon-btn active" id="w1">1: PLASMA</button> | |
| <button class="weapon-btn" id="w2">2: SPREAD</button> | |
| <button class="weapon-btn" id="w3">3: FLAMETHROWER</button> | |
| <button class="weapon-btn" id="w4">4: SONIC WAVE</button> | |
| <button class="weapon-btn" id="w5">5: RAILGUN</button> | |
| <button class="weapon-btn" id="w6" style="opacity: 0.4;">6: LOCKED (500)</button> | |
| <button class="weapon-btn" id="w7" style="opacity: 0.4;">7: LOCKED (1000)</button> | |
| <button class="weapon-btn" id="w8" style="opacity: 0.4;">8: LOCKED (2000)</button> | |
| </div> | |
| <div id="game-container"> | |
| <div id="powerup-msg"></div> | |
| <div id="witty-comment">"Scanning your playstyle..."</div> | |
| <div id="overlay"> | |
| <h1 style="font-size: 3rem; color: #fff;">READY DEFENDER?</h1> | |
| <p style="color: #888; margin-bottom: 25px;">The AI adapts to your voice. Use 1-5 keys to switch guns. | |
| </p> | |
| <button onclick="startGame()">Start Mission</button> | |
| </div> | |
| <canvas id="gameCanvas" width="800" height="500"></canvas> | |
| </div> | |
| <div id="suit-ui"> | |
| <h3>SUITS</h3> | |
| <button class="suit-btn active" id="s1" onclick="changeSuit(1)">Q: Army Camo</button> | |
| <button class="suit-btn" id="s2" onclick="changeSuit(2)">W: India 🇮🇳</button> | |
| <button class="suit-btn" id="s3" onclick="changeSuit(3)">E: USA 🇺🇸</button> | |
| <button class="suit-btn" id="s4" onclick="changeSuit(4)">R: UK 🇬🇧</button> | |
| <button class="suit-btn" id="s5" onclick="changeSuit(5)">T: Brazil 🇧🇷</button> | |
| <button class="suit-btn" id="s6" style="opacity: 0.3;">Y: LOCKED (2000)</button> | |
| <button class="suit-btn" id="s7" style="opacity: 0.3;">U: LOCKED (3000)</button> | |
| <button class="suit-btn" id="s8" style="opacity: 0.3;">I: LOCKED (4500)</button> | |
| </div> | |
| <div id="status-overlay" | |
| style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(10,10,18,0.8); z-index:10000; flex-direction:column; justify-content:center; align-items:center;"> | |
| <h1 id="status-text" style="font-size: 5rem; letter-spacing: 15px; text-transform: uppercase; margin:0;"> | |
| </h1> | |
| <p id="status-subtext" style="color: #888; font-style: italic; letter-spacing: 2px; margin-top:10px;"></p> | |
| </div> | |
| <div id="level-up-flash" | |
| style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); z-index:11000; pointer-events:none; flex-direction:column; align-items:center;"> | |
| <h1 id="level-flash-text" | |
| style="font-size: 8rem; color: #ffcc00; margin:0; text-shadow: 0 0 40px #ffcc00; letter-spacing: 20px;"> | |
| </h1> | |
| <p style="color: #fff; font-size: 1.5rem; letter-spacing: 5px;">THREAT CEILING INCREASED</p> | |
| </div> | |
| <div id="summary-screen"> | |
| <h1 style="color:#e94560">MISSION BRIEFING</h1> | |
| <div id="final-stats" style="text-align: center; width: 100%;"> | |
| </div> | |
| <div id="high-score-display" style="margin: 10px 0; font-size: 0.9rem; color: #888;"></div> | |
| <div class="chart-container"> | |
| <canvas id="sessionChart"></canvas> | |
| </div> | |
| <div class="btn-row"> | |
| <button onclick="backToGame()">WANNA PLAY AGAIN?</button> | |
| <button class="btn-quit" onclick="location.reload()">QUIT TO HANGAR</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script> | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // We use that to build the API and WebSocket links dynamically | |
| const isProd = window.location.hostname !== 'localhost'; | |
| const apiUrl = isProd ? `${window.location.origin}` : `http://localhost:8000`; | |
| // --- Core State Variables --- | |
| let myChart = null | |
| let isGameActive = false; | |
| let isGameOverSequence = false; | |
| let playerName = "Pilot"; | |
| let currentLevel = 1; | |
| let levelStartTime = 0; | |
| const LEVEL_DURATION = 60; // 60 seconds (1 minute) per level | |
| let isPaused = false; | |
| let levelTimeSeconds = 0; | |
| let ceilingBonus = 0; | |
| let highScoreToBeat = 0; // The threshold for the flash message | |
| let recordBroken = false; // Prevents the flash message from repeating every frame | |
| let currentMode = 'login'; | |
| let isSignupMode = false; | |
| // --- PROGRESSION VARIABLES --- | |
| let unlockedWeapons = [1, 2, 3, 4, 5]; // Default weapons start unlocked | |
| let unlockedSuits = [1, 2, 3, 4, 5]; // Default suits start unlocked | |
| let milestonesReached = []; // Tracks which score rewards you've already claimed | |
| let grenadeCharges = 0; // Becomes 4 once score hits 1500 | |
| let bossActive = false; // Prevents multiple bosses at once | |
| let bosses = []; // Array to hold boss objects | |
| let bossSpawnedThisLevel = false; | |
| const soundEngine = { | |
| ctx: new (window.AudioContext || window.webkitAudioContext)(), | |
| play(type) { | |
| // Resume context if suspended (browser security requirement) | |
| if (this.ctx.state === 'suspended') this.ctx.resume(); | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.connect(gain); | |
| gain.connect(this.ctx.destination); | |
| const now = this.ctx.currentTime; | |
| switch (type) { | |
| case 'plasma': // High pitched short laser | |
| osc.type = 'triangle'; | |
| osc.frequency.setValueAtTime(800, now); | |
| osc.frequency.exponentialRampToValueAtTime(100, now + 0.1); | |
| gain.gain.setValueAtTime(0.2, now); | |
| gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1); | |
| osc.start(); osc.stop(now + 0.1); | |
| break; | |
| case 'spread': // Rapid low bursts | |
| osc.type = 'square'; | |
| osc.frequency.setValueAtTime(200, now); | |
| gain.gain.setValueAtTime(0.1, now); | |
| gain.gain.linearRampToValueAtTime(0, now + 0.05); | |
| osc.start(); osc.stop(now + 0.05); | |
| break; | |
| case 'flame': // Soft white noise hiss | |
| osc.type = 'sawtooth'; | |
| osc.frequency.setValueAtTime(150, now); | |
| gain.gain.setValueAtTime(0.05, now); | |
| gain.gain.linearRampToValueAtTime(0, now + 0.2); | |
| osc.start(); osc.stop(now + 0.2); | |
| break; | |
| case 'rail': // Heavy metallic "Clang" | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(1200, now); | |
| osc.frequency.exponentialRampToValueAtTime(40, now + 0.3); | |
| gain.gain.setValueAtTime(0.3, now); | |
| gain.gain.linearRampToValueAtTime(0, now + 0.3); | |
| osc.start(); osc.stop(now + 0.3); | |
| break; | |
| case 'singularity': // Deep "Wub" vacuum sound | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(50, now); | |
| osc.frequency.linearRampToValueAtTime(300, now + 0.5); | |
| gain.gain.setValueAtTime(0.4, now); | |
| gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5); | |
| osc.start(); osc.stop(now + 0.5); | |
| break; | |
| case 'lightning': // Crackling zap | |
| osc.type = 'sawtooth'; | |
| osc.frequency.setValueAtTime(2000, now); | |
| osc.frequency.setValueAtTime(500, now + 0.05); | |
| gain.gain.setValueAtTime(0.2, now); | |
| gain.gain.linearRampToValueAtTime(0, now + 0.1); | |
| osc.start(); osc.stop(now + 0.1); | |
| break; | |
| case 'batarang': // Whoosh/Whistle | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(400, now); | |
| osc.frequency.linearRampToValueAtTime(600, now + 0.15); | |
| gain.gain.setValueAtTime(0.1, now); | |
| gain.gain.linearRampToValueAtTime(0, now + 0.2); | |
| osc.start(); osc.stop(now + 0.2); | |
| break; | |
| } | |
| } | |
| }; | |
| async function handleAuth(type) { | |
| const username = document.getElementById('user-in').value; | |
| const email = document.getElementById('email-in').value; | |
| const password = document.getElementById('pass-in').value; | |
| const otp = document.getElementById('otp-in').value; | |
| const body = type === 'signup' ? { username, password, email, otp } : { username, password }; | |
| if (!username || !password) { | |
| document.getElementById('auth-msg').innerText = "Identify yourself, Pilot! (Missing credentials)"; | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`${apiUrl}/${type}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body) | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| // SUCCESS: Takes the user to the game | |
| playerName = username; | |
| // To Hide the authentication screen | |
| document.getElementById('auth-overlay').style.display = 'none'; | |
| // To Show the game sidebars and top UI | |
| document.getElementById('main-layout').style.display = 'flex'; | |
| document.getElementById('ui-layer').style.display = 'flex'; | |
| // To Show the End Mission button (ensure ID matches) | |
| const endBtn = document.getElementById('end-mission-btn') || document.getElementById('end-btn'); | |
| if (endBtn) endBtn.style.display = 'block'; | |
| syncPersonalBest(playerName); | |
| // To Show the success message in the game | |
| showMsg(result.message.toUpperCase(), "#00ffcc"); | |
| } else { | |
| // FAILURE: Show the witty error message | |
| const msgEl = document.getElementById('auth-msg'); | |
| msgEl.innerText = result.message; | |
| msgEl.style.color = "#e94560"; // Red for error | |
| } | |
| } catch (e) { | |
| document.getElementById('auth-msg').innerText = "Connection lost. Is the server running?"; | |
| } | |
| } | |
| async function requestReset() { | |
| const email = document.getElementById('email-in').value; | |
| const msgEl = document.getElementById('auth-msg'); | |
| if (!email) { | |
| msgEl.innerText = "I need a Gmail address to scan the database!"; | |
| return; | |
| } | |
| // --- STEP 1: UI FEEDBACK (Prevents the "stuck" feeling) --- | |
| msgEl.style.color = "#00ffcc"; | |
| msgEl.innerText = "Scanning deep space logs for Pilot ID..."; | |
| try { | |
| const response = await fetch(`${apiUrl}/request-reset`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email }) | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| // --- STEP 2: TRANSITION UI --- | |
| // Hide the initial reset button | |
| document.getElementById('reset-req-btn').style.display = 'none'; | |
| // Reveal the fields for the new identity | |
| document.getElementById('user-in').style.display = 'block'; | |
| document.getElementById('user-in').placeholder = "New Callsign (Username)"; | |
| document.getElementById('pass-in').style.display = 'block'; | |
| document.getElementById('pass-in').placeholder = "New Access Code (Password)"; | |
| document.getElementById('confirm-pass-in').style.display = 'block'; | |
| document.getElementById('otp-in').style.display = 'block'; | |
| // Switch the primary button to "Update" mode | |
| const primaryBtn = document.getElementById('primary-auth-btn'); | |
| primaryBtn.style.display = 'block'; | |
| primaryBtn.innerText = "RE-INITIALIZE PILOT"; | |
| msgEl.style.color = "#00ffcc"; | |
| // Displays the message from the server (Real OTP or Demo fallback) | |
| msgEl.innerText = result.message || "Pilot located. Verification code dispatched."; | |
| } else { | |
| // --- STEP 3: HANDLE FAILURE --- | |
| msgEl.style.color = "#e94560"; | |
| msgEl.innerHTML = `${result.message} <br> <span onclick="toggleAuthMode(true)" style="color:#00ffcc; cursor:pointer; text-decoration:underline;">Click here to Register</span>`; | |
| } | |
| } catch (e) { | |
| // This triggers if the server is literally unreachable | |
| msgEl.style.color = "#e94560"; | |
| msgEl.innerText = "Hangar offline: Connection to cloud link failed."; | |
| console.error("Transmission Error:", e); | |
| } | |
| } | |
| async function endGame(reason = "MISSION COMPLETE") { | |
| isGameOver = true; | |
| isGameActive = false; | |
| const activeName = (playerName && playerName !== "Pilot") ? playerName : "Captain"; | |
| totalTimePlayed = Math.floor((Date.now() - gameStartTime) / 1000) || 1; | |
| const ratio = (score / totalTimePlayed).toFixed(1); | |
| const overlay = document.getElementById('status-overlay'); | |
| const statusTextEl = document.getElementById('status-text'); | |
| if (statusTextEl) { | |
| statusTextEl.innerText = reason; | |
| statusTextEl.style.color = reason.includes("FAILED") ? "#ff4b2b" : "#00ffcc"; | |
| overlay.style.display = 'flex'; | |
| } | |
| // --- SYNC POINT: Save first, then wait --- | |
| if (score > 0) { | |
| try { | |
| // We await the save so the database is updated BEFORE we show the briefing | |
| await fetch(`${apiUrl}/save-session`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| username: activeName, | |
| score: score, | |
| duration: totalTimePlayed | |
| }) | |
| }); | |
| } catch (e) { console.error("Cloud sync failed:", e); } | |
| } | |
| setTimeout(async () => { | |
| if (overlay) overlay.style.display = 'none'; | |
| document.getElementById('summary-screen').style.display = 'flex'; | |
| // Fetch the LATEST history including the game we just finished | |
| await new Promise(resolve => requestAnimationFrame(resolve)); | |
| await renderChart(activeName); | |
| const rankTag = calculateTag(score / 10); | |
| // highScoreToBeat is now updated because renderChart was awaited | |
| const finalPB = Math.max(score, highScoreToBeat); | |
| // CLEAN INJECTION: Relying ONLY on the Database variable | |
| document.getElementById('final-stats').innerHTML = ` | |
| <h2 style="color: #ff4b2b; letter-spacing:2px; margin-bottom:5px; font-size: 1rem; opacity: 0.8;"> | |
| ${reason} | |
| </h2> | |
| <div style="color: #ffcc00; font-weight: 900; font-size: 1.6rem; margin-bottom: 15px; text-shadow: 0 0 15px #ffcc00;"> | |
| RANK: ${rankTag.toUpperCase()} | |
| </div> | |
| <div style="font-size: 2.5rem; color:#00ffcc; font-weight:900;">${score} PTS</div> | |
| <div style="color: #00ffcc; font-weight: 800; font-size: 1.2rem; margin-top: 15px; border-top: 1px solid #333; padding-top: 10px;"> | |
| PERSONAL BEST: ${finalPB} PTS | |
| </div> | |
| <div style="color: #ffcc00; font-weight: 800; font-size: 1rem; margin: 10px 0;"> | |
| FINAL LEVEL: ${currentLevel} | |
| </div> | |
| <p style="color: #888; font-size: 0.85rem;"> | |
| Efficiency: ${ratio} p/s | Mission Time: ${totalTimePlayed}s | |
| </p> | |
| `; | |
| }, 800); | |
| } | |
| // Helper to calculate the witty performance tag | |
| function calculateTag(ratio) { | |
| if (ratio < 5) return "Rookie Watcher"; | |
| if (ratio < 10) return "Casual Observer"; | |
| if (ratio < 15) return "Stellar Scout"; | |
| if (ratio < 20) return "Orbit Explorer"; | |
| if (ratio < 30) return "Space Cadet"; | |
| if (ratio < 40) return "Cosmic Ranger"; | |
| if (ratio < 50) return "Galactic Sentinel"; | |
| if (ratio < 60) return "Elite Guardian"; | |
| if (ratio < 75) return "Nova Champion"; | |
| if (ratio < 90) return "Celestial Commander"; | |
| return "Legendary Vanguard"; | |
| } | |
| async function renderChart(targetName) { | |
| const nameToFetch = targetName || playerName; | |
| const canvasEl = document.getElementById('sessionChart'); | |
| if (!canvasEl) return; | |
| const uniqueId = Date.now(); | |
| canvasEl.width = canvasEl.offsetWidth || 440; | |
| canvasEl.height = canvasEl.offsetHeight || 160; | |
| try { | |
| //const response = await fetch(`${apiUrl}/session-history/${nameToFetch}`); | |
| const response = await fetch(`${apiUrl}/session-history/${nameToFetch}?v=${uniqueId}`); | |
| const data = await response.json(); | |
| if (myChart instanceof Chart) { | |
| myChart.destroy(); | |
| } | |
| if (data.scores && data.scores.length > 0) { | |
| // Identify the absolute peak from the database history | |
| highScoreToBeat = Math.max(...data.scores); | |
| console.log("High Score synced from DB:", highScoreToBeat); | |
| } | |
| const chartCtx = canvasEl.getContext('2d'); | |
| //if (myChart) myChart.destroy(); | |
| // Check for Trendline data (needs 2 points) | |
| if (!data.scores || data.scores.length < 2) { | |
| chartCtx.font = "14px Segoe UI"; | |
| chartCtx.fillStyle = "#555"; | |
| chartCtx.textAlign = "center"; | |
| chartCtx.fillText("Mission logged. Complete 1 more to see trends!", canvasEl.width / 2, canvasEl.height / 2); | |
| return; | |
| } | |
| myChart = new Chart(chartCtx, { | |
| type: 'line', | |
| data: { | |
| labels: data.labels, | |
| datasets: [{ | |
| label: 'SCORE', | |
| data: data.scores, | |
| borderColor: '#e94560', | |
| backgroundColor: 'rgba(233, 69, 96, 0.2)', | |
| pointBackgroundColor: '#00ffcc', | |
| tension: 0.4, | |
| fill: true | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { legend: { display: false } }, | |
| scales: { | |
| y: { beginAtZero: true, grid: { color: '#222' }, ticks: { color: '#888' } }, | |
| x: { grid: { display: false }, ticks: { color: '#888', font: { size: 9 } } } | |
| } | |
| } | |
| }); | |
| } catch (e) { console.error("❌ Chart system error:", e); } | |
| } | |
| async function forgotPassword() { | |
| const email = document.getElementById('email-in').value; | |
| if (!email) { | |
| document.getElementById('auth-msg').innerText = "I need your email to find your memory files!"; | |
| return; | |
| } | |
| // This calls a route I will add to server.py | |
| document.getElementById('auth-msg').innerText = "Scanning database for your email..."; | |
| // For now, redirecting to a simple prompt or alert | |
| alert("Interstellar password recovery is being routed to: " + email); | |
| } | |
| async function requestOTP() { | |
| const email = document.getElementById('email-in').value; | |
| if (!email.includes("@") || !email.includes(".")) { | |
| document.getElementById('auth-msg').innerText = "Enter a valid email to receive your authorization code!"; | |
| return; | |
| } | |
| document.getElementById('auth-msg').style.color = "#00ffcc"; | |
| document.getElementById('auth-msg').innerText = "Transmitting OTP..."; | |
| const response = await fetch(`${apiUrl}/send-otp`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email }) | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| document.getElementById('otp-in').style.display = "block"; | |
| document.getElementById('otp-req-btn').style.display = "none"; | |
| const primaryBtn = document.getElementById('primary-auth-btn'); | |
| primaryBtn.style.display = "block"; | |
| primaryBtn.innerText = "VERIFY & SIGNUP"; | |
| document.getElementById('auth-msg').innerText = "OTP received in your hangar (inbox)!"; | |
| } else { | |
| document.getElementById('auth-msg').innerText = "Transmission failed: " + result.message; | |
| } | |
| } | |
| async function processAuth() { | |
| const username = document.getElementById('user-in').value; | |
| const password = document.getElementById('pass-in').value; | |
| const confirmPass = document.getElementById('confirm-pass-in').value; | |
| const email = document.getElementById('email-in').value; | |
| const otp = document.getElementById('otp-in').value; | |
| const msgEl = document.getElementById('auth-msg'); | |
| if (currentMode === 'forgot' && password !== confirmPass) { | |
| msgEl.innerText = "Passwords don't match!"; | |
| return; | |
| } | |
| let endpoint = currentMode === 'forgot' ? 'confirm-reset' : currentMode; | |
| // CRITICAL: Ensure username is included in all payloads to avoid "undefined" names | |
| let payload = { username, password, email, otp }; | |
| try { | |
| const response = await fetch(`${apiUrl}/${endpoint}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| // Update the global playerName so the chart knows who to fetch | |
| playerName = username || "Pilot"; | |
| if (currentMode === 'forgot') { | |
| msgEl.style.color = "#00ffcc"; | |
| msgEl.innerHTML = ` | |
| <div style="margin-bottom:15px; font-weight:bold;">PILOT RECORD UPDATED!</div> | |
| <div class="btn-row"> | |
| <button onclick="launchDirectly('${playerName}')" style="font-size:0.8rem; padding:10px;">LAUNCH GAME</button> | |
| <button class="btn-quit" onclick="location.reload()" style="font-size:0.8rem; padding:10px;">QUIT</button> | |
| </div> | |
| `; | |
| document.getElementById('auth-buttons').style.display = 'none'; | |
| return; | |
| } | |
| launchDirectly(playerName); | |
| } else { | |
| msgEl.style.color = "#e94560"; | |
| msgEl.innerText = result.message; | |
| } | |
| } catch (e) { msgEl.innerText = "Connection lost."; } | |
| } | |
| function toggleAuthMode(mode) { | |
| // Correctly setting currentMode | |
| if (typeof mode === 'boolean') { | |
| currentMode = mode ? 'signup' : 'login'; | |
| } else { | |
| currentMode = mode; | |
| } | |
| const elements = { | |
| title: document.getElementById('auth-title'), | |
| userIn: document.getElementById('user-in'), | |
| emailIn: document.getElementById('email-in'), | |
| passIn: document.getElementById('pass-in'), | |
| confirmPass: document.getElementById('confirm-pass-in'), | |
| otpIn: document.getElementById('otp-in'), | |
| primaryBtn: document.getElementById('primary-auth-btn'), | |
| otpReqBtn: document.getElementById('otp-req-btn'), | |
| resetReqBtn: document.getElementById('reset-req-btn'), | |
| toggleP: document.getElementById('toggle-text'), | |
| msg: document.getElementById('auth-msg') | |
| }; | |
| // Hide everything first | |
| [elements.userIn, elements.emailIn, elements.passIn, elements.confirmPass, elements.otpIn, elements.primaryBtn, elements.otpReqBtn, elements.resetReqBtn].forEach(el => el.style.display = 'none'); | |
| elements.msg.innerText = ""; | |
| if (currentMode === 'signup') { | |
| elements.title.innerText = "PILOT REGISTRATION"; | |
| elements.userIn.style.display = 'block'; | |
| elements.emailIn.style.display = 'block'; | |
| elements.passIn.style.display = 'block'; | |
| elements.otpReqBtn.style.display = 'block'; | |
| elements.toggleP.innerHTML = `Back to <span onclick="toggleAuthMode('login')" style="color: #e94560; text-decoration: underline;">Login</span>`; | |
| } else if (currentMode === 'forgot') { | |
| elements.title.innerText = "RECOVER PILOT ID"; | |
| elements.emailIn.style.display = 'block'; | |
| elements.resetReqBtn.style.display = 'block'; | |
| elements.toggleP.innerHTML = `Back to <span onclick="toggleAuthMode('login')" style="color: #e94560; text-decoration: underline;">Login</span>`; | |
| } else { | |
| elements.title.innerText = "PILOT LOGIN"; | |
| elements.userIn.style.display = 'block'; | |
| elements.passIn.style.display = 'block'; | |
| elements.primaryBtn.style.display = 'block'; | |
| elements.primaryBtn.innerText = "LOGIN"; | |
| elements.toggleP.innerHTML = `New Pilot? <span onclick="toggleAuthMode('signup')" style="color: #e94560; text-decoration: underline;">Create Account</span>`; | |
| } | |
| } | |
| // Helper to launch the game from the Auth screen | |
| function launchDirectly(name) { | |
| playerName = name || "Pilot"; | |
| document.getElementById('auth-overlay').style.display = 'none'; | |
| document.getElementById('main-layout').style.display = 'flex'; | |
| document.getElementById('ui-layer').style.display = 'flex'; | |
| // Ensure the End Mission button is visible | |
| const endBtn = document.getElementById('end-mission-btn'); | |
| if (endBtn) endBtn.style.display = 'block'; | |
| // Reset the "Ready Defender" overlay so they can start the mission | |
| document.getElementById('overlay').style.display = 'flex'; | |
| } | |
| function resetSidebarUI() { | |
| // Reset Weapons 6, 7, 8 | |
| const weaponsToLock = [ | |
| { id: 'w6', text: '6: LOCKED (500)' }, | |
| { id: 'w7', text: '7: LOCKED (1000)' }, | |
| { id: 'w8', text: '8: LOCKED (2000)' } | |
| ]; | |
| weaponsToLock.forEach(w => { | |
| const btn = document.getElementById(w.id); | |
| if (btn) { | |
| btn.innerText = w.text; | |
| btn.style.opacity = "0.4"; | |
| btn.style.borderColor = "#4e4e6a"; // Reset to dark border | |
| btn.classList.remove('active'); | |
| } | |
| }); | |
| // Reset Suits 6, 7, 8 | |
| const suitsToLock = [ | |
| { id: 's6', text: 'Y: LOCKED (2000)' }, | |
| { id: 's7', text: 'U: LOCKED (3000)' }, | |
| { id: 's8', text: 'I: LOCKED (4500)' } | |
| ]; | |
| suitsToLock.forEach(s => { | |
| const btn = document.getElementById(s.id); | |
| if (btn) { | |
| btn.innerText = s.text; | |
| btn.style.opacity = "0.3"; | |
| btn.style.borderColor = "#4e4e6a"; | |
| btn.classList.remove('active'); | |
| } | |
| }); | |
| // Set Weapon 1 and Suit 1 as Active | |
| document.querySelectorAll('.weapon-btn, .suit-btn').forEach(b => b.classList.remove('active')); | |
| document.getElementById('w1').classList.add('active'); | |
| document.getElementById('s1').classList.add('active'); | |
| } | |
| async function syncPersonalBest(name) { | |
| try { | |
| const response = await fetch(`${apiUrl}/session-history/${name}`); | |
| const data = await response.json(); | |
| if (data.scores && data.scores.length > 0) { | |
| // Set the global variable to the absolute maximum in the DB | |
| highScoreToBeat = Math.max(...data.scores); | |
| console.log(`🏆 Record Synced from Cloud: ${highScoreToBeat}`); | |
| } | |
| } catch (e) { | |
| console.error("Failed to sync records from hangar.", e); | |
| } | |
| } | |
| function resizeCanvas() { | |
| const container = document.getElementById('game-container'); | |
| const ratio = 800 / 500; // Original aspect ratio | |
| let newWidth = window.innerWidth * 0.9; | |
| let newHeight = newWidth / ratio; | |
| if (newHeight > window.innerHeight * 0.6) { | |
| newHeight = window.innerHeight * 0.6; | |
| newWidth = newHeight * ratio; | |
| } | |
| canvas.style.width = `${newWidth}px`; | |
| canvas.style.height = `${newHeight}px`; | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| resizeCanvas(); // Calling on load | |
| // Helper to go from Summary Screen back to the Start Mission overlay | |
| function backToGame() { | |
| recordBroken = false; | |
| // --- UI Screen Resets --- | |
| isGameOver = false; | |
| isPaused = false; | |
| document.getElementById('summary-screen').style.display = 'none'; | |
| document.getElementById('overlay').style.display = 'flex'; | |
| // --- Threat & Difficulty Reset --- | |
| score = 0; | |
| timeElapsed = 0; | |
| baseDifficulty = 1.0; | |
| ddaMultiplier = 1.0; | |
| currentLevel = 1; | |
| ceilingBonus = 0; | |
| // --- Progression & Arsenal RESET (THE FIX) --- | |
| unlockedWeapons = [1, 2, 3, 4, 5]; // Back to basic weapons only | |
| unlockedSuits = [1, 2, 3, 4, 5]; // Back to basic suits only | |
| milestonesReached = []; // Clear memory of reached milestones | |
| grenadeCharges = 0; | |
| currentWeapon = 1; // Force reset to Plasma Gun | |
| currentSuit = 1; // Force reset to Army Camo | |
| if (window.myChart instanceof Chart) { | |
| window.myChart.destroy(); | |
| window.myChart = null; | |
| } | |
| // --- Reset HUD visuals --- | |
| document.getElementById('score').innerText = "0"; | |
| document.getElementById('level-val').innerText = "1"; | |
| document.getElementById('danger-lvl').innerText = "1.0x (0s)"; | |
| const gHUD = document.getElementById('grenade-count'); | |
| if (gHUD) gHUD.innerText = "0"; | |
| // --- Boss & State Resets --- | |
| bossActive = false; | |
| bossSpawnedThisLevel = false; | |
| bosses = []; | |
| enemies = []; | |
| bullets = []; | |
| // --- RE-LOCK THE SIDEBARS --- | |
| resetSidebarUI(); | |
| // Set clock for the new session | |
| levelStartTime = Date.now(); | |
| gameStartTime = Date.now(); | |
| syncPersonalBest(playerName); | |
| } | |
| let score = 0, timeElapsed = 0, baseDifficulty = 1.0, ddaMultiplier = 1.0, evolutionTimer = 0; | |
| let player = { x: 400, y: 440, size: 50, speed: 10 }; | |
| let enemies = [], bullets = [], particles = [], stars = []; | |
| let hasShield = false, tripleShotTimer = 0; | |
| const ENEMY_TYPES = ["👾", "👹", "👻", "🦇", "🛸", "🐉", "🦈", "🪬", "🌪️", "🧟"]; | |
| const keys = { ArrowLeft: false, ArrowRight: false, ArrowUp: false, ArrowDown: false, Space: false }; | |
| let superItems = []; // To track the Chrono-Crystals | |
| let timeWarpTimer = 0; // Timer for the superpower effect | |
| let currentWeapon = 1; | |
| let gameStartTime = 0; | |
| let totalTimePlayed = 0; | |
| let isGameOver = false; | |
| let currentSuit = 1; | |
| // Storage Logic | |
| const savedData = JSON.parse(localStorage.getItem('adaptiveGameStats')) || { score: 0, time: 0 }; | |
| const WITTY_REMARKS = [ | |
| "Are we playing or just watching the stars?", | |
| "Impressive! Even the AI is taking notes.", | |
| "You call that a shot? My grandma aim better.", | |
| "Detected a slight sarcasm in your playstyle...", | |
| "Threat level rising... unlike your score.", | |
| "Wow, you're actually good. I'm shocked.", | |
| "Is the game too easy? Careful what you wish for.", | |
| "I've seen smarter toasters than these enemies.", "Your ship's name must be 'Lag' because it's always behind.", | |
| "If you were any slower, you'd be going backwards.", "Your reflexes are so bad, even a snail would win.", | |
| "Are you sure you're not controlling the ship with a potato?", "Your score is so low, it's in the negatives. Are you trying to lose?", | |
| "I hope you have a good insurance policy for that ship.", "Your enemies are so confused, they think you're a friendly NPC.", "I didn't know it was possible to miss that many shots.", | |
| "Your ship's motto must be 'Better Luck Next Time'.", "Are you sure you're not playing with a controller from the 90s?", "Your ship's name must be 'S.O.S' because it's always in distress.", | |
| "Your score is so low, it's in the single digits. Are you sure you're trying to win?", "Your enemies are so confused, they think you're a friendly NPC.", "I didn't know it was possible to miss that many shots.", | |
| "Your ship's motto must be 'Better Luck Next Time'.", "Are you sure you're not playing with a controller from the 90s?", "Your ship's name must be 'S.O.S' because it's always in distress.", | |
| "Are you sure you're trying to win?", "Your enemies are so confused, they think you're a friendly NPC.", "I didn't know it was possible to miss that many shots.", | |
| "Your ship's motto must be 'Better Luck Next Time'.", "Are you sure you're not playing with a controller from the 90s?", "Your ship's name must be 'S.O.S' because it's always in distress." | |
| ]; | |
| // --- Background Setup --- | |
| for (let i = 0; i < 100; i++) stars.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, size: Math.random() * 2, speed: Math.random() * 3 + 0.5 }); | |
| function changeSuit(id) { | |
| currentSuit = id; | |
| document.querySelectorAll('.suit-btn').forEach(b => b.classList.remove('active')); | |
| document.getElementById('s' + id).classList.add('active'); | |
| } | |
| function drawSpaceship(x, y, suit) { | |
| ctx.save(); | |
| ctx.translate(x + 25, y + 25); | |
| // 1. Draw Fins (Constant Shape) | |
| ctx.fillStyle = "#333"; | |
| ctx.beginPath(); | |
| ctx.moveTo(-25, 20); ctx.lineTo(0, -10); ctx.lineTo(25, 20); ctx.fill(); | |
| // 2. Draw Main Body (Constant Shape, Dynamic Costume) | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -25); | |
| ctx.bezierCurveTo(-15, -10, -15, 20, 0, 25); | |
| ctx.bezierCurveTo(15, 20, 15, -10, 0, -25); | |
| ctx.clip(); // Only paint inside the rocket body | |
| // 3. APPLY COSTUMES | |
| if (suit === 1) { // Army Camo | |
| ctx.fillStyle = "#4b5320"; ctx.fillRect(-20, -30, 40, 60); | |
| ctx.fillStyle = "#2b3014"; ctx.beginPath(); ctx.arc(-5, -5, 10, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = "#6b8e23"; ctx.beginPath(); ctx.arc(8, 10, 8, 0, Math.PI * 2); ctx.fill(); | |
| } else if (suit === 2) { // India 🇮🇳 | |
| ctx.fillStyle = "#FF9933"; ctx.fillRect(-20, -30, 40, 20); | |
| ctx.fillStyle = "#FFFFFF"; ctx.fillRect(-20, -10, 40, 20); | |
| ctx.fillStyle = "#138808"; ctx.fillRect(-20, 10, 40, 20); | |
| ctx.strokeStyle = "#000080"; ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI * 2); ctx.stroke(); | |
| } else if (suit === 3) { // USA 🇺🇸 | |
| ctx.fillStyle = "#B22234"; ctx.fillRect(-20, -30, 40, 60); | |
| for (let i = 0; i < 5; i++) { ctx.fillStyle = "#fff"; ctx.fillRect(-20, -30 + (i * 12), 40, 6); } | |
| ctx.fillStyle = "#3C3B6E"; ctx.fillRect(-20, -30, 20, 25); | |
| } else if (suit === 4) { // UK 🇬🇧 | |
| ctx.fillStyle = "#012169"; ctx.fillRect(-20, -30, 40, 60); | |
| ctx.fillStyle = "white"; ctx.fillRect(-20, -3, 40, 6); ctx.fillRect(-3, -30, 6, 60); | |
| ctx.fillStyle = "#C8102E"; ctx.fillRect(-20, -1.5, 40, 3); ctx.fillRect(-1.5, -30, 3, 60); | |
| } else if (suit === 5) { // Brazil 🇧🇷 | |
| ctx.fillStyle = "#009739"; ctx.fillRect(-20, -30, 40, 60); | |
| ctx.fillStyle = "#FEDD00"; ctx.beginPath(); ctx.moveTo(0, -15); ctx.lineTo(15, 0); ctx.lineTo(0, 15); ctx.lineTo(-15, 0); ctx.fill(); | |
| ctx.fillStyle = "#012169"; ctx.beginPath(); ctx.arc(0, 0, 6, 0, Math.PI * 2); ctx.fill(); | |
| } | |
| else if (suit === 6) { // IRON AVENGER | |
| ctx.fillStyle = "#B22234"; ctx.fillRect(-20, -30, 40, 60); // Red | |
| ctx.fillStyle = "#D4AF37"; ctx.fillRect(-10, -20, 20, 40); // Gold | |
| ctx.fillStyle = "#00f2ff"; ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI * 2); ctx.fill(); // Reactor | |
| } else if (suit === 7) { // DARK KNIGHT | |
| ctx.fillStyle = "#1a1a1a"; ctx.fillRect(-22, -30, 44, 60); // Black | |
| ctx.fillStyle = "#ffcc00"; ctx.beginPath(); ctx.ellipse(0, 5, 12, 6, 0, 0, Math.PI * 2); ctx.fill(); // Oval | |
| } else if (suit === 8) { // COSMIC GUARDIAN | |
| ctx.fillStyle = "#4B0082"; ctx.fillRect(-20, -30, 40, 60); // Indigo | |
| ctx.fillStyle = "white"; ctx.fillRect(Math.random() * 10 - 5, Math.random() * 10 - 5, 2, 2); // Stars | |
| } | |
| ctx.restore(); | |
| // Draw the Cockpit (Glass) | |
| ctx.fillStyle = "rgba(255,255,255,0.5)"; | |
| ctx.beginPath(); ctx.arc(x + 25, y + 15, 6, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = "white"; | |
| ctx.font = "bold 12px Segoe UI"; | |
| ctx.textAlign = "center"; | |
| const nameToDisplay = (playerName || "PILOT").toUpperCase(); | |
| ctx.fillText(nameToDisplay, x + 25, y - 10); | |
| ctx.textAlign = "left"; // Reset for other drawings | |
| } | |
| function checkUnlocks() { | |
| const milestones = [ | |
| { score: 500, type: 'weapon', id: 6, key: '6', name: 'NOVA GRENADE', msg: 'Congrats Pilot! Nova Grenade Unlocked (Press 6)' }, | |
| { score: 1000, type: 'weapon', id: 7, key: '7', name: 'MJOLNIR STRIKE', msg: 'New Weapon! Lightning Power Unlocked (Press 7)' }, | |
| { score: 2000, type: 'both', wId: 8, sId: 6, wKey: '8', sKey: 'Y', wName: 'BATARANG', sName: 'IRON AVENGER', msg: "Level Up, Pilot! New Suit (Y) and Weapon (8) Unlocked" }, | |
| { score: 3000, type: 'suit', id: 7, key: 'U', name: 'DARK KNIGHT', msg: 'Congrats Pilot! New Suit Unlocked (Press U)' }, | |
| { score: 4500, type: 'suit', id: 8, key: 'I', name: 'COSMIC GUARDIAN', msg: 'LEGENDARY! Final Cosmic Suit Unlocked (Press I)' } | |
| ]; | |
| milestones.forEach(m => { | |
| // Use the 'name' property to check if this specific milestone was reached | |
| if (score >= m.score && !milestonesReached.includes(m.name)) { | |
| milestonesReached.push(m.name); | |
| showMsg(m.msg, "#ffcc00"); | |
| if (m.type === 'weapon' || m.type === 'both') { | |
| const wid = m.wId || m.id; | |
| if (!unlockedWeapons.includes(wid)) unlockedWeapons.push(wid); | |
| const btn = document.getElementById('w' + wid); | |
| if (btn) { | |
| btn.innerText = `${m.wKey || m.key}: ${m.wName || m.name}`; | |
| btn.style.opacity = "1"; | |
| btn.style.borderColor = "#00ffcc"; | |
| btn.style.pointerEvents = "auto"; // Enable clicking | |
| } | |
| if (wid === 6) { | |
| grenadeCharges = 4; | |
| const gHUD = document.getElementById('grenade-count'); | |
| if (gHUD) gHUD.innerText = "4"; | |
| } | |
| } | |
| if (m.type === 'suit' || m.type === 'both') { | |
| const sid = m.sId || m.id; | |
| if (!unlockedSuits.includes(sid)) unlockedSuits.push(sid); | |
| const btn = document.getElementById('s' + sid); | |
| if (btn) { | |
| btn.innerText = `${m.sKey || m.key}: ${m.sName || m.name}`; | |
| btn.style.opacity = "1"; | |
| btn.style.borderColor = "#ffcc00"; | |
| btn.style.pointerEvents = "auto"; | |
| btn.onclick = () => changeSuit(sid); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function startGame() { | |
| document.getElementById('overlay').style.display = 'none'; | |
| isGameActive = true; | |
| gameStartTime = Date.now(); // SETTING THE START TIME | |
| levelStartTime = Date.now(); // Initialize the level clock | |
| setupMic(); | |
| } | |
| function showMsg(txt, color) { | |
| const el = document.getElementById('powerup-msg'); | |
| el.innerText = txt; el.style.color = color; | |
| setTimeout(() => el.innerText = "", 3000); | |
| } | |
| function triggerWittyComment() { | |
| const el = document.getElementById('witty-comment'); | |
| el.innerText = `"${WITTY_REMARKS[Math.floor(Math.random() * WITTY_REMARKS.length)]}"`; | |
| el.style.opacity = 1; | |
| setTimeout(() => el.style.opacity = 0, 4000); // Hide after 4 seconds | |
| } | |
| // --- Visual Effects System --- | |
| function createExplosion(x, y, color) { | |
| soundEngine.play('spread'); | |
| for (let i = 0; i < 12; i++) { | |
| particles.push({ | |
| x, y, | |
| vx: (Math.random() - 0.5) * 12, | |
| vy: (Math.random() - 0.5) * 12, | |
| life: 40, | |
| color | |
| }); | |
| } | |
| } | |
| function renderCanvasGameOver() { | |
| // Dim the background slightly | |
| ctx.fillStyle = "rgba(0, 0, 0, 0.4)"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Style the text | |
| ctx.save(); | |
| ctx.fillStyle = "#ff4b2b"; | |
| ctx.font = "bold 70px Segoe UI"; | |
| ctx.textAlign = "center"; | |
| ctx.shadowBlur = 25; | |
| ctx.shadowColor = "#ff4b2b"; | |
| // Draw the main text | |
| ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2 + 20); | |
| // Adding a smaller sub-text | |
| ctx.shadowBlur = 0; | |
| ctx.fillStyle = "#fff"; | |
| ctx.font = "20px Segoe UI"; | |
| ctx.fillText("CRITICAL HULL FAILURE DETECTED", canvas.width / 2, canvas.height / 2 + 60); | |
| ctx.restore(); | |
| } | |
| function shoot() { | |
| if (!isGameActive || isGameOver) return; | |
| const yPos = player.y - 30; | |
| if (tripleShotTimer > 0) { | |
| // Wider spread for better visibility | |
| bullets.push({ x: player.x - 20, y: player.y - 20 }); | |
| bullets.push({ x: player.x + 15, y: player.y - 45 }); | |
| bullets.push({ x: player.x + 50, y: player.y - 20 }); | |
| } else { | |
| bullets.push({ x: player.x + 15, y: player.y - 35 }); | |
| } | |
| switch (currentWeapon) { | |
| case 1: // Plasma | |
| bullets.push({ x: player.x + 25, y: yPos, type: 'standard' }); | |
| soundEngine.play('plasma'); | |
| break; | |
| case 2: // Spread | |
| bullets.push({ x: player.x, y: yPos, type: 'spread' }); | |
| bullets.push({ x: player.x + 25, y: yPos - 15, type: 'spread' }); | |
| bullets.push({ x: player.x + 50, y: yPos, type: 'spread' }); | |
| soundEngine.play('spread'); | |
| break; | |
| case 3: // Flamethrower | |
| for (let i = 0; i < 4; i++) { // Fire a burst | |
| bullets.push({ x: player.x + 20 + Math.random() * 10, y: yPos, type: 'flame', life: 25 }); | |
| soundEngine.play('flame'); | |
| } | |
| break; | |
| case 4: // Sonic Wave | |
| bullets.push({ x: player.x + 25, y: yPos, type: 'sonic', width: 40 }); | |
| soundEngine.play('sonic'); | |
| break; | |
| case 5: // Railgun | |
| bullets.push({ x: player.x + 25, y: yPos, type: 'rail' }); | |
| soundEngine.play('rail'); | |
| break; | |
| case 6: // NOVA GRENADE: Creates a Singularity | |
| if (grenadeCharges > 0) { | |
| soundEngine.play('singularity'); | |
| bullets.push({ | |
| x: player.x + 25, y: player.y - 50, | |
| type: 'singularity', | |
| radius: 10, | |
| life: 120, // Duration of the "pull" effect | |
| maxRadius: 150 | |
| }); | |
| grenadeCharges--; | |
| document.getElementById('grenade-count').innerText = grenadeCharges; | |
| } | |
| break; | |
| case 7: // MJOLNIR: Chain Lightning | |
| // Fires a seed that "jumps" between targets | |
| bullets.push({ | |
| x: player.x + 25, y: yPos, | |
| type: 'lightning_seed', | |
| targetsHit: [], | |
| jumpCount: 0 | |
| }); | |
| soundEngine.play('lightning'); | |
| break; | |
| case 8: // BATARANG: Boomerang Physics | |
| bullets.push({ | |
| x: player.x + 25, y: yPos, | |
| startX: player.x + 25, | |
| type: 'batarang', | |
| state: 'forward', // forward -> returning | |
| dist: 0, | |
| angle: 0 | |
| }); | |
| soundEngine.play('batarang'); | |
| break; | |
| default: // Standard 1-5 Weapons | |
| bullets.push({ x: player.x + 25, y: yPos, type: 'standard' }); | |
| soundEngine.play('plasma'); | |
| } | |
| if (currentWeapon === 6 && grenadeCharges > 0) { | |
| bullets.push({ x: player.x + 25, y: player.y, type: 'grenade', color: '#00ffcc' }); | |
| grenadeCharges--; | |
| return; | |
| } | |
| } | |
| // --- Controls --- | |
| window.addEventListener('mousemove', (e) => { | |
| if (!isGameActive) return; | |
| const rect = canvas.getBoundingClientRect(); | |
| player.x = e.clientX - rect.left - 25; | |
| }); | |
| window.addEventListener('keydown', (e) => { | |
| if (keys.hasOwnProperty(e.code)) { keys[e.code] = true; if (e.code === "Space") shoot(); e.preventDefault(); } | |
| }); | |
| // Add Keyboard listeners for switching weapons | |
| window.addEventListener('keydown', (e) => { | |
| // 1. Setup Data | |
| const key = (e.key || "").toLowerCase(); | |
| // --- PAUSE LOGIC --- | |
| if (key === 'p') { | |
| if (isGameActive && !isGameOverSequence && !isGameOver) { | |
| isPaused = !isPaused; | |
| if (isPaused) { | |
| showMsg("SYSTEM PAUSED", "#ffcc00"); | |
| } else { | |
| showMsg("RESUMING MISSION", "#00ffcc"); | |
| } | |
| } | |
| return; | |
| } | |
| if (isPaused) return; | |
| // Mapping keys to Weapon IDs | |
| const weaponMap = { | |
| '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, | |
| '6': 6, 'g': 6, // Both 6 and G work for Grenade | |
| '7': 7, '8': 8 | |
| }; | |
| // Mapping keys to Suit IDs | |
| const suitMap = { | |
| 'q': 1, 'w': 2, 'e': 3, 'r': 4, 't': 5, | |
| 'y': 6, 'u': 7, 'i': 8 | |
| }; | |
| // Handle Weapon Switching | |
| if (weaponMap[key]) { | |
| const weaponId = weaponMap[key]; | |
| if (unlockedWeapons.includes(weaponId)) { | |
| currentWeapon = weaponId; | |
| // UI Update: Arsenal Sidebar | |
| document.querySelectorAll('.weapon-btn').forEach(b => b.classList.remove('active')); | |
| // Use the weaponId to find the button (since key might be 'g') | |
| const btn = document.getElementById('w' + weaponId); | |
| if (btn) { | |
| btn.classList.add('active'); | |
| // Display the name of the weapon from the button text | |
| const weaponName = btn.innerText.split(': ')[1] || "SYSTEM ARSENAL"; | |
| showMsg("WEAPON: " + weaponName, "#00ffcc"); | |
| } | |
| } else { | |
| showMsg("WEAPON LOCKED! REACH TARGET SCORE.", "#ff4b2b"); | |
| } | |
| } | |
| // Handle Suit Switching | |
| if (suitMap[key]) { | |
| const suitId = suitMap[key]; | |
| if (unlockedSuits.includes(suitId)) { | |
| changeSuit(suitId); // This function handles the UI active class and messages | |
| } else { | |
| showMsg("SUIT LOCKED! KEEP FIGHTING.", "#ff4b2b"); | |
| } | |
| } | |
| // Handle Combat | |
| if (e.code === "Space") { | |
| shoot(); | |
| e.preventDefault(); // Prevents page scrolling when playing | |
| } | |
| }); | |
| window.addEventListener('keyup', (e) => { if (keys.hasOwnProperty(e.code)) keys[e.code] = false; }); | |
| window.addEventListener('mousedown', () => shoot()); | |
| canvas.addEventListener('touchstart', (e) => { | |
| if (!isGameActive) return; | |
| e.preventDefault(); // Prevents zooming/scrolling | |
| shoot(); | |
| }, { passive: false }); | |
| window.addEventListener('deviceorientation', (event) => { | |
| if (!isGameActive || isPaused) return; | |
| // Detect if we are in Landscape (horizontal) or Portrait (vertical) | |
| const isLandscape = window.innerWidth > window.innerHeight; | |
| // In Landscape, 'beta' usually handles the left/right tilt | |
| // In Portrait, 'gamma' handles it. | |
| let tilt = isLandscape ? event.beta : event.gamma; | |
| if (tilt !== null) { | |
| // --- DEAD ZONE --- | |
| // Prevents the ship from "crawling" when the phone is mostly flat | |
| if (Math.abs(tilt) < 5) return; | |
| // --- AXIS NORMALIZATION --- | |
| // If in landscape, beta ranges from -90 to 90. | |
| // We constrain it to a 30-degree "active window" for responsive movement. | |
| let constrainedTilt = Math.max(-30, Math.min(30, tilt)); | |
| // --- MOVEMENT CALCULATION --- | |
| // Increase 1.5 to 2.0 if you want the ship to be even faster/more sensitive | |
| let moveX = (constrainedTilt / 30) * (player.speed * 1.8); | |
| player.x += moveX; | |
| // --- BOUNDARY GUARD --- | |
| // Keeps the ship from getting stuck off-screen | |
| if (player.x < 0) player.x = 0; | |
| if (player.x > canvas.width - player.size) player.x = canvas.width - player.size; | |
| } | |
| }); | |
| function createEnemy(x, y, type = "normal") { | |
| const danger = baseDifficulty * ddaMultiplier; | |
| // Evolution logic: Change icon based on threat level | |
| let icon = ENEMY_TYPES[Math.floor(Math.random() * ENEMY_TYPES.length)]; // Default random | |
| //if (danger > 1.8) icon = "👹"; | |
| //if (danger > 3.0) icon = "🐉"; | |
| return { | |
| x: x, | |
| y: y, | |
| size: 40, | |
| icon: icon, | |
| speed: (Math.random() * 2 + 1) * danger, | |
| evoType: type | |
| }; | |
| } | |
| function spawnBoss() { | |
| bossActive = true; | |
| bosses.push({ | |
| x: canvas.width / 2 - 50, | |
| y: -100, | |
| hp: 40 + (currentLevel * 10), | |
| maxHp: 40 + (currentLevel * 10), | |
| size: 100, | |
| icon: "👺", | |
| vx: 2 | |
| }); | |
| showMsg("WARNING: BOSS DETECTED!", "#ff4b2b"); | |
| } | |
| function update() { | |
| // 1. CLEAR & BACKGROUND | |
| ctx.fillStyle = "#0a0a12"; ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // 2. STARS | |
| ctx.fillStyle = "white"; | |
| stars.forEach(s => { | |
| s.y += s.speed; if (s.y > canvas.height) s.y = 0; | |
| ctx.fillRect(s.x, s.y, s.size, s.size); | |
| }); | |
| // PAUSE OVERLAY | |
| if (isPaused) { | |
| ctx.fillStyle = "rgba(0, 0, 0, 0.6)"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.fillStyle = "#ffcc00"; | |
| ctx.font = "bold 50px Segoe UI"; | |
| ctx.textAlign = "center"; | |
| ctx.fillText("PAUSED", canvas.width / 2, canvas.height / 2); | |
| ctx.font = "20px Segoe UI"; | |
| ctx.fillStyle = "#fff"; | |
| ctx.fillText("PRESS 'P' TO RESUME", canvas.width / 2, canvas.height / 2 + 40); | |
| ctx.textAlign = "left"; // Reset for other drawings | |
| requestAnimationFrame(update); | |
| return; | |
| } | |
| // NEW: If we are in the Game Over sequence, draw the text and stay here | |
| if (isGameOverSequence) { | |
| renderCanvasGameOver(); | |
| requestAnimationFrame(update); | |
| return; | |
| } | |
| if (!isGameActive) { | |
| requestAnimationFrame(update); | |
| return; | |
| } | |
| // PROGRESSION & UNLOCKS | |
| timeElapsed++; | |
| checkUnlocks(); // CALL THE UNLOCK CHECKER HERE | |
| // --- Boss & Level Progression Logic --- | |
| let levelTime = Math.floor((Date.now() - levelStartTime) / 1000); | |
| // Spawning boss at 50 seconds if it's a boss level (every 3rd level) and it hasn't spawned yet | |
| if (currentLevel % 3 === 1 && !bossSpawnedThisLevel && levelTime >= 50) { | |
| spawnBoss(); | |
| bossSpawnedThisLevel = true; | |
| } | |
| // DRAW & UPDATE BOSSES | |
| bosses.forEach((b, bi) => { | |
| b.y += 0.5; | |
| b.x += b.vx; | |
| if (b.x > canvas.width - 100 || b.x < 0) b.vx *= -1; | |
| ctx.font = "80px Arial"; | |
| ctx.fillText(b.icon, b.x, b.y); | |
| // Boss Health Bar | |
| ctx.fillStyle = "#333"; | |
| ctx.fillRect(b.x, b.y - 40, 100, 10); | |
| ctx.fillStyle = "#ff4b2b"; | |
| ctx.fillRect(b.x, b.y - 40, (b.hp / b.maxHp) * 100, 10); | |
| // CRITICAL: Player vs Boss Collision | |
| // Using a slightly tighter bounding box for the boss (size 100) vs player (size 50) | |
| if (Math.abs((player.x + 25) - (b.x + 50)) < 60 && Math.abs((player.y + 25) - b.y) < 60) { | |
| if (!hasShield) { | |
| isGameActive = false; | |
| isGameOverSequence = true; | |
| createExplosion(player.x + 25, player.y + 25, "#ff4b2b"); | |
| createExplosion(b.x + 50, b.y, "#ffcc00"); | |
| setTimeout(() => { | |
| isGameOverSequence = false; | |
| endGame("KILLED BY THE BOSS: MISSION FAILED"); | |
| }, 2000); | |
| return; | |
| } else { | |
| // Shield survival | |
| hasShield = false; | |
| showMsg("SHIELD SHATTERED BY BOSS!", "#fff"); | |
| createExplosion(player.x + 25, player.y + 25, "#00ffcc"); | |
| // Push boss back slightly so you don't get hit twice | |
| b.y -= 50; | |
| } | |
| } | |
| // Collision with bullets | |
| bullets.forEach((bul, bui) => { | |
| if (Math.abs(bul.x - (b.x + 50)) < 50 && Math.abs(bul.y - b.y) < 50) { | |
| b.hp -= 1; | |
| bullets.splice(bui, 1); | |
| if (b.hp <= 0) { | |
| createExplosion(b.x + 50, b.y, "#ffcc00"); | |
| bosses.splice(bi, 1); | |
| bossActive = true; // Set to true so another doesn't spawn this level | |
| score += 1000; | |
| showMsg("BOSS ELIMINATED! +1000 PTS", "#00ffcc"); | |
| } | |
| } | |
| }); | |
| // Boss Passes By (The "No Problem" Logic) | |
| if (b.y > canvas.height + 100) { | |
| bosses.splice(bi, 1); | |
| bossActive = false; // Setting this to false allows level progression to resume | |
| showMsg("BOSS RETREATED. LEVEL CLEAR!...", "#888"); | |
| } | |
| }); | |
| // TIMERS & DIFFICULTY | |
| timeElapsed++; | |
| if (evolutionTimer > 0) evolutionTimer--; | |
| if (tripleShotTimer > 0) tripleShotTimer--; | |
| if (timeWarpTimer > 0) timeWarpTimer--; | |
| // LEVEL PROGRESSION | |
| let totalElapsedSeconds = Math.floor((Date.now() - levelStartTime) / 1000); | |
| /*if (totalElapsedSeconds >= LEVEL_DURATION) { | |
| currentLevel++; | |
| levelStartTime = Date.now(); | |
| triggerLevelFlash(currentLevel); | |
| document.getElementById('level-val').innerText = currentLevel; | |
| bossActive = false; // Reset boss flag for next cycle | |
| }*/ | |
| if (levelTime >= 60 && bosses.length === 0) { | |
| currentLevel++; | |
| levelStartTime = Date.now(); // Reset level timer | |
| bossSpawnedThisLevel = false; // Allow boss to spawn in the next cycle | |
| ceilingBonus = 0; // Reset emotional ceiling expansion for new level | |
| // UI Updates | |
| document.getElementById('level-val').innerText = currentLevel; | |
| triggerLevelFlash(currentLevel); // Show the "Welcome" message | |
| } | |
| const currentCeiling = (1.0 + (currentLevel * 0.5)) + ceilingBonus; | |
| // Apply DDA but cap it at the Level's Maximum Threat | |
| let calculatedThreat = Math.min(baseDifficulty * ddaMultiplier, currentCeiling); | |
| if (calculatedThreat > currentCeiling) calculatedThreat = currentCeiling; | |
| if (timeElapsed % 600 === 0) baseDifficulty += 0.1; | |
| const currentDanger = baseDifficulty * ddaMultiplier; | |
| const displayThreat = calculatedThreat.toFixed(2); | |
| const currentSessionTime = Math.floor((Date.now() - gameStartTime) / 1000); | |
| //document.getElementById('danger-lvl').innerText = currentDanger.toFixed(1) + "x"; | |
| document.getElementById('danger-lvl').innerText = `${displayThreat}x (Max: ${currentCeiling.toFixed(2)}x)`; | |
| // PLAYER MOVEMENT (Keyboard) | |
| if (keys.ArrowLeft && player.x > 0) player.x -= player.speed; | |
| if (keys.ArrowRight && player.x < canvas.width - player.size) player.x += player.speed; | |
| // BULLET LOGIC (Movement & Unique Visuals) | |
| bullets.forEach((b, i) => { | |
| ctx.save(); | |
| if (b.type === 'flame') { | |
| b.y -= 7; b.x += (Math.random() - 0.5) * 15; b.life--; | |
| let size = 10 + (25 - b.life); | |
| ctx.shadowBlur = 20; ctx.shadowColor = "orange"; | |
| ctx.fillStyle = `rgba(255, ${b.life * 5}, 0, ${b.life / 25})`; | |
| ctx.beginPath(); ctx.arc(b.x, b.y, size / 2, 0, Math.PI * 2); ctx.fill(); | |
| if (b.life <= 0) bullets.splice(i, 1); | |
| } else if (b.type === 'rail') { | |
| b.y -= 25; ctx.shadowBlur = 25; ctx.shadowColor = "#00f2ff"; | |
| ctx.strokeStyle = "#00f2ff"; ctx.lineWidth = 4; | |
| ctx.beginPath(); ctx.moveTo(b.x, b.y); ctx.lineTo(b.x, b.y + 30); ctx.stroke(); | |
| } else if (b.type === 'sonic') { | |
| b.y -= 5; b.width += 2; | |
| ctx.strokeStyle = `rgba(0, 255, 204, 0.5)`; ctx.lineWidth = 3; | |
| ctx.beginPath(); ctx.ellipse(b.x, b.y, b.width / 2, 10, 0, 0, Math.PI * 2); ctx.stroke(); | |
| } else if (b.type === 'spread') { | |
| b.y -= 15; ctx.fillStyle = "#ffff00"; | |
| ctx.beginPath(); ctx.moveTo(b.x, b.y); ctx.lineTo(b.x - 5, b.y + 10); ctx.lineTo(b.x + 5, b.y + 10); ctx.fill(); | |
| } | |
| else if (b.type === 'singularity') { | |
| // --- WEAPON 6: VORTEX EFFECT --- | |
| b.life--; | |
| // Pulsing glow | |
| ctx.shadowBlur = 30; | |
| ctx.shadowColor = "#00ffcc"; | |
| ctx.fillStyle = `rgba(0, 255, 204, ${b.life / 120})`; | |
| ctx.beginPath(); | |
| ctx.arc(b.x, b.y, b.radius + Math.sin(timeElapsed * 0.2) * 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Pulling enemies toward the center | |
| enemies.forEach((e, ei) => { | |
| let dx = b.x - e.x; | |
| let dy = b.y - e.y; | |
| let dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < b.maxRadius) { | |
| e.x += dx * 0.05; // Suck them in | |
| e.y += dy * 0.05; | |
| if (dist < 20) { // Die if they touch the core | |
| enemies.splice(ei, 1); | |
| score += 20; | |
| createExplosion(e.x, e.y, "#00ffcc"); | |
| } | |
| } | |
| }); | |
| if (b.life <= 0) { // Final explosion | |
| createExplosion(b.x, b.y, "#fff"); | |
| bullets.splice(i, 1); | |
| } | |
| } | |
| else if (b.type === 'lightning_seed') { | |
| // --- WEAPON 7: CHAIN LIGHTNING --- | |
| let lastX = b.x, lastY = b.y; | |
| let nearest = null; | |
| let minDist = 300; | |
| // To Find next enemy that hasn't been hit yet | |
| enemies.forEach(e => { | |
| let d = Math.sqrt((e.x - lastX) ** 2 + (e.y - lastY) ** 2); | |
| if (d < minDist && !b.targetsHit.includes(e)) { | |
| minDist = d; | |
| nearest = e; | |
| } | |
| }); | |
| if (nearest) { | |
| // Drawing branching bolt visuals | |
| ctx.save(); | |
| ctx.strokeStyle = "#4db8ff"; | |
| ctx.lineWidth = 4; | |
| ctx.shadowBlur = 15; | |
| ctx.shadowColor = "#4db8ff"; | |
| ctx.beginPath(); | |
| ctx.moveTo(lastX, lastY); | |
| // Procedural Zig-zag path | |
| let midX = (lastX + nearest.x) / 2 + (Math.random() - 0.5) * 60; | |
| let midY = (lastY + nearest.y) / 2 + (Math.random() - 0.5) * 60; | |
| ctx.lineTo(midX, midY); | |
| ctx.lineTo(nearest.x + 20, nearest.y + 20); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| // Updating the bolt state | |
| b.x = nearest.x + 20; | |
| b.y = nearest.y + 20; | |
| b.targetsHit.push(nearest); | |
| b.jumpCount++; | |
| // THE KILL LOGIC | |
| createExplosion(b.x, b.y, "#fff"); // Visual FX | |
| score += 20; // Add points | |
| document.getElementById('score').innerText = score; // Update HUD | |
| // REMOVING THE ENEMY: Filtering out the specific enemy that was just hit | |
| enemies = enemies.filter(e => e !== nearest); | |
| } | |
| // Cleanup of the bolt if it runs out of jumps or targets | |
| if (b.jumpCount > 5 || !nearest) { | |
| bullets.splice(i, 1); | |
| } | |
| } | |
| else if (b.type === 'batarang') { | |
| // --- WEAPON 8: BOOMERANG PATH --- | |
| b.angle += 0.3; // Spinning visual | |
| if (b.state === 'forward') { | |
| b.y -= 12; | |
| b.dist += 12; | |
| if (b.dist > 350) b.state = 'returning'; | |
| } else { | |
| // Returns toward player's CURRENT X position | |
| let dx = (player.x + 25) - b.x; | |
| let dy = (player.y + 25) - b.y; | |
| b.x += dx * 0.1; | |
| b.y += dy * 0.1; | |
| if (Math.abs(dy) < 20) bullets.splice(i, 1); | |
| } | |
| ctx.translate(b.x, b.y); | |
| ctx.rotate(b.angle); | |
| ctx.fillStyle = "#ffff00"; | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = "yellow"; | |
| // Draw a "Bat" shape (crescent) | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 15, 0, Math.PI, true); | |
| ctx.lineTo(0, -5); | |
| ctx.fill(); | |
| } else { | |
| b.y -= 12; ctx.shadowBlur = 15; ctx.shadowColor = "#00ffcc"; ctx.fillStyle = "#fff"; | |
| ctx.beginPath(); ctx.arc(b.x, b.y, 6, 0, Math.PI * 2); ctx.fill(); | |
| } | |
| ctx.restore(); | |
| if (b.y < -50) bullets.splice(i, 1); | |
| }); | |
| // --- HIGH SCORE TRACKER --- | |
| if (score > highScoreToBeat && highScoreToBeat > 0 && !recordBroken) { | |
| recordBroken = true; // Only flash once per session | |
| showMsg("🏆 NEW PERSONAL BEST ESTABLISHED!", "#ffcc00"); | |
| // Optional: Add a screen shake or extra particles for impact | |
| createExplosion(player.x + 25, player.y, "#ffcc00"); | |
| } | |
| // SUPER ITEM LOGIC (Chrono-Crystal) | |
| if (Math.random() < 0.002) superItems.push({ x: Math.random() * 760, y: -50, speed: 2 }); | |
| superItems.forEach((s, i) => { | |
| s.y += s.speed; | |
| ctx.font = "35px Arial"; ctx.shadowBlur = 15; ctx.shadowColor = "#00ffcc"; | |
| ctx.fillText("⭐", s.x, s.y); ctx.shadowBlur = 0; | |
| bullets.forEach((b, bi) => { | |
| if (Math.abs(b.x - s.x) < 30 && Math.abs(b.y - s.y) < 30) { | |
| superItems.splice(i, 1); bullets.splice(bi, 1); | |
| timeWarpTimer = 400; showMsg("⌛ TIME WARP: ENEMIES SLOWED!", "#00ffcc"); | |
| } | |
| }); | |
| if (s.y > canvas.height) superItems.splice(i, 1); | |
| }); | |
| // ENEMY LOGIC (Spawning & Movement) | |
| if (Math.random() < 0.03 * currentDanger) { | |
| let type = (evolutionTimer > 0) ? ["kamikaze", "splitter", "social"][Math.floor(Math.random() * 3)] : "normal"; | |
| enemies.push(createEnemy(Math.random() * 760, -50, type)); | |
| } | |
| enemies.forEach((e, i) => { | |
| let moveSpeed = timeWarpTimer > 0 ? e.speed * 0.3 : e.speed; | |
| if (e.evoType === "kamikaze") { | |
| e.x += (player.x - e.x) * 0.03; e.y += moveSpeed * 1.4; | |
| ctx.shadowBlur = 15; ctx.shadowColor = "red"; | |
| } else if (e.evoType === "social") { | |
| enemies.forEach(o => { if (o !== e && Math.abs(o.y - e.y) < 30) e.x += (o.x - e.x) * 0.015; }); | |
| e.y += moveSpeed * 0.8; ctx.shadowBlur = 10; ctx.shadowColor = "yellow"; | |
| } else { | |
| e.y += moveSpeed; ctx.shadowBlur = 0; | |
| } | |
| ctx.font = "40px Arial"; ctx.fillText(e.icon, e.x, e.y); ctx.shadowBlur = 0; | |
| // Collision: Bullet vs Enemy | |
| bullets.forEach((b, bi) => { | |
| if (Math.abs(b.x - e.x) < 35 && Math.abs(b.y - e.y) < 35) { | |
| createExplosion(e.x + 20, e.y + 20, "#ff5e7e"); | |
| if (e.evoType === "splitter") { | |
| enemies.push(createEnemy(e.x - 20, e.y, "normal")); | |
| enemies.push(createEnemy(e.x + 20, e.y, "normal")); | |
| } | |
| enemies.splice(i, 1); bullets.splice(bi, 1); score += 20; | |
| document.getElementById('score').innerText = score; | |
| } | |
| }); | |
| // --- Collision: Player vs Enemy --- | |
| if (Math.abs(player.x - e.x) < 40 && Math.abs(player.y - e.y) < 40) { | |
| if (hasShield) { | |
| // If the AI gave you a shield, you survive! | |
| hasShield = false; | |
| enemies.splice(i, 1); // Remove the enemy that hit you | |
| showMsg("SHIELD ABSORBED IMPACT!", "#fff"); | |
| createExplosion(player.x + 25, player.y + 25, "#00ffcc"); | |
| } else { | |
| // NO SHIELD: Mission Ends Immediately | |
| isGameActive = false; // Stop the game loop | |
| isGameOverSequence = true; | |
| createExplosion(player.x + 25, player.y + 25, "#ff4b2b"); | |
| createExplosion(e.x + 20, e.y + 20, "#ffcc00"); | |
| // Brief delay so the player sees the explosion before the screen pops up | |
| setTimeout(() => { | |
| isGameOverSequence = false; | |
| endGame("HULL BREACH: MISSION FAILED"); | |
| }, 2000); // 2 seconds to enjoy the explosion | |
| return; | |
| } | |
| } | |
| }); | |
| // FINAL DRAWING: PLAYER & UI | |
| drawSpaceship(player.x, player.y, currentSuit); // Called once per frame, correctly | |
| if (hasShield) { | |
| ctx.beginPath(); ctx.arc(player.x + 27, player.y + 25, 45, 0, Math.PI * 2); | |
| ctx.strokeStyle = "#00ffcc"; ctx.lineWidth = 4; ctx.stroke(); | |
| } | |
| if (tripleShotTimer > 0) { | |
| ctx.fillStyle = "#00ffcc"; ctx.font = "bold 14px Arial"; | |
| ctx.fillText(`TRIPLE SHOT: ${Math.ceil(tripleShotTimer / 60)}s`, player.x, player.y + 70); | |
| } | |
| // Timer text | |
| if (isGameActive && !isGameOver) { | |
| let currentSessionTime = Math.floor((Date.now() - gameStartTime) / 1000); | |
| document.getElementById('danger-lvl').innerText = (baseDifficulty * ddaMultiplier).toFixed(1) + "x (" + currentSessionTime + "s)"; | |
| } | |
| requestAnimationFrame(update); | |
| } | |
| function triggerLevelFlash(level) { | |
| const flashEl = document.getElementById('level-up-flash'); | |
| const textEl = document.getElementById('level-flash-text'); | |
| // Update the message text | |
| textEl.innerText = `WELCOME TO LEVEL ${level}`; | |
| flashEl.style.display = 'flex'; | |
| // Play a "Level Up" sound effect feeling through CSS animation | |
| flashEl.animate([ | |
| { opacity: 0, transform: 'translate(-50%, -50%) scale(0.5)' }, | |
| { opacity: 1, transform: 'translate(-50%, -50%) scale(1.1)' }, | |
| { opacity: 0, transform: 'translate(-50%, -50%) scale(1.5)' } | |
| ], { duration: 1500, iterations: 1 }); | |
| setTimeout(() => { | |
| flashEl.style.display = 'none'; | |
| }, 1500); | |
| } | |
| update(); | |
| const MOTIVATIONAL_QUOTES = [ | |
| "The AI is hungry for your vibes. Don't keep it waiting.", | |
| "Your spaceship is ready. Your vocal cords? We'll see.", | |
| "Try to sound brave. The monsters can smell boredom.", | |
| "Login to sync your soul with the cockpit.", | |
| "The higher your emotions, the hotter the fire. Let's go.", | |
| "Warning: Sarcasm levels are currently unmonitored. Proceed at your own risk." | |
| ]; | |
| // Set a random quote on load | |
| const randomQuote = MOTIVATIONAL_QUOTES[Math.floor(Math.random() * MOTIVATIONAL_QUOTES.length)]; | |
| document.getElementById('motivational-quote').innerText = `"${randomQuote}"`; | |
| </script> | |
| </body> | |
| </html> |