StaticDefender / index.html
Yatharth999's picture
Update index.html
7d31b21 verified
<!DOCTYPE html>
<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 !important;
border: 2px solid #e94560 !important;
box-shadow: none !important;
}
.btn-quit:hover {
background: #e94560 !important;
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 !important;
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>