ParthSadaria commited on
Commit
855afc9
·
verified ·
1 Parent(s): 9881758

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +567 -885
index.html CHANGED
@@ -617,7 +617,7 @@
617
  </div>
618
 
619
  <script>
620
- // --- [ Existing Mouse Cursor Effects Code - Keep As Is - No changes needed here] ---
621
  const cursor = document.querySelector('.cursor');
622
  const glow = document.querySelector('.glow');
623
  const hoverables = document.querySelectorAll('a, .model-badge, .game-toggle, h1'); // Added h1
@@ -761,181 +761,221 @@
761
  glowHalfHeight = glow.offsetHeight / 2;
762
  });
763
 
764
- // --- [ Game Logic - START - Overhauled ] ---
 
 
765
  const gameToggle = document.getElementById('game-toggle');
766
  const gameContainer = document.getElementById('game-container');
767
  const canvas = document.getElementById('game-canvas');
768
  const scoreDisplay = document.getElementById('score-display');
769
  const gameMessage = document.getElementById('game-message');
770
- const pauseOverlay = document.getElementById('pause-overlay'); // Get pause overlay
771
 
772
  let ctx = null;
773
  if (canvas) {
774
- ctx = canvas.getContext('2d');
 
 
 
 
 
 
 
 
 
775
  } else {
776
  console.error("Game canvas element not found!");
777
  }
778
 
779
  // Game state
780
- let gameActive = false; // Is the game logic running (not paused)?
781
- let gameVisible = false; // Is the game container displayed?
782
- let isPaused = false; // Is the game explicitly paused?
783
  let animationFrameId = null;
784
  let score = 0;
785
  let lastTimestamp = 0;
786
  let deltaTime = 0;
787
- let timeScale = 1.0; // To allow for slow-mo effects maybe later
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788
 
789
- // Canvas sizing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
790
  function resizeCanvas() {
791
- if (!canvas) return;
 
 
 
792
  canvas.width = window.innerWidth;
793
  canvas.height = window.innerHeight;
794
- // Re-create obstacles/powerups if needed after resize, or adjust positions
795
- // Simple approach: Reset game on resize if active? Or just redraw background.
796
- // For now, let's redraw background. More complex requires repositioning everything.
797
  if(gameActive && ctx) {
798
- drawBackground(ctx); // Redraw static background immediately
799
- // Consider repositioning objects if bounds change significantly
800
  }
801
  }
802
-
803
  window.addEventListener('resize', resizeCanvas);
804
- if (canvas) resizeCanvas();
805
-
806
-
807
- // --- Physics Constants ---
808
- const FRICTION = 0.98; // General velocity damping
809
- const ANGULAR_FRICTION = 0.95; // Rotational damping
810
- const CAR_ENGINE_FORCE = 13000; // Force applied by engine (adjust for feel)
811
- const CAR_BRAKE_FORCE = 18000; // Force applied when braking (S key)
812
- const CAR_HANDBRAKE_FORCE = 25000; // Stronger force for handbrake drift
813
- const CAR_TURN_TORQUE = 100000; // Torque applied for steering
814
- const CAR_MASS = 1000; // Kilograms (for physics calc)
815
- const CAR_INERTIA = CAR_MASS * 500; // Approx moment of inertia (adjust for rotational feel)
816
- const OBSTACLE_DENSITY = 5; // kg per pixel area (for mass calc)
817
- const COLLISION_RESTITUTION = 0.3; // Bounciness (0-1)
818
- const SCREEN_BOUND_RESTITUTION = 0.4; // How much objects bounce off walls
819
- const TIRE_LATERAL_GRIP = 6.0; // How strongly tires resist sideways motion (higher = less drift)
820
- const TIRE_HANDBRAKE_GRIP_FACTOR = 0.2; // Reduced lateral grip during handbrake
821
-
822
-
823
- // Car properties - Physics Based
824
- const car = {
825
- x: 0, y: 0, // Position (meters, scaled from pixels)
826
- vx: 0, vy: 0, // Velocity (m/s)
827
- angle: 0, // Angle (radians)
828
- angularVelocity: 0, // Angular velocity (rad/s)
829
- width: 25, height: 45, // Dimensions (visual pixels, use for drawing)
830
- physicsWidth: 2.0, physicsHeight: 4.0, // Approx physics dimensions (meters)
831
- mass: CAR_MASS,
832
- invMass: 1 / CAR_MASS,
833
- inertia: CAR_INERTIA, // Moment of inertia (kg*m^2)
834
- invInertia: 1 / CAR_INERTIA,
835
- color: '#00ffff',
836
- shadowColor: 'rgba(0, 255, 255, 0.3)',
837
- drifting: false,
838
- tireMarks: [],
839
- // Turbo properties remain similar
840
- turboMode: false,
841
- turboTimer: 0, turboMaxTime: 3,
842
- turboCooldown: 0, turboCooldownMax: 5,
843
- turboForceBoost: 10000, // Added force during turbo
844
- };
845
 
846
 
847
  // --- Game Toggle and State Management ---
848
  if (gameToggle && gameContainer) {
 
849
  gameToggle.addEventListener('click', () => {
 
850
  if (!gameVisible) {
851
  startGame();
852
  } else {
853
- stopGame(); // Exit completely
854
  }
855
  });
856
  } else {
857
- console.error("Game toggle button or container not found!");
858
  }
859
 
860
  function startGame() {
861
- if (!gameContainer || !ctx) return;
 
 
 
862
  gameVisible = true;
863
  gameActive = true;
864
  isPaused = false;
865
- isGameFocused = true; // Let cursor logic know
866
- gameContainer.classList.add('active'); // Fade in
867
- pauseOverlay.style.display = 'none';
868
- document.body.style.cursor = 'none'; // Use game cursor or hide system cursor
869
- cursor.style.display = 'none'; // Hide custom cursor element
870
- glow.style.display = 'none'; // Hide glow element
871
- resizeCanvas();
872
- resetGame();
873
- lastTimestamp = performance.now(); // Use performance.now for higher precision
874
- if (animationFrameId) cancelAnimationFrame(animationFrameId); // Clear any previous loop
 
 
 
 
 
 
 
 
 
 
875
  animationFrameId = requestAnimationFrame(gameLoop);
876
  }
877
 
878
  function stopGame() {
879
- if (!gameContainer) return;
 
880
  gameVisible = false;
881
  gameActive = false;
882
  isPaused = false;
883
- isGameFocused = false; // Release focus
884
- gameContainer.classList.remove('active'); // Fade out
885
- // Reset body cursor AFTER fade out transition? Or immediately?
886
- document.body.style.cursor = 'none'; // Keep custom cursor visible on page
887
- cursor.style.display = ''; // Show custom cursor
888
- glow.style.display = ''; // Show glow
889
  updateCursorState(); // Recalculate hover state
890
  if (animationFrameId) {
891
  cancelAnimationFrame(animationFrameId);
 
892
  animationFrameId = null;
893
  }
894
  lastTimestamp = 0;
895
  }
896
 
897
  function pauseGame() {
898
- if (!gameActive) return; // Can only pause if active
 
899
  isPaused = true;
900
- gameActive = false; // Stop game logic updates
901
- pauseOverlay.style.display = 'flex';
902
- // Keep gameVisible = true
903
  }
904
 
905
  function resumeGame() {
906
- if (!gameVisible || !isPaused) return; // Can only resume if visible and paused
 
907
  isPaused = false;
908
- gameActive = true; // Resume game logic
909
- pauseOverlay.style.display = 'none';
910
- lastTimestamp = performance.now(); // Reset timestamp to avoid jump
911
- if (!animationFrameId) { // Restart loop if it somehow stopped
 
912
  animationFrameId = requestAnimationFrame(gameLoop);
913
  }
914
  }
915
 
916
- // Key handling
917
  const keys = { up: false, down: false, left: false, right: false, handbrake: false };
