lokiai / index.html
ParthSadaria's picture
Update index.html
855afc9 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loki.AI - Access Premium AI Models Free</title>
<!-- Favicon (Optional but recommended) -->
<link rel="icon" href="favicon.ico" type="image/x-icon">
<!-- Google Fonts: DM Sans (Bold 700, Regular 400) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--text-color-muted: #777;
--accent-color: #00ffff; /* Cyan */
--glow-color: rgba(0, 255, 255, 0.15);
--hover-bg: rgba(0, 255, 255, 0.1);
--border-color: rgba(255, 255, 255, 0.1);
--font-main: 'DM Sans', sans-serif;
--transition-speed: 0.3s;
--transition-cubic: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 16px;
scroll-behavior: smooth; /* Optional: for smooth scrolling if page grows */
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-main);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
position: relative;
overflow: hidden;
cursor: none; /* Hide default cursor */
perspective: 1000px;
}
/* Subtle Animated Background Gradient */
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 30% 70%, rgba(0, 60, 60, 0.3), transparent 55%),
radial-gradient(circle at 70% 30%, rgba(60, 0, 60, 0.25), transparent 55%);
opacity: 0;
animation: fadeInBg 3s ease-out forwards, pulseBg 15s infinite ease-in-out alternate;
z-index: 0;
}
@keyframes fadeInBg {
to { opacity: 0.8; } /* Slightly more visible */
}
@keyframes pulseBg {
0% { transform: scale(1); opacity: 0.5; }
100% { transform: scale(1.05); opacity: 0.8; } /* Slightly less scale */
}
.container {
position: relative;
z-index: 2;
padding: 2rem;
max-width: 900px;
width: 90%;
animation: slideInUp 1s var(--transition-cubic) 0.5s forwards;
opacity: 0;
transform: translateY(30px);
}
@keyframes slideInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
h1 {
font-size: clamp(3rem, 10vw, 6rem);
font-weight: 700;
letter-spacing: -0.05em;
margin-bottom: 0.2rem;
color: var(--text-color);
text-shadow: 0 0 15px rgba(255, 255, 255, 0.1), 0 0 25px var(--glow-color); /* Added subtle glow */
transition: color var(--transition-speed) ease, text-shadow var(--transition-speed) ease;
}
/* --- Enhancement: H1 Hover --- */
h1:hover {
color: var(--accent-color);
text-shadow: 0 0 10px rgba(255, 255, 255, 0.2), 0 0 30px var(--glow-color), 0 0 50px rgba(0, 255, 255, 0.3);
}
.subtitle {
font-size: clamp(0.9rem, 2.5vw, 1.2rem);
font-weight: 400;
color: var(--text-color-muted);
margin-bottom: 2.5rem;
letter-spacing: 0.05em;
transition: color var(--transition-speed) ease;
}
.models-section {
margin-bottom: 3rem;
opacity: 0;
animation: fadeIn 1s ease 1s forwards;
}
.models-section h2 {
font-size: clamp(1rem, 3vw, 1.3rem);
font-weight: 700;
color: var(--accent-color);
margin-bottom: 1.5rem;
text-transform: uppercase;
letter-spacing: 0.1em;
text-shadow: 0 0 10px var(--glow-color);
}
.models-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.8rem;
}
.model-badge {
display: inline-block;
background-color: rgba(255, 255, 255, 0.05);
color: var(--text-color-muted);
font-size: clamp(0.75rem, 2vw, 0.9rem);
font-weight: 400;
padding: 0.5em 1em;
border-radius: 15px;
border: 1px solid var(--border-color);
transition: all var(--transition-speed) var(--transition-cubic);
transform: scale(1);
}
.model-badge:hover {
background-color: var(--hover-bg);
color: var(--accent-color);
border-color: var(--accent-color);
transform: scale(1.05) translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 255, 255, 0.2);
}
.playgrounds-section {
opacity: 0;
animation: fadeIn 1s ease 1.2s forwards;
display: flex;
flex-direction: column; /* Stack links vertically on mobile */
align-items: center;
gap: 1rem;
}
@media (min-width: 600px) {
.playgrounds-section {
flex-direction: row; /* Side-by-side on larger screens */
justify-content: center;
gap: 1.5rem;
}
}
.playground-link {
display: inline-block;
color: var(--text-color);
text-decoration: none;
font-size: clamp(0.9rem, 2.5vw, 1.1rem);
font-weight: 700;
padding: 0.8em 1.8em;
border: 2px solid var(--border-color);
border-radius: 5px;
position: relative;
overflow: hidden;
transition: all var(--transition-speed) ease;
z-index: 1;
will-change: transform; /* Hint browser for smoother animation */
}
.playground-link::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: var(--accent-color);
transition: left var(--transition-speed) ease;
z-index: -1;
}
.playground-link:hover {
color: var(--bg-color);
border-color: var(--accent-color);
transform: translateY(-3px);
box-shadow: 0 5px 20px rgba(0, 255, 255, 0.3);
}
.playground-link:hover::before {
left: 0;
}
.playground-link span {
display: inline-block;
transition: transform 0.2s ease;
will-change: transform;
}
.playground-link:hover span {
transform: translateX(3px);
}
/* --- Mouse Effects --- */
.glow {
position: fixed;
left: 0;
top: 0;
width: 800px;
height: 800px;
background: radial-gradient(circle at center, var(--glow-color) 0%, rgba(0, 0, 0, 0) 60%);
border-radius: 50%;
pointer-events: none;
opacity: 0;
transition: opacity 0.4s ease; /* Only transition opacity */
/* transform: translate(-50%, -50%); JS handles transform */
z-index: 1;
filter: blur(10px);
will-change: transform, opacity; /* Optimize animation */
}
body:hover .glow {
opacity: 1;
}
.cursor {
position: fixed;
left: 0;
top: 0;
width: 25px;
height: 25px;
border: 2px solid var(--accent-color);
border-radius: 50%;
pointer-events: none;
transition: background-color 0.2s ease, border-color 0.2s ease, border-width 0.2s ease, width 0.2s ease, height 0.2s ease; /* Smooth style changes, transform handled by JS loop */
/* transform: translate(-50%, -50%); JS handles transform */
z-index: 9999;
mix-blend-mode: difference; /* Keeping this cool effect */
background-color: transparent;
will-change: transform; /* Optimize animation */
}
/* Cursor interaction states (applied via JS) */
.cursor.hover-link {
/* Scale handled by JS */
background-color: var(--accent-color);
border-color: transparent;
}
.cursor.hover-text {
/* Scale handled by JS */
border-width: 4px;
border-color: rgba(255,255,255, 0.5);
}
.cursor.clicking {
/* Scale handled by JS */
background-color: rgba(255,255,255,0.3);
}
@keyframes fadeIn {
to { opacity: 1; }
}
/* Responsive adjustments */
@media (max-width: 768px) {
h1 {
letter-spacing: -0.03em;
}
.container {
padding: 1.5rem;
}
.models-grid {
gap: 0.6rem;
}
.playgrounds-section {
gap: 0.8rem;
}
.playground-link {
padding: 0.7em 1.5em;
}
/* Reduce glow size on smaller screens */
.glow {
width: 500px;
height: 500px;
}
/* Reduce glass ball size */
.glass-ball {
width: 100px;
height: 100px;
top: 20px;
right: 20px;
}
}
/* 3D Glass Ball Style */
.glass-ball {
position: absolute;
top: 40px;
right: 40px;
width: 150px;
height: 150px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%,
rgba(255, 255, 255, 0.15) 0%, /* Slightly less opaque highlight */
rgba(0, 255, 255, 0.1) 40%,
rgba(0, 0, 0, 0.1) 100%);
box-shadow:
inset 0 0 20px rgba(0, 255, 255, 0.25), /* Slightly stronger inset */
0 0 30px rgba(0, 255, 255, 0.15); /* Reduced outer glow */
animation: float 7s infinite ease-in-out; /* Slower float */
transform-style: preserve-3d;
perspective: 1000px;
z-index: 3;
transition: width 0.3s ease, height 0.3s ease, top 0.3s ease, right 0.3s ease; /* For responsive */
}
.glass-ball::before { /* Highlight */
content: '';
position: absolute;
top: 15%;
left: 15%;
width: 20%;
height: 20%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5); /* Slightly dimmer */
filter: blur(3px); /* Slightly more blur */
}
.glass-ball::after { /* Inner glow */
content: '';
position: absolute;
top: 45%;
left: 45%;
width: 70%;
height: 70%;
border-radius: 50%;
background: radial-gradient(circle at center,
rgba(0, 255, 255, 0.08) 0%, /* More subtle inner glow */
rgba(0, 0, 0, 0) 70%);
animation: pulse 5s infinite alternate; /* Slower pulse */
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-15px) rotate(8deg); } /* Less movement */
}
@keyframes pulse {
0% { opacity: 0.3; transform: scale(0.85); }
100% { opacity: 0.5; transform: scale(1.05); }
}
/* Loading Animation */
.loading-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--bg-color);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
perspective: 1000px;
transition: opacity 0.6s ease-out, visibility 0s linear 0.6s; /* Added visibility transition */
opacity: 1;
visibility: visible;
}
/* Class to hide loader */
.loading-container.hidden {
opacity: 0;
visibility: hidden;
}
.loading-cube {
width: 100px;
height: 100px;
position: relative;
transform-style: preserve-3d;
animation: spin 4s infinite linear;
}
@keyframes spin {
0% { transform: rotateX(0deg) rotateY(0deg); }
100% { transform: rotateX(360deg) rotateY(360deg); }
}
.cube-face {
position: absolute;
width: 100px;
height: 100px;
background: rgba(0, 255, 255, 0.1);
border: 2px solid var(--accent-color);
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
color: var(--accent-color);
text-shadow: 0 0 10px var(--accent-color);
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
backface-visibility: visible;
/* Slightly transparent text for depth */
color: rgba(0, 255, 255, 0.9);
}
.front { transform: translateZ(50px); }
.back { transform: translateZ(-50px) rotateY(180deg); }
.right { transform: translateX(50px) rotateY(90deg); }
.left { transform: translateX(-50px) rotateY(-90deg); }
.top { transform: translateY(-50px) rotateX(90deg); }
.bottom { transform: translateY(50px) rotateX(-90deg); }
/* Game Related Styles */
.game-toggle {
position: fixed;
bottom: 20px;
right: 20px;
background-color: var(--accent-color);
color: var(--bg-color);
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
font-size: 24px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer; /* Re-enable pointer for the button itself */
z-index: 1000;
box-shadow: 0 0 15px rgba(0, 255, 255, 0.5);
transition: all 0.3s ease;
}
/* Ensure default cursor shows on the button */
.game-toggle:hover {
transform: scale(1.1) rotate(10deg); /* Added slight rotation */
box-shadow: 0 0 25px rgba(0, 255, 255, 0.7);
}
#game-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.95); /* Slightly darker overlay */
z-index: 5000;
display: none; /* Initially hidden */
opacity: 0; /* Start faded out */
visibility: hidden; /* Start hidden */
transition: opacity 0.4s ease, visibility 0s linear 0.4s; /* Fade transition */
overflow: hidden;
}
/* Class to show game */
#game-container.active {
display: block; /* Need display:block before animating opacity */
opacity: 1;
visibility: visible;
transition: opacity 0.4s ease, visibility 0s linear 0s;
}
#game-canvas {
width: 100%;
height: 100%;
display: block;
}
.controls-info {
position: absolute;
bottom: 20px;
left: 50%; /* Center align */
transform: translateX(-50%);
color: var(--text-color-muted); /* Muted color */
background-color: rgba(0, 0, 0, 0.6); /* Slightly less opaque */
padding: 8px 15px;
border-radius: 5px;
font-size: 13px; /* Slightly smaller */
z-index: 5001;
pointer-events: none; /* Don't interfere with game clicks */
white-space: nowrap; /* Prevent wrapping */
}
.score-display {
position: absolute;
top: 20px;
left: 20px;
color: var(--accent-color);
font-size: clamp(18px, 3vw, 24px); /* Responsive font size */
font-weight: bold;
z-index: 5001;
text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
pointer-events: none;
}
.game-message {
position: absolute;
top: 40%; /* Slightly higher */
left: 50%;
transform: translate(-50%, -50%);
color: var(--accent-color);
font-size: clamp(30px, 6vw, 45px); /* Responsive */
font-weight: bold;
text-align: center;
z-index: 5002;
text-shadow: 0 0 20px rgba(0, 255, 255, 0.7), 0 0 30px rgba(0, 255, 255, 0.4);
display: none; /* Managed by JS */
opacity: 0; /* For fade in/out */
transition: opacity 0.3s ease;
pointer-events: none;
}
.game-message.visible {
display: block; /* Needed to apply opacity */
opacity: 1;
}
</style>
</head>
<body>
<!-- Loading Animation -->
<div class="loading-container" id="loading-screen">
<div class="loading-cube">
<div class="cube-face front">LOKI.AI</div>
<div class="cube-face back">LOADING</div>
<div class="cube-face right">AI</div>
<div class="cube-face left">MODELS</div>
<div class="cube-face top">PREMIUM</div>
<div class="cube-face bottom">FREE</div>
</div>
</div>
<!-- 3D Glass Ball -->
<div class="glass-ball"></div>
<!-- Mouse Effect Elements -->
<div class="cursor"></div>
<div class="glow"></div>
<!-- Main Content -->
<div class="container">
<h1>Loki.AI</h1>
<p class="subtitle">By Parth Sadaria</p>
<div class="models-section">
<h2>Free Access To Premium Models</h2>
<div class="models-grid">
<!-- Model list kept the same -->
<span class="model-badge">GPT-4o</span>
<span class="model-badge">GPT-4o-mini</span>
<span class="model-badge">OpenAI o1</span>
<span class="model-badge">OpenAI o3-mini</span>
<span class="model-badge">GPT-4.5</span>
<span class="model-badge">Claude 3.7 Sonnet</span>
<span class="model-badge">Gemini 1.5 Pro</span>
<span class="model-badge">Gemini 1.5 Flash</span>
<span class="model-badge">And More Top Models...</span>
</div>
</div>
<div class="playgrounds-section">
<a href="https://parthsadaria-lokiai.hf.space/playground" class="playground-link" target="_blank" rel="noopener noreferrer">
<span>AI Playground</span>
</a>
<a href="https://parthsadaria-lokiai.hf.space/image-playground" class="playground-link" target="_blank" rel="noopener noreferrer">
<span>Image Playground</span>
</a>
</div>
</div>
<!-- Car Game Toggle Button -->
<!-- Enhancement: Added aria-label -->
<button class="game-toggle" id="game-toggle" aria-label="Toggle Car Game">🏎️</button>
<!-- Game Container -->
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div class="controls-info">
WASD/Arrows: Drive | SPACE: Drift | ESC: Exit
</div>
<div class="score-display" id="score-display">Score: 0</div>
<div class="game-message" id="game-message"></div>
<!-- Added a simple pause overlay -->
<div id="pause-overlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: none; justify-content: center; align-items: center; z-index: 5003; font-size: 3em; color: var(--accent-color); text-shadow: 0 0 15px var(--accent-color);">PAUSED</div>
</div>
<script>
// --- [ Mouse Cursor Effects Code - Unchanged - Keep As Is ] ---
const cursor = document.querySelector('.cursor');
const glow = document.querySelector('.glow');
const hoverables = document.querySelectorAll('a, .model-badge, .game-toggle, h1'); // Added h1
const textElements = document.querySelectorAll('p, h2, .model-badge'); // Excluded h1 here
let mouseX = 0;
let mouseY = 0;
let cursorX = 0;
let cursorY = 0;
let glowX = 0;
let glowY = 0;
const cursorSpeed = 0.15; // Fine-tune if needed
const glowSpeed = 0.1;
let currentCursorScale = 1;
const baseCursorScale = 1;
const hoverLinkCursorScale = 1.5; // For buttons, links
const hoverTextCursorScale = 0.7; // For body text, badges
const hoverTitleCursorScale = 1.1; // Special scale for H1
const clickCursorScale = 0.6;
let cursorHalfWidth = cursor.offsetWidth / 2;
let cursorHalfHeight = cursor.offsetHeight / 2;
let glowHalfWidth = glow.offsetWidth / 2;
let glowHalfHeight = glow.offsetHeight / 2;
let isGameFocused = false; // Track if game has focus (for cursor style)
window.addEventListener('resize', () => {
cursorHalfWidth = cursor.offsetWidth / 2;
cursorHalfHeight = cursor.offsetHeight / 2;
glowHalfWidth = glow.offsetWidth / 2;
glowHalfHeight = glow.offsetHeight / 2;
});
function animateCursor() {
cursorX += (mouseX - cursorX) * cursorSpeed;
cursorY += (mouseY - cursorY) * cursorSpeed;
glowX += (mouseX - glowX) * glowSpeed;
glowY += (mouseY - glowY) * glowSpeed;
const cursorTranslateX = cursorX - cursorHalfWidth;
const cursorTranslateY = cursorY - cursorHalfHeight;
const glowTranslateX = glowX - glowHalfWidth;
const glowTranslateY = glowY - glowHalfHeight;
// Apply transform only if cursor is not hidden for game
if (!isGameFocused || document.body.style.cursor !== 'none') {
cursor.style.transform = `translate(${cursorTranslateX}px, ${cursorTranslateY}px) scale(${currentCursorScale})`;
} else {
cursor.style.transform = `translate(${cursorTranslateX}px, ${cursorTranslateY}px) scale(0)`; // Hide if game focused & using default cursor
}
glow.style.transform = `translate(${glowTranslateX}px, ${glowTranslateY}px)`;
requestAnimationFrame(animateCursor);
}
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
if (!isGameFocused) { // Only update hover states if game isn't active/focused
updateCursorState();
}
});
animateCursor(); // Start animation loop
function updateCursorState() {
let targetScale = baseCursorScale;
let isHoveringLink = false;
let isHoveringText = false;
let isHoveringTitle = false;
// Direct check based on current mouse position
const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);
if (elementUnderMouse) {
if (elementUnderMouse.closest('a, .playground-link, .game-toggle')) { // More specific link types
isHoveringLink = true;
} else if (elementUnderMouse.closest('h1')) {
isHoveringTitle = true;
} else if (elementUnderMouse.closest('p, h2, .model-badge')) { // Specific text elements
isHoveringText = true;
}
}
cursor.classList.toggle('hover-link', isHoveringLink);
cursor.classList.toggle('hover-text', isHoveringText && !isHoveringLink && !isHoveringTitle);
cursor.classList.toggle('hover-title', isHoveringTitle); // New class for title hover
if (isHoveringLink) {
targetScale = hoverLinkCursorScale;
} else if (isHoveringTitle) {
targetScale = hoverTitleCursorScale;
} else if (isHoveringText) {
targetScale = hoverTextCursorScale;
}
if (cursor.classList.contains('clicking')) {
targetScale = clickCursorScale;
}
currentCursorScale = targetScale;
}
document.addEventListener('mousedown', () => {
if (!isGameFocused) {
cursor.classList.add('clicking');
updateCursorState();
}
});
document.addEventListener('mouseup', () => {
if (!isGameFocused) {
cursor.classList.remove('clicking');
updateCursorState();
}
});
// Initial state
updateCursorState();
// --- [ End of Mouse Cursor Effects Code ] ---
// Loading screen animation
window.addEventListener('load', () => {
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) {
setTimeout(() => {
loadingScreen.classList.add('hidden'); // Use class to trigger transition
}, 1500); // Reduced duration slightly
} else {
console.error("Loading screen element not found!");
}
// Recalculate cursor/glow size after load/potential style changes
cursorHalfWidth = cursor.offsetWidth / 2;
cursorHalfHeight = cursor.offsetHeight / 2;
glowHalfWidth = glow.offsetWidth / 2;
glowHalfHeight = glow.offsetHeight / 2;
});
// --- [ Game Logic - START - Debugging Version ] ---
console.log("Game Script Initializing..."); // DEBUG
const gameToggle = document.getElementById('game-toggle');
const gameContainer = document.getElementById('game-container');
const canvas = document.getElementById('game-canvas');
const scoreDisplay = document.getElementById('score-display');
const gameMessage = document.getElementById('game-message');
const pauseOverlay = document.getElementById('pause-overlay');
let ctx = null;
if (canvas) {
try {
ctx = canvas.getContext('2d');
if (!ctx) {
console.error("Failed to get 2D context from canvas.");
} else {
console.log("Canvas context obtained."); // DEBUG
}
} catch (e) {
console.error("Error getting canvas context:", e);
}
} else {
console.error("Game canvas element not found!");
}
// Game state
let gameActive = false;
let gameVisible = false;
let isPaused = false;
let animationFrameId = null;
let score = 0;
let lastTimestamp = 0;
let deltaTime = 0;
let timeScale = 1.0;
// --- Physics Constants (Unchanged) ---
const FRICTION = 0.98;
const ANGULAR_FRICTION = 0.95;
const CAR_ENGINE_FORCE = 13000;
const CAR_BRAKE_FORCE = 18000;
const CAR_HANDBRAKE_FORCE = 25000;
const CAR_TURN_TORQUE = 100000;
const CAR_MASS = 1000;
const CAR_INERTIA = CAR_MASS * 500;
const OBSTACLE_DENSITY = 5;
const COLLISION_RESTITUTION = 0.3;
const SCREEN_BOUND_RESTITUTION = 0.4;
const TIRE_LATERAL_GRIP = 6.0;
const TIRE_HANDBRAKE_GRIP_FACTOR = 0.2;
// --- Car properties (Add safety for invMass/invInertia) ---
const car = {
x: 0, y: 0, vx: 0, vy: 0, angle: 0, angularVelocity: 0,
width: 25, height: 45,
physicsWidth: 2.0, physicsHeight: 4.0,
mass: CAR_MASS,
invMass: CAR_MASS > 0 ? 1 / CAR_MASS : 0, // SAFETY CHECK
inertia: CAR_INERTIA,
invInertia: CAR_INERTIA > 0 ? 1 / CAR_INERTIA : 0, // SAFETY CHECK
color: '#00ffff', shadowColor: 'rgba(0, 255, 255, 0.3)',
drifting: false, tireMarks: [], vertices: [], axes: [], // Add vertices/axes here too
turboMode: false, turboTimer: 0, turboMaxTime: 3,
turboCooldown: 0, turboCooldownMax: 5, turboForceBoost: 10000,
};
// Obstacles & Powerups Arrays
const obstacles = [];
const powerUps = [];
const obstacleTypes = [ /* Same as before */
{ text: "GPT-4o", width: 100, height: 40, color: "#ff5a5a" },
{ text: "Claude 3.7", width: 120, height: 40, color: "#5a92ff" },
{ text: "Gemini", width: 90, height: 40, color: "#5aff7f" },
{ text: "Loki.AI", width: 120, height: 50, color: "#ffaa5a" },
{ text: "AI", width: 60, height: 40, color: "#aa5aff" },
{ text: "Premium", width: 110, height: 40, color: "#ff5aaa" }
];
const maxObstacles = 20;
const maxPowerUps = 3;
// --- Canvas sizing ---
function resizeCanvas() {
console.log("resizeCanvas called"); // DEBUG
if (!canvas) {
console.error("resizeCanvas: Canvas not found"); return;
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
console.log(`Canvas resized to ${canvas.width}x${canvas.height}`); // DEBUG
if(gameActive && ctx) {
drawBackground(ctx);
}
}
window.addEventListener('resize', resizeCanvas);
// Initial size set needs ctx to be ready
if (canvas && ctx) {
resizeCanvas();
} else if (canvas && !ctx) {
console.warn("resizeCanvas: ctx not ready during initial call.");
}
// --- Helper Functions (Unchanged) ---
function worldToScreen(x, y) { return { x: x, y: y }; }
function screenToWorld(x, y) { return { x: x, y: y }; }
function vecAdd(v1, v2) { return { x: v1.x + v2.x, y: v1.y + v2.y }; }
function vecSub(v1, v2) { return { x: v1.x - v2.x, y: v1.y - v2.y }; }
function vecScale(v, s) { return { x: v.x * s, y: v.y * s }; }
function vecLength(v) { return Math.sqrt(v.x * v.x + v.y * v.y); }
function vecNormalize(v) { const l = vecLength(v); return l > 0.0001 ? { x: v.x / l, y: v.y / l } : { x: 0, y: 0 }; } // Added tolerance
function vecDot(v1, v2) { return v1.x * v2.x + v1.y * v2.y; }
function vecRotate(v, angle) { const c=Math.cos(angle), s=Math.sin(angle); return { x: v.x*c - v.y*s, y: v.x*s + v.y*c }; }
function vecCross2D(r, F) { return r.x * F.y - r.y * F.x; }
// --- Game Toggle and State Management ---
if (gameToggle && gameContainer) {
console.log("Attaching listener to game toggle button."); // DEBUG
gameToggle.addEventListener('click', () => {
console.log("Game toggle clicked. gameVisible:", gameVisible); // DEBUG
if (!gameVisible) {
startGame();
} else {
stopGame();
}
});
} else {
console.error("Game toggle button or container not found! Cannot attach listener.");
}
function startGame() {
console.log("startGame called"); // DEBUG
if (!gameContainer) { console.error("startGame: gameContainer not found."); return; }
if (!ctx) { console.error("startGame: Canvas context (ctx) is not available."); return; }
gameVisible = true;
gameActive = true;
isPaused = false;
isGameFocused = true;
gameContainer.classList.add('active');
if(pauseOverlay) pauseOverlay.style.display = 'none';
document.body.style.cursor = 'none';
if(cursor) cursor.style.display = 'none';
if(glow) glow.style.display = 'none';
try {
resizeCanvas(); // Ensure canvas is sized correctly
resetGame(); // Reset state and create objects
} catch(e) {
console.error("Error during resize/reset in startGame:", e);
stopGame(); // Attempt to gracefully stop if init fails
return;
}
lastTimestamp = performance.now();
if (animationFrameId) cancelAnimationFrame(animationFrameId);
console.log("Requesting first game loop frame..."); // DEBUG
animationFrameId = requestAnimationFrame(gameLoop);
}
function stopGame() {
console.log("stopGame called"); // DEBUG
if (!gameContainer) return; // Should not happen if called from toggle, but safety check
gameVisible = false;
gameActive = false;
isPaused = false;
isGameFocused = false;
gameContainer.classList.remove('active');
document.body.style.cursor = 'none'; // Keep custom cursor style
if(cursor) cursor.style.display = ''; // Show custom cursor
if(glow) glow.style.display = ''; // Show glow
updateCursorState(); // Recalculate hover state
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
console.log("Cancelled animation frame:", animationFrameId); // DEBUG
animationFrameId = null;
}
lastTimestamp = 0;
}
function pauseGame() {
console.log("pauseGame called"); // DEBUG
if (!gameActive) return;
isPaused = true;
gameActive = false;
if(pauseOverlay) pauseOverlay.style.display = 'flex';
}
function resumeGame() {
console.log("resumeGame called"); // DEBUG
if (!gameVisible || !isPaused) return;
isPaused = false;
gameActive = true;
if(pauseOverlay) pauseOverlay.style.display = 'none';
lastTimestamp = performance.now();
if (!animationFrameId && gameVisible) { // Restart loop only if it somehow stopped AND game should be visible
console.log("Restarting game loop from resumeGame..."); //DEBUG
animationFrameId = requestAnimationFrame(gameLoop);
}
}
// Key handling (Unchanged, assumed correct)
const keys = { up: false, down: false, left: false, right: false, handbrake: false };
window.addEventListener('keydown', (e) => {
if (!gameVisible) return;
if ([' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'w', 'a', 's', 'd', 'W', 'A', 'S', 'D'].includes(e.key)) {
e.preventDefault();
}
if (e.key === 'Escape') {
if (isPaused) { stopGame(); } else if (gameActive) { pauseGame(); }
return;
}
if (isPaused) return;
switch(e.key.toLowerCase()) {
case 'arrowup': case 'w': keys.up = true; break;
case 'arrowdown': case 's': keys.down = true; break;
case 'arrowleft': case 'a': keys.left = true; break;
case 'arrowright': case 'd': keys.right = true; break;
case ' ': keys.handbrake = true; break;
}
});
window.addEventListener('keyup', (e) => {
switch(e.key.toLowerCase()) {
case 'arrowup': case 'w': keys.up = false; break;
case 'arrowdown': case 's': keys.down = false; break;
case 'arrowleft': case 'a': keys.left = false; break;
case 'arrowright': case 'd': keys.right = false; break;
case ' ': keys.handbrake = false; break;
}
});
// --- Object Creation ---
function createObstacles() {
console.log("Creating obstacles..."); // DEBUG
if (!canvas) { console.error("createObstacles: Canvas not found."); return; }
obstacles.length = 0;
const margin = 50;
const canvasW = canvas.width;
const canvasH = canvas.height;
for (let i = 0; i < maxObstacles; i++) {
try { // Wrap individual obstacle creation
const type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];
const width = type.width;
const height = type.height;
const mass = width * height * OBSTACLE_DENSITY;
const inertia = mass * (width*width + height*height) / 12;
// SAFETY CHECKS for inverse mass/inertia
const invMass = mass > 0.0001 ? 1 / mass : 0;
const invInertia = inertia > 0.0001 ? 1 / inertia : 0;
// Placement logic (simplified check for debug)
let x = Math.random() * (canvasW - width - 2 * margin) + margin + width / 2;
let y = Math.random() * (canvasH - height - 2 * margin) + margin + height / 2;
// Basic check to avoid center spawn
if (Math.hypot(x - canvasW/2, y - canvasH/2) < 200) {
x = margin + width / 2 + Math.random() * 100; // Move near edge if too close to center
}
const newObstacle = {
x: x, y: y, vx: 0, vy: 0,
angle: Math.random() * Math.PI * 2,
angularVelocity: (Math.random() - 0.5) * 0.3,
width: width, height: height,
mass: mass, invMass: invMass,
inertia: inertia, invInertia: invInertia,
text: type.text, color: type.color,
vertices: [], axes: [] // Initialize empty
};
obstacles.push(newObstacle);
// console.log(`Created obstacle ${i}: mass=${mass.toFixed(1)}, invMass=${invMass.toExponential(2)}`); // DEBUG detail
} catch (e) {
console.error(`Error creating obstacle ${i}:`, e);
}
}
// IMPORTANT: Update vertices AFTER all obstacles are in the array
obstacles.forEach(obs => {
try {
updateVerticesAndAxes(obs);
} catch(e) {
console.error("Error updating vertices for obstacle:", obs, e);
}
});
console.log(`Finished creating ${obstacles.length} obstacles.`); // DEBUG
}
function createPowerUps() {
console.log("Creating powerups..."); // DEBUG
if (!canvas) { console.error("createPowerUps: Canvas not found."); return; }
powerUps.length = 0;
// Placement logic (simplified for debug, less strict overlap check)
const margin = 60;
for (let i = 0; i < maxPowerUps; i++) {
try {
const type = Math.random() < 0.5 ? 'turbo' : 'score';
const x = Math.random() * (canvas.width - 2 * margin) + margin;
const y = Math.random() * (canvas.height - 2 * margin) + margin;
const radius = 18;
// Basic check against car start
if (Math.hypot(x - canvas.width/2, y - canvas.height/2) < 150) continue; // Skip if too close
powerUps.push({
x: x, y: y, radius: radius,
type: type, color: type === 'turbo' ? '#ff00ff' : '#ffff00',
active: true, pulseOffset: Math.random() * Math.PI * 2
});
} catch (e) {
console.error(`Error creating powerup ${i}:`, e);
}
}
console.log(`Finished creating ${powerUps.length} powerups.`); // DEBUG
}
function showGameMessage(text, color, duration = 1500) {
// Assume this works, no changes needed unless message itself breaks
if (!gameMessage) return;
gameMessage.textContent = text;
gameMessage.style.color = color;
gameMessage.classList.add('visible');
setTimeout(() => {
gameMessage.classList.remove('visible');
}, duration);
}
function activateTurbo() { /* Unchanged */
if (car.turboCooldown <= 0) {
car.turboMode = true;
car.turboTimer = car.turboMaxTime;
showGameMessage("TURBO!", '#ff00ff', 1500);
}
}
function addScore(amount = 100, position = null) { /* Unchanged */
score += amount;
updateScore();
if (amount >= 200) {
showGameMessage(`+${amount} PTS!`, '#ffff00', 1000);
}
}
function resetGame() {
console.log("resetGame called"); // DEBUG
if (!canvas) { console.error("resetGame: Canvas not found."); return; }
// Reset car state
car.x = canvas.width / 2;
car.y = canvas.height / 2;
car.vx = 0; car.vy = 0;
car.angle = -Math.PI / 2;
car.angularVelocity = 0;
car.tireMarks = [];
car.turboMode = false;
car.turboTimer = 0;
car.turboCooldown = 0;
// Ensure car vertices are reset too (or updated immediately after position change)
updateVerticesAndAxes(car); // Update car geometry based on reset state
score = 0;
updateScore();
// Recreate objects
createObstacles();
createPowerUps();
console.log("Game reset finished."); // DEBUG
}
function updateScore() {
if (scoreDisplay) { scoreDisplay.textContent = `Score: ${score}`; }
}
// --- Drawing Functions (Assumed correct, no changes unless errors point here) ---
function drawCar(ctx) { /* Unchanged */
if (!ctx) return;
const { x: screenX, y: screenY } = worldToScreen(car.x, car.y);
ctx.save();
ctx.translate(screenX, screenY);
ctx.rotate(car.angle);
ctx.shadowColor = car.shadowColor;
ctx.shadowBlur = car.drifting ? 25 : 15;
ctx.shadowOffsetX = 5; ctx.shadowOffsetY = 5;
const drawWidth = car.width; const drawHeight = car.height;
ctx.fillStyle = car.turboMode ? '#ff00ff' : car.color;
ctx.fillRect(-drawWidth/2, -drawHeight/2, drawWidth, drawHeight);
ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0;
ctx.fillStyle = "#223344";
ctx.fillRect(-drawWidth/2 * 0.8, -drawHeight/2 * 0.7, drawWidth * 0.8, drawHeight * 0.3);
ctx.fillRect(-drawWidth/2 * 0.7, drawHeight/2 * 0.4, drawWidth * 0.7, drawHeight * 0.2);
ctx.fillStyle = "#ffffaa";
ctx.fillRect(-drawWidth/2 * 0.4, -drawHeight/2 - 2, drawWidth * 0.2, 4);
ctx.fillRect( drawWidth/2 * 0.2, -drawHeight/2 - 2, drawWidth * 0.2, 4);
ctx.fillStyle = "#ffaaaa";
ctx.fillRect(-drawWidth/2 * 0.4, drawHeight/2 - 2, drawWidth * 0.2, 4);
ctx.fillRect( drawWidth/2 * 0.2, drawHeight/2 - 2, drawWidth * 0.2, 4);
if (car.turboMode) { /* flames */ }
if (car.drifting && Math.hypot(car.vx, car.vy) > 5) { /* smoke */ }
ctx.restore();
}
function drawTireMarks(ctx) { /* Unchanged */
if (!ctx || car.tireMarks.length === 0) return;
ctx.lineCap = "round";
const maxMarksToDraw = 100;
const startIdx = Math.max(0, car.tireMarks.length - maxMarksToDraw);
for (let i = startIdx; i < car.tireMarks.length; i++) {
const mark = car.tireMarks[i];
mark.life -= deltaTime * timeScale;
if (mark.life <= 0) { continue; }
const alpha = Math.max(0, Math.min(1, mark.life / mark.maxLife));
ctx.strokeStyle = `rgba(40, 40, 40, ${alpha * 0.6})`;
ctx.lineWidth = mark.width * alpha;
ctx.beginPath(); ctx.moveTo(mark.lx1, mark.ly1); ctx.lineTo(mark.lx2, mark.ly2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(mark.rx1, mark.ry1); ctx.lineTo(mark.rx2, mark.ry2); ctx.stroke();
}
if (Math.random() < 0.05) { car.tireMarks = car.tireMarks.filter(mark => mark.life > 0); }
if (car.tireMarks.length > 250) { car.tireMarks.splice(0, car.tireMarks.length - 200); }
}
function addTireMarkSegment() { /* Unchanged */
const wheelOffset = car.width * 0.4; const axleOffset = car.height * 0.35;
const cosA = Math.cos(car.angle); const sinA = Math.sin(car.angle);
const { x: screenX, y: screenY } = worldToScreen(car.x, car.y);
const rlX = screenX - sinA*wheelOffset - cosA*axleOffset; const rlY = screenY + cosA*wheelOffset - sinA*axleOffset;
const rrX = screenX + sinA*wheelOffset - cosA*axleOffset; const rrY = screenY - cosA*wheelOffset - sinA*axleOffset;
const maxLife = 1.8; const markWidth = 4;
const lastMark = car.tireMarks.length > 0 ? car.tireMarks[car.tireMarks.length - 1] : null;
if (lastMark && lastMark.life > 0) { lastMark.lx2=rlX; lastMark.ly2=rlY; lastMark.rx2=rrX; lastMark.ry2=rrY; }
car.tireMarks.push({ lx1:rlX, ly1:rlY, lx2:rlX, ly2:rlY, rx1:rrX, ry1:rrY, rx2:rrX, ry2:rrY, life:maxLife, maxLife:maxLife, width:markWidth });
}
function drawObstacles(ctx) { /* Unchanged */
if (!ctx) return;
for (const obs of obstacles) {
const { x: screenX, y: screenY } = worldToScreen(obs.x, obs.y);
ctx.save(); ctx.translate(screenX, screenY); ctx.rotate(obs.angle);
ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.fillRect(-obs.width/2 + 3, -obs.height/2 + 3, obs.width, obs.height);
ctx.fillStyle = obs.color; ctx.fillRect(-obs.width/2, -obs.height/2, obs.width, obs.height);
ctx.font = 'bold 13px "DM Sans", sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = '#ffffff'; ctx.fillText(obs.text, 0, 1);
ctx.restore();
}
}
function drawPowerUps(ctx) { /* Unchanged */
if (!ctx) return;
for (const powerUp of powerUps) { /* ... drawing logic ... */ }
}
function drawBackground(ctx) { /* Unchanged */
if (!ctx || !canvas) return;
ctx.fillStyle = '#08080A'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle='rgba(0, 255, 255, 0.06)'; ctx.lineWidth=1; const gridSize=50;
for (let x=0; x<canvas.width; x+=gridSize) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,canvas.height); ctx.stroke(); }
for (let y=0; y<canvas.height; y+=gridSize) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(canvas.width,y); ctx.stroke(); }
// Vignette...
}
function drawGameInfo(ctx) { /* Unchanged */
if (!ctx || !canvas) return; /* ... HUD drawing logic ... */
}
// --- Physics Update Functions ---
function applyForces(obj, deltaTime) { // Generalized slightly
// Only apply player input to car
let isPlayerCar = (obj === car);
let totalForce = { x: 0, y: 0 };
let totalTorque = 0;
const forwardVec = { x: Math.sin(obj.angle), y: -Math.cos(obj.angle) };
const rightVec = { x: -forwardVec.y, y: forwardVec.x };
const currentSpeed = vecLength({x: obj.vx, y: obj.vy});
if (isPlayerCar) {
let engineForceMag = 0;
if (keys.up) { engineForceMag += CAR_ENGINE_FORCE; if (obj.turboMode) engineForceMag += obj.turboForceBoost; }
if (keys.down) { const movingForward = vecDot({x: obj.vx, y: obj.vy}, forwardVec)>0; if (currentSpeed > 0.5 && movingForward) { engineForceMag -= CAR_BRAKE_FORCE; } else { engineForceMag -= CAR_ENGINE_FORCE*0.6; } }
totalForce = vecAdd(totalForce, vecScale(forwardVec, engineForceMag));
const steeringEffectiveness = Math.max(0.2, 1.0 - (currentSpeed / 40));
if (keys.left) { totalTorque -= CAR_TURN_TORQUE * steeringEffectiveness; }
if (keys.right) { totalTorque += CAR_TURN_TORQUE * steeringEffectiveness; }
obj.drifting = keys.handbrake && currentSpeed > 5;
if (obj.drifting) { const brakeDir = currentSpeed > 0.1 ? vecScale(vecNormalize({x: obj.vx, y: obj.vy}),-1) : {x:0, y:0}; totalForce = vecAdd(totalForce, vecScale(brakeDir, CAR_HANDBRAKE_FORCE*0.5)); addTireMarkSegment(); }
}
// Friction/Drag (Apply to all objects)
totalForce = vecAdd(totalForce, vecScale({x: obj.vx, y: obj.vy}, -FRICTION * 30));
// Lateral Tire Friction (Apply more strongly to car, less to obstacles?)
// For simplicity, apply same logic but maybe scaled down for obstacles? Let's apply only to car for now.
let lateralFrictionMag = 0;
if(isPlayerCar) {
const lateralVelocity = vecDot({x: obj.vx, y: obj.vy}, rightVec);
let gripFactor = (isPlayerCar && keys.handbrake) ? TIRE_HANDBRAKE_GRIP_FACTOR : 1.0;
lateralFrictionMag = -lateralVelocity * TIRE_LATERAL_GRIP * gripFactor * obj.mass;
totalForce = vecAdd(totalForce, vecScale(rightVec, lateralFrictionMag));
}
// --- Apply forces ---
// SAFETY CHECK for invMass/invInertia before applying
if (obj.invMass > 0) {
const linearAccel = vecScale(totalForce, obj.invMass);
obj.vx += linearAccel.x * deltaTime * timeScale;
obj.vy += linearAccel.y * deltaTime * timeScale;
}
if (obj.invInertia > 0) {
const angularAccel = totalTorque * obj.invInertia;
obj.angularVelocity += angularAccel * deltaTime * timeScale;
}
// Angular damping
obj.angularVelocity *= (1 - (1 - ANGULAR_FRICTION) * deltaTime * 60);
// --- Integration ---
obj.x += obj.vx * deltaTime * timeScale;
obj.y += obj.vy * deltaTime * timeScale;
obj.angle += obj.angularVelocity * deltaTime * timeScale;
// Update Turbo Timer (Only for car)
if (isPlayerCar) {
if (obj.turboMode) { obj.turboTimer -= deltaTime*timeScale; if (obj.turboTimer <= 0) { obj.turboMode=false; obj.turboTimer=0; obj.turboCooldown=obj.turboCooldownMax; } }
else if (obj.turboCooldown > 0) { obj.turboCooldown -= deltaTime*timeScale; if (obj.turboCooldown < 0) { obj.turboCooldown = 0; } }
}
}
function updateObstaclesPhysics(deltaTime) {
if (!canvas) return;
const dt = deltaTime * timeScale;
obstacles.forEach(obs => {
try {
// --- Apply basic forces (damping) ---
// Only damping applied here, more complex forces in applyForces if needed later
obs.vx *= (1 - (1 - FRICTION) * dt * 30);
obs.vy *= (1 - (1 - FRICTION) * dt * 30);
obs.angularVelocity *= (1 - (1 - ANGULAR_FRICTION) * dt * 60);
// --- Integration ---
obs.x += obs.vx * dt;
obs.y += obs.vy * dt;
obs.angle += obs.angularVelocity * dt;
// Update geometry and check screen bounds
updateVerticesAndAxes(obs);
handleScreenCollision(obs, canvas.width, canvas.height);
} catch (e) {
console.error("Error updating physics for obstacle:", obs, e);
}
});
}
// --- Collision Detection & Resolution (SAT - Assumed mostly correct, added logging) ---
function updateVerticesAndAxes(obj) {
// SAFETY check: Ensure obj and its properties are valid
if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.angle !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number') {
console.error("Invalid object passed to updateVerticesAndAxes:", obj);
obj.vertices = []; obj.axes = []; // Prevent further errors using these
return;
}
const w = obj.width / 2;
const h = obj.height / 2;
const cosA = Math.cos(obj.angle);
const sinA = Math.sin(obj.angle);
const x = obj.x;
const y = obj.y;
obj.vertices = [ // Ensure this calculation is correct
{ x: x + (-w*cosA - -h*sinA), y: y + (-w*sinA + -h*cosA) },
{ x: x + ( w*cosA - -h*sinA), y: y + ( w*sinA + -h*cosA) },
{ x: x + ( w*cosA - h*sinA), y: y + ( w*sinA + h*cosA) },
{ x: x + (-w*cosA - h*sinA), y: y + (-w*sinA + h*cosA) }
];
obj.axes = [];
for (let i = 0; i < 4; i++) {
const p1 = obj.vertices[i];
const p2 = obj.vertices[(i + 1) % 4];
// SAFETY Check: Ensure vertices are valid before calculating edge
if (typeof p1.x !== 'number' || typeof p2.x !== 'number') {
console.error("Invalid vertices found in updateVerticesAndAxes for obj:", obj);
continue; // Skip this axis
}
const edge = vecSub(p2, p1);
const normal = vecNormalize({ x: -edge.y, y: edge.x });
obj.axes.push(normal);
}
}
function projectPolygon(vertices, axis) {
// SAFETY check
if (!vertices || vertices.length === 0 || !axis || typeof axis.x !== 'number') {
console.error("Invalid input to projectPolygon", vertices, axis);
return { min: 0, max: 0};
}
let min = vecDot(vertices[0], axis); let max = min;
for (let i = 1; i < vertices.length; i++) {
// SAFETY check vertex
if (typeof vertices[i]?.x !== 'number') {
console.error("Invalid vertex in projectPolygon", vertices[i]); continue;
}
const p = vecDot(vertices[i], axis);
if (p < min) { min = p; } else if (p > max) { max = p; }
}
return { min: min, max: max };
}
function checkSATCollision(objA, objB) {
// SAFETY Check inputs
if (!objA || !objB || !objA.axes || !objB.axes || !objA.vertices || !objB.vertices) {
console.error("Invalid objects for SAT check:", objA, objB); return null;
}
// Ensure vertices are current
updateVerticesAndAxes(objA); // Update A (e.g., car)
// B (obstacle) should be updated in its own physics loop, but update again just in case? Maybe not needed.
const axes = [...objA.axes, ...objB.axes];
let minOverlap = Infinity;
let collisionNormal = null;
for (const axis of axes) {
// SAFETY check axis
if (typeof axis?.x !== 'number') { console.error("Invalid axis in SAT:", axis); continue; }
const projA = projectPolygon(objA.vertices, axis);
const projB = projectPolygon(objB.vertices, axis);
const overlap = Math.min(projA.max, projB.max) - Math.max(projA.min, projB.min);
if (overlap <= 0.0001) { return null; } // Use tolerance
if (overlap < minOverlap) { minOverlap = overlap; collisionNormal = axis; }
}
if (!collisionNormal) return null; // Should not happen if overlap > 0, but safety
const direction = vecSub({x:objA.x,y:objA.y}, {x:objB.x,y:objB.y});
if (vecDot(direction, collisionNormal) < 0) { collisionNormal = vecScale(collisionNormal, -1); }
// console.log("SAT Collision Detected:", { overlap: minOverlap, normal: collisionNormal }); // DEBUG (can be noisy)
return { overlap: minOverlap, normal: collisionNormal };
}
function resolveCollision(objA, objB, collisionInfo) {
// SAFETY check inputs
if (!objA || !objB || !collisionInfo || !collisionInfo.normal || typeof collisionInfo.overlap !== 'number') {
console.error("Invalid input to resolveCollision", objA, objB, collisionInfo); return;
}
const { overlap, normal } = collisionInfo;
// SAFETY check normal validity
if(typeof normal.x !== 'number' || typeof normal.y !== 'number' || Math.abs(normal.x*normal.x + normal.y*normal.y - 1.0) > 0.01) {
console.error("Invalid collision normal:", normal);
return; // Avoid resolving with bad normal
}
// 1. Positional Correction
const totalInvMass = objA.invMass + objB.invMass;
if (totalInvMass > 0.00001) { // Use tolerance
const separationAmount = overlap / totalInvMass;
const correctionScale = 0.8; // Penetration resolution percentage (prevent jitter)
objA.x += normal.x * separationAmount * objA.invMass * correctionScale;
objA.y += normal.y * separationAmount * objA.invMass * correctionScale;
objB.x -= normal.x * separationAmount * objB.invMass * correctionScale;
objB.y -= normal.y * separationAmount * objB.invMass * correctionScale;
// Re-update vertices after position change is important if checking collisions multiple times per frame
updateVerticesAndAxes(objA);
updateVerticesAndAxes(objB);
}
// 2. Impulse Calculation
const collisionPoint = { x: (objA.x + objB.x) / 2, y: (objA.y + objB.y) / 2 }; // Approx center
const rA = vecSub(collisionPoint, {x: objA.x, y: objA.y});
const rB = vecSub(collisionPoint, {x: objB.x, y: objB.y});
const vA = { x: objA.vx + (-objA.angularVelocity * rA.y), y: objA.vy + (objA.angularVelocity * rA.x) };
const vB = { x: objB.vx + (-objB.angularVelocity * rB.y), y: objB.vy + (objB.angularVelocity * rB.x) };
const relativeVelocity = vecSub(vA, vB);
const velocityAlongNormal = vecDot(relativeVelocity, normal);
if (velocityAlongNormal > 0) return; // Moving apart
const restitution = COLLISION_RESTITUTION;
const rACrossN = vecCross2D(rA, normal);
const rBCrossN = vecCross2D(rB, normal);
// SAFETY check for invInertia being valid numbers
const invInertiaA = (typeof objA.invInertia === 'number' && isFinite(objA.invInertia)) ? objA.invInertia : 0;
const invInertiaB = (typeof objB.invInertia === 'number' && isFinite(objB.invInertia)) ? objB.invInertia : 0;
const invInertiaSum = (rACrossN * rACrossN * invInertiaA) + (rBCrossN * rBCrossN * invInertiaB);
const denominator = invMassSum + invInertiaSum;
// SAFETY Check denominator
if (denominator < 0.00001) {
console.warn("Collision denominator too small, skipping impulse."); return;
}
let j = -(1 + restitution) * velocityAlongNormal;
j /= denominator;
// 3. Apply Impulse
const impulse = vecScale(normal, j);
if(objA.invMass > 0){ objA.vx += impulse.x * objA.invMass; objA.vy += impulse.y * objA.invMass; }
if(objB.invMass > 0){ objB.vx -= impulse.x * objB.invMass; objB.vy -= impulse.y * objB.invMass; }
if(invInertiaA > 0){ objA.angularVelocity += vecCross2D(rA, impulse) * invInertiaA; }
if(invInertiaB > 0){ objB.angularVelocity -= vecCross2D(rB, impulse) * invInertiaB; }
// Scoring & Feedback
if (objA === car || objB === car) {
const impactMagnitude = Math.abs(j);
const scoreToAdd = Math.min(60, Math.max(2, Math.floor(impactMagnitude / 1000)));
addScore(scoreToAdd, collisionPoint);
if (impactMagnitude > 5000 && gameContainer) { /* Screen shake */ }
}
}
function handleScreenCollision(obj, width, height) {
// SAFETY check obj and properties needed
if (!obj || typeof obj.x !== 'number' || typeof obj.width !== 'number' || !obj.vertices || obj.vertices.length !== 4) {
// console.warn("Invalid object for screen collision:", obj); // Can be noisy
return;
}
const objRadius = Math.max(obj.width, obj.height) / 2; // Approx
obj.vertices.forEach((v, i) => {
let collided = false;
let normal = {x:0, y:0};
let penetration = 0;
if (v.x < 0) { collided = true; normal = {x: 1, y: 0}; penetration = -v.x; }
if (v.x > width) { collided = true; normal = {x:-1, y: 0}; penetration = v.x - width; }
if (v.y < 0) { collided = true; normal = {x: 0, y: 1}; penetration = -v.y; }
if (v.y > height) { collided = true; normal = {x: 0, y:-1}; penetration = v.y - height; }
if (collided) {
// 1. Positional Correction (Move object back slightly) - Simplified
// Only apply correction if penetration is significant
if(penetration > 0.1 && obj.invMass > 0) { // Don't move static objects
obj.x += normal.x * penetration * 0.8; // Correct based on penetration depth
obj.y += normal.y * penetration * 0.8;
// Need to update vertices after positional correction for accurate impulse
updateVerticesAndAxes(obj);
}
// 2. Impulse Response
const velocity = { x: obj.vx, y: obj.vy };
const dot = vecDot(velocity, normal);
// Only apply impulse if moving *into* the wall
if (dot < 0) {
const impulseMag = -(1 + SCREEN_BOUND_RESTITUTION) * dot;
const impulse = vecScale(normal, impulseMag);
// Apply only if object can move
if(obj.invMass > 0){
obj.vx += impulse.x * obj.invMass;
obj.vy += impulse.y * obj.invMass;
}
// Apply angular impulse? Less critical for wall hits unless specific effect desired.
// if(obj.invInertia > 0) obj.angularVelocity += (Math.random() - 0.5) * 0.1 * impulseMag * obj.invInertia;
}
}
});
}
function checkAllCollisions() {
if (!canvas) return;
try { // Wrap collision checks
// --- Car vs Obstacles ---
for (const obstacle of obstacles) {
const collisionInfo = checkSATCollision(car, obstacle);
if (collisionInfo) {
resolveCollision(car, obstacle, collisionInfo);
}
}
// --- Obstacle vs Obstacles ---
for (let i = 0; i < obstacles.length; i++) {
for (let j = i + 1; j < obstacles.length; j++) {
const obsA = obstacles[i];
const obsB = obstacles[j];
const collisionInfo = checkSATCollision(obsA, obsB);
if (collisionInfo) {
resolveCollision(obsA, obsB, collisionInfo);
}
}
}
// --- Car vs Power-Ups --- (Simple Circle Collision)
const carRadius = car.width / 2;
for (let i = powerUps.length - 1; i >= 0; i--) {
const powerUp = powerUps[i];
if (!powerUp.active) continue;
const dist = Math.hypot(car.x - powerUp.x, car.y - powerUp.y);
if (dist < carRadius + powerUp.radius) {
powerUp.active = false;
if (powerUp.type === 'turbo') activateTurbo(); else if (powerUp.type === 'score') addScore(250);
setTimeout(() => { const cIdx = powerUps.findIndex(p=>p===powerUp); if(cIdx!==-1) powerUps.splice(cIdx, 1); createPowerUps(); }, 2000+Math.random()*2000);
}
}
} catch (e) {
console.error("Error during collision checking/resolution:", e);
}
}
// --- Main Game Loop ---
let frameCount = 0; // DEBUG
function gameLoop(timestamp) {
// console.log(`gameLoop start. Visible: ${gameVisible}`); // DEBUG (Very noisy)
if (!gameVisible) { animationFrameId = null; return; }
const now = performance.now();
// Robust deltaTime calculation
deltaTime = (now - (lastTimestamp || now)) / 1000; // Handle first frame case
lastTimestamp = now;
deltaTime = Math.min(deltaTime, 1 / 20); // Cap delta time
// console.log(`Frame: ${frameCount++}, DeltaTime: ${deltaTime.toFixed(4)}, Active: ${gameActive}, Paused: ${isPaused}`); // DEBUG
if (gameActive && !isPaused) {
try { // Wrap physics updates
// --- UPDATE ---
applyForces(car, deltaTime);
updateObstaclesPhysics(deltaTime); // Updates positions, geometry, screen bounds
checkAllCollisions();
} catch (e) {
console.error("Error during game update logic:", e);
// Consider pausing game on error?
// pauseGame();
}
} else {
// Keep timestamp updated even when paused/inactive to prevent jump on resume
// lastTimestamp = performance.now(); // Reconsider this - might cause jump if paused long
}
// --- DRAW ---
if (ctx && canvas) {
try { // Wrap drawing
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground(ctx);
drawTireMarks(ctx);
drawObstacles(ctx);
drawPowerUps(ctx);
drawCar(ctx);
drawGameInfo(ctx);
} catch (e) {
console.error("Error during drawing:", e);
// Might indicate issues with object properties being drawn
}
} else if (!ctx) {
// console.warn("gameLoop: ctx is null, skipping draw."); // DEBUG
}
// Request next frame
animationFrameId = requestAnimationFrame(gameLoop);
}
// --- [ Game Logic - END ] ---
// Final check after everything is defined
console.log("Game script initialized. Waiting for load event and user interaction."); // DEBUG
</script>
</body>
</html>