ParthSadaria commited on
Commit
b506f24
·
verified ·
1 Parent(s): 6b4f469

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +741 -549
index.html CHANGED
@@ -564,8 +564,8 @@
564
  <div class="game-message" id="game-message"></div>
565
  </div>
566
 
567
- <script>
568
- // Mouse cursor effects
569
  const cursor = document.querySelector('.cursor');
570
  const glow = document.querySelector('.glow');
571
  const hoverables = document.querySelectorAll('a, .model-badge, .game-toggle');
@@ -578,25 +578,20 @@
578
  let glowX = 0;
579
  let glowY = 0;
580
 
581
- // Adjust speed for desired smoothness (lower = smoother, more 'lag')
582
  const cursorSpeed = 0.15;
583
  const glowSpeed = 0.1;
584
 
585
- // Store current scale factor for smooth transitions
586
  let currentCursorScale = 1;
587
  const baseCursorScale = 1;
588
  const hoverLinkCursorScale = 1.5;
589
  const hoverTextCursorScale = 0.7;
590
  const clickCursorScale = 0.6;
591
 
592
- // Pre-calculate half-widths/heights ONCE (assuming they don't change)
593
- // Note: If elements resize dynamically, might need recalculation
594
  let cursorHalfWidth = cursor.offsetWidth / 2;
595
  let cursorHalfHeight = cursor.offsetHeight / 2;
596
  let glowHalfWidth = glow.offsetWidth / 2;
597
  let glowHalfHeight = glow.offsetHeight / 2;
598
 
599
- // Recalculate on resize just in case
600
  window.addEventListener('resize', () => {
601
  cursorHalfWidth = cursor.offsetWidth / 2;
602
  cursorHalfHeight = cursor.offsetHeight / 2;
@@ -605,14 +600,11 @@
605
  });
606
 