918
-
919
  window.addEventListener('keydown', (e) => {
920
- if (!gameVisible) return; // Only handle keys if game container is visible
921
-
922
- // Prevent default browser behavior for game keys
923
  if ([' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'w', 'a', 's', 'd', 'W', 'A', 'S', 'D'].includes(e.key)) {
924
  e.preventDefault();
925
  }
926
-
927
  if (e.key === 'Escape') {
928
- if (isPaused) {
929
- stopGame(); // Second escape exits game
930
- } else if (gameActive) {
931
- pauseGame(); // First escape pauses
932
- }
933
- return; // Don't process other keys if escape was hit
934
  }
935
-
936
- // If paused, don't handle movement keys
937
  if (isPaused) return;
938
-
939
  switch(e.key.toLowerCase()) {
940
  case 'arrowup': case 'w': keys.up = true; break;
941
  case 'arrowdown': case 's': keys.down = true; break;
@@ -944,9 +984,7 @@
944
  case ' ': keys.handbrake = true; break;
945
  }
946
  });
947
-
948
  window.addEventListener('keyup', (e) => {
949
- // Always reset key state regardless of game state to avoid stuck keys
950
  switch(e.key.toLowerCase()) {
951
  case 'arrowup': case 'w': keys.up = false; break;
952
  case 'arrowdown': case 's': keys.down = false; break;
@@ -957,979 +995,620 @@
957
  });
958
 
959
 
960
- // Obstacles (Revised structure for physics)
961
- const obstacleTypes = [ /* Same as before */
962
- { text: "GPT-4o", width: 100, height: 40, color: "#ff5a5a" },
963
- { text: "Claude 3.7", width: 120, height: 40, color: "#5a92ff" },
964
- { text: "Gemini", width: 90, height: 40, color: "#5aff7f" },
965
- { text: "Loki.AI", width: 120, height: 50, color: "#ffaa5a" },
966
- { text: "AI", width: 60, height: 40, color: "#aa5aff" },
967
- { text: "Premium", width: 110, height: 40, color: "#ff5aaa" }
968
- ];
969
- const obstacles = [];
970
- const maxObstacles = 20; // Reduced slightly for potentially heavier physics
971
-
972
- // PowerUps (Revised structure for physics)
973
- const powerUps = [];
974
- const maxPowerUps = 3;
975
-
976
- // --- Helper Functions ---
977
- function worldToScreen(x, y) {
978
- // Simple 1:1 mapping for now, but could introduce scaling/camera later
979
- return { x: x, y: y };
980
- }
981
- function screenToWorld(x, y) {
982
- return { x: x, y: y };
983
- }
984
- // Simple Vector operations (can be expanded into a class)
985
- function vecAdd(v1, v2) { return { x: v1.x + v2.x, y: v1.y + v2.y }; }
986
- function vecSub(v1, v2) { return { x: v1.x - v2.x, y: v1.y - v2.y }; }
987
- function vecScale(v, s) { return { x: v.x * s, y: v.y * s }; }
988
- function vecLength(v) { return Math.sqrt(v.x * v.x + v.y * v.y); }
989
- function vecNormalize(v) { const l = vecLength(v); return l > 0 ? { x: v.x / l, y: v.y / l } : { x: 0, y: 0 }; }
990
- function vecDot(v1, v2) { return v1.x * v2.x + v1.y * v2.y; }
991
- function vecRotate(v, angle) {
992
- const cosA = Math.cos(angle);
993
- const sinA = Math.sin(angle);
994
- return { x: v.x * cosA - v.y * sinA, y: v.x * sinA + v.y * cosA };
995
- }
996
- // Cross product in 2D (for torque calculation) r x F -> scalar
997
- function vecCross2D(r, F) { return r.x * F.y - r.y * F.x; }
998
-
999
-
1000
  // --- Object Creation ---
1001
  function createObstacles() {
1002
- if (!canvas) return;
1003
- obstacles.length = 0;
1004
- const margin = 50; // Margin from edges
1005
- const canvasW = canvas.width;
1006
- const canvasH = canvas.height;
1007
-
1008
- for (let i = 0; i < maxObstacles; i++) {
1009
- const type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];
1010
- const width = type.width;
1011
- const height = type.height;
1012
- const mass = width * height * OBSTACLE_DENSITY;
1013
- const inertia = mass * (width*width + height*height) / 12; // Rectangular prism inertia
1014
-
1015
- // Try to place without overlap (simple distance check for start)
1016
- let placed = false;
1017
- let attempts = 0;
1018
- let x, y;
1019
- while (!placed && attempts < 20) {
1020
- x = Math.random() * (canvasW - width - 2 * margin) + margin + width / 2;
1021
- y = Math.random() * (canvasH - height - 2 * margin) + margin + height / 2;
1022
- let tooClose = false;
1023
- for(const existing of obstacles) {
1024
- const dist = Math.hypot(x - existing.x, y - existing.y);
1025
- if (dist < (Math.max(width, height) + Math.max(existing.width, existing.height))/1.5) { // Increased spacing
1026
- tooClose = true;
1027
- break;
1028
- }
1029
- }
1030
- // Avoid spawning near center start position
1031
  if (Math.hypot(x - canvasW/2, y - canvasH/2) < 200) {
1032
- tooClose = true;
1033
  }
1034
- if (!tooClose) placed = true;
1035
- attempts++;
 
 
 
 
 
 
 
 
 
 
 
 
 
1036
  }
1037
- if (!placed) { // Fallback if overlap check fails
1038
- x = Math.random() * (canvasW - width - 2 * margin) + margin + width / 2;
1039
- y = Math.random() * (canvasH - height - 2 * margin) + margin + height / 2;
 
 
 
 
1040
  }
1041
-
1042
-
1043
- obstacles.push({
1044
- x: x, y: y,
1045
- vx: 0, vy: 0,
1046
- angle: Math.random() * Math.PI * 2,
1047
- angularVelocity: (Math.random() - 0.5) * 0.3,
1048
- width: width, height: height,
1049
- mass: mass, invMass: 1 / mass,
1050
- inertia: inertia, invInertia: 1 / inertia,
1051
- text: type.text, color: type.color,
1052
- vertices: [], // To store calculated vertices for SAT
1053
- axes: [] // To store calculated axes for SAT
1054
- });
1055
- }
1056
- obstacles.forEach(updateVerticesAndAxes); // Calculate initial geometry
1057
- }
1058
 
1059
  function createPowerUps() {
1060
- // Similar logic, maybe slightly simpler placement check
1061
- if (!canvas) return;
1062
  powerUps.length = 0;
 
1063
  const margin = 60;
1064
- let attempts = 0;
1065
- while (powerUps.length < maxPowerUps && attempts < maxPowerUps * 10) {
1066
- const type = Math.random() < 0.5 ? 'turbo' : 'score'; // 50% chance turbo
1067
- const x = Math.random() * (canvas.width - 2 * margin) + margin;
1068
- const y = Math.random() * (canvas.height - 2 * margin) + margin;
1069
- const radius = 18;
1070
- let overlaps = false;
1071
- // Check against obstacles
1072
- for (const obs of obstacles) {
1073
- if (Math.hypot(x - obs.x, y - obs.y) < radius + Math.max(obs.width, obs.height) / 2 + 20) {
1074
- overlaps = true; break;
1075
- }
1076
- }
1077
- // Check against other powerups
1078
- if (!overlaps) {
1079
- for (const pup of powerUps) {
1080
- if (Math.hypot(x - pup.x, y - pup.y) < radius * 2 + 10) {
1081
- overlaps = true; break;
1082
- }
1083
- }
1084
- }
1085
- // Check against car start
1086
- if (Math.hypot(x - canvas.width/2, y - canvas.height/2) < 150) {
1087
- overlaps = true;
1088
- }
1089
 
1090
- if (!overlaps) {
1091
  powerUps.push({
1092
  x: x, y: y, radius: radius,
1093
  type: type, color: type === 'turbo' ? '#ff00ff' : '#ffff00',
1094
  active: true, pulseOffset: Math.random() * Math.PI * 2
1095
  });
 
 
1096
  }
1097
- attempts++;
1098
  }
 
1099
  }
1100
 
1101
  function showGameMessage(text, color, duration = 1500) {
 
1102
  if (!gameMessage) return;
1103
  gameMessage.textContent = text;
1104
  gameMessage.style.color = color;
1105
- gameMessage.classList.add('visible'); // Use class for fade in
1106
  setTimeout(() => {
1107
- gameMessage.classList.remove('visible'); // Fade out
1108
  }, duration);
1109
  }
1110
-
1111
- function activateTurbo() {
1112
  if (car.turboCooldown <= 0) {
1113
  car.turboMode = true;
1114
  car.turboTimer = car.turboMaxTime;
1115
  showGameMessage("TURBO!", '#ff00ff', 1500);
1116
  }
1117
  }
1118
-
1119
- function addScore(amount = 100, position = null) {
1120
  score += amount;
1121
  updateScore();
1122
- // Optional: Show score popup at collision point
1123
- // if(position) { createScorePopup(amount, position.x, position.y); }
1124
-
1125
- // Show general score message only for significant scores (e.g., powerup)
1126
  if (amount >= 200) {
1127
  showGameMessage(`+${amount} PTS!`, '#ffff00', 1000);
1128
  }
1129
  }
1130
 
1131
  function resetGame() {
1132
- if (!canvas) return;
1133
- car.x = canvas.width / 2;
1134
- car.y = canvas.height / 2;
1135
- car.vx = 0; car.vy = 0;
1136
- car.angle = -Math.PI / 2;
1137
- car.angularVelocity = 0;
1138
- car.tireMarks = [];
1139
- car.turboMode = false;
1140
- car.turboTimer = 0;
1141
- car.turboCooldown = 0;
1142
-
1143
- score = 0;
1144
- updateScore();
1145
-
1146
- obstacles.length = 0;
1147
- powerUps.length = 0;
1148
-
1149
- createObstacles();
1150
- createPowerUps();
 
 
 
1151
  }
1152
 
1153
  function updateScore() {
1154
- if (scoreDisplay) {
1155
- scoreDisplay.textContent = `Score: ${score}`; // Simplified score display
1156
- }
1157
  }
1158
 
1159
- // --- Drawing Functions (Mostly unchanged, but using physics coords) ---
1160
- function drawCar(ctx) {
1161
  if (!ctx) return;
1162
  const { x: screenX, y: screenY } = worldToScreen(car.x, car.y);
1163
-
1164
  ctx.save();
1165
  ctx.translate(screenX, screenY);
1166
  ctx.rotate(car.angle);
1167
-
1168
- // Car shadow
1169
  ctx.shadowColor = car.shadowColor;
1170
- ctx.shadowBlur = car.drifting ? 25 : 15; // More blur when drifting
1171
- ctx.shadowOffsetX = 5;
1172
- ctx.shadowOffsetY = 5;
1173
-
1174
- // Car body (use fixed pixel dimensions)
1175
- const drawWidth = car.width;
1176
- const drawHeight = car.height;
1177
  ctx.fillStyle = car.turboMode ? '#ff00ff' : car.color;
1178
  ctx.fillRect(-drawWidth/2, -drawHeight/2, drawWidth, drawHeight);
1179
-
1180
- // Reset shadow
1181
- ctx.shadowColor = 'transparent';
1182
- ctx.shadowBlur = 0;
1183
-
1184
- // Details (simplified for clarity)
1185
- ctx.fillStyle = "#223344"; // Windows
1186
  ctx.fillRect(-drawWidth/2 * 0.8, -drawHeight/2 * 0.7, drawWidth * 0.8, drawHeight * 0.3);
1187
  ctx.fillRect(-drawWidth/2 * 0.7, drawHeight/2 * 0.4, drawWidth * 0.7, drawHeight * 0.2);
1188
- ctx.fillStyle = "#ffffaa"; // Headlights
1189
  ctx.fillRect(-drawWidth/2 * 0.4, -drawHeight/2 - 2, drawWidth * 0.2, 4);
1190
  ctx.fillRect( drawWidth/2 * 0.2, -drawHeight/2 - 2, drawWidth * 0.2, 4);
1191
- ctx.fillStyle = "#ffaaaa"; // Taillights
1192
  ctx.fillRect(-drawWidth/2 * 0.4, drawHeight/2 - 2, drawWidth * 0.2, 4);
1193
  ctx.fillRect( drawWidth/2 * 0.2, drawHeight/2 - 2, drawWidth * 0.2, 4);
1194
-
1195
- // Turbo exhaust flames (Improved intensity)
1196
- if (car.turboMode) {
1197
- for (let i = 0; i < 3; i++) { // Draw multiple layers
1198
- const flameLength = 25 + Math.random() * 20;
1199
- const flameWidth = 12 + Math.random() * 6;
1200
- const offsetX = (Math.random() - 0.5) * 5;
1201
- ctx.fillStyle = `rgba(255, ${Math.random() * 100 + 100}, 0, ${0.5 + Math.random() * 0.3})`;
1202
- ctx.beginPath();
1203
- ctx.moveTo(offsetX - flameWidth / 3, drawHeight / 2);
1204
- ctx.lineTo(offsetX, drawHeight / 2 + flameLength);
1205
- ctx.lineTo(offsetX + flameWidth / 3, drawHeight / 2);
1206
- ctx.closePath();
1207
- ctx.fill();
1208
- }
1209
- }
1210
- // Drifting smoke/dust effect (subtle particles)
1211
- if (car.drifting && Math.hypot(car.vx, car.vy) > 5) { // Only if moving and drifting
1212
- const wheelOffset = drawWidth * 0.4;
1213
- const axleOffset = drawHeight * 0.4;
1214
- const cosA = Math.cos(car.angle);
1215
- const sinA = Math.sin(car.angle);
1216
- // Rear wheel positions approx
1217
- const rearLeftX = screenX - sinA * wheelOffset - cosA * axleOffset;
1218
- const rearLeftY = screenY + cosA * wheelOffset - sinA * axleOffset;
1219
- const rearRightX = screenX + sinA * wheelOffset - cosA * axleOffset;
1220
- const rearRightY = screenY - cosA * wheelOffset - sinA * axleOffset;
1221
-
1222
- ctx.fillStyle = `rgba(180, 180, 180, ${0.1 + Math.random() * 0.1})`; // Semi-transparent grey
1223
- for (let i = 0; i < 3; i++) { // Few particles per frame
1224
- const size = 2 + Math.random() * 4;
1225
- const lifeOffsetX = (Math.random() - 0.5) * 15;
1226
- const lifeOffsetY = (Math.random() - 0.5) * 15;
1227
- ctx.fillRect(rearLeftX + lifeOffsetX - size / 2, rearLeftY + lifeOffsetY - size / 2, size, size);
1228
- ctx.fillRect(rearRightX + lifeOffsetX - size / 2, rearRightY + lifeOffsetY - size / 2, size, size);
1229
- }
1230
- }
1231
-
1232
-
1233
  ctx.restore();
1234
  }
1235
-
1236
- function drawTireMarks(ctx) {
1237
- if (!ctx || car.tireMarks.length === 0) return;
1238
-
1239
- ctx.lineCap = "round";
1240
- const maxMarksToDraw = 100; // Limit drawing for performance
1241
- const startIdx = Math.max(0, car.tireMarks.length - maxMarksToDraw);
1242
-
1243
- for (let i = startIdx; i < car.tireMarks.length; i++) {
1244
- const mark = car.tireMarks[i];
1245
- mark.life -= deltaTime * timeScale;
1246
-
1247
- if (mark.life <= 0) {
1248
- // Instead of splicing (slow), mark as dead or filter later
1249
- continue; // Skip drawing dead marks
1250
- }
1251
-
1252
- const alpha = Math.max(0, Math.min(1, mark.life / mark.maxLife));
1253
- ctx.strokeStyle = `rgba(40, 40, 40, ${alpha * 0.6})`; // Darker grey, slightly more opaque
1254
- ctx.lineWidth = mark.width * alpha; // Fade width too
1255
-
1256
- // Draw left track segment
1257
- ctx.beginPath();
1258
- ctx.moveTo(mark.lx1, mark.ly1);
1259
- ctx.lineTo(mark.lx2, mark.ly2);
1260
- ctx.stroke();
1261
-
1262
- // Draw right track segment
1263
- ctx.beginPath();
1264
- ctx.moveTo(mark.rx1, mark.ry1);
1265
- ctx.lineTo(mark.rx2, mark.ry2);
1266
- ctx.stroke();
1267
- }
1268
-
1269
- // Cleanup old marks occasionally (less frequent than splice per frame)
1270
- if (Math.random() < 0.05) { // ~5% chance per frame
1271
- car.tireMarks = car.tireMarks.filter(mark => mark.life > 0);
1272
- }
1273
- // Hard limit
1274
- if (car.tireMarks.length > 250) {
1275
- car.tireMarks.splice(0, car.tireMarks.length - 200);
1276
- }
1277
  }
1278
-
1279
- function addTireMarkSegment() {
1280
- const wheelOffset = car.width * 0.4; // Visual wheel offset
1281
- const axleOffset = car.height * 0.35; // Rear axle relative to center
1282
-
1283
- const cosA = Math.cos(car.angle);
1284
- const sinA = Math.sin(car.angle);
1285
-
1286
- // Calculate current screen positions of rear wheels
1287
  const { x: screenX, y: screenY } = worldToScreen(car.x, car.y);
1288
- const rearLeftX = screenX - sinA * wheelOffset - cosA * axleOffset;
1289
- const rearLeftY = screenY + cosA * wheelOffset - sinA * axleOffset;
1290
- const rearRightX = screenX + sinA * wheelOffset - cosA * axleOffset;
1291
- const rearRightY = screenY - cosA * wheelOffset - sinA * axleOffset;
1292
-
1293
- const maxLife = 1.8; // Seconds
1294
- const markWidth = 4; // Pixels
1295
-
1296
- // Get the last mark segment
1297
  const lastMark = car.tireMarks.length > 0 ? car.tireMarks[car.tireMarks.length - 1] : null;
1298
-
1299
- if (lastMark && lastMark.life > 0) {
1300
- // Update the end position of the *previous* segment
1301
- lastMark.lx2 = rearLeftX;
1302
- lastMark.ly2 = rearLeftY;
1303
- lastMark.rx2 = rearRightX;
1304
- lastMark.ry2 = rearRightY;
1305
- }
1306
- // Add a *new* segment starting and ending at the current position
1307
- // This new segment's end point will be updated next frame
1308
- car.tireMarks.push({
1309
- lx1: rearLeftX, ly1: rearLeftY, lx2: rearLeftX, ly2: rearLeftY, // Left track
1310
- rx1: rearRightX, ry1: rearRightY, rx2: rearRightX, ry2: rearRightY, // Right track
1311
- life: maxLife, maxLife: maxLife, width: markWidth
1312
- });
1313
  }
1314
-
1315
-
1316
- function drawObstacles(ctx) {
1317
  if (!ctx) return;
1318
  for (const obs of obstacles) {
1319
  const { x: screenX, y: screenY } = worldToScreen(obs.x, obs.y);
1320
- ctx.save();
1321
- ctx.translate(screenX, screenY);
1322
- ctx.rotate(obs.angle);
1323
-
1324
- // Simple shadow for obstacles
1325
- ctx.fillStyle = 'rgba(0,0,0,0.4)';
1326
- ctx.fillRect(-obs.width/2 + 3, -obs.height/2 + 3, obs.width, obs.height);
1327
-
1328
- // Body
1329
- ctx.fillStyle = obs.color;
1330
- ctx.fillRect(-obs.width/2, -obs.height/2, obs.width, obs.height);
1331
-
1332
- // Text
1333
- ctx.font = 'bold 13px "DM Sans", sans-serif'; // Slightly smaller
1334
- ctx.textAlign = 'center';
1335
- ctx.textBaseline = 'middle';
1336
- ctx.fillStyle = '#ffffff';
1337
- ctx.fillText(obs.text, 0, 1); // Slight offset down
1338
-
1339
  ctx.restore();
1340
-
1341
- // --- DEBUG: Draw Collision Vertices ---
1342
- /*
1343
- if (obs.vertices.length === 4) {
1344
- ctx.strokeStyle = 'yellow';
1345
- ctx.lineWidth = 1;
1346
- ctx.beginPath();
1347
- const v0 = worldToScreen(obs.vertices[0].x, obs.vertices[0].y);
1348
- ctx.moveTo(v0.x, v0.y);
1349
- for(let i=1; i<4; i++){
1350
- const vi = worldToScreen(obs.vertices[i].x, obs.vertices[i].y);
1351
- ctx.lineTo(vi.x, vi.y);
1352
- }
1353
- ctx.closePath();
1354
- ctx.stroke();
1355
- }
1356
- */
1357
  }
1358
  }
1359
-
1360
- function drawPowerUps(ctx) {
1361
- // Same drawing logic as before, just ensure coords are screen coords
1362
  if (!ctx) return;
1363
- for (const powerUp of powerUps) {
1364
- if (!powerUp.active) continue;
1365
- const { x: screenX, y: screenY } = worldToScreen(powerUp.x, powerUp.y);
1366
-
1367
- const scale = 1 + Math.sin(Date.now() / 250 + powerUp.pulseOffset) * 0.15;
1368
- const radius = powerUp.radius * scale;
1369
-
1370
- ctx.save();
1371
- ctx.translate(screenX, screenY);
1372
-
1373
- // Glow
1374
- const gradient = ctx.createRadialGradient(0, 0, radius * 0.5, 0, 0, radius * 1.3); // Wider glow
1375
- gradient.addColorStop(0, powerUp.color);
1376
- gradient.addColorStop(0.7, powerUp.color + '80'); // Mid alpha
1377
- gradient.addColorStop(1, powerUp.color + '00'); // Transparent end
1378
- ctx.fillStyle = gradient;
1379
- ctx.fillRect(-radius * 1.3, -radius * 1.3, radius * 2.6, radius * 2.6);
1380
-
1381
- // Circle
1382
- ctx.beginPath();
1383
- ctx.arc(0, 0, powerUp.radius, 0, Math.PI * 2);
1384
- ctx.fillStyle = powerUp.color;
1385
- ctx.fill();
1386
- // Outline
1387
- ctx.strokeStyle = 'rgba(255,255,255,0.5)';
1388
- ctx.lineWidth = 1;
1389
- ctx.stroke();
1390
-
1391
- // Text
1392
- ctx.fillStyle = '#111111';
1393
- ctx.font = 'bold 9px "DM Sans", sans-serif'; // Smaller text
1394
- ctx.textAlign = 'center';
1395
- ctx.textBaseline = 'middle';
1396
- ctx.fillText(powerUp.type === 'turbo' ? 'TURBO' : 'SCORE', 0, 1);
1397
-
1398
- ctx.restore();
1399
- }
1400
  }
1401
-
1402
- function drawBackground(ctx) {
1403
  if (!ctx || !canvas) return;
1404
- // Dark background
1405
- ctx.fillStyle = '#08080A';
1406
- ctx.fillRect(0, 0, canvas.width, canvas.height);
1407
-
1408
- // Grid
1409
- ctx.strokeStyle = 'rgba(0, 255, 255, 0.06)'; // Even fainter
1410
- ctx.lineWidth = 1;
1411
- const gridSize = 50;
1412
- // Consider offsetting grid based on camera/car position later if needed
1413
- for (let x = 0; x < canvas.width; x += gridSize) {
1414
- ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
1415
- }
1416
- for (let y = 0; y < canvas.height; y += gridSize) {
1417
- ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
1418
- }
1419
-
1420
- // Vignette (unchanged)
1421
- const vignetteRadius = canvas.width * 0.7;
1422
- const gradient = ctx.createRadialGradient(
1423
- canvas.width / 2, canvas.height / 2, vignetteRadius * 0.5,
1424
- canvas.width / 2, canvas.height / 2, vignetteRadius * 1.5
1425
- );
1426
- gradient.addColorStop(0, 'rgba(5, 5, 5, 0)');
1427
- gradient.addColorStop(1, 'rgba(5, 5, 5, 0.6)'); // Slightly stronger vignette
1428
- ctx.fillStyle = gradient;
1429
- ctx.fillRect(0, 0, canvas.width, canvas.height);
1430
- }
1431
-
1432
- function drawGameInfo(ctx) {
1433
- // HUD drawing (Speed, Turbo) - Kept similar logic
1434
- if (!ctx || !canvas) return;
1435
- const barWidth = 120;
1436
- const barHeight = 18;
1437
- const margin = 20;
1438
- const bottomY = canvas.height - margin - barHeight;
1439
- const rightX = canvas.width - margin;
1440
-
1441
- ctx.font = 'bold 12px "DM Sans", sans-serif';
1442
- ctx.textBaseline = 'middle';
1443
- ctx.textAlign = 'right';
1444
-
1445
- // --- Speed Gauge ---
1446
- const currentSpeed = vecLength({x: car.vx, y: car.vy});
1447
- const maxVisualSpeed = 50; // Speed corresponding to full bar (adjust for feel)
1448
- const speedPercent = Math.min(1, currentSpeed / maxVisualSpeed);
1449
- const speedColor = car.turboMode ? '#ff00ff' : '#00ffff';
1450
-
1451
- ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
1452
- ctx.fillRect(rightX - barWidth, bottomY, barWidth, barHeight);
1453
- ctx.fillStyle = speedColor;
1454
- ctx.fillRect(rightX - barWidth, bottomY, barWidth * speedPercent, barHeight);
1455
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
1456
- ctx.strokeRect(rightX - barWidth, bottomY, barWidth, barHeight);
1457
- ctx.fillStyle = '#ffffff';
1458
- ctx.fillText(`SPD: ${Math.floor(currentSpeed * 5)}`, rightX - barWidth - 10, bottomY + barHeight / 2); // Scaled for display
1459
-
1460
-
1461
- // --- Turbo Gauge ---
1462
- const turboY = bottomY - barHeight - 10;
1463
- let turboText = "TURBO READY";
1464
- let turboFillPercent = 0;
1465
- let turboColor = '#ff00ff';
1466
-
1467
- if (car.turboMode) {
1468
- turboText = "ACTIVE";
1469
- turboFillPercent = car.turboTimer / car.turboMaxTime;
1470
- } else if (car.turboCooldown > 0) {
1471
- turboText = "RECHARGE";
1472
- turboFillPercent = 1 - (car.turboCooldown / car.turboCooldownMax);
1473
- turboColor = 'rgba(255, 0, 255, 0.5)';
1474
- } else {
1475
- turboFillPercent = 1;
1476
- }
1477
-
1478
- ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
1479
- ctx.fillRect(rightX - barWidth, turboY, barWidth, barHeight);
1480
- ctx.fillStyle = turboColor;
1481
- ctx.fillRect(rightX - barWidth, turboY, barWidth * turboFillPercent, barHeight);
1482
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
1483
- ctx.strokeRect(rightX - barWidth, turboY, barWidth, barHeight);
1484
- ctx.fillStyle = '#ffffff';
1485
- ctx.fillText(turboText, rightX - barWidth - 10, turboY + barHeight / 2);
1486
  }
1487
 
1488
  // --- Physics Update Functions ---
1489
 
1490
- function applyForces(deltaTime) {
1491
- // Reset forces/torque each frame
 
 
1492
  let totalForce = { x: 0, y: 0 };
1493
  let totalTorque = 0;
1494
-
1495
- // --- Calculate Forward/Right Vectors ---
1496
- const forwardVec = { x: Math.sin(car.angle), y: -Math.cos(car.angle) };
1497
- const rightVec = { x: -forwardVec.y, y: forwardVec.x }; // Perpendicular to forward
1498
-
1499
- // --- Engine/Braking Force ---
1500
- let engineForceMag = 0;
1501
- if (keys.up) {
1502
- engineForceMag += CAR_ENGINE_FORCE;
1503
- if (car.turboMode) engineForceMag += car.turboForceBoost;
1504
- }
1505
- if (keys.down) {
1506
- // Apply braking force against current velocity direction if moving forward,
1507
- // or apply reverse force if stationary or moving backward.
1508
- const currentSpeed = vecLength({x: car.vx, y: car.vy});
1509
- const movingForward = vecDot({x: car.vx, y: car.vy}, forwardVec) > 0;
1510
- if (currentSpeed > 0.5 && movingForward) {
1511
- engineForceMag -= CAR_BRAKE_FORCE; // Strong braking
1512
- } else {
1513
- engineForceMag -= CAR_ENGINE_FORCE * 0.6; // Reverse force (less powerful)
1514
- }
1515
- }
1516
- totalForce = vecAdd(totalForce, vecScale(forwardVec, engineForceMag));
1517
-
1518
- // --- Steering Torque ---
1519
- // Apply torque based on turning keys - more effective at lower speeds?
1520
- const currentSpeed = vecLength({x: car.vx, y: car.vy});
1521
- const steeringEffectiveness = Math.max(0.2, 1.0 - (currentSpeed / 40)); // Reduce steering at high speed
1522
- if (keys.left) {
1523
- totalTorque -= CAR_TURN_TORQUE * steeringEffectiveness;
1524
- }
1525
- if (keys.right) {
1526
- totalTorque += CAR_TURN_TORQUE * steeringEffectiveness;
1527
  }
1528
 
1529
- // --- Friction / Drag ---
1530
- // Simple velocity damping (air resistance)
1531
- totalForce = vecAdd(totalForce, vecScale({x: car.vx, y: car.vy}, -FRICTION * 30)); // Linear drag term
1532
-
1533
- // --- Lateral Tire Friction (Grip) ---
1534
- // Calculate velocity component perpendicular to the car's direction
1535
- const lateralVelocity = vecDot({x: car.vx, y: car.vy}, rightVec);
1536
- // Determine grip factor (reduced during handbrake)
1537
- let gripFactor = keys.handbrake ? TIRE_HANDBRAKE_GRIP_FACTOR : 1.0;
1538
- // Force opposing lateral velocity
1539
- const lateralFrictionMag = -lateralVelocity * TIRE_LATERAL_GRIP * gripFactor * car.mass; // Grip force proportional to mass
1540
- totalForce = vecAdd(totalForce, vecScale(rightVec, lateralFrictionMag));
1541
-
1542
- // --- Handbrake ---
1543
- car.drifting = keys.handbrake && currentSpeed > 5; // Set drifting flag
1544
- if (car.drifting) {
1545
- // Add extra braking force when handbraking
1546
- const brakeDir = vecLength({x: car.vx, y: car.vy}) > 0.1 ? vecScale(vecNormalize({x: car.vx, y: car.vy}), -1) : {x:0, y:0};
1547
- totalForce = vecAdd(totalForce, vecScale(brakeDir, CAR_HANDBRAKE_FORCE * 0.5));
1548
- addTireMarkSegment(); // Add marks continuously while drifting
1549
- } else if (keys.up || keys.down) {
1550
- // Add tire marks only when accelerating/braking significantly and turning?
1551
- // Or just based on lateral slip? Let's add based on high lateral force.
1552
- if (Math.abs(lateralFrictionMag / car.mass) > 50) { // If lateral accel is high
1553
- //addTireMarkSegment(); // Add marks during hard turns too
1554
- }
1555
  }
1556
 
 
 
 
 
 
 
 
 
 
 
 
1557
 
1558
- // --- Apply forces/torque to update acceleration ---
1559
- const linearAccel = vecScale(totalForce, car.invMass);
1560
- const angularAccel = totalTorque * car.invInertia;
1561
-
1562
- // --- Integration (Euler method) ---
1563
- car.vx += linearAccel.x * deltaTime * timeScale;
1564
- car.vy += linearAccel.y * deltaTime * timeScale;
1565
- car.angularVelocity += angularAccel * deltaTime * timeScale;
1566
-
1567
- // Apply angular friction/damping
1568
- car.angularVelocity *= (1 - (1 - ANGULAR_FRICTION) * deltaTime * 60); // Frame-rate independent damping
1569
-
1570
- // Update position and angle
1571
- car.x += car.vx * deltaTime * timeScale;
1572
- car.y += car.vy * deltaTime * timeScale;
1573
- car.angle += car.angularVelocity * deltaTime * timeScale;
1574
 
1575
- // Keep angle within 0 to 2PI
1576
- // car.angle = car.angle % (2 * Math.PI);
1577
- // if (car.angle < 0) car.angle += 2 * Math.PI;
 
1578
 
1579
- // Update Turbo Timer and Cooldown (same logic)
1580
- if (car.turboMode) {
1581
- car.turboTimer -= deltaTime * timeScale;
1582
- if (car.turboTimer <= 0) {
1583
- car.turboMode = false;
1584
- car.turboTimer = 0;
1585
- car.turboCooldown = car.turboCooldownMax;
1586
- }
1587
- } else if (car.turboCooldown > 0) {
1588
- car.turboCooldown -= deltaTime * timeScale;
1589
- if (car.turboCooldown < 0) {
1590
- car.turboCooldown = 0;
1591
- }
1592
  }
1593
  }
1594
 
1595
  function updateObstaclesPhysics(deltaTime) {
1596
  if (!canvas) return;
1597
  const dt = deltaTime * timeScale;
1598
-
1599
  obstacles.forEach(obs => {
1600
- // Apply damping
1601
- obs.vx *= (1 - (1 - FRICTION) * dt * 30); // Less damping than car maybe
1602
- obs.vy *= (1 - (1 - FRICTION) * dt * 30);
1603
- obs.angularVelocity *= (1 - (1 - ANGULAR_FRICTION) * dt * 60);
1604
-
1605
- // Update position and angle
1606
- obs.x += obs.vx * dt;
1607
- obs.y += obs.vy * dt;
1608
- obs.angle += obs.angularVelocity * dt;
1609
-
1610
- // Update vertices for collision detection
1611
- updateVerticesAndAxes(obs);
1612
-
1613
- // Screen boundary collisions (Bounce)
1614
- handleScreenCollision(obs, canvas.width, canvas.height);
 
 
 
1615
  });
1616
  }
1617
 
 
1618
 
1619
- // --- Collision Detection & Resolution (Using SAT) ---
1620
-
1621
- // Update vertices based on position, angle, width, height
1622
  function updateVerticesAndAxes(obj) {
 
 
 
 
 
 
 
1623
  const w = obj.width / 2;
1624
  const h = obj.height / 2;
1625
  const cosA = Math.cos(obj.angle);
1626
  const sinA = Math.sin(obj.angle);
1627
-
1628
- // Calculate corners relative to center, then add center position
1629
- obj.vertices = [
1630
- { x: obj.x + (-w * cosA - -h * sinA), y: obj.y + (-w * sinA + -h * cosA) }, // Top-Left
1631
- { x: obj.x + ( w * cosA - -h * sinA), y: obj.y + ( w * sinA + -h * cosA) }, // Top-Right
1632
- { x: obj.x + ( w * cosA - h * sinA), y: obj.y + ( w * sinA + h * cosA) }, // Bottom-Right
1633
- { x: obj.x + (-w * cosA - h * sinA), y: obj.y + (-w * sinA + h * cosA) } // Bottom-Left
 
1634
  ];
1635
-
1636
- // Calculate edge normals (axes for SAT)
1637
  obj.axes = [];
1638
  for (let i = 0; i < 4; i++) {
1639
  const p1 = obj.vertices[i];
1640
  const p2 = obj.vertices[(i + 1) % 4];
 
 
 
 
 
1641
  const edge = vecSub(p2, p1);
1642
- const normal = vecNormalize({ x: -edge.y, y: edge.x }); // Perpendicular normalized
1643
  obj.axes.push(normal);
1644
  }
1645
  }
1646
 
1647
- // Project polygon vertices onto an axis
1648
  function projectPolygon(vertices, axis) {
1649
- let min = vecDot(vertices[0], axis);
1650
- let max = min;
 
 
 
 
1651
  for (let i = 1; i < vertices.length; i++) {
 
 
 
 
1652
  const p = vecDot(vertices[i], axis);
1653
- if (p < min) {
1654
- min = p;
1655
- } else if (p > max) {
1656
- max = p;
1657
- }
1658
  }
1659
  return { min: min, max: max };
1660
  }
1661
 
1662
- // Check collision between two objects using SAT
1663
  function checkSATCollision(objA, objB) {
1664
- // Update vertices if needed (car vertices change every frame)
1665
- updateVerticesAndAxes(objA); // Assuming objA is car or dynamic
1666
- // objB vertices updated in updateObstaclesPhysics
 
 
 
 
1667
 
1668
- const axes = [...objA.axes, ...objB.axes]; // Combine axes from both objects
1669
  let minOverlap = Infinity;
1670
  let collisionNormal = null;
1671
 
1672
  for (const axis of axes) {
 
 
 
1673
  const projA = projectPolygon(objA.vertices, axis);
1674
  const projB = projectPolygon(objB.vertices, axis);
1675
-
1676
  const overlap = Math.min(projA.max, projB.max) - Math.max(projA.min, projB.min);
1677
 
1678
- if (overlap <= 0) {
1679
- return null; // Separating axis found, no collision
1680
- }
1681
-
1682
- // Track minimum overlap and corresponding axis
1683
- if (overlap < minOverlap) {
1684
- minOverlap = overlap;
1685
- collisionNormal = axis;
1686
- }
1687
  }
1688
 
1689
- // Ensure the collision normal points from B to A
1690
- const centerA = { x: objA.x, y: objA.y };
1691
- const centerB = { x: objB.x, y: objB.y };
1692
- const direction = vecSub(centerA, centerB);
1693
- if (vecDot(direction, collisionNormal) < 0) {
1694
- collisionNormal = vecScale(collisionNormal, -1); // Flip normal
1695
- }
1696
 
 
1697
  return { overlap: minOverlap, normal: collisionNormal };
1698
  }
1699
 
1700
-
1701
- // Resolve collision using impulse
1702
  function resolveCollision(objA, objB, collisionInfo) {
1703
- const { overlap, normal } = collisionInfo;
1704
-
1705
- // --- 1. Positional Correction (Separation) ---
1706
- // Move objects apart based on inverse mass ratio
1707
- const totalInvMass = objA.invMass + objB.invMass;
1708
- if (totalInvMass <= 0) return; // Both objects are static/infinite mass
1709
-
1710
- const separationAmount = overlap / totalInvMass;
1711
- objA.x += normal.x * separationAmount * objA.invMass;
1712
- objA.y += normal.y * separationAmount * objA.invMass;
1713
- objB.x -= normal.x * separationAmount * objB.invMass;
1714
- objB.y -= normal.y * separationAmount * objB.invMass;
1715
 
1716
- // Re-update vertices after position change before impulse calc
1717
- updateVerticesAndAxes(objA);
1718
- updateVerticesAndAxes(objB);
 
 
 
1719
 
1720
 
1721
- // --- 2. Impulse Calculation ---
1722
- // Find collision point (approximate as contact point on line between centers)
1723
- // More accurate: Find closest points on edges, but complex. Average centers is simpler.
1724
- const collisionPoint = {
1725
- x: (objA.x + objB.x) / 2,
1726
- y: (objA.y + objB.y) / 2
1727
- };
 
 
 
 
 
 
1728
 
1729
- // Calculate relative vector from center of mass to collision point
 
1730
  const rA = vecSub(collisionPoint, {x: objA.x, y: objA.y});
1731
  const rB = vecSub(collisionPoint, {x: objB.x, y: objB.y});
1732
-
1733
- // Calculate velocity of collision point on each object (Linear + Angular)
1734
- const vA = {
1735
- x: objA.vx + (-objA.angularVelocity * rA.y),
1736
- y: objA.vy + (objA.angularVelocity * rA.x)
1737
- };
1738
- const vB = {
1739
- x: objB.vx + (-objB.angularVelocity * rB.y),
1740
- y: objB.vy + (objB.angularVelocity * rB.x)
1741
- };
1742
-
1743
- // Relative velocity at collision point
1744
  const relativeVelocity = vecSub(vA, vB);
1745
  const velocityAlongNormal = vecDot(relativeVelocity, normal);
1746
 
1747
- // Don't resolve if objects are moving apart
1748
- if (velocityAlongNormal > 0) return;
1749
-
1750
- const restitution = COLLISION_RESTITUTION; // Bounciness
1751
 
1752
- // Calculate impulse scalar (j)
1753
  const rACrossN = vecCross2D(rA, normal);
1754
  const rBCrossN = vecCross2D(rB, normal);
1755
- const invMassSum = objA.invMass + objB.invMass;
1756
- const invInertiaSum = (rACrossN * rACrossN * objA.invInertia) + (rBCrossN * rBCrossN * objB.invInertia);
 
 
 
 
 
 
 
 
 
1757
 
1758
  let j = -(1 + restitution) * velocityAlongNormal;
1759
- j /= (invMassSum + invInertiaSum);
1760
 
1761
- // --- 3. Apply Impulse ---
1762
  const impulse = vecScale(normal, j);
 
 
 
 
1763
 
1764
- // Apply linear impulse
1765
- objA.vx += impulse.x * objA.invMass;
1766
- objA.vy += impulse.y * objA.invMass;
1767
- objB.vx -= impulse.x * objB.invMass;
1768
- objB.vy -= impulse.y * objB.invMass;
1769
-
1770
- // Apply angular impulse (torque)
1771
- objA.angularVelocity += vecCross2D(rA, impulse) * objA.invInertia;
1772
- objB.angularVelocity -= vecCross2D(rB, impulse) * objB.invInertia;
1773
 
1774
- // Add score based on impulse magnitude (more realistic impact score)
1775
- const impactMagnitude = Math.abs(j);
1776
- const scoreToAdd = Math.min(60, Math.max(2, Math.floor(impactMagnitude / 1000))); // Adjust scaling factor
1777
- // Only add score if car was involved
1778
  if (objA === car || objB === car) {
1779
- addScore(scoreToAdd, collisionPoint);
1780
- // Visual feedback? Screen shake? Particle burst?
1781
- if (impactMagnitude > 5000 && gameContainer) { // Trigger shake on bigger impacts
1782
- const intensity = Math.min(6, impactMagnitude / 5000);
1783
- gameContainer.style.transform = `translate(${(Math.random() - 0.5) * intensity}px, ${(Math.random() - 0.5) * intensity}px)`;
1784
- setTimeout(() => { if(gameContainer) gameContainer.style.transform = 'none'; }, 70);
1785
- }
1786
  }
1787
  }
1788
 
1789
- // Basic screen boundary collision
1790
- function handleScreenCollision(obj, width, height) {
1791
- const objRadius = Math.max(obj.width, obj.height) / 2; // Approximation
1792
-
1793
- for (const v of obj.vertices) {
1794
- let collided = false;
1795
- let normal = {x:0, y:0};
1796
-
1797
- if (v.x < 0) { v.x = 0; collided = true; normal = {x: 1, y: 0}; }
1798
- if (v.x > width) { v.x = width; collided = true; normal = {x:-1, y: 0}; }
1799
- if (v.y < 0) { v.y = 0; collided = true; normal = {x: 0, y: 1}; }
1800
- if (v.y > height) { v.y = height; collided = true; normal = {x: 0, y:-1}; }
1801
-
1802
- if (collided) {
1803
- // Simple velocity reflection (less physically accurate than impulse)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1804
  const velocity = { x: obj.vx, y: obj.vy };
1805
  const dot = vecDot(velocity, normal);
1806
- const impulseMag = -(1 + SCREEN_BOUND_RESTITUTION) * dot; // Only apply if moving towards wall
1807
-
1808
- if(impulseMag > 0){ // Ensure impulse is positive (acting away from wall)
1809
- const impulse = vecScale(normal, impulseMag);
1810
- obj.vx += impulse.x * obj.invMass;
1811
- obj.vy += impulse.y * obj.invMass;
1812
- // Maybe add slight random angular velocity on wall hit
1813
- obj.angularVelocity += (Math.random() - 0.5) * 0.1 * impulseMag * obj.invInertia;
1814
- }
1815
 
1816
- // Crude position correction to prevent sinking
1817
- if (v.x <= objRadius && normal.x > 0) obj.x = Math.max(obj.x, objRadius);
1818
- if (v.x >= width - objRadius && normal.x < 0) obj.x = Math.min(obj.x, width - objRadius);
1819
- if (v.y <= objRadius && normal.y > 0) obj.y = Math.max(obj.y, objRadius);
1820
- if (v.y >= height - objRadius && normal.y < 0) obj.y = Math.min(obj.y, height - objRadius);
1821
-
1822
- }
1823
- }
1824
- // Handle car screen wrap separately? Or bounce too? Let's bounce car too.
1825
- handleCarScreenCollision(car, width, height); // Apply wall bounce to car too
1826
- }
1827
- // Specific handler for car screen collision if needed (currently same as obstacle)
1828
- function handleCarScreenCollision(obj, width, height){
1829
- // Reuse handleScreenCollision for now, could customize later if needed
1830
- updateVerticesAndAxes(obj); // Ensure car vertices are up to date
1831
- handleScreenCollision(obj, width, height);
1832
  }
1833
 
1834
-
1835
  function checkAllCollisions() {
1836
  if (!canvas) return;
1837
 
1838
- // --- Car vs Obstacles ---
1839
- for (const obstacle of obstacles) {
1840
- const collisionInfo = checkSATCollision(car, obstacle);
1841
- if (collisionInfo) {
1842
- resolveCollision(car, obstacle, collisionInfo);
 
 
1843
  }
1844
- }
1845
 
1846
- // --- Obstacle vs Obstacles ---
1847
- for (let i = 0; i < obstacles.length; i++) {
1848
- for (let j = i + 1; j < obstacles.length; j++) {
1849
- const obsA = obstacles[i];
1850
- const obsB = obstacles[j];
1851
- const collisionInfo = checkSATCollision(obsA, obsB);
1852
- if (collisionInfo) {
1853
- resolveCollision(obsA, obsB, collisionInfo);
 
1854
  }
1855
  }
1856
- }
1857
 
1858
- // --- Car vs Power-Ups --- (Simple Circle Collision)
1859
- const carRadius = car.width / 2; // Approx radius
1860
- for (let i = powerUps.length - 1; i >= 0; i--) {
1861
- const powerUp = powerUps[i];
1862
- if (!powerUp.active) continue;
1863
- const dist = Math.hypot(car.x - powerUp.x, car.y - powerUp.y);
1864
-
1865
- if (dist < carRadius + powerUp.radius) {
1866
- powerUp.active = false;
1867
- if (powerUp.type === 'turbo') {
1868
- activateTurbo();
1869
- } else if (powerUp.type === 'score') {
1870
- addScore(250);
1871
  }
1872
- // Respawn a new one after delay
1873
- setTimeout(() => {
1874
- // Remove the collected one (find its index again)
1875
- const currentIdx = powerUps.findIndex(p => p === powerUp);
1876
- if(currentIdx !== -1) powerUps.splice(currentIdx, 1);
1877
- createPowerUps(); // Regenerate (simple way to add one back)
1878
- }, 2000 + Math.random() * 2000); // Delay before respawn
1879
  }
 
 
1880
  }
1881
  }
1882
 
1883
 
1884
  // --- Main Game Loop ---
 
1885
  function gameLoop(timestamp) {
1886
- if (!gameVisible) { // Stop if game container is hidden
1887
- animationFrameId = null;
1888
- return;
1889
- }
1890
 
1891
- // Calculate delta time
1892
  const now = performance.now();
1893
- deltaTime = (now - lastTimestamp) / 1000; // Seconds
 
1894
  lastTimestamp = now;
1895
-
1896
- // Cap deltaTime to prevent physics explosion after long pauses/lag
1897
- deltaTime = Math.min(deltaTime, 1 / 20); // Max delta = 1/20th sec (50ms)
1898
-
1899
- if (gameActive && !isPaused) { // Only update if game is active and not paused
1900
- // --- Physics Simulation Steps ---
1901
- // Fixed timestep simulation loop (optional but better for stability)
1902
- /*
1903
- const fixedDeltaTime = 1 / 60;
1904
- accumulator += deltaTime;
1905
- while (accumulator >= fixedDeltaTime) {
1906
- applyForces(fixedDeltaTime);
1907
- updateObstaclesPhysics(fixedDeltaTime);
1908
- checkAllCollisions(); // Check collisions *within* fixed step
1909
- accumulator -= fixedDeltaTime;
1910
- }
1911
- */
1912
- // --- Variable timestep (simpler for now) ---
1913
- applyForces(deltaTime);
1914
- updateObstaclesPhysics(deltaTime);
1915
- checkAllCollisions();
1916
 
1917
  } else {
1918
- // If paused, maybe still allow some visual updates? e.g., particle effects fade out?
1919
- // For now, just skip updates.
1920
  }
1921
 
1922
 
1923
  // --- DRAW ---
1924
  if (ctx && canvas) {
1925
- ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas
1926
-
1927
- drawBackground(ctx);
1928
- drawTireMarks(ctx);
1929
- drawObstacles(ctx);
1930
- drawPowerUps(ctx);
1931
- drawCar(ctx);
1932
- drawGameInfo(ctx); // Draw HUD elements
 
 
 
 
 
 
1933
  }
1934
 
1935
  // Request next frame
@@ -1938,6 +1617,9 @@
1938
 
1939
  // --- [ Game Logic - END ] ---
1940
 
 
 
 
1941
  </script>
1942
  </body>
1943
  </html>
 
617
  </div>
618
 
619
  <script>
620
+ // --- [ Mouse Cursor Effects Code - Unchanged - Keep As Is ] ---
621
  const cursor = document.querySelector('.cursor');
622
  const glow = document.querySelector('.glow');
623
  const hoverables = document.querySelectorAll('a, .model-badge, .game-toggle, h1'); // Added h1
 
761
  glowHalfHeight = glow.offsetHeight / 2;
762
  });
763
 
764
+ // --- [ Game Logic - START - Debugging Version ] ---
765
+ console.log("Game Script Initializing..."); // DEBUG
766
+
767
  const gameToggle = document.getElementById('game-toggle');
768
  const gameContainer = document.getElementById('game-container');
769
  const canvas = document.getElementById('game-canvas');
770
  const scoreDisplay = document.getElementById('score-display');
771
  const gameMessage = document.getElementById('game-message');
772
+ const pauseOverlay = document.getElementById('pause-overlay');
773
 
774
  let ctx = null;
775
  if (canvas) {
776
+ try {
777
+ ctx = canvas.getContext('2d');
778
+ if (!ctx) {
779
+ console.error("Failed to get 2D context from canvas.");
780
+ } else {
781
+ console.log("Canvas context obtained."); // DEBUG
782
+ }
783
+ } catch (e) {
784
+ console.error("Error getting canvas context:", e);
785
+ }
786
  } else {
787
  console.error("Game canvas element not found!");
788
  }
789
 
790
  // Game state
791
+ let gameActive = false;
792
+ let gameVisible = false;
793
+ let isPaused = false;
794
  let animationFrameId = null;
795
  let score = 0;
796
  let lastTimestamp = 0;
797
  let deltaTime = 0;
798
+ let timeScale = 1.0;
799
+
800
+ // --- Physics Constants (Unchanged) ---
801
+ const FRICTION = 0.98;
802
+ const ANGULAR_FRICTION = 0.95;
803
+ const CAR_ENGINE_FORCE = 13000;
804
+ const CAR_BRAKE_FORCE = 18000;
805
+ const CAR_HANDBRAKE_FORCE = 25000;
806
+ const CAR_TURN_TORQUE = 100000;
807
+ const CAR_MASS = 1000;
808
+ const CAR_INERTIA = CAR_MASS * 500;
809
+ const OBSTACLE_DENSITY = 5;
810
+ const COLLISION_RESTITUTION = 0.3;
811
+ const SCREEN_BOUND_RESTITUTION = 0.4;
812
+ const TIRE_LATERAL_GRIP = 6.0;
813
+ const TIRE_HANDBRAKE_GRIP_FACTOR = 0.2;
814
+
815
+ // --- Car properties (Add safety for invMass/invInertia) ---
816
+ const car = {
817
+ x: 0, y: 0, vx: 0, vy: 0, angle: 0, angularVelocity: 0,
818
+ width: 25, height: 45,
819
+ physicsWidth: 2.0, physicsHeight: 4.0,
820
+ mass: CAR_MASS,
821
+ invMass: CAR_MASS > 0 ? 1 / CAR_MASS : 0, // SAFETY CHECK
822
+ inertia: CAR_INERTIA,
823
+ invInertia: CAR_INERTIA > 0 ? 1 / CAR_INERTIA : 0, // SAFETY CHECK
824
+ color: '#00ffff', shadowColor: 'rgba(0, 255, 255, 0.3)',
825
+ drifting: false, tireMarks: [], vertices: [], axes: [], // Add vertices/axes here too
826
+ turboMode: false, turboTimer: 0, turboMaxTime: 3,
827
+ turboCooldown: 0, turboCooldownMax: 5, turboForceBoost: 10000,
828
+ };
829
 