607
  function animate() {
608
- // Lerp for smooth following
609
  cursorX += (mouseX - cursorX) * cursorSpeed;
610
  cursorY += (mouseY - cursorY) * cursorSpeed;
611
  glowX += (mouseX - glowX) * glowSpeed;
612
  glowY += (mouseY - glowY) * glowSpeed;
613
 
614
- // Apply transform for positioning and scaling
615
- // Calculate the top-left position needed to center the element
616
  const cursorTranslateX = cursorX - cursorHalfWidth;
617
  const cursorTranslateY = cursorY - cursorHalfHeight;
618
  const glowTranslateX = glowX - glowHalfWidth;
@@ -627,32 +619,31 @@
627
  document.addEventListener('mousemove', (e) => {
628
  mouseX = e.clientX;
629
  mouseY = e.clientY;
 
630
  });
631
 
632
- // Start the animation loop
633
- animate();
634
 
635
  function updateCursorState() {
636
  let targetScale = baseCursorScale;
637
  let isHoveringLink = false;
638
  let isHoveringText = false;
639
 
640
- // Check hover states directly
641
- hoverables.forEach(el => {
642
- const rect = el.getBoundingClientRect();
643
- if (mouseX >= rect.left && mouseX <= rect.right && mouseY >= rect.top && mouseY <= rect.bottom) {
 
644
  isHoveringLink = true;
 
 
645
  }
646
- });
 
 
 
 
647
 
648
- textElements.forEach(el => {
649
- if (!isHoveringLink) {
650
- const rect = el.getBoundingClientRect();
651
- if (mouseX >= rect.left && mouseX <= rect.right && mouseY >= rect.top && mouseY <= rect.bottom) {
652
- isHoveringText = true;
653
- }
654
- }
655
- });
656
 
657
  cursor.classList.toggle('hover-link', isHoveringLink);
658
  cursor.classList.toggle('hover-text', isHoveringText && !isHoveringLink);
@@ -670,8 +661,6 @@
670
  currentCursorScale = targetScale;
671
  }
672
 
673
- document.addEventListener('mousemove', updateCursorState);
674
-
675
  document.addEventListener('mousedown', () => {
676
  cursor.classList.add('clicking');
677
  updateCursorState();
@@ -682,29 +671,52 @@
682
  updateCursorState();
683
  });
684
 
 
685
  updateCursorState();
 
 
686
 
687
  // Loading screen animation
688
  window.addEventListener('load', () => {
689
  const loadingScreen = document.getElementById('loading-screen');
690
- setTimeout(() => {
691
- loadingScreen.style.opacity = '0';
 
692
  setTimeout(() => {
693
- loadingScreen.style.display = 'none';
694
- }, 500);
695
- }, 2000);
 
 
 
 
 
 
 
 
 
 
 
696
  });
697
 
698
- // Game logic
699
  const gameToggle = document.getElementById('game-toggle');
700
  const gameContainer = document.getElementById('game-container');
701
  const canvas = document.getElementById('game-canvas');
702
- const ctx = canvas.getContext('2d');
703
  const scoreDisplay = document.getElementById('score-display');
704
  const gameMessage = document.getElementById('game-message');
705
 
 
 
 
 
 
 
 
 
706
  // Game state
707
  let gameActive = false;
 
708
  let score = 0;
709
  let obstaclesHit = 0;
710
  let lastTimestamp = 0;
@@ -712,25 +724,54 @@
712
 
713
  // Canvas sizing
714
  function resizeCanvas() {
 
715
  canvas.width = window.innerWidth;
716
  canvas.height = window.innerHeight;
717
  }
718
 
719
  window.addEventListener('resize', resizeCanvas);
720
- resizeCanvas();
 
 
 
721
 
722
  // Toggle game display
723
- gameToggle.addEventListener('click', () => {
724
- if (gameContainer.style.display === 'block') {
725
- gameContainer.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
726
  gameActive = false;
 
 
 
 
 
 
 
 
727
  } else {
728
- gameContainer.style.display = 'block';
729
  gameActive = true;
 
 
730
  resetGame();
731
- gameLoop(performance.now());
 
 
 
 
732
  }
733
- });
734
 
735
  // Key handling for the game
736
  const keys = {
@@ -743,58 +784,55 @@
743
 
744
  window.addEventListener('keydown', (e) => {
745
  if (!gameActive) return;
746
-
747
- switch(e.key) {
748
- case 'ArrowUp':
 
 
 
 
 
749
  case 'w':
750
- case 'W':
751
  keys.up = true;
752
  break;
753
- case 'ArrowDown':
754
  case 's':
755
- case 'S':
756
  keys.down = true;
757
  break;
758
- case 'ArrowLeft':
759
  case 'a':
760
- case 'A':
761
  keys.left = true;
762
  break;
763
- case 'ArrowRight':
764
  case 'd':
765
- case 'D':
766
  keys.right = true;
767
  break;
768
- case ' ':
769
  keys.handbrake = true;
770
  break;
771
- case 'Escape':
772
- gameContainer.style.display = 'none';
773
- gameActive = false;
774
  break;
775
  }
776
  });
777
 
778
  window.addEventListener('keyup', (e) => {
779
- switch(e.key) {
780
- case 'ArrowUp':
 
781
  case 'w':
782
- case 'W':
783
  keys.up = false;
784
  break;
785
- case 'ArrowDown':
786
  case 's':
787
- case 'S':
788
  keys.down = false;
789
  break;
790
- case 'ArrowLeft':
791
  case 'a':
792
- case 'A':
793
  keys.left = false;
794
  break;
795
- case 'ArrowRight':
796
  case 'd':
797
- case 'D':
798
  keys.right = false;
799
  break;
800
  case ' ':
@@ -809,28 +847,30 @@
809
  y: 0,
810
  width: 40,
811
  height: 70,
812
- angle: 0,
813
- speed: 0,
814
- acceleration: 200,
815
- deceleration: 120,
816
- maxSpeed: 500,
817
  reverseMaxSpeed: -200,
818
- turnSpeed: 2.5,
819
- driftFactor: 0.95,
820
- friction: 0.97,
821
- brakeStrength: 0.85,
822
  drifting: false,
823
  tireMarks: [],
824
- color: '#00ffff',
825
- shadowColor: 'rgba(0, 255, 255, 0.5)',
826
  turboMode: false,
827
- turboTimer: 0,
828
  turboMaxTime: 3,
829
- turboCooldown: 0,
830
- turboCooldownMax: 5
 
 
831
  };
832
 
833
- // Obstacles based on website elements
834
  const obstacleTypes = [
835
  { text: "GPT-4o", width: 100, height: 40, color: "#ff5a5a" },
836
  { text: "Claude 3.7", width: 120, height: 40, color: "#5a92ff" },
@@ -839,660 +879,812 @@
839
  { text: "AI", width: 60, height: 40, color: "#aa5aff" },
840
  { text: "Premium", width: 110, height: 40, color: "#ff5aaa" }
841
  ];
842
-
843
  const obstacles = [];
844
- const maxObstacles = 30;
845
 
 
846
  const powerUps = [];
847
- const maxPowerUps = 5;
848
 
849
  function createObstacles() {
850
- const margin = 100; // Keep obstacles away from edges
851
-
852
- while (obstacles.length < maxObstacles) {
853
- const type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];
854
- // Continuing from where the code left off in the createObstacles function:
855
 
 
 
856
  const obstacle = {
857
- x: Math.random() * (canvas.width - type.width - 2 * margin) + margin,
858
- y: Math.random() * (canvas.height - type.height - 2 * margin) + margin,
859
  width: type.width,
860
  height: type.height,
861
  text: type.text,
862
  color: type.color,
863
- vx: 0,
864
- vy: 0,
865
  angle: Math.random() * Math.PI * 2,
866
- angularVelocity: 0,
867
- mass: type.width * type.height / 1000
868
  };
869
-
870
- // Ensure obstacles don't overlap
871
  let overlaps = false;
872
  for (const existing of obstacles) {
873
  const dx = obstacle.x - existing.x;
874
  const dy = obstacle.y - existing.y;
 
875
  const distance = Math.sqrt(dx * dx + dy * dy);
876
- if (distance < obstacle.width + existing.width) {
 
877
  overlaps = true;
878
  break;
879
  }
880
  }
881
-
882
  if (!overlaps) {
883
  obstacles.push(obstacle);
884
  }
 
885
  }
 
 
 
886
  }
887
 
888
  function createPowerUps() {
889
- while (powerUps.length < maxPowerUps) {
 
 
 
 
 
890
  const powerUp = {
891
- x: Math.random() * (canvas.width - 30),
892
- y: Math.random() * (canvas.height - 30),
893
  radius: 15,
894
- type: Math.random() < 0.3 ? 'turbo' : 'score',
895
- color: Math.random() < 0.3 ? '#ff00ff' : '#ffff00',
896
- collectEffect: Math.random() < 0.3 ? activateTurbo : addScore,
897
- active: true
898
  };
899
-
900
- // Ensure power-ups don't overlap with obstacles
901
  let overlaps = false;
902
- for (const obstacle of obstacles) {
903
- const dx = powerUp.x - obstacle.x;
904
- const dy = powerUp.y - obstacle.y;
905
- const distance = Math.sqrt(dx * dx + dy * dy);
906
- if (distance < powerUp.radius + Math.max(obstacle.width, obstacle.height) / 2) {
907
- overlaps = true;
908
- break;
909
- }
910
- }
911
-
 
 
 
 
 
 
 
 
 
 
 
 
912
  if (!overlaps) {
913
  powerUps.push(powerUp);
914
  }
915
- }
 
916
  }
917
 
918
  function activateTurbo() {
919
  if (car.turboCooldown <= 0) {
920
  car.turboMode = true;
921
  car.turboTimer = car.turboMaxTime;
922
- car.maxSpeed = 800;
923
- car.acceleration = 400;
924
-
925
  // Show turbo message
926
- gameMessage.textContent = "TURBO ACTIVATED!";
927
- gameMessage.style.display = "block";
928
- gameMessage.style.color = "#ff00ff";
929
-
930
- setTimeout(() => {
931
- gameMessage.style.display = "none";
932
- }, 1500);
933
  }
934
  }
935
 
936
- function addScore() {
937
- score += 100;
938
  updateScore();
939
-
940
  // Show score message
941
- gameMessage.textContent = "+100 POINTS!";
942
- gameMessage.style.display = "block";
943
- gameMessage.style.color = "#ffff00";
944
-
945
- setTimeout(() => {
946
- gameMessage.style.display = "none";
947
- }, 1000);
948
  }
949
 
950
  function resetGame() {
 
951
  car.x = canvas.width / 2;
952
  car.y = canvas.height / 2;
953
- car.angle = 0;
954
  car.speed = 0;
955
  car.tireMarks = [];
956
  car.turboMode = false;
957
  car.turboTimer = 0;
958
  car.turboCooldown = 0;
959
- car.maxSpeed = 500;
960
- car.acceleration = 200;
961
-
962
  score = 0;
963
  obstaclesHit = 0;
964
  updateScore();
965
-
966
  obstacles.length = 0;
967
  powerUps.length = 0;
968
-
969
  createObstacles();
970
  createPowerUps();
971
  }
972
 
973
  function updateScore() {
974
- scoreDisplay.textContent = `Score: ${score} | Hits: ${obstaclesHit}`;
 
 
975
  }
976
 
977
  function drawCar(ctx) {
 
978
  ctx.save();
979
  ctx.translate(car.x, car.y);
980
  ctx.rotate(car.angle);
981
-
982
- // Car shadow
983
- ctx.fillStyle = car.shadowColor;
984
- ctx.fillRect(-car.width/2 - 2, -car.height/2 - 2, car.width + 4, car.height + 4);
985
-
 
 
986
  // Car body
987
- ctx.fillStyle = car.turboMode ? '#ff00ff' : car.color;
988
  ctx.fillRect(-car.width/2, -car.height/2, car.width, car.height);
989
-
990
- // Windows
991
- ctx.fillStyle = "#333";
992
- ctx.fillRect(-car.width/2 + 5, -car.height/2 + 10, car.width - 10, car.height/3);
993
- ctx.fillRect(-car.width/2 + 5, car.height/2 - 20, car.width - 10, 10);
994
-
995
- // Wheels
996
- ctx.fillStyle = "#222";
997
- ctx.fillRect(-car.width/2 - 3, -car.height/2 + 10, 5, 15);
998
- ctx.fillRect(car.width/2 - 2, -car.height/2 + 10, 5, 15);
999
- ctx.fillRect(-car.width/2 - 3, car.height/2 - 25, 5, 15);
1000
- ctx.fillRect(car.width/2 - 2, car.height/2 - 25, 5, 15);
1001
-
1002
- // Turbo effect
 
 
 
 
 
 
 
 
 
 
 
1003
  if (car.turboMode) {
 
 
 
 
1004
  ctx.beginPath();
1005
- ctx.moveTo(-car.width/2, car.height/2);
1006
- ctx.lineTo(-car.width/2 - 10, car.height/2 + 20);
1007
- ctx.lineTo(car.width/2, car.height/2);
1008
- ctx.fillStyle = "#ff00ff";
1009
- ctx.fill();
1010
-
1011
- ctx.beginPath();
1012
- ctx.moveTo(-car.width/2, car.height/2);
1013
- ctx.lineTo(-car.width/2 - 5, car.height/2 + 30);
1014
- ctx.lineTo(car.width/2, car.height/2);
1015
- ctx.fillStyle = "#ffff00";
1016
  ctx.fill();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1017
  }
1018
-
1019
  ctx.restore();
1020
  }
1021
 
1022
  function drawTireMarks(ctx) {
1023
- ctx.lineWidth = 2;
1024
-
1025
- for (let i = 0; i < car.tireMarks.length; i++) {
 
 
1026
  const mark = car.tireMarks[i];
1027
-
1028
- // Fade out tire marks over time
1029
  mark.life -= deltaTime;
 
1030
  if (mark.life <= 0) {
1031
  car.tireMarks.splice(i, 1);
1032
- i--;
1033
  continue;
1034
  }
1035
-
1036
- const alpha = mark.life / mark.maxLife;
1037
- ctx.strokeStyle = `rgba(0, 0, 0, ${alpha * 0.6})`;
1038
-
1039
  ctx.beginPath();
1040
  ctx.moveTo(mark.x1, mark.y1);
1041
  ctx.lineTo(mark.x2, mark.y2);
1042
  ctx.stroke();
 
 
 
 
 
 
 
 
1043
  }
1044
  }
1045
 
1046
  function addTireMark() {
1047
- // Calculate wheel positions
1048
- const wheelDistance = car.width / 2;
1049
- const backAxleOffset = car.height / 3;
1050
-
1051
- // Left rear wheel
1052
- const leftX = car.x - Math.cos(car.angle) * wheelDistance + Math.sin(car.angle) * backAxleOffset;
1053
- const leftY = car.y - Math.sin(car.angle) * wheelDistance - Math.cos(car.angle) * backAxleOffset;
1054
-
1055
- // Right rear wheel
1056
- const rightX = car.x + Math.cos(car.angle) * wheelDistance + Math.sin(car.angle) * backAxleOffset;
1057
- const rightY = car.y + Math.sin(car.angle) * wheelDistance - Math.cos(car.angle) * backAxleOffset;
1058
-
1059
- // Previous positions
1060
- const prevMark = car.tireMarks[car.tireMarks.length - 1];
1061
-
1062
- if (prevMark && car.drifting) {
1063
- car.tireMarks.push({
1064
- x1: prevMark.x2,
1065
- y1: prevMark.y2,
1066
- x2: leftX,
1067
- y2: leftY,
1068
- life: 2,
1069
- maxLife: 2
1070
- });
1071
-
1072
- car.tireMarks.push({
1073
- x1: prevMark.x2b,
1074
- y1: prevMark.y2b,
1075
- x2: rightX,
1076
- y2: rightY,
1077
- life: 2,
1078
- maxLife: 2
1079
- });
1080
- } else {
1081
- car.tireMarks.push({
1082
- x1: leftX,
1083
- y1: leftY,
1084
- x2: leftX,
1085
- y2: leftY,
1086
- x2b: rightX,
1087
- y2b: rightY,
1088
- life: 2,
1089
- maxLife: 2
1090
- });
1091
- }
1092
-
1093
- // Limit the number of tire marks
1094
- if (car.tireMarks.length > 100) {
1095
- car.tireMarks.shift();
1096
  }
1097
  }
1098
 
1099
  function drawObstacles(ctx) {
 
1100
  for (const obstacle of obstacles) {
1101
  ctx.save();
1102
  ctx.translate(obstacle.x, obstacle.y);
1103
  ctx.rotate(obstacle.angle);
1104
-
1105
  // Draw obstacle body
1106
  ctx.fillStyle = obstacle.color;
1107
  ctx.fillRect(-obstacle.width/2, -obstacle.height/2, obstacle.width, obstacle.height);
1108
-
1109
- // Draw text
1110
- ctx.fillStyle = '#fff';
1111
- ctx.font = '16px var(--font-main), sans-serif';
1112
- ctx.textAlign = 'center';
1113
- ctx.textBaseline = 'middle';
1114
- ctx.fillText(obstacle.text, 0, 0);
1115
-
 
 
 
 
1116
  ctx.restore();
1117
  }
1118
  }
1119
 
1120
  function drawPowerUps(ctx) {
 
1121
  for (const powerUp of powerUps) {
1122
  if (!powerUp.active) continue;
1123
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1124
  ctx.beginPath();
1125
- ctx.arc(powerUp.x, powerUp.y, powerUp.radius, 0, Math.PI * 2);
1126
  ctx.fillStyle = powerUp.color;
1127
  ctx.fill();
1128
-
1129
- ctx.fillStyle = '#000';
1130
- ctx.font = '12px var(--font-main), sans-serif';
 
1131
  ctx.textAlign = 'center';
1132
  ctx.textBaseline = 'middle';
1133
- ctx.fillText(powerUp.type === 'turbo' ? 'TURBO' : 'SCORE', powerUp.x, powerUp.y);
1134
-
1135
- // Pulsing effect
1136
- powerUp.radius = 15 + Math.sin(Date.now() / 200) * 2;
1137
  }
1138
  }
1139
 
1140
  function updateCarPosition(deltaTime) {
1141
- // Handle input
 
 
 
 
 
 
 
 
 
 
 
1142
  if (keys.up) {
1143
- car.speed += car.acceleration * deltaTime;
1144
  } else if (keys.down) {
1145
- car.speed -= car.deceleration * deltaTime;
 
1146
  } else {
1147
- // Apply friction
1148
- car.speed *= car.friction;
 
 
1149
  }
1150
-
1151
- // Limit speed
1152
- car.speed = Math.max(car.reverseMaxSpeed, Math.min(car.maxSpeed, car.speed));
1153
-
1154
- // Apply braking
1155
- if (keys.handbrake) {
1156
- car.speed *= car.brakeStrength;
1157
- car.drifting = Math.abs(car.speed) > 100;
1158
-
1159
- if (car.drifting) {
1160
- addTireMark();
1161
- }
1162
  } else {
1163
  car.drifting = false;
1164
  }
1165
-
1166
- // Steering
1167
- let turnFactor = (car.speed / car.maxSpeed) * car.turnSpeed;
1168
-
1169
- // Reduce turning at very low speeds
1170
- if (Math.abs(car.speed) < 50) {
1171
- turnFactor *= Math.abs(car.speed) / 50;
1172
- }
1173
-
1174
- // Apply drift effect
1175
- if (car.drifting) {
1176
- turnFactor *= 1.5; // More responsive turning while drifting
1177
- }
1178
-
1179
- if (keys.left) {
1180
- car.angle -= turnFactor * deltaTime;
1181
- }
1182
- if (keys.right) {
1183
- car.angle += turnFactor * deltaTime;
 
1184
  }
1185
-
1186
- // Update position based on velocity
1187
- const moveAngle = car.drifting ? car.angle - (car.speed > 0 ? 0.2 : -0.2) : car.angle;
1188
- car.x += Math.sin(moveAngle) * car.speed * deltaTime;
1189
- car.y -= Math.cos(moveAngle) * car.speed * deltaTime;
1190
-
1191
- // Handle turbo mode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1192
  if (car.turboMode) {
1193
  car.turboTimer -= deltaTime;
1194
-
1195
  if (car.turboTimer <= 0) {
1196
  car.turboMode = false;
1197
- car.maxSpeed = 500;
1198
- car.acceleration = 200;
1199
- car.turboCooldown = car.turboCooldownMax;
1200
  }
1201
- }
1202
-
1203
- // Update turbo cooldown
1204
- if (car.turboCooldown > 0) {
1205
  car.turboCooldown -= deltaTime;
1206
- }
1207
-
1208
- // Screen boundaries with bounce
1209
- if (car.x < 0) {
1210
- car.x = 0;
1211
- car.speed *= -0.5;
1212
- }
1213
- if (car.x > canvas.width) {
1214
- car.x = canvas.width;
1215
- car.speed *= -0.5;
1216
- }
1217
- if (car.y < 0) {
1218
- car.y = 0;
1219
- car.speed *= -0.5;
1220
- }
1221
- if (car.y > canvas.height) {
1222
- car.y = canvas.height;
1223
- car.speed *= -0.5;
1224
  }
1225
  }
1226
 
 
1227
  function checkCollisions() {
1228
- // Check collisions with obstacles
 
 
 
1229
  for (const obstacle of obstacles) {
1230
- // Calculate distance between car and obstacle centers
1231
  const dx = car.x - obstacle.x;
1232
  const dy = car.y - obstacle.y;
1233
  const distance = Math.sqrt(dx * dx + dy * dy);
1234
-
1235
- // Simple collision detection based on distance
1236
- const minDistance = Math.max(car.width, car.height) / 2 + Math.max(obstacle.width, obstacle.height) / 2;
1237
-
1238
- if (distance < minDistance * 0.8) {
1239
- // Calculate collision normal
 
 
 
1240
  const nx = dx / distance;
1241
  const ny = dy / distance;
1242
-
1243
- // Calculate relative velocity
1244
- const speed = Math.abs(car.speed);
1245
- const impactSpeed = speed * 0.8;
1246
-
1247
- // Apply impulse to obstacle
1248
- obstacle.vx += nx * impactSpeed * 0.1;
1249
- obstacle.vy += ny * impactSpeed * 0.1;
1250
- obstacle.angularVelocity += (Math.random() - 0.5) * impactSpeed * 0.01;
1251
-
1252
- // Apply reaction to car
1253
- car.speed *= -0.3;
1254
-
1255
- // Add score and update counter
1256
- score += Math.floor(speed / 10);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1257
  obstaclesHit++;
1258
  updateScore();
1259
-
1260
- // Screen shake effect for strong impacts
1261
- if (speed > 200) {
1262
- const shakeIntensity = speed / 100;
1263
- document.getElementById('game-container').style.transform = `translate(${(Math.random() - 0.5) * shakeIntensity}px, ${(Math.random() - 0.5) * shakeIntensity}px)`;
1264
- setTimeout(() => {
1265
- document.getElementById('game-container').style.transform = 'translate(0, 0)';
1266
- }, 100);
1267
- }
1268
- }
1269
- }
1270
-
1271
- // Check collisions with power-ups
1272
- for (let i = 0; i < powerUps.length; i++) {
1273
- const powerUp = powerUps[i];
1274
- if (!powerUp.active) continue;
1275
-
1276
- // Calculate distance
1277
- const dx = car.x - powerUp.x;
1278
- const dy = car.y - powerUp.y;
1279
- const distance = Math.sqrt(dx * dx + dy * dy);
1280
-
1281
- if (distance < car.width / 2 + powerUp.radius) {
1282
- // Collect power-up
1283
- powerUp.active = false;
1284
- powerUp.collectEffect();
1285
-
1286
- // Create a new power-up
1287
- setTimeout(() => {
1288
- powerUps.splice(i, 1);
1289
- createPowerUps();
1290
- }, 1000);
1291
- }
1292
- }
1293
- }
1294
 
1295
- function updateObstacles(deltaTime) {
1296
- for (const obstacle of obstacles) {
1297
- // Apply physics
1298
- obstacle.x += obstacle.vx * deltaTime;
1299
- obstacle.y += obstacle.vy * deltaTime;
1300
- obstacle.angle += obstacle.angularVelocity * deltaTime;
1301
-
1302
- // Apply friction
1303
- obstacle.vx *= 0.98;
1304
- obstacle.vy *= 0.98;
1305
- obstacle.angularVelocity *= 0.95;
1306
-
1307
- // Keep on screen with bounce
1308
- if (obstacle.x < 0) {
1309
- obstacle.x = 0;
1310
- obstacle.vx *= -0.7;
1311
- }
1312
- if (obstacle.x > canvas.width) {
1313
- obstacle.x = canvas.width;
1314
- obstacle.vx *= -0.7;
1315
- }
1316
- if (obstacle.y < 0) {
1317
- obstacle.y = 0;
1318
- obstacle.vy *= -0.7;
1319
- }
1320
- if (obstacle.y > canvas.height) {
1321
- obstacle.y = canvas.height;
1322
- obstacle.vy *= -0.7;
1323
- }
1324
-
1325
- // Obstacle-obstacle collisions
1326
- for (const other of obstacles) {
1327
- if (obstacle === other) continue;
1328
-
1329
- const dx = obstacle.x - other.x;
1330
- const dy = obstacle.y - other.y;
1331
- const distance = Math.sqrt(dx * dx + dy * dy);
1332
- const minDistance = (obstacle.width + other.width) / 2;
1333
-
1334
- if (distance < minDistance) {
1335
- // Calculate collision response
1336
- const nx = dx / distance;
1337
- const ny = dy / distance;
1338
- const overlap = minDistance - distance;
1339
-
1340
- // Separate objects
1341
- obstacle.x += nx * overlap * 0.5;
1342
- obstacle.y += ny * overlap * 0.5;
1343
- other.x -= nx * overlap * 0.5;
1344
- other.y -= ny * overlap * 0.5;
1345
-
1346
- // Exchange momentum (simplified)
1347
- const p = 2 * (obstacle.vx * nx + obstacle.vy * ny - other.vx * nx - other.vy * ny)
1348
- / (obstacle.mass + other.mass);
1349
-
1350
- obstacle.vx -= p * other.mass * nx;
1351
- obstacle.vy -= p * other.mass * ny;
1352
- other.vx += p * obstacle.mass * nx;
1353
- other.vy += p * obstacle.mass * ny;
1354
-
1355
- // Add some random rotation
1356
- obstacle.angularVelocity += (Math.random() - 0.5) * 2;
1357
- other.angularVelocity += (Math.random() - 0.5) * 2;
1358
  }
1359
  }
1360
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1361
  }
1362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1363
  function drawBackground(ctx) {
1364
- // Grid pattern
1365
- ctx.fillStyle = '#050505';
 
1366
  ctx.fillRect(0, 0, canvas.width, canvas.height);
1367
-
1368
- ctx.strokeStyle = 'rgba(0, 255, 255, 0.1)';
 
1369
  ctx.lineWidth = 1;
1370
-
1371
- const gridSize = 50;
1372
- const offsetX = car.x % gridSize;
1373
- const offsetY = car.y % gridSize;
1374
-
1375
- for (let x = -offsetX; x < canvas.width; x += gridSize) {
1376
  ctx.beginPath();
1377
  ctx.moveTo(x, 0);
1378
  ctx.lineTo(x, canvas.height);
1379
  ctx.stroke();
1380
  }
1381
-
1382
- for (let y = -offsetY; y < canvas.height; y += gridSize) {
1383
  ctx.beginPath();
1384
  ctx.moveTo(0, y);
1385
  ctx.lineTo(canvas.width, y);
1386
  ctx.stroke();
1387
  }
1388
-
1389
- // Track border
1390
- ctx.strokeStyle = 'rgba(0, 255, 255, 0.3)';
1391
- ctx.lineWidth = 10;
1392
- ctx.strokeRect(50, 50, canvas.width - 100, canvas.height - 100);
 
 
 
 
 
 
1393
  }
1394
 
 
1395
  function drawGameInfo(ctx) {
1396
- // Speed gauge
1397
- const speedPercent = Math.abs(car.speed) / car.maxSpeed;
1398
- ctx.fillStyle = car.turboMode ? '#ff00ff' : '#00ffff';
1399
- ctx.fillRect(canvas.width - 120, canvas.height - 30, 100 * speedPercent, 15);
1400
-
1401
- ctx.strokeStyle = '#ffffff';
1402
- ctx.strokeRect(canvas.width - 120, canvas.height - 30, 100, 15);
1403
-
1404
- ctx.fillStyle = '#ffffff';
1405
- ctx.font = '12px var(--font-main), sans-serif';
1406
- ctx.textAlign = 'right';
1407
- ctx.fillText(`${Math.abs(Math.floor(car.speed))} km/h`, canvas.width - 125, canvas.height - 20);
1408
-
1409
- // Turbo cooldown indicator
1410
- if (car.turboCooldown > 0) {
1411
- ctx.fillStyle = 'rgba(255, 0, 255, 0.3)';
1412
- ctx.fillRect(canvas.width - 120, canvas.height - 50, 100 * (1 - car.turboCooldown / car.turboCooldownMax), 15);
1413
- ctx.strokeStyle = '#ff00ff';
1414
- ctx.strokeRect(canvas.width - 120, canvas.height - 50, 100, 15);
1415
- ctx.fillStyle = '#ff00ff';
1416
- ctx.textAlign = 'right';
1417
- ctx.fillText('TURBO RECHARGING', canvas.width - 125, canvas.height - 40);
1418
- } else if (car.turboMode) {
1419
- ctx.fillStyle = 'rgba(255, 0, 255, 0.8)';
1420
- ctx.fillRect(canvas.width - 120, canvas.height - 50, 100 * (car.turboTimer / car.turboMaxTime), 15);
1421
- ctx.strokeStyle = '#ff00ff';
1422
- ctx.strokeRect(canvas.width - 120, canvas.height - 50, 100, 15);
1423
- ctx.fillStyle = '#ff00ff';
1424
- ctx.textAlign = 'right';
1425
- ctx.fillText('TURBO ACTIVE', canvas.width - 125, canvas.height - 40);
1426
- } else {
1427
- ctx.fillStyle = '#ff00ff';
1428
- ctx.textAlign = 'right';
1429
- ctx.fillText('TURBO READY', canvas.width - 125, canvas.height - 40);
1430
- }
1431
- }
 
 
 
 
 
 
 
 
 
 
1432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1433
  function gameLoop(timestamp) {
1434
- if (!gameActive) return;
1435
-
1436
- // Calculate delta time for smooth animation
1437
- if (!lastTimestamp) lastTimestamp = timestamp;
1438
- deltaTime = (timestamp - lastTimestamp) / 1000; // Convert to seconds
1439
- deltaTime = Math.min(deltaTime, 0.1); // Cap delta time to prevent large jumps
 
 
 
 
1440
  lastTimestamp = timestamp;
1441
-
1442
- // Clear canvas
1443
- ctx.clearRect(0, 0, canvas.width, canvas.height);
1444
-
1445
- // Draw background
1446
- drawBackground(ctx);
1447
-
1448
- // Update car position
1449
  updateCarPosition(deltaTime);
1450
-
1451
- // Update obstacles
1452
- updateObstacles(deltaTime);
1453
-
1454
- // Check collisions
1455
- checkCollisions();
1456
-
1457
- // Draw tire marks
1458
  drawTireMarks(ctx);
1459
-
1460
- // Draw obstacles
1461
  drawObstacles(ctx);
1462
-
1463
- // Draw power-ups
1464
  drawPowerUps(ctx);
1465
-
1466
- // Draw car
1467
  drawCar(ctx);
1468
-
1469
- // Draw game information
1470
- drawGameInfo(ctx);
1471
-
1472
- requestAnimationFrame(gameLoop);
1473
  }
1474
 
1475
- // Initialize game when toggled
1476
- gameToggle.addEventListener('click', () => {
1477
- if (!gameActive) {
1478
- gameActive = true;
1479
- gameContainer.style.display = 'block';
1480
- resetGame();
1481
- lastTimestamp = 0;
1482
- requestAnimationFrame(gameLoop);
1483
- } else {
1484
- gameActive = false;
1485
- gameContainer.style.display = 'none';
1486
- }
1487
- });
1488
 
1489
- // Also close the game when pressing ESC
1490
- window.addEventListener('keydown', (e) => {
1491
- if (e.key === 'Escape' && gameActive) {
1492
- gameActive = false;
1493
- gameContainer.style.display = 'none';
1494
- }
1495
- });
1496
  </script>
1497
  </body>
1498
  </html>
 
564
  <div class="game-message" id="game-message"></div>
565
  </div>
566
 
567
+ <script>
568
+ // --- [ Existing Mouse Cursor Effects Code - Keep As Is ] ---
569
  const cursor = document.querySelector('.cursor');
570
  const glow = document.querySelector('.glow');
571
  const hoverables = document.querySelectorAll('a, .model-badge, .game-toggle');
 
578
  let glowX = 0;
579
  let glowY = 0;
580
 
 
581
  const cursorSpeed = 0.15;
582
  const glowSpeed = 0.1;
583
 
 
584
  let currentCursorScale = 1;
585
  const baseCursorScale = 1;
586
  const hoverLinkCursorScale = 1.5;
587
  const hoverTextCursorScale = 0.7;
588
  const clickCursorScale = 0.6;
589
 
 
 
590
  let cursorHalfWidth = cursor.offsetWidth / 2;
591
  let cursorHalfHeight = cursor.offsetHeight / 2;
592
  let glowHalfWidth = glow.offsetWidth / 2;
593
  let glowHalfHeight = glow.offsetHeight / 2;
594
 
 
595
  window.addEventListener('resize', () => {
596
  cursorHalfWidth = cursor.offsetWidth / 2;
597
  cursorHalfHeight = cursor.offsetHeight / 2;
 
600
  });
601
 
602
  function animate() {
 
603
  cursorX += (mouseX - cursorX) * cursorSpeed;
604
  cursorY += (mouseY - cursorY) * cursorSpeed;
605
  glowX += (mouseX - glowX) * glowSpeed;
606
  glowY += (mouseY - glowY) * glowSpeed;
607
 
 
 
608
  const cursorTranslateX = cursorX - cursorHalfWidth;
609
  const cursorTranslateY = cursorY - cursorHalfHeight;
610
  const glowTranslateX = glowX - glowHalfWidth;
 
619
  document.addEventListener('mousemove', (e) => {
620
  mouseX = e.clientX;
621
  mouseY = e.clientY;
622
+ updateCursorState(); // Update state on move too
623
  });
624
 
625
+ animate(); // Start animation loop
 
626
 
627
  function updateCursorState() {
628
  let targetScale = baseCursorScale;
629
  let isHoveringLink = false;
630
  let isHoveringText = false;
631
 
632
+ // Direct check based on current mouse position
633
+ const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);
634
+
635
+ if (elementUnderMouse) {
636
+ if (elementUnderMouse.closest('a, .model-badge, .game-toggle')) {
637
  isHoveringLink = true;
638
+ } else if (elementUnderMouse.closest('h1, p, h2')) {
639
+ isHoveringText = true;
640
  }
641
+ // Check model badges specifically if not already caught by closest
642
+ if (!isHoveringLink && !isHoveringText && elementUnderMouse.classList.contains('model-badge')) {
643
+ isHoveringText = true; // Treat badge text hover like other text
644
+ }
645
+ }
646
 
 
 
 
 
 
 
 
 
647
 
648
  cursor.classList.toggle('hover-link', isHoveringLink);
649
  cursor.classList.toggle('hover-text', isHoveringText && !isHoveringLink);
 
661
  currentCursorScale = targetScale;
662
  }
663
 
 
 
664
  document.addEventListener('mousedown', () => {
665
  cursor.classList.add('clicking');
666
  updateCursorState();
 
671
  updateCursorState();
672
  });
673
 
674
+ // Initial state
675
  updateCursorState();
676
+ // --- [ End of Mouse Cursor Effects Code ] ---
677
+
678
 
679
  // Loading screen animation
680
  window.addEventListener('load', () => {
681
  const loadingScreen = document.getElementById('loading-screen');
682
+ // Ensure the screen is visible initially
683
+ if (loadingScreen) {
684
+ loadingScreen.style.opacity = '1'; // Make sure it's visible
685
  setTimeout(() => {
686
+ loadingScreen.style.opacity = '0';
687
+ setTimeout(() => {
688
+ loadingScreen.style.display = 'none';
689
+ }, 500); // Wait for fade out
690
+ }, 2000); // Duration loading screen is shown
691
+ } else {
692
+ console.error("Loading screen element not found!");
693
+ }
694
+
695
+ // Recalculate cursor/glow size after load/potential style changes
696
+ cursorHalfWidth = cursor.offsetWidth / 2;
697
+ cursorHalfHeight = cursor.offsetHeight / 2;
698
+ glowHalfWidth = glow.offsetWidth / 2;
699
+ glowHalfHeight = glow.offsetHeight / 2;
700
  });
701
 
702
+ // --- [ Game Logic - START ] ---
703
  const gameToggle = document.getElementById('game-toggle');
704
  const gameContainer = document.getElementById('game-container');
705
  const canvas = document.getElementById('game-canvas');
 
706
  const scoreDisplay = document.getElementById('score-display');
707
  const gameMessage = document.getElementById('game-message');
708
 
709
+ // Ensure canvas context is retrieved only if canvas exists
710
+ let ctx = null;
711
+ if (canvas) {
712
+ ctx = canvas.getContext('2d');
713
+ } else {
714
+ console.error("Game canvas element not found!");
715
+ }
716
+
717
  // Game state
718
  let gameActive = false;
719
+ let animationFrameId = null; // To store the requestAnimationFrame ID
720
  let score = 0;
721
  let obstaclesHit = 0;
722
  let lastTimestamp = 0;
 
724
 
725
  // Canvas sizing
726
  function resizeCanvas() {
727
+ if (!canvas) return;
728
  canvas.width = window.innerWidth;
729
  canvas.height = window.innerHeight;
730
  }
731
 
732
  window.addEventListener('resize', resizeCanvas);
733
+ // Initial size set
734
+ if (canvas) {
735
+ resizeCanvas();
736
+ }
737
 
738
  // Toggle game display
739
+ if (gameToggle && gameContainer) {
740
+ gameToggle.addEventListener('click', toggleGame);
741
+ } else {
742
+ console.error("Game toggle button or container not found!");
743
+ }
744
+
745
+ function toggleGame() {
746
+ if (!gameContainer || !ctx) {
747
+ console.error("Cannot toggle game: container or context missing.");
748
+ return;
749
+ }
750
+
751
+ if (gameActive) {
752
+ // Stop the game
753
  gameActive = false;
754
+ gameContainer.style.display = 'none';
755
+ if (animationFrameId) {
756
+ cancelAnimationFrame(animationFrameId);
757
+ animationFrameId = null;
758
+ }
759
+ lastTimestamp = 0; // Reset timestamp for next start
760
+ // Re-enable default cursor when game stops
761
+ document.body.style.cursor = 'none';
762
  } else {
763
+ // Start the game
764
  gameActive = true;
765
+ gameContainer.style.display = 'block';
766
+ resizeCanvas(); // Ensure canvas is correct size
767
  resetGame();
768
+ lastTimestamp = 0; // Reset timestamp
769
+ // Disable default cursor while game is active
770
+ document.body.style.cursor = 'none'; // Keep custom cursor
771
+ // Start the loop
772
+ animationFrameId = requestAnimationFrame(gameLoop);
773
  }
774
+ }
775
 
776
  // Key handling for the game
777
  const keys = {
 
784
 
785
  window.addEventListener('keydown', (e) => {
786
  if (!gameActive) return;
787
+
788
+ // Prevent default browser behavior for spacebar (scrolling)
789
+ if (e.key === ' ') {
790
+ e.preventDefault();
791
+ }
792
+
793
+ switch(e.key.toLowerCase()) { // Use toLowerCase for case-insensitivity
794
+ case 'arrowup':
795
  case 'w':
 
796
  keys.up = true;
797
  break;
798
+ case 'arrowdown':
799
  case 's':
 
800
  keys.down = true;
801
  break;
802
+ case 'arrowleft':
803
  case 'a':
 
804
  keys.left = true;
805
  break;
806
+ case 'arrowright':
807
  case 'd':
 
808
  keys.right = true;
809
  break;
810
+ case ' ': // Space bar
811
  keys.handbrake = true;
812
  break;
813
+ case 'escape':
814
+ toggleGame(); // Use toggle function to properly stop
 
815
  break;
816
  }
817
  });
818
 
819
  window.addEventListener('keyup', (e) => {
820
+ // No need to check gameActive here, just reset the key state
821
+ switch(e.key.toLowerCase()) {
822
+ case 'arrowup':
823
  case 'w':
 
824
  keys.up = false;
825
  break;
826
+ case 'arrowdown':
827
  case 's':
 
828
  keys.down = false;
829
  break;
830
+ case 'arrowleft':
831
  case 'a':
 
832
  keys.left = false;
833
  break;
834
+ case 'arrowright':
835
  case 'd':
 
836
  keys.right = false;
837
  break;
838
  case ' ':
 
847
  y: 0,
848
  width: 40,
849
  height: 70,
850
+ angle: 0, // in radians
851
+ speed: 0, // pixels per second
852
+ acceleration: 250, // pixels per second squared
853
+ deceleration: 150, // pixels per second squared (when braking/coasting)
854
+ maxSpeed: 550,
855
  reverseMaxSpeed: -200,
856
+ turnSpeed: 3.0, // radians per second at max speed
857
+ driftFactor: 0.94, // How much speed sideways movement retains
858
+ friction: 0.98, // Speed multiplier when not accelerating
859
+ brakeStrength: 0.90, // Speed multiplier when braking hard (space)
860
  drifting: false,
861
  tireMarks: [],
862
+ color: '#00ffff', // Cyan
863
+ shadowColor: 'rgba(0, 255, 255, 0.3)',
864
  turboMode: false,
865
+ turboTimer: 0, // seconds remaining
866
  turboMaxTime: 3,
867
+ turboCooldown: 0, // seconds remaining until available
868
+ turboCooldownMax: 5,
869
+ turboMaxSpeedBoost: 300, // Added speed during turbo
870
+ turboAccelBoost: 150 // Added acceleration during turbo
871
  };
872
 
873
+ // Obstacles
874
  const obstacleTypes = [
875
  { text: "GPT-4o", width: 100, height: 40, color: "#ff5a5a" },
876
  { text: "Claude 3.7", width: 120, height: 40, color: "#5a92ff" },
 
879
  { text: "AI", width: 60, height: 40, color: "#aa5aff" },
880
  { text: "Premium", width: 110, height: 40, color: "#ff5aaa" }
881
  ];
 
882
  const obstacles = [];
883
+ const maxObstacles = 25; // Reduced slightly for performance potentially
884
 
885
+ // PowerUps
886
  const powerUps = [];
887
+ const maxPowerUps = 4;
888
 
889
  function createObstacles() {
890
+ if (!canvas) return;
891
+ obstacles.length = 0; // Clear existing
892
+ const margin = 100;
893
+ let attempts = 0; // Prevent infinite loop if packing is impossible
 
894
 
895
+ while (obstacles.length < maxObstacles && attempts < maxObstacles * 10) {
896
+ const type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];
897
  const obstacle = {
898
+ x: Math.random() * (canvas.width - type.width - 2 * margin) + margin + type.width / 2,
899
+ y: Math.random() * (canvas.height - type.height - 2 * margin) + margin + type.height / 2,
900
  width: type.width,
901
  height: type.height,
902
  text: type.text,
903
  color: type.color,
904
+ vx: (Math.random() - 0.5) * 30, // Slight initial drift
905
+ vy: (Math.random() - 0.5) * 30,
906
  angle: Math.random() * Math.PI * 2,
907
+ angularVelocity: (Math.random() - 0.5) * 0.5, // Radians per second
908
+ mass: (type.width * type.height) / 1000 // Simple mass estimation
909
  };
910
+
911
+ // Ensure obstacles don't overlap significantly at start
912
  let overlaps = false;
913
  for (const existing of obstacles) {
914
  const dx = obstacle.x - existing.x;
915
  const dy = obstacle.y - existing.y;
916
+ // Check distance between centers vs sum of half-diagonals (approx)
917
  const distance = Math.sqrt(dx * dx + dy * dy);
918
+ const minDistance = Math.sqrt(obstacle.width**2 + obstacle.height**2)/2 + Math.sqrt(existing.width**2 + existing.height**2)/2;
919
+ if (distance < minDistance * 1.2) { // Add a little buffer
920
  overlaps = true;
921
  break;
922
  }
923
  }
924
+
925
  if (!overlaps) {
926
  obstacles.push(obstacle);
927
  }
928
+ attempts++;
929
  }
930
+ if (attempts >= maxObstacles * 10) {
931
+ console.warn("Could not place all obstacles without overlap.");
932
+ }
933
  }
934
 
935
  function createPowerUps() {
936
+ if (!canvas) return;
937
+ powerUps.length = 0; // Clear existing
938
+ let attempts = 0;
939
+
940
+ while (powerUps.length < maxPowerUps && attempts < maxPowerUps * 10) {
941
+ const type = Math.random() < 0.4 ? 'turbo' : 'score'; // 40% chance turbo
942
  const powerUp = {
943
+ x: Math.random() * (canvas.width - 80) + 40, // Avoid edges
944
+ y: Math.random() * (canvas.height - 80) + 40,
945
  radius: 15,
946
+ type: type,
947
+ color: type === 'turbo' ? '#ff00ff' : '#ffff00', // Magenta for turbo, Yellow for score
948
+ active: true,
949
+ pulseOffset: Math.random() * Math.PI * 2 // For varied pulsing
950
  };
951
+
952
+ // Ensure power-ups don't overlap obstacles too much
953
  let overlaps = false;
954
+ for (const obstacle of obstacles) {
955
+ const dx = powerUp.x - obstacle.x;
956
+ const dy = powerUp.y - obstacle.y;
957
+ const distance = Math.sqrt(dx * dx + dy * dy);
958
+ if (distance < powerUp.radius + Math.max(obstacle.width, obstacle.height) / 2 + 10) { // Added buffer
959
+ overlaps = true;
960
+ break;
961
+ }
962
+ }
963
+ // Ensure power-ups don't overlap each other
964
+ if (!overlaps) {
965
+ for (const existingPowerUp of powerUps) {
966
+ const dx = powerUp.x - existingPowerUp.x;
967
+ const dy = powerUp.y - existingPowerUp.y;
968
+ const distance = Math.sqrt(dx * dx + dy * dy);
969
+ if (distance < powerUp.radius + existingPowerUp.radius + 10) {
970
+ overlaps = true;
971
+ break;
972
+ }
973
+ }
974
+ }
975
+
976
  if (!overlaps) {
977
  powerUps.push(powerUp);
978
  }
979
+ attempts++;
980
+ }
981
  }
982
 
983
  function activateTurbo() {
984
  if (car.turboCooldown <= 0) {
985
  car.turboMode = true;
986
  car.turboTimer = car.turboMaxTime;
987
+ // Boosts are applied in updateCarPosition
988
+
 
989
  // Show turbo message
990
+ if (gameMessage) {
991
+ gameMessage.textContent = "TURBO ACTIVE!";
992
+ gameMessage.style.display = "block";
993
+ gameMessage.style.color = "#ff00ff";
994
+ setTimeout(() => { gameMessage.style.display = "none"; }, 1500);
995
+ }
 
996
  }
997
  }
998
 
999
+ function addScore(amount = 100) { // Allow variable score amounts
1000
+ score += amount;
1001
  updateScore();
1002
+
1003
  // Show score message
1004
+ if (gameMessage) {
1005
+ gameMessage.textContent = `+${amount} POINTS!`;
1006
+ gameMessage.style.display = "block";
1007
+ gameMessage.style.color = "#ffff00";
1008
+ setTimeout(() => { gameMessage.style.display = "none"; }, 1000);
1009
+ }
 
1010
  }
1011
 
1012
  function resetGame() {
1013
+ if (!canvas) return;
1014
  car.x = canvas.width / 2;
1015
  car.y = canvas.height / 2;
1016
+ car.angle = -Math.PI / 2; // Start facing upwards
1017
  car.speed = 0;
1018
  car.tireMarks = [];
1019
  car.turboMode = false;
1020
  car.turboTimer = 0;
1021
  car.turboCooldown = 0;
1022
+
 
 
1023
  score = 0;
1024
  obstaclesHit = 0;
1025
  updateScore();
1026
+
1027
  obstacles.length = 0;
1028
  powerUps.length = 0;
1029
+
1030
  createObstacles();
1031
  createPowerUps();
1032
  }
1033
 
1034
  function updateScore() {
1035
+ if (scoreDisplay) {
1036
+ scoreDisplay.textContent = `Score: ${score} | Hits: ${obstaclesHit}`;
1037
+ }
1038
  }
1039
 
1040
  function drawCar(ctx) {
1041
+ if (!ctx) return;
1042
  ctx.save();
1043
  ctx.translate(car.x, car.y);
1044
  ctx.rotate(car.angle);
1045
+
1046
+ // Car shadow with blur
1047
+ ctx.shadowColor = car.shadowColor;
1048
+ ctx.shadowBlur = 15;
1049
+ ctx.shadowOffsetX = 3;
1050
+ ctx.shadowOffsetY = 3;
1051
+
1052
  // Car body
1053
+ ctx.fillStyle = car.turboMode ? '#ff00ff' : car.color; // Magenta when turbo
1054
  ctx.fillRect(-car.width/2, -car.height/2, car.width, car.height);
1055
+
1056
+ // Reset shadow for other elements
1057
+ ctx.shadowColor = 'transparent';
1058
+ ctx.shadowBlur = 0;
1059
+ ctx.shadowOffsetX = 0;
1060
+ ctx.shadowOffsetY = 0;
1061
+
1062
+ // Simple details (windows, lights)
1063
+ ctx.fillStyle = "#223344"; // Dark blue/grey
1064
+ // Windshield
1065
+ ctx.fillRect(-car.width/2 * 0.8, -car.height/2 * 0.7, car.width * 0.8, car.height * 0.3);
1066
+ // Rear window
1067
+ ctx.fillRect(-car.width/2 * 0.7, car.height/2 * 0.4, car.width * 0.7, car.height * 0.2);
1068
+
1069
+ // Headlights (yellowish)
1070
+ ctx.fillStyle = "#ffffaa";
1071
+ ctx.fillRect(-car.width/2 * 0.4, -car.height/2 - 2, car.width * 0.2, 4);
1072
+ ctx.fillRect( car.width/2 * 0.2, -car.height/2 - 2, car.width * 0.2, 4);
1073
+
1074
+ // Taillights (reddish)
1075
+ ctx.fillStyle = "#ffaaaa";
1076
+ ctx.fillRect(-car.width/2 * 0.4, car.height/2 - 2, car.width * 0.2, 4);
1077
+ ctx.fillRect( car.width/2 * 0.2, car.height/2 - 2, car.width * 0.2, 4);
1078
+
1079
+ // Turbo exhaust flames
1080
  if (car.turboMode) {
1081
+ const flameLength = 20 + Math.random() * 15;
1082
+ const flameWidth = 10 + Math.random() * 5;
1083
+ // Central flame
1084
+ ctx.fillStyle = `rgba(255, ${Math.random() * 150 + 100}, 0, 0.8)`; // Orange/Yellow
1085
  ctx.beginPath();
1086
+ ctx.moveTo(0, car.height/2);
1087
+ ctx.lineTo(-flameWidth / 2, car.height/2 + flameLength * 0.6);
1088
+ ctx.lineTo(0, car.height/2 + flameLength);
1089
+ ctx.lineTo(flameWidth / 2, car.height/2 + flameLength * 0.6);
1090
+ ctx.closePath();
 
 
 
 
 
 
1091
  ctx.fill();
1092
+ // Side flames (smaller)
1093
+ ctx.fillStyle = `rgba(255, 0, ${Math.random() * 100 + 100}, 0.7)`; // Red/Magenta
1094
+ ctx.beginPath();
1095
+ ctx.moveTo(-car.width/4, car.height/2);
1096
+ ctx.lineTo(-car.width/4 - flameWidth/4, car.height/2 + flameLength*0.4);
1097
+ ctx.lineTo(-car.width/4, car.height/2 + flameLength*0.7);
1098
+ ctx.lineTo(-car.width/4 + flameWidth/4, car.height/2 + flameLength*0.4);
1099
+ ctx.closePath();
1100
+ ctx.fill();
1101
+ ctx.beginPath();
1102
+ ctx.moveTo(car.width/4, car.height/2);
1103
+ ctx.lineTo(car.width/4 - flameWidth/4, car.height/2 + flameLength*0.4);
1104
+ ctx.lineTo(car.width/4, car.height/2 + flameLength*0.7);
1105
+ ctx.lineTo(car.width/4 + flameWidth/4, car.height/2 + flameLength*0.4);
1106
+ ctx.closePath();
1107
+ ctx.fill();
1108
  }
1109
+
1110
  ctx.restore();
1111
  }
1112
 
1113
  function drawTireMarks(ctx) {
1114
+ if (!ctx) return;
1115
+ ctx.lineWidth = 3; // Slightly thicker marks
1116
+ ctx.lineCap = "round";
1117
+
1118
+ for (let i = car.tireMarks.length - 1; i >= 0; i--) { // Iterate backwards for removal
1119
  const mark = car.tireMarks[i];
 
 
1120
  mark.life -= deltaTime;
1121
+
1122
  if (mark.life <= 0) {
1123
  car.tireMarks.splice(i, 1);
 
1124
  continue;
1125
  }
1126
+
1127
+ const alpha = Math.max(0, mark.life / mark.maxLife); // Ensure alpha doesn't go negative
1128
+ ctx.strokeStyle = `rgba(40, 40, 40, ${alpha * 0.5})`; // Darker grey marks
1129
+
1130
  ctx.beginPath();
1131
  ctx.moveTo(mark.x1, mark.y1);
1132
  ctx.lineTo(mark.x2, mark.y2);
1133
  ctx.stroke();
1134
+
1135
+ // If it's a paired mark (from drifting)
1136
+ if (mark.x1b !== undefined) {
1137
+ ctx.beginPath();
1138
+ ctx.moveTo(mark.x1b, mark.y1b);
1139
+ ctx.lineTo(mark.x2b, mark.y2b);
1140
+ ctx.stroke();
1141
+ }
1142
  }
1143
  }
1144
 
1145
  function addTireMark() {
1146
+ const wheelOffset = car.width * 0.4; // Position of wheels relative to center line
1147
+ const axleOffset = car.height * 0.4; // Position of rear axle relative to center
1148
+
1149
+ // Calculate current rear wheel positions
1150
+ const cosAngle = Math.cos(car.angle);
1151
+ const sinAngle = Math.sin(car.angle);
1152
+
1153
+ const rearLeftX = car.x - sinAngle * wheelOffset - cosAngle * axleOffset;
1154
+ const rearLeftY = car.y + cosAngle * wheelOffset - sinAngle * axleOffset;
1155
+ const rearRightX = car.x + sinAngle * wheelOffset - cosAngle * axleOffset;
1156
+ const rearRightY = car.y - cosAngle * wheelOffset - sinAngle * axleOffset;
1157
+
1158
+ // Add new mark segment(s)
1159
+ const maxLife = 1.5; // How long marks last in seconds
1160
+ car.tireMarks.push({
1161
+ x1: rearLeftX, y1: rearLeftY, // Start at current position
1162
+ x2: rearLeftX, y2: rearLeftY, // End at current position (will be updated next frame)
1163
+ x1b: rearRightX, y1b: rearRightY,
1164
+ x2b: rearRightX, y2b: rearRightY,
1165
+ life: maxLife,
1166
+ maxLife: maxLife
1167
+ });
1168
+
1169
+
1170
+ // Update the *previous* mark's end position to the *current* start position
1171
+ if (car.tireMarks.length > 1) {
1172
+ const prevMark = car.tireMarks[car.tireMarks.length - 2];
1173
+ prevMark.x2 = rearLeftX;
1174
+ prevMark.y2 = rearLeftY;
1175
+ if (prevMark.x1b !== undefined) { // Update right wheel too if exists
1176
+ prevMark.x2b = rearRightX;
1177
+ prevMark.y2b = rearRightY;
1178
+ }
1179
+ }
1180
+
1181
+ // Limit total tire mark segments for performance
1182
+ if (car.tireMarks.length > 150) {
1183
+ car.tireMarks.shift(); // Remove the oldest segment
 
 
 
 
 
 
 
 
 
 
 
1184
  }
1185
  }
1186
 
1187
  function drawObstacles(ctx) {
1188
+ if (!ctx) return;
1189
  for (const obstacle of obstacles) {
1190
  ctx.save();
1191
  ctx.translate(obstacle.x, obstacle.y);
1192
  ctx.rotate(obstacle.angle);
1193
+
1194
  // Draw obstacle body
1195
  ctx.fillStyle = obstacle.color;
1196
  ctx.fillRect(-obstacle.width/2, -obstacle.height/2, obstacle.width, obstacle.height);
1197
+
1198
+ // Draw text (optional outline for readability)
1199
+ ctx.strokeStyle = 'rgba(0,0,0,0.5)';
1200
+ ctx.lineWidth = 2;
1201
+ ctx.font = 'bold 14px "DM Sans", sans-serif'; // Use specified font
1202
+ ctx.textAlign = 'center';
1203
+ ctx.textBaseline = 'middle';
1204
+ ctx.strokeText(obstacle.text, 0, 1); // Offset slightly for pseudo-outline
1205
+
1206
+ ctx.fillStyle = '#ffffff';
1207
+ ctx.fillText(obstacle.text, 0, 0);
1208
+
1209
  ctx.restore();
1210
  }
1211
  }
1212
 
1213
  function drawPowerUps(ctx) {
1214
+ if (!ctx) return;
1215
  for (const powerUp of powerUps) {
1216
  if (!powerUp.active) continue;
1217
+
1218
+ const scale = 1 + Math.sin(Date.now() / 250 + powerUp.pulseOffset) * 0.15; // Pulsing effect
1219
+ const radius = powerUp.radius * scale;
1220
+
1221
+ ctx.save();
1222
+ ctx.translate(powerUp.x, powerUp.y);
1223
+
1224
+ // Draw outer glow
1225
+ const gradient = ctx.createRadialGradient(0, 0, radius * 0.5, 0, 0, radius * 1.2);
1226
+ gradient.addColorStop(0, powerUp.color);
1227
+ gradient.addColorStop(1, 'rgba(0,0,0,0)');
1228
+ ctx.fillStyle = gradient;
1229
+ ctx.fillRect(-radius * 1.2, -radius * 1.2, radius * 2.4, radius * 2.4);
1230
+
1231
+
1232
+ // Draw circle
1233
  ctx.beginPath();
1234
+ ctx.arc(0, 0, powerUp.radius, 0, Math.PI * 2); // Use base radius for solid circle
1235
  ctx.fillStyle = powerUp.color;
1236
  ctx.fill();
1237
+
1238
+ // Draw text
1239
+ ctx.fillStyle = '#111111'; // Dark text
1240
+ ctx.font = 'bold 10px "DM Sans", sans-serif';
1241
  ctx.textAlign = 'center';
1242
  ctx.textBaseline = 'middle';
1243
+ ctx.fillText(powerUp.type.toUpperCase(), 0, 1); // Uppercase
1244
+
1245
+ ctx.restore();
 
1246
  }
1247
  }
1248
 
1249
  function updateCarPosition(deltaTime) {
1250
+ if (deltaTime <= 0) return; // Avoid updates if deltaTime is zero or negative
1251
+
1252
+ let currentMaxSpeed = car.maxSpeed;
1253
+ let currentAcceleration = car.acceleration;
1254
+
1255
+ // Apply turbo boosts
1256
+ if (car.turboMode) {
1257
+ currentMaxSpeed += car.turboMaxSpeedBoost;
1258
+ currentAcceleration += car.turboAccelBoost;
1259
+ }
1260
+
1261
+ // Acceleration/Deceleration
1262
  if (keys.up) {
1263
+ car.speed += currentAcceleration * deltaTime;
1264
  } else if (keys.down) {
1265
+ // Apply stronger deceleration when braking (reverse key)
1266
+ car.speed -= (currentAcceleration * 1.5) * deltaTime;
1267
  } else {
1268
+ // Apply natural friction when coasting
1269
+ car.speed *= (1 - (1 - car.friction) * deltaTime * 60); // Make friction frame-rate independent (approx)
1270
+ // Ensure speed decays reasonably fast when coasting
1271
+ if (Math.abs(car.speed) < 5) car.speed = 0;
1272
  }
1273
+
1274
+ // Clamp speed to max/reverse max
1275
+ car.speed = Math.max(car.reverseMaxSpeed, Math.min(currentMaxSpeed, car.speed));
1276
+
1277
+ // Handbrake / Drifting
1278
+ if (keys.handbrake && Math.abs(car.speed) > 50) {
1279
+ car.drifting = true;
1280
+ // Apply strong braking force during handbrake turn
1281
+ car.speed *= (1 - (1 - car.brakeStrength) * deltaTime * 60);
1282
+ addTireMark(); // Add marks while drifting
 
 
1283
  } else {
1284
  car.drifting = false;
1285
  }
1286
+
1287
+ // Steering - only allow steering if moving
1288
+ if (Math.abs(car.speed) > 5) {
1289
+ // Adjust turn speed based on current speed (less turn at high speed, more at low speed)
1290
+ // Non-linear relationship: sharper turns at lower speeds
1291
+ const speedFactor = 1.0 - Math.min(1, Math.abs(car.speed) / (currentMaxSpeed * 0.8)); // Factor decreases as speed increases
1292
+ let currentTurnSpeed = car.turnSpeed * (0.3 + speedFactor * 0.7); // Base turn speed + speed-dependent part
1293
+
1294
+ // Increase turn radius significantly when drifting
1295
+ if (car.drifting) {
1296
+ currentTurnSpeed *= 1.6;
1297
+ }
1298
+
1299
+ // Apply steering input
1300
+ if (keys.left) {
1301
+ car.angle -= currentTurnSpeed * deltaTime;
1302
+ }
1303
+ if (keys.right) {
1304
+ car.angle += currentTurnSpeed * deltaTime;
1305
+ }
1306
  }
1307
+
1308
+ // Update position
1309
+ // Apply drift effect: car continues partially in its previous direction
1310
+ let moveAngle = car.angle;
1311
+ if (car.drifting) {
1312
+ // Angle car moves is slightly different from where it points
1313
+ const driftAngleEffect = 0.6 * (keys.left ? -1 : keys.right ? 1 : 0); // Sideways slip angle based on turn
1314
+ moveAngle += driftAngleEffect * (car.speed > 0 ? 1 : -1); // Reverse drift effect when reversing
1315
+
1316
+ // Reduce forward speed more when drifting
1317
+ car.speed *= (1 - (1 - car.driftFactor) * deltaTime * 60);
1318
+ }
1319
+
1320
+ car.x += Math.sin(car.angle) * car.speed * deltaTime; // Use car's facing angle for movement direction
1321
+ car.y -= Math.cos(car.angle) * car.speed * deltaTime;
1322
+
1323
+
1324
+ // --- Screen Boundaries (Wrap around instead of bounce) ---
1325
+ const wrapMargin = 10; // How far offscreen before wrapping
1326
+ if (car.x < -wrapMargin) car.x = canvas.width + wrapMargin;
1327
+ if (car.x > canvas.width + wrapMargin) car.x = -wrapMargin;
1328
+ if (car.y < -wrapMargin) car.y = canvas.height + wrapMargin;
1329
+ if (car.y > canvas.height + wrapMargin) car.y = -wrapMargin;
1330
+
1331
+
1332
+ // Update Turbo Timer and Cooldown
1333
  if (car.turboMode) {
1334
  car.turboTimer -= deltaTime;
 
1335
  if (car.turboTimer <= 0) {
1336
  car.turboMode = false;
1337
+ car.turboTimer = 0;
1338
+ car.turboCooldown = car.turboCooldownMax; // Start cooldown
 
1339
  }
1340
+ } else if (car.turboCooldown > 0) {
 
 
 
1341
  car.turboCooldown -= deltaTime;
1342
+ if (car.turboCooldown < 0) {
1343
+ car.turboCooldown = 0; // Ready
1344
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1345
  }
1346
  }
1347
 
1348
+
1349
  function checkCollisions() {
1350
+ if (!canvas) return;
1351
+ const carRadius = Math.max(car.width, car.height) / 2; // Approximation
1352
+
1353
+ // --- Car vs Obstacles ---
1354
  for (const obstacle of obstacles) {
 
1355
  const dx = car.x - obstacle.x;
1356
  const dy = car.y - obstacle.y;
1357
  const distance = Math.sqrt(dx * dx + dy * dy);
1358
+ const minDistance = carRadius + Math.max(obstacle.width, obstacle.height) / 2;
1359
+
1360
+ if (distance < minDistance * 0.85) { // 85% overlap threshold for collision
1361
+ // *** FIX: Check for zero distance before dividing ***
1362
+ if (distance < 0.001) continue; // Skip if objects are exactly on top
1363
+
1364
+ const overlap = minDistance - distance;
1365
+
1366
+ // Collision normal (vector from obstacle center to car center)
1367
  const nx = dx / distance;
1368
  const ny = dy / distance;
1369
+
1370
+ // Separate car and obstacle slightly to prevent sticking
1371
+ const separationFactor = 0.5; // How much each object moves
1372
+ car.x += nx * overlap * separationFactor;
1373
+ car.y += ny * overlap * separationFactor;
1374
+ obstacle.x -= nx * overlap * separationFactor;
1375
+ obstacle.y -= ny * overlap * separationFactor;
1376
+
1377
+
1378
+ // --- Collision Response (Simplified Physics) ---
1379
+ const carMass = 1; // Assume car mass
1380
+ const totalMass = carMass + obstacle.mass;
1381
+
1382
+ // Relative velocity
1383
+ const relativeVx = (Math.sin(car.angle) * car.speed) - obstacle.vx;
1384
+ const relativeVy = (-Math.cos(car.angle) * car.speed) - obstacle.vy;
1385
+
1386
+ // Velocity component along the normal
1387
+ const velocityAlongNormal = relativeVx * nx + relativeVy * ny;
1388
+
1389
+ // Only resolve if objects are moving towards each other
1390
+ if (velocityAlongNormal > 0) continue;
1391
+
1392
+ const restitution = 0.4; // Bounciness (0=inelastic, 1=perfectly elastic)
1393
+ let impulse = -(1 + restitution) * velocityAlongNormal;
1394
+ impulse /= (1 / carMass + 1 / obstacle.mass);
1395
+
1396
+ // Apply impulse to car (convert back to speed/angle change - tricky)
1397
+ // Simpler: Reflect car speed partially based on impact
1398
+ const impactSpeed = Math.abs(car.speed);
1399
+ car.speed *= -0.2 * restitution; // Lose some speed and bounce back slightly
1400
+
1401
+ // Apply impulse to obstacle velocities
1402
+ const impulseX = impulse * nx;
1403
+ const impulseY = impulse * ny;
1404
+
1405
+ obstacle.vx += impulseX / obstacle.mass;
1406
+ obstacle.vy += impulseY / obstacle.mass;
1407
+
1408
+ // Add some spin to the obstacle based on impact point (simplified)
1409
+ obstacle.angularVelocity += (Math.random() - 0.5) * impactSpeed * 0.005;
1410
+
1411
+
1412
+ // Add score based on impact speed and increment hit count
1413
+ const scoreToAdd = Math.min(50, Math.max(5, Math.floor(impactSpeed / 10))); // Score between 5 and 50
1414
+ addScore(scoreToAdd);
1415
  obstaclesHit++;
1416
  updateScore();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1417
 
1418
+ // Visual feedback: Screen shake (subtle)
1419
+ if (impactSpeed > 150 && gameContainer) {
1420
+ const intensity = Math.min(5, impactSpeed / 100);
1421
+ gameContainer.style.transform = `translate(${(Math.random() - 0.5) * intensity}px, ${(Math.random() - 0.5) * intensity}px)`;
1422
+ setTimeout(() => { gameContainer.style.transform = 'none'; }, 80);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1423
  }
1424
  }
1425
  }
1426
+
1427
+ // --- Car vs Power-Ups ---
1428
+ for (let i = powerUps.length - 1; i >= 0; i--) {
1429
+ const powerUp = powerUps[i];
1430
+ if (!powerUp.active) continue;
1431
+
1432
+ const dx = car.x - powerUp.x;
1433
+ const dy = car.y - powerUp.y;
1434
+ const distance = Math.sqrt(dx * dx + dy * dy);
1435
+
1436
+ if (distance < carRadius + powerUp.radius) {
1437
+ powerUp.active = false; // Deactivate immediately
1438
+ if (powerUp.type === 'turbo') {
1439
+ activateTurbo();
1440
+ } else if (powerUp.type === 'score') {
1441
+ addScore(250); // Higher score for powerup
1442
+ }
1443
+
1444
+ // Remove and respawn a new one after a delay
1445
+ setTimeout(() => {
1446
+ // Find the index again in case array changed
1447
+ const currentIndex = powerUps.findIndex(p => p === powerUp);
1448
+ if (currentIndex !== -1) {
1449
+ powerUps.splice(currentIndex, 1);
1450
+ }
1451
+ createPowerUps(); // Regenerate list (simple way to add one back)
1452
+ }, 500); // Short delay before respawn logic
1453
+ }
1454
+ }
1455
  }
1456
 
1457
+ function updateObstacles(deltaTime) {
1458
+ if (deltaTime <= 0) return;
1459
+
1460
+ for (let i = 0; i < obstacles.length; i++) {
1461
+ const obs1 = obstacles[i];
1462
+
1463
+ // Update position based on velocity
1464
+ obs1.x += obs1.vx * deltaTime;
1465
+ obs1.y += obs1.vy * deltaTime;
1466
+ obs1.angle += obs1.angularVelocity * deltaTime;
1467
+
1468
+ // Apply friction/damping
1469
+ const damping = 0.99;
1470
+ obs1.vx *= (1 - (1 - damping) * deltaTime * 60);
1471
+ obs1.vy *= (1 - (1 - damping) * deltaTime * 60);
1472
+ obs1.angularVelocity *= (1 - (1 - damping*0.95) * deltaTime * 60); // Angular damping
1473
+
1474
+
1475
+ // --- Obstacle vs Obstacle Collisions ---
1476
+ for (let j = i + 1; j < obstacles.length; j++) {
1477
+ const obs2 = obstacles[j];
1478
+
1479
+ const dx = obs1.x - obs2.x;
1480
+ const dy = obs1.y - obs2.y;
1481
+ const distance = Math.sqrt(dx * dx + dy * dy);
1482
+ const minDistance = (Math.max(obs1.width, obs1.height) / 2) + (Math.max(obs2.width, obs2.height) / 2);
1483
+
1484
+
1485
+ if (distance < minDistance) {
1486
+ // *** FIX: Check for zero distance before dividing ***
1487
+ if (distance < 0.001) continue; // Skip if exactly overlapping
1488
+
1489
+ const overlap = minDistance - distance;
1490
+ const nx = dx / distance;
1491
+ const ny = dy / distance;
1492
+
1493
+ // Separate them
1494
+ const separationFactor = 0.5;
1495
+ obs1.x += nx * overlap * separationFactor;
1496
+ obs1.y += ny * overlap * separationFactor;
1497
+ obs2.x -= nx * overlap * separationFactor;
1498
+ obs2.y -= ny * overlap * separationFactor;
1499
+
1500
+ // Collision response (similar to car-obstacle)
1501
+ const relativeVx = obs1.vx - obs2.vx;
1502
+ const relativeVy = obs1.vy - obs2.vy;
1503
+ const velocityAlongNormal = relativeVx * nx + relativeVy * ny;
1504
+
1505
+ if (velocityAlongNormal > 0) continue; // Moving away
1506
+
1507
+ const restitution = 0.3; // Less bouncy for obstacle-obstacle
1508
+ let impulse = -(1 + restitution) * velocityAlongNormal;
1509
+ impulse /= (1 / obs1.mass + 1 / obs2.mass);
1510
+
1511
+ const impulseX = impulse * nx;
1512
+ const impulseY = impulse * ny;
1513
+
1514
+ obs1.vx -= impulseX / obs1.mass;
1515
+ obs1.vy -= impulseY / obs1.mass;
1516
+ obs2.vx += impulseX / obs2.mass;
1517
+ obs2.vy += impulseY / obs2.mass;
1518
+
1519
+ // Add some spin exchange (simplified)
1520
+ obs1.angularVelocity += (Math.random() - 0.5) * impulse * 0.01 / obs1.mass;
1521
+ obs2.angularVelocity -= (Math.random() - 0.5) * impulse * 0.01 / obs2.mass;
1522
+ }
1523
+ }
1524
+
1525
+ // --- Obstacle vs Screen Boundaries (Bounce) ---
1526
+ const bounceFactor = -0.5; // How much velocity is reversed
1527
+ const halfWidth = obs1.width / 2;
1528
+ const halfHeight = obs1.height / 2; // Use dimensions for better boundary check
1529
+
1530
+ if (obs1.x - halfWidth < 0 && obs1.vx < 0) {
1531
+ obs1.x = halfWidth;
1532
+ obs1.vx *= bounceFactor;
1533
+ }
1534
+ if (obs1.x + halfWidth > canvas.width && obs1.vx > 0) {
1535
+ obs1.x = canvas.width - halfWidth;
1536
+ obs1.vx *= bounceFactor;
1537
+ }
1538
+ if (obs1.y - halfHeight < 0 && obs1.vy < 0) {
1539
+ obs1.y = halfHeight;
1540
+ obs1.vy *= bounceFactor;
1541
+ }
1542
+ if (obs1.y + halfHeight > canvas.height && obs1.vy > 0) {
1543
+ obs1.y = canvas.height - halfHeight;
1544
+ obs1.vy *= bounceFactor;
1545
+ }
1546
+ }
1547
+ }
1548
+
1549
  function drawBackground(ctx) {
1550
+ if (!ctx || !canvas) return;
1551
+ // Solid dark background
1552
+ ctx.fillStyle = '#08080A'; // Very dark grey/black
1553
  ctx.fillRect(0, 0, canvas.width, canvas.height);
1554
+
1555
+ // Subtle Grid (relative to view, not world)
1556
+ ctx.strokeStyle = 'rgba(0, 255, 255, 0.08)'; // Fainter cyan grid
1557
  ctx.lineWidth = 1;
1558
+ const gridSize = 60;
1559
+
1560
+ for (let x = 0; x < canvas.width; x += gridSize) {
 
 
 
1561
  ctx.beginPath();
1562
  ctx.moveTo(x, 0);
1563
  ctx.lineTo(x, canvas.height);
1564
  ctx.stroke();
1565
  }
1566
+ for (let y = 0; y < canvas.height; y += gridSize) {
 
1567
  ctx.beginPath();
1568
  ctx.moveTo(0, y);
1569
  ctx.lineTo(canvas.width, y);
1570
  ctx.stroke();
1571
  }
1572
+
1573
+ // Optional: Fading Vignette effect
1574
+ const vignetteRadius = canvas.width * 0.7;
1575
+ const gradient = ctx.createRadialGradient(
1576
+ canvas.width / 2, canvas.height / 2, vignetteRadius * 0.5,
1577
+ canvas.width / 2, canvas.height / 2, vignetteRadius * 1.5
1578
+ );
1579
+ gradient.addColorStop(0, 'rgba(5, 5, 5, 0)');
1580
+ gradient.addColorStop(1, 'rgba(5, 5, 5, 0.5)'); // Darken edges
1581
+ ctx.fillStyle = gradient;
1582
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1583
  }
1584
 
1585
+
1586
  function drawGameInfo(ctx) {
1587
+ if (!ctx || !canvas) return;
1588
+ const barWidth = 120;
1589
+ const barHeight = 18;
1590
+ const margin = 20;
1591
+ const bottomY = canvas.height - margin - barHeight;
1592
+ const rightX = canvas.width - margin;
1593
+
1594
+ ctx.font = 'bold 12px "DM Sans", sans-serif';
1595
+ ctx.textBaseline = 'middle';
1596
+
1597
+ // --- Speed Gauge ---
1598
+ const speedPercent = Math.min(1, Math.abs(car.speed) / car.maxSpeed);
1599
+ const speedColor = car.turboMode ? '#ff00ff' : '#00ffff'; // Magenta if turbo, else cyan
1600
+
1601
+ // Background bar
1602
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
1603
+ ctx.fillRect(rightX - barWidth, bottomY, barWidth, barHeight);
1604
+ // Fill bar
1605
+ ctx.fillStyle = speedColor;
1606
+ ctx.fillRect(rightX - barWidth, bottomY, barWidth * speedPercent, barHeight);
1607
+ // Outline
1608
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
1609
+ ctx.strokeRect(rightX - barWidth, bottomY, barWidth, barHeight);
1610
+ // Text Label
1611
+ ctx.fillStyle = '#ffffff';
1612
+ ctx.textAlign = 'right';
1613
+ ctx.fillText(`SPEED: ${Math.abs(Math.floor(car.speed))}`, rightX - barWidth - 10, bottomY + barHeight / 2);
1614
+
1615
+
1616
+ // --- Turbo Gauge ---
1617
+ const turboY = bottomY - barHeight - 10; // Position above speed gauge
1618
+ let turboText = "TURBO READY";
1619
+ let turboFillPercent = 0;
1620
+ let turboColor = '#ff00ff'; // Magenta
1621
+
1622
+ if (car.turboMode) {
1623
+ turboText = "TURBO ACTIVE";
1624
+ turboFillPercent = car.turboTimer / car.turboMaxTime;
1625
+ } else if (car.turboCooldown > 0) {
1626
+ turboText = "RECHARGING";
1627
+ turboFillPercent = 1 - (car.turboCooldown / car.turboCooldownMax);
1628
+ turboColor = 'rgba(255, 0, 255, 0.5)'; // Dimmed magenta during cooldown
1629
+ } else {
1630
+ // Ready state
1631
+ turboFillPercent = 1; // Full bar indicates ready
1632
+ }
1633
 
1634
+ // Background bar
1635
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
1636
+ ctx.fillRect(rightX - barWidth, turboY, barWidth, barHeight);
1637
+ // Fill bar
1638
+ ctx.fillStyle = turboColor;
1639
+ ctx.fillRect(rightX - barWidth, turboY, barWidth * turboFillPercent, barHeight);
1640
+ // Outline
1641
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
1642
+ ctx.strokeRect(rightX - barWidth, turboY, barWidth, barHeight);
1643
+ // Text Label
1644
+ ctx.fillStyle = '#ffffff';
1645
+ ctx.textAlign = 'right';
1646
+ ctx.fillText(turboText, rightX - barWidth - 10, turboY + barHeight / 2);
1647
+ }
1648
+
1649
+ // --- Main Game Loop ---
1650
  function gameLoop(timestamp) {
1651
+ if (!gameActive || !ctx) {
1652
+ animationFrameId = null; // Ensure ID is cleared if loop stops unexpectedly
1653
+ return; // Stop loop if game becomes inactive or context lost
1654
+ }
1655
+
1656
+ // Calculate delta time robustly
1657
+ if (!lastTimestamp || timestamp - lastTimestamp > 1000) { // Reset if first frame or large gap (e.g., tab hidden)
1658
+ lastTimestamp = timestamp;
1659
+ }
1660
+ deltaTime = (timestamp - lastTimestamp) / 1000; // Seconds
1661
  lastTimestamp = timestamp;
1662
+
1663
+ // Cap deltaTime to prevent physics explosion after lag
1664
+ deltaTime = Math.min(deltaTime, 1 / 30); // Max delta = 1/30th sec (equiv to 30fps min)
1665
+
1666
+
1667
+ // --- UPDATE ---
 
 
1668
  updateCarPosition(deltaTime);
1669
+ updateObstacles(deltaTime); // Update obstacles physics and collisions
1670
+ checkCollisions(); // Check car collisions with obstacles and powerups
1671
+
1672
+ // --- DRAW ---
1673
+ ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas
1674
+
1675
+ drawBackground(ctx);
 
1676
  drawTireMarks(ctx);
 
 
1677
  drawObstacles(ctx);
 
 
1678
  drawPowerUps(ctx);
 
 
1679
  drawCar(ctx);
1680
+ drawGameInfo(ctx); // Draw HUD elements
1681
+
1682
+ // Request next frame
1683
+ animationFrameId = requestAnimationFrame(gameLoop);
 
1684
  }
1685
 
1686
+ // --- [ Game Logic - END ] ---
 
 
 
 
 
 
 
 
 
 
 
 
1687
 
 
 
 
 
 
 
 
1688
  </script>
1689
  </body>
1690
  </html>