830
+ // Obstacles & Powerups Arrays
831
+ const obstacles = [];
832
+ const powerUps = [];
833
+ const obstacleTypes = [ /* Same as before */
834
+ { text: "GPT-4o", width: 100, height: 40, color: "#ff5a5a" },
835
+ { text: "Claude 3.7", width: 120, height: 40, color: "#5a92ff" },
836
+ { text: "Gemini", width: 90, height: 40, color: "#5aff7f" },
837
+ { text: "Loki.AI", width: 120, height: 50, color: "#ffaa5a" },
838
+ { text: "AI", width: 60, height: 40, color: "#aa5aff" },
839
+ { text: "Premium", width: 110, height: 40, color: "#ff5aaa" }
840
+ ];
841
+ const maxObstacles = 20;
842
+ const maxPowerUps = 3;
843
+
844
+
845
+ // --- Canvas sizing ---
846
  function resizeCanvas() {
847
+ console.log("resizeCanvas called"); // DEBUG
848
+ if (!canvas) {
849
+ console.error("resizeCanvas: Canvas not found"); return;
850
+ }
851
  canvas.width = window.innerWidth;
852
  canvas.height = window.innerHeight;
853
+ console.log(`Canvas resized to ${canvas.width}x${canvas.height}`); // DEBUG
 
 
854
  if(gameActive && ctx) {
855
+ drawBackground(ctx);
 
856
  }
857
  }
 
858
  window.addEventListener('resize', resizeCanvas);
859
+ // Initial size set needs ctx to be ready
860
+ if (canvas && ctx) {
861
+ resizeCanvas();
862
+ } else if (canvas && !ctx) {
863
+ console.warn("resizeCanvas: ctx not ready during initial call.");
864
+ }
865
+
866
+
867
+ // --- Helper Functions (Unchanged) ---
868
+ function worldToScreen(x, y) { return { x: x, y: y }; }
869
+ function screenToWorld(x, y) { return { x: x, y: y }; }
870
+ function vecAdd(v1, v2) { return { x: v1.x + v2.x, y: v1.y + v2.y }; }
871
+ function vecSub(v1, v2) { return { x: v1.x - v2.x, y: v1.y - v2.y }; }
872
+ function vecScale(v, s) { return { x: v.x * s, y: v.y * s }; }
873
+ function vecLength(v) { return Math.sqrt(v.x * v.x + v.y * v.y); }
874
+ 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
875
+ function vecDot(v1, v2) { return v1.x * v2.x + v1.y * v2.y; }
876
+ 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 }; }
877
+ function vecCross2D(r, F) { return r.x * F.y - r.y * F.x; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
878
 
879
 
880
  // --- Game Toggle and State Management ---
881
  if (gameToggle && gameContainer) {
882
+ console.log("Attaching listener to game toggle button."); // DEBUG
883
  gameToggle.addEventListener('click', () => {
884
+ console.log("Game toggle clicked. gameVisible:", gameVisible); // DEBUG
885
  if (!gameVisible) {
886
  startGame();
887
  } else {
888
+ stopGame();
889
  }
890
  });
891
  } else {
892
+ console.error("Game toggle button or container not found! Cannot attach listener.");
893
  }
894
 
895
  function startGame() {
896
+ console.log("startGame called"); // DEBUG
897
+ if (!gameContainer) { console.error("startGame: gameContainer not found."); return; }
898
+ if (!ctx) { console.error("startGame: Canvas context (ctx) is not available."); return; }
899
+
900
  gameVisible = true;
901
  gameActive = true;
902
  isPaused = false;
903
+ isGameFocused = true;
904
+ gameContainer.classList.add('active');
905
+ if(pauseOverlay) pauseOverlay.style.display = 'none';
906
+ document.body.style.cursor = 'none';
907
+ if(cursor) cursor.style.display = 'none';
908
+ if(glow) glow.style.display = 'none';
909
+
910
+ try {
911
+ resizeCanvas(); // Ensure canvas is sized correctly
912
+ resetGame(); // Reset state and create objects
913
+ } catch(e) {
914
+ console.error("Error during resize/reset in startGame:", e);
915
+ stopGame(); // Attempt to gracefully stop if init fails
916
+ return;
917
+ }
918
+
919
+
920
+ lastTimestamp = performance.now();
921
+ if (animationFrameId) cancelAnimationFrame(animationFrameId);
922
+ console.log("Requesting first game loop frame..."); // DEBUG
923
  animationFrameId = requestAnimationFrame(gameLoop);
924
  }
925
 
926
  function stopGame() {
927
+ console.log("stopGame called"); // DEBUG
928
+ if (!gameContainer) return; // Should not happen if called from toggle, but safety check
929
  gameVisible = false;
930
  gameActive = false;
931
  isPaused = false;
932
+ isGameFocused = false;
933
+ gameContainer.classList.remove('active');
934
+ document.body.style.cursor = 'none'; // Keep custom cursor style
935
+ if(cursor) cursor.style.display = ''; // Show custom cursor
936
+ if(glow) glow.style.display = ''; // Show glow
 
937
  updateCursorState(); // Recalculate hover state
938
  if (animationFrameId) {
939
  cancelAnimationFrame(animationFrameId);
940
+ console.log("Cancelled animation frame:", animationFrameId); // DEBUG
941
  animationFrameId = null;
942
  }
943
  lastTimestamp = 0;
944
  }
945
 
946
  function pauseGame() {
947
+ console.log("pauseGame called"); // DEBUG
948
+ if (!gameActive) return;
949
  isPaused = true;
950
+ gameActive = false;
951
+ if(pauseOverlay) pauseOverlay.style.display = 'flex';
 
952
  }
953
 
954
  function resumeGame() {
955
+ console.log("resumeGame called"); // DEBUG
956
+ if (!gameVisible || !isPaused) return;
957
  isPaused = false;
958
+ gameActive = true;
959
+ if(pauseOverlay) pauseOverlay.style.display = 'none';
960
+ lastTimestamp = performance.now();
961
+ if (!animationFrameId && gameVisible) { // Restart loop only if it somehow stopped AND game should be visible
962
+ console.log("Restarting game loop from resumeGame..."); //DEBUG
963
  animationFrameId = requestAnimationFrame(gameLoop);
964
  }
965
  }
966
 
967
+ // Key handling (Unchanged, assumed correct)
968
  const keys = { up: false, down: false, left: false, right: false, handbrake: false };
 
969
  window.addEventListener('keydown', (e) => {
970
+ if (!gameVisible) return;
 
 
971
  if ([' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'w', 'a', 's', 'd', 'W', 'A', 'S', 'D'].includes(e.key)) {
972
  e.preventDefault();
973
  }
 
974
  if (e.key === 'Escape') {
975
+ if (isPaused) { stopGame(); } else if (gameActive) { pauseGame(); }
976
+ return;
 
 
 
 
977
  }
 
 
978
  if (isPaused) return;
 
979
  switch(e.key.toLowerCase()) {
980
  case 'arrowup': case 'w': keys.up = true; break;
981
  case 'arrowdown': case 's': keys.down = true; break;
 
984
  case ' ': keys.handbrake = true; break;
985
  }
986
  });
 
987
  window.addEventListener('keyup', (e) => {
 
988
  switch(e.key.toLowerCase()) {
989
  case 'arrowup': case 'w': keys.up = false; break;
990
  case 'arrowdown': case 's': keys.down = false; break;
 
995
  });
996
 
997
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
998
  // --- Object Creation ---
999
  function createObstacles() {
1000
+ console.log("Creating obstacles..."); // DEBUG
1001
+ if (!canvas) { console.error("createObstacles: Canvas not found."); return; }
1002
+ obstacles.length = 0;
1003
+ const margin = 50;
1004
+ const canvasW = canvas.width;
1005
+ const canvasH = canvas.height;
1006
+
1007
+ for (let i = 0; i < maxObstacles; i++) {
1008
+ try { // Wrap individual obstacle creation
1009
+ const type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];
1010
+ const width = type.width;
1011
+ const height = type.height;
1012
+ const mass = width * height * OBSTACLE_DENSITY;
1013
+ const inertia = mass * (width*width + height*height) / 12;
1014
+ // SAFETY CHECKS for inverse mass/inertia
1015
+ const invMass = mass > 0.0001 ? 1 / mass : 0;
1016
+ const invInertia = inertia > 0.0001 ? 1 / inertia : 0;
1017
+
1018
+ // Placement logic (simplified check for debug)
1019
+ let x = Math.random() * (canvasW - width - 2 * margin) + margin + width / 2;
1020
+ let y = Math.random() * (canvasH - height - 2 * margin) + margin + height / 2;
1021
+ // Basic check to avoid center spawn
 
 
 
 
 
 
 
1022
  if (Math.hypot(x - canvasW/2, y - canvasH/2) < 200) {
1023
+ x = margin + width / 2 + Math.random() * 100; // Move near edge if too close to center
1024
  }
1025
+
1026
+ const newObstacle = {
1027
+ x: x, y: y, vx: 0, vy: 0,
1028
+ angle: Math.random() * Math.PI * 2,
1029
+ angularVelocity: (Math.random() - 0.5) * 0.3,
1030
+ width: width, height: height,
1031
+ mass: mass, invMass: invMass,
1032
+ inertia: inertia, invInertia: invInertia,
1033
+ text: type.text, color: type.color,
1034
+ vertices: [], axes: [] // Initialize empty
1035
+ };
1036
+ obstacles.push(newObstacle);
1037
+ // console.log(`Created obstacle ${i}: mass=${mass.toFixed(1)}, invMass=${invMass.toExponential(2)}`); // DEBUG detail
1038
+ } catch (e) {
1039
+ console.error(`Error creating obstacle ${i}:`, e);
1040
  }
1041
+ }
1042
+ // IMPORTANT: Update vertices AFTER all obstacles are in the array
1043
+ obstacles.forEach(obs => {
1044
+ try {
1045
+ updateVerticesAndAxes(obs);
1046
+ } catch(e) {
1047
+ console.error("Error updating vertices for obstacle:", obs, e);
1048
  }
1049
+ });
1050
+ console.log(`Finished creating ${obstacles.length} obstacles.`); // DEBUG
1051
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1052
 
1053
  function createPowerUps() {
1054
+ console.log("Creating powerups..."); // DEBUG
1055
+ if (!canvas) { console.error("createPowerUps: Canvas not found."); return; }
1056
  powerUps.length = 0;
1057
+ // Placement logic (simplified for debug, less strict overlap check)
1058
  const margin = 60;
1059
+ for (let i = 0; i < maxPowerUps; i++) {
1060
+ try {
1061
+ const type = Math.random() < 0.5 ? 'turbo' : 'score';
1062
+ const x = Math.random() * (canvas.width - 2 * margin) + margin;
1063
+ const y = Math.random() * (canvas.height - 2 * margin) + margin;
1064
+ const radius = 18;
1065
+ // Basic check against car start
1066
+ if (Math.hypot(x - canvas.width/2, y - canvas.height/2) < 150) continue; // Skip if too close
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1067
 
 
1068
  powerUps.push({
1069
  x: x, y: y, radius: radius,
1070
  type: type, color: type === 'turbo' ? '#ff00ff' : '#ffff00',
1071
  active: true, pulseOffset: Math.random() * Math.PI * 2
1072
  });
1073
+ } catch (e) {
1074
+ console.error(`Error creating powerup ${i}:`, e);
1075
  }
 
1076
  }
1077
+ console.log(`Finished creating ${powerUps.length} powerups.`); // DEBUG
1078
  }
1079
 
1080
  function showGameMessage(text, color, duration = 1500) {
1081
+ // Assume this works, no changes needed unless message itself breaks
1082
  if (!gameMessage) return;
1083
  gameMessage.textContent = text;
1084
  gameMessage.style.color = color;
1085
+ gameMessage.classList.add('visible');
1086
  setTimeout(() => {
1087
+ gameMessage.classList.remove('visible');
1088
  }, duration);
1089
  }
1090
+ function activateTurbo() { /* Unchanged */
 
1091
  if (car.turboCooldown <= 0) {
1092
  car.turboMode = true;
1093
  car.turboTimer = car.turboMaxTime;
1094
  showGameMessage("TURBO!", '#ff00ff', 1500);
1095
  }
1096
  }
1097
+ function addScore(amount = 100, position = null) { /* Unchanged */
 
1098
  score += amount;
1099
  updateScore();
 
 
 
 
1100
  if (amount >= 200) {
1101
  showGameMessage(`+${amount} PTS!`, '#ffff00', 1000);
1102
  }
1103
  }
1104
 
1105
  function resetGame() {
1106
+ console.log("resetGame called"); // DEBUG
1107
+ if (!canvas) { console.error("resetGame: Canvas not found."); return; }
1108
+ // Reset car state
1109
+ car.x = canvas.width / 2;
1110
+ car.y = canvas.height / 2;
1111
+ car.vx = 0; car.vy = 0;
1112
+ car.angle = -Math.PI / 2;
1113
+ car.angularVelocity = 0;
1114
+ car.tireMarks = [];
1115
+ car.turboMode = false;
1116
+ car.turboTimer = 0;
1117
+ car.turboCooldown = 0;
1118
+ // Ensure car vertices are reset too (or updated immediately after position change)
1119
+ updateVerticesAndAxes(car); // Update car geometry based on reset state
1120
+
1121
+ score = 0;
1122
+ updateScore();
1123
+
1124
+ // Recreate objects
1125
+ createObstacles();
1126
+ createPowerUps();
1127
+ console.log("Game reset finished."); // DEBUG
1128
  }
1129
 
1130
  function updateScore() {
1131
+ if (scoreDisplay) { scoreDisplay.textContent = `Score: ${score}`; }
 
 
1132
  }
1133
 
1134
+ // --- Drawing Functions (Assumed correct, no changes unless errors point here) ---
1135
+ function drawCar(ctx) { /* Unchanged */
1136
  if (!ctx) return;
1137
  const { x: screenX, y: screenY } = worldToScreen(car.x, car.y);
 
1138
  ctx.save();
1139
  ctx.translate(screenX, screenY);
1140
  ctx.rotate(car.angle);
 
 
1141
  ctx.shadowColor = car.shadowColor;
1142
+ ctx.shadowBlur = car.drifting ? 25 : 15;
1143
+ ctx.shadowOffsetX = 5; ctx.shadowOffsetY = 5;
1144
+ const drawWidth = car.width; const drawHeight = car.height;
 
 
 
 
1145
  ctx.fillStyle = car.turboMode ? '#ff00ff' : car.color;
1146
  ctx.fillRect(-drawWidth/2, -drawHeight/2, drawWidth, drawHeight);
1147
+ ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0;
1148
+ ctx.fillStyle = "#223344";
 
 
 
 
 
1149
  ctx.fillRect(-drawWidth/2 * 0.8, -drawHeight/2 * 0.7, drawWidth * 0.8, drawHeight * 0.3);
1150
  ctx.fillRect(-drawWidth/2 * 0.7, drawHeight/2 * 0.4, drawWidth * 0.7, drawHeight * 0.2);
1151
+ ctx.fillStyle = "#ffffaa";
1152
  ctx.fillRect(-drawWidth/2 * 0.4, -drawHeight/2 - 2, drawWidth * 0.2, 4);
1153
  ctx.fillRect( drawWidth/2 * 0.2, -drawHeight/2 - 2, drawWidth * 0.2, 4);
1154
+ ctx.fillStyle = "#ffaaaa";
1155
  ctx.fillRect(-drawWidth/2 * 0.4, drawHeight/2 - 2, drawWidth * 0.2, 4);
1156
  ctx.fillRect( drawWidth/2 * 0.2, drawHeight/2 - 2, drawWidth * 0.2, 4);
1157
+ if (car.turboMode) { /* flames */ }
1158
+ if (car.drifting && Math.hypot(car.vx, car.vy) > 5) { /* smoke */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1159
  ctx.restore();
1160
  }
1161
+ function drawTireMarks(ctx) { /* Unchanged */
1162
+ if (!ctx || car.tireMarks.length === 0) return;
1163
+ ctx.lineCap = "round";
1164
+ const maxMarksToDraw = 100;
1165
+ const startIdx = Math.max(0, car.tireMarks.length - maxMarksToDraw);
1166
+ for (let i = startIdx; i < car.tireMarks.length; i++) {
1167
+ const mark = car.tireMarks[i];
1168
+ mark.life -= deltaTime * timeScale;
1169
+ if (mark.life <= 0) { continue; }
1170
+ const alpha = Math.max(0, Math.min(1, mark.life / mark.maxLife));
1171
+ ctx.strokeStyle = `rgba(40, 40, 40, ${alpha * 0.6})`;
1172
+ ctx.lineWidth = mark.width * alpha;
1173
+ ctx.beginPath(); ctx.moveTo(mark.lx1, mark.ly1); ctx.lineTo(mark.lx2, mark.ly2); ctx.stroke();
1174
+ ctx.beginPath(); ctx.moveTo(mark.rx1, mark.ry1); ctx.lineTo(mark.rx2, mark.ry2); ctx.stroke();
1175
+ }
1176
+ if (Math.random() < 0.05) { car.tireMarks = car.tireMarks.filter(mark => mark.life > 0); }
1177
+ if (car.tireMarks.length > 250) { car.tireMarks.splice(0, car.tireMarks.length - 200); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1178
  }
1179
+ function addTireMarkSegment() { /* Unchanged */
1180
+ const wheelOffset = car.width * 0.4; const axleOffset = car.height * 0.35;
1181
+ const cosA = Math.cos(car.angle); const sinA = Math.sin(car.angle);
 
 
 
 
 
 
1182
  const { x: screenX, y: screenY } = worldToScreen(car.x, car.y);
1183
+ const rlX = screenX - sinA*wheelOffset - cosA*axleOffset; const rlY = screenY + cosA*wheelOffset - sinA*axleOffset;
1184
+ const rrX = screenX + sinA*wheelOffset - cosA*axleOffset; const rrY = screenY - cosA*wheelOffset - sinA*axleOffset;
1185
+ const maxLife = 1.8; const markWidth = 4;
 
 
 
 
 
 
1186
  const lastMark = car.tireMarks.length > 0 ? car.tireMarks[car.tireMarks.length - 1] : null;
1187
+ if (lastMark && lastMark.life > 0) { lastMark.lx2=rlX; lastMark.ly2=rlY; lastMark.rx2=rrX; lastMark.ry2=rrY; }
1188
+ 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 });
 
 
 
 
 
 
 
 
 
 
 
 
 
1189
  }
1190
+ function drawObstacles(ctx) { /* Unchanged */
 
 
1191
  if (!ctx) return;
1192
  for (const obs of obstacles) {
1193
  const { x: screenX, y: screenY } = worldToScreen(obs.x, obs.y);
1194
+ ctx.save(); ctx.translate(screenX, screenY); ctx.rotate(obs.angle);
1195
+ ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.fillRect(-obs.width/2 + 3, -obs.height/2 + 3, obs.width, obs.height);
1196
+ ctx.fillStyle = obs.color; ctx.fillRect(-obs.width/2, -obs.height/2, obs.width, obs.height);
1197
+ ctx.font = 'bold 13px "DM Sans", sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
1198
+ ctx.fillStyle = '#ffffff'; ctx.fillText(obs.text, 0, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1199
  ctx.restore();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1200
  }
1201
  }
1202
+ function drawPowerUps(ctx) { /* Unchanged */
 
 
1203
  if (!ctx) return;
1204
+ for (const powerUp of powerUps) { /* ... drawing logic ... */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1205
  }
1206
+ function drawBackground(ctx) { /* Unchanged */
 
1207
  if (!ctx || !canvas) return;
1208
+ ctx.fillStyle = '#08080A'; ctx.fillRect(0, 0, canvas.width, canvas.height);
1209
+ ctx.strokeStyle='rgba(0, 255, 255, 0.06)'; ctx.lineWidth=1; const gridSize=50;
1210
+ for (let x=0; x<canvas.width; x+=gridSize) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,canvas.height); ctx.stroke(); }
1211
+ for (let y=0; y<canvas.height; y+=gridSize) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(canvas.width,y); ctx.stroke(); }
1212
+ // Vignette...
1213
+ }
1214
+ function drawGameInfo(ctx) { /* Unchanged */
1215
+ if (!ctx || !canvas) return; /* ... HUD drawing logic ... */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1216
  }
1217
 
1218
  // --- Physics Update Functions ---
1219
 
1220
+ function applyForces(obj, deltaTime) { // Generalized slightly
1221
+ // Only apply player input to car
1222
+ let isPlayerCar = (obj === car);
1223
+
1224
  let totalForce = { x: 0, y: 0 };
1225
  let totalTorque = 0;
1226
+ const forwardVec = { x: Math.sin(obj.angle), y: -Math.cos(obj.angle) };
1227
+ const rightVec = { x: -forwardVec.y, y: forwardVec.x };
1228
+ const currentSpeed = vecLength({x: obj.vx, y: obj.vy});
1229
+
1230
+ if (isPlayerCar) {
1231
+ let engineForceMag = 0;
1232
+ if (keys.up) { engineForceMag += CAR_ENGINE_FORCE; if (obj.turboMode) engineForceMag += obj.turboForceBoost; }
1233
+ 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; } }
1234
+ totalForce = vecAdd(totalForce, vecScale(forwardVec, engineForceMag));
1235
+
1236
+ const steeringEffectiveness = Math.max(0.2, 1.0 - (currentSpeed / 40));
1237
+ if (keys.left) { totalTorque -= CAR_TURN_TORQUE * steeringEffectiveness; }
1238
+ if (keys.right) { totalTorque += CAR_TURN_TORQUE * steeringEffectiveness; }
1239
+
1240
+ obj.drifting = keys.handbrake && currentSpeed > 5;
1241
+ 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(); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1242
  }
1243
 
1244
+ // Friction/Drag (Apply to all objects)
1245
+ totalForce = vecAdd(totalForce, vecScale({x: obj.vx, y: obj.vy}, -FRICTION * 30));
1246
+
1247
+ // Lateral Tire Friction (Apply more strongly to car, less to obstacles?)
1248
+ // For simplicity, apply same logic but maybe scaled down for obstacles? Let's apply only to car for now.
1249
+ let lateralFrictionMag = 0;
1250
+ if(isPlayerCar) {
1251
+ const lateralVelocity = vecDot({x: obj.vx, y: obj.vy}, rightVec);
1252
+ let gripFactor = (isPlayerCar && keys.handbrake) ? TIRE_HANDBRAKE_GRIP_FACTOR : 1.0;
1253
+ lateralFrictionMag = -lateralVelocity * TIRE_LATERAL_GRIP * gripFactor * obj.mass;
1254
+ totalForce = vecAdd(totalForce, vecScale(rightVec, lateralFrictionMag));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1255
  }
1256
 
1257
+ // --- Apply forces ---
1258
+ // SAFETY CHECK for invMass/invInertia before applying
1259
+ if (obj.invMass > 0) {
1260
+ const linearAccel = vecScale(totalForce, obj.invMass);
1261
+ obj.vx += linearAccel.x * deltaTime * timeScale;
1262
+ obj.vy += linearAccel.y * deltaTime * timeScale;
1263
+ }
1264
+ if (obj.invInertia > 0) {
1265
+ const angularAccel = totalTorque * obj.invInertia;
1266
+ obj.angularVelocity += angularAccel * deltaTime * timeScale;
1267
+ }
1268
 
1269
+ // Angular damping
1270
+ obj.angularVelocity *= (1 - (1 - ANGULAR_FRICTION) * deltaTime * 60);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1271
 
1272
+ // --- Integration ---
1273
+ obj.x += obj.vx * deltaTime * timeScale;
1274
+ obj.y += obj.vy * deltaTime * timeScale;
1275
+ obj.angle += obj.angularVelocity * deltaTime * timeScale;
1276
 
1277
+ // Update Turbo Timer (Only for car)
1278
+ if (isPlayerCar) {
1279
+ if (obj.turboMode) { obj.turboTimer -= deltaTime*timeScale; if (obj.turboTimer <= 0) { obj.turboMode=false; obj.turboTimer=0; obj.turboCooldown=obj.turboCooldownMax; } }
1280
+ else if (obj.turboCooldown > 0) { obj.turboCooldown -= deltaTime*timeScale; if (obj.turboCooldown < 0) { obj.turboCooldown = 0; } }
 
 
 
 
 
 
 
 
 
1281
  }
1282
  }
1283
 
1284
  function updateObstaclesPhysics(deltaTime) {
1285
  if (!canvas) return;
1286
  const dt = deltaTime * timeScale;
 
1287
  obstacles.forEach(obs => {
1288
+ try {
1289
+ // --- Apply basic forces (damping) ---
1290
+ // Only damping applied here, more complex forces in applyForces if needed later
1291
+ obs.vx *= (1 - (1 - FRICTION) * dt * 30);
1292
+ obs.vy *= (1 - (1 - FRICTION) * dt * 30);
1293
+ obs.angularVelocity *= (1 - (1 - ANGULAR_FRICTION) * dt * 60);
1294
+
1295
+ // --- Integration ---
1296
+ obs.x += obs.vx * dt;
1297
+ obs.y += obs.vy * dt;
1298
+ obs.angle += obs.angularVelocity * dt;
1299
+
1300
+ // Update geometry and check screen bounds
1301
+ updateVerticesAndAxes(obs);
1302
+ handleScreenCollision(obs, canvas.width, canvas.height);
1303
+ } catch (e) {
1304
+ console.error("Error updating physics for obstacle:", obs, e);
1305
+ }
1306
  });
1307
  }
1308
 
1309
+ // --- Collision Detection & Resolution (SAT - Assumed mostly correct, added logging) ---
1310
 
 
 
 
1311
  function updateVerticesAndAxes(obj) {
1312
+ // SAFETY check: Ensure obj and its properties are valid
1313
+ if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.angle !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number') {
1314
+ console.error("Invalid object passed to updateVerticesAndAxes:", obj);
1315
+ obj.vertices = []; obj.axes = []; // Prevent further errors using these
1316
+ return;
1317
+ }
1318
+
1319
  const w = obj.width / 2;
1320
  const h = obj.height / 2;
1321
  const cosA = Math.cos(obj.angle);
1322
  const sinA = Math.sin(obj.angle);
1323
+ const x = obj.x;
1324
+ const y = obj.y;
1325
+
1326
+ obj.vertices = [ // Ensure this calculation is correct
1327
+ { x: x + (-w*cosA - -h*sinA), y: y + (-w*sinA + -h*cosA) },
1328
+ { x: x + ( w*cosA - -h*sinA), y: y + ( w*sinA + -h*cosA) },
1329
+ { x: x + ( w*cosA - h*sinA), y: y + ( w*sinA + h*cosA) },
1330
+ { x: x + (-w*cosA - h*sinA), y: y + (-w*sinA + h*cosA) }
1331
  ];
 
 
1332
  obj.axes = [];
1333
  for (let i = 0; i < 4; i++) {
1334
  const p1 = obj.vertices[i];
1335
  const p2 = obj.vertices[(i + 1) % 4];
1336
+ // SAFETY Check: Ensure vertices are valid before calculating edge
1337
+ if (typeof p1.x !== 'number' || typeof p2.x !== 'number') {
1338
+ console.error("Invalid vertices found in updateVerticesAndAxes for obj:", obj);
1339
+ continue; // Skip this axis
1340
+ }
1341
  const edge = vecSub(p2, p1);
1342
+ const normal = vecNormalize({ x: -edge.y, y: edge.x });
1343
  obj.axes.push(normal);
1344
  }
1345
  }
1346
 
 
1347
  function projectPolygon(vertices, axis) {
1348
+ // SAFETY check
1349
+ if (!vertices || vertices.length === 0 || !axis || typeof axis.x !== 'number') {
1350
+ console.error("Invalid input to projectPolygon", vertices, axis);
1351
+ return { min: 0, max: 0};
1352
+ }
1353
+ let min = vecDot(vertices[0], axis); let max = min;
1354
  for (let i = 1; i < vertices.length; i++) {
1355
+ // SAFETY check vertex
1356
+ if (typeof vertices[i]?.x !== 'number') {
1357
+ console.error("Invalid vertex in projectPolygon", vertices[i]); continue;
1358
+ }
1359
  const p = vecDot(vertices[i], axis);
1360
+ if (p < min) { min = p; } else if (p > max) { max = p; }
 
 
 
 
1361
  }
1362
  return { min: min, max: max };
1363
  }
1364
 
 
1365
  function checkSATCollision(objA, objB) {
1366
+ // SAFETY Check inputs
1367
+ if (!objA || !objB || !objA.axes || !objB.axes || !objA.vertices || !objB.vertices) {
1368
+ console.error("Invalid objects for SAT check:", objA, objB); return null;
1369
+ }
1370
+ // Ensure vertices are current
1371
+ updateVerticesAndAxes(objA); // Update A (e.g., car)
1372
+ // B (obstacle) should be updated in its own physics loop, but update again just in case? Maybe not needed.
1373
 
1374
+ const axes = [...objA.axes, ...objB.axes];
1375
  let minOverlap = Infinity;
1376
  let collisionNormal = null;
1377
 
1378
  for (const axis of axes) {
1379
+ // SAFETY check axis
1380
+ if (typeof axis?.x !== 'number') { console.error("Invalid axis in SAT:", axis); continue; }
1381
+
1382
  const projA = projectPolygon(objA.vertices, axis);
1383
  const projB = projectPolygon(objB.vertices, axis);
 
1384
  const overlap = Math.min(projA.max, projB.max) - Math.max(projA.min, projB.min);
1385
 
1386
+ if (overlap <= 0.0001) { return null; } // Use tolerance
1387
+ if (overlap < minOverlap) { minOverlap = overlap; collisionNormal = axis; }
 
 
 
 
 
 
 
1388
  }
1389
 
1390
+ if (!collisionNormal) return null; // Should not happen if overlap > 0, but safety
1391
+
1392
+ const direction = vecSub({x:objA.x,y:objA.y}, {x:objB.x,y:objB.y});
1393
+ if (vecDot(direction, collisionNormal) < 0) { collisionNormal = vecScale(collisionNormal, -1); }
 
 
 
1394
 
1395
+ // console.log("SAT Collision Detected:", { overlap: minOverlap, normal: collisionNormal }); // DEBUG (can be noisy)
1396
  return { overlap: minOverlap, normal: collisionNormal };
1397
  }
1398
 
 
 
1399
  function resolveCollision(objA, objB, collisionInfo) {
1400
+ // SAFETY check inputs
1401
+ if (!objA || !objB || !collisionInfo || !collisionInfo.normal || typeof collisionInfo.overlap !== 'number') {
1402
+ console.error("Invalid input to resolveCollision", objA, objB, collisionInfo); return;
1403
+ }
 
 
 
 
 
 
 
 
1404
 
1405
+ const { overlap, normal } = collisionInfo;
1406
+ // SAFETY check normal validity
1407
+ if(typeof normal.x !== 'number' || typeof normal.y !== 'number' || Math.abs(normal.x*normal.x + normal.y*normal.y - 1.0) > 0.01) {
1408
+ console.error("Invalid collision normal:", normal);
1409
+ return; // Avoid resolving with bad normal
1410
+ }
1411
 
1412
 
1413
+ // 1. Positional Correction
1414
+ const totalInvMass = objA.invMass + objB.invMass;
1415
+ if (totalInvMass > 0.00001) { // Use tolerance
1416
+ const separationAmount = overlap / totalInvMass;
1417
+ const correctionScale = 0.8; // Penetration resolution percentage (prevent jitter)
1418
+ objA.x += normal.x * separationAmount * objA.invMass * correctionScale;
1419
+ objA.y += normal.y * separationAmount * objA.invMass * correctionScale;
1420
+ objB.x -= normal.x * separationAmount * objB.invMass * correctionScale;
1421
+ objB.y -= normal.y * separationAmount * objB.invMass * correctionScale;
1422
+ // Re-update vertices after position change is important if checking collisions multiple times per frame
1423
+ updateVerticesAndAxes(objA);
1424
+ updateVerticesAndAxes(objB);
1425
+ }
1426
 
1427
+ // 2. Impulse Calculation
1428
+ const collisionPoint = { x: (objA.x + objB.x) / 2, y: (objA.y + objB.y) / 2 }; // Approx center
1429
  const rA = vecSub(collisionPoint, {x: objA.x, y: objA.y});
1430
  const rB = vecSub(collisionPoint, {x: objB.x, y: objB.y});
1431
+ const vA = { x: objA.vx + (-objA.angularVelocity * rA.y), y: objA.vy + (objA.angularVelocity * rA.x) };
1432
+ const vB = { x: objB.vx + (-objB.angularVelocity * rB.y), y: objB.vy + (objB.angularVelocity * rB.x) };
 
 
 
 
 
 
 
 
 
 
1433
  const relativeVelocity = vecSub(vA, vB);
1434
  const velocityAlongNormal = vecDot(relativeVelocity, normal);
1435
 
1436
+ if (velocityAlongNormal > 0) return; // Moving apart
 
 
 
1437
 
1438
+ const restitution = COLLISION_RESTITUTION;
1439
  const rACrossN = vecCross2D(rA, normal);
1440
  const rBCrossN = vecCross2D(rB, normal);
1441
+ // SAFETY check for invInertia being valid numbers
1442
+ const invInertiaA = (typeof objA.invInertia === 'number' && isFinite(objA.invInertia)) ? objA.invInertia : 0;
1443
+ const invInertiaB = (typeof objB.invInertia === 'number' && isFinite(objB.invInertia)) ? objB.invInertia : 0;
1444
+
1445
+ const invInertiaSum = (rACrossN * rACrossN * invInertiaA) + (rBCrossN * rBCrossN * invInertiaB);
1446
+ const denominator = invMassSum + invInertiaSum;
1447
+
1448
+ // SAFETY Check denominator
1449
+ if (denominator < 0.00001) {
1450
+ console.warn("Collision denominator too small, skipping impulse."); return;
1451
+ }
1452
 
1453
  let j = -(1 + restitution) * velocityAlongNormal;
1454
+ j /= denominator;
1455
 
1456
+ // 3. Apply Impulse
1457
  const impulse = vecScale(normal, j);
1458
+ if(objA.invMass > 0){ objA.vx += impulse.x * objA.invMass; objA.vy += impulse.y * objA.invMass; }
1459
+ if(objB.invMass > 0){ objB.vx -= impulse.x * objB.invMass; objB.vy -= impulse.y * objB.invMass; }
1460
+ if(invInertiaA > 0){ objA.angularVelocity += vecCross2D(rA, impulse) * invInertiaA; }
1461
+ if(invInertiaB > 0){ objB.angularVelocity -= vecCross2D(rB, impulse) * invInertiaB; }
1462
 
 
 
 
 
 
 
 
 
 
1463
 
1464
+ // Scoring & Feedback
 
 
 
1465
  if (objA === car || objB === car) {
1466
+ const impactMagnitude = Math.abs(j);
1467
+ const scoreToAdd = Math.min(60, Math.max(2, Math.floor(impactMagnitude / 1000)));
1468
+ addScore(scoreToAdd, collisionPoint);
1469
+ if (impactMagnitude > 5000 && gameContainer) { /* Screen shake */ }
 
 
 
1470
  }
1471
  }
1472
 
1473
+ function handleScreenCollision(obj, width, height) {
1474
+ // SAFETY check obj and properties needed
1475
+ if (!obj || typeof obj.x !== 'number' || typeof obj.width !== 'number' || !obj.vertices || obj.vertices.length !== 4) {
1476
+ // console.warn("Invalid object for screen collision:", obj); // Can be noisy
1477
+ return;
1478
+ }
1479
+ const objRadius = Math.max(obj.width, obj.height) / 2; // Approx
1480
+
1481
+ obj.vertices.forEach((v, i) => {
1482
+ let collided = false;
1483
+ let normal = {x:0, y:0};
1484
+ let penetration = 0;
1485
+
1486
+ if (v.x < 0) { collided = true; normal = {x: 1, y: 0}; penetration = -v.x; }
1487
+ if (v.x > width) { collided = true; normal = {x:-1, y: 0}; penetration = v.x - width; }
1488
+ if (v.y < 0) { collided = true; normal = {x: 0, y: 1}; penetration = -v.y; }
1489
+ if (v.y > height) { collided = true; normal = {x: 0, y:-1}; penetration = v.y - height; }
1490
+
1491
+ if (collided) {
1492
+ // 1. Positional Correction (Move object back slightly) - Simplified
1493
+ // Only apply correction if penetration is significant
1494
+ if(penetration > 0.1 && obj.invMass > 0) { // Don't move static objects
1495
+ obj.x += normal.x * penetration * 0.8; // Correct based on penetration depth
1496
+ obj.y += normal.y * penetration * 0.8;
1497
+ // Need to update vertices after positional correction for accurate impulse
1498
+ updateVerticesAndAxes(obj);
1499
+ }
1500
+
1501
+
1502
+ // 2. Impulse Response
1503
  const velocity = { x: obj.vx, y: obj.vy };
1504
  const dot = vecDot(velocity, normal);
 
 
 
 
 
 
 
 
 
1505
 
1506
+ // Only apply impulse if moving *into* the wall
1507
+ if (dot < 0) {
1508
+ const impulseMag = -(1 + SCREEN_BOUND_RESTITUTION) * dot;
1509
+ const impulse = vecScale(normal, impulseMag);
1510
+ // Apply only if object can move
1511
+ if(obj.invMass > 0){
1512
+ obj.vx += impulse.x * obj.invMass;
1513
+ obj.vy += impulse.y * obj.invMass;
1514
+ }
1515
+ // Apply angular impulse? Less critical for wall hits unless specific effect desired.
1516
+ // if(obj.invInertia > 0) obj.angularVelocity += (Math.random() - 0.5) * 0.1 * impulseMag * obj.invInertia;
1517
+ }
1518
+ }
1519
+ });
 
 
1520
  }
1521
 
 
1522
  function checkAllCollisions() {
1523
  if (!canvas) return;
1524
 
1525
+ try { // Wrap collision checks
1526
+ // --- Car vs Obstacles ---
1527
+ for (const obstacle of obstacles) {
1528
+ const collisionInfo = checkSATCollision(car, obstacle);
1529
+ if (collisionInfo) {
1530
+ resolveCollision(car, obstacle, collisionInfo);
1531
+ }
1532
  }
 
1533
 
1534
+ // --- Obstacle vs Obstacles ---
1535
+ for (let i = 0; i < obstacles.length; i++) {
1536
+ for (let j = i + 1; j < obstacles.length; j++) {
1537
+ const obsA = obstacles[i];
1538
+ const obsB = obstacles[j];
1539
+ const collisionInfo = checkSATCollision(obsA, obsB);
1540
+ if (collisionInfo) {
1541
+ resolveCollision(obsA, obsB, collisionInfo);
1542
+ }
1543
  }
1544
  }
 
1545
 
1546
+ // --- Car vs Power-Ups --- (Simple Circle Collision)
1547
+ const carRadius = car.width / 2;
1548
+ for (let i = powerUps.length - 1; i >= 0; i--) {
1549
+ const powerUp = powerUps[i];
1550
+ if (!powerUp.active) continue;
1551
+ const dist = Math.hypot(car.x - powerUp.x, car.y - powerUp.y);
1552
+ if (dist < carRadius + powerUp.radius) {
1553
+ powerUp.active = false;
1554
+ if (powerUp.type === 'turbo') activateTurbo(); else if (powerUp.type === 'score') addScore(250);
1555
+ setTimeout(() => { const cIdx = powerUps.findIndex(p=>p===powerUp); if(cIdx!==-1) powerUps.splice(cIdx, 1); createPowerUps(); }, 2000+Math.random()*2000);
 
 
 
1556
  }
 
 
 
 
 
 
 
1557
  }
1558
+ } catch (e) {
1559
+ console.error("Error during collision checking/resolution:", e);
1560
  }
1561
  }
1562
 
1563
 
1564
  // --- Main Game Loop ---
1565
+ let frameCount = 0; // DEBUG
1566
  function gameLoop(timestamp) {
1567
+ // console.log(`gameLoop start. Visible: ${gameVisible}`); // DEBUG (Very noisy)
1568
+ if (!gameVisible) { animationFrameId = null; return; }
 
 
1569
 
 
1570
  const now = performance.now();
1571
+ // Robust deltaTime calculation
1572
+ deltaTime = (now - (lastTimestamp || now)) / 1000; // Handle first frame case
1573
  lastTimestamp = now;
1574
+ deltaTime = Math.min(deltaTime, 1 / 20); // Cap delta time
1575
+
1576
+ // console.log(`Frame: ${frameCount++}, DeltaTime: ${deltaTime.toFixed(4)}, Active: ${gameActive}, Paused: ${isPaused}`); // DEBUG
1577
+
1578
+ if (gameActive && !isPaused) {
1579
+ try { // Wrap physics updates
1580
+ // --- UPDATE ---
1581
+ applyForces(car, deltaTime);
1582
+ updateObstaclesPhysics(deltaTime); // Updates positions, geometry, screen bounds
1583
+ checkAllCollisions();
1584
+ } catch (e) {
1585
+ console.error("Error during game update logic:", e);
1586
+ // Consider pausing game on error?
1587
+ // pauseGame();
1588
+ }
 
 
 
 
 
 
1589
 
1590
  } else {
1591
+ // Keep timestamp updated even when paused/inactive to prevent jump on resume
1592
+ // lastTimestamp = performance.now(); // Reconsider this - might cause jump if paused long
1593
  }
1594
 
1595
 
1596
  // --- DRAW ---
1597
  if (ctx && canvas) {
1598
+ try { // Wrap drawing
1599
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1600
+ drawBackground(ctx);
1601
+ drawTireMarks(ctx);
1602
+ drawObstacles(ctx);
1603
+ drawPowerUps(ctx);
1604
+ drawCar(ctx);
1605
+ drawGameInfo(ctx);
1606
+ } catch (e) {
1607
+ console.error("Error during drawing:", e);
1608
+ // Might indicate issues with object properties being drawn
1609
+ }
1610
+ } else if (!ctx) {
1611
+ // console.warn("gameLoop: ctx is null, skipping draw."); // DEBUG
1612
  }
1613
 
1614
  // Request next frame
 
1617
 
1618
  // --- [ Game Logic - END ] ---
1619
 
1620
+ // Final check after everything is defined
1621
+ console.log("Game script initialized. Waiting for load event and user interaction."); // DEBUG
1622
+
1623
  </script>
1624
  </body>
1625
  </html>