Spaces:
Running
Running
Update index.html
Browse files- index.html +567 -885
index.html
CHANGED
@@ -617,7 +617,7 @@
|
|
617 |
</div>
|
618 |
|
619 |
<script>
|
620 |
-
// --- [
|
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 -
|
|
|
|
|
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');
|
771 |
|
772 |
let ctx = null;
|
773 |
if (canvas) {
|
774 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
775 |
} else {
|
776 |
console.error("Game canvas element not found!");
|
777 |
}
|
778 |
|
779 |
// Game state
|
780 |
-
let gameActive = false;
|
781 |
-
let gameVisible = false;
|
782 |
-
let isPaused = false;
|
783 |
let animationFrameId = null;
|
784 |
let score = 0;
|
785 |
let lastTimestamp = 0;
|
786 |
let deltaTime = 0;
|
787 |
-
let timeScale = 1.0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
788 |
|
789 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
790 |
function resizeCanvas() {
|
791 |
-
|
|
|
|
|
|
|
792 |
canvas.width = window.innerWidth;
|
793 |
canvas.height = window.innerHeight;
|
794 |
-
|
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);
|
799 |
-
// Consider repositioning objects if bounds change significantly
|
800 |
}
|
801 |
}
|
802 |
-
|
803 |
window.addEventListener('resize', resizeCanvas);
|
804 |
-
|
805 |
-
|
806 |
-
|
807 |
-
|
808 |
-
|
809 |
-
|
810 |
-
|
811 |
-
|
812 |
-
|
813 |
-
|
814 |
-
|
815 |
-
|
816 |
-
|
817 |
-
|
818 |
-
|
819 |
-
const
|
820 |
-
|
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();
|
854 |
}
|
855 |
});
|
856 |
} else {
|
857 |
-
console.error("Game toggle button or container not found!");
|
858 |
}
|
859 |
|
860 |
function startGame() {
|
861 |
-
|
|
|
|
|
|
|
862 |
gameVisible = true;
|
863 |
gameActive = true;
|
864 |
isPaused = false;
|
865 |
-
isGameFocused = true;
|
866 |
-
gameContainer.classList.add('active');
|
867 |
-
pauseOverlay.style.display = 'none';
|
868 |
-
document.body.style.cursor = 'none';
|
869 |
-
cursor.style.display = 'none';
|
870 |
-
glow.style.display = 'none';
|
871 |
-
|
872 |
-
|
873 |
-
|
874 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
875 |
animationFrameId = requestAnimationFrame(gameLoop);
|
876 |
}
|
877 |
|
878 |
function stopGame() {
|
879 |
-
|
|
|
880 |
gameVisible = false;
|
881 |
gameActive = false;
|
882 |
isPaused = false;
|
883 |
-
isGameFocused = false;
|
884 |
-
gameContainer.classList.remove('active');
|
885 |
-
|
886 |
-
|
887 |
-
|
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 |
-
|
|
|
899 |
isPaused = true;
|
900 |
-
gameActive = false;
|
901 |
-
pauseOverlay.style.display = 'flex';
|
902 |
-
// Keep gameVisible = true
|
903 |
}
|
904 |
|
905 |
function resumeGame() {
|
906 |
-
|
|
|
907 |
isPaused = false;
|
908 |
-
gameActive = true;
|
909 |
-
pauseOverlay.style.display = 'none';
|
910 |
-
lastTimestamp = performance.now();
|
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;
|
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 |
-
|
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 |
-
|
1003 |
-
|
1004 |
-
|
1005 |
-
|
1006 |
-
|
1007 |
-
|
1008 |
-
|
1009 |
-
|
1010 |
-
|
1011 |
-
|
1012 |
-
|
1013 |
-
|
1014 |
-
|
1015 |
-
|
1016 |
-
|
1017 |
-
|
1018 |
-
|
1019 |
-
|
1020 |
-
|
1021 |
-
|
1022 |
-
let
|
1023 |
-
|
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 |
-
|
1033 |
}
|
1034 |
-
|
1035 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1036 |
}
|
1037 |
-
|
1038 |
-
|
1039 |
-
|
|
|
|
|
|
|
|
|
1040 |
}
|
1041 |
-
|
1042 |
-
|
1043 |
-
|
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 |
-
|
1061 |
-
if (!canvas) return;
|
1062 |
powerUps.length = 0;
|
|
|
1063 |
const margin = 60;
|
1064 |
-
let
|
1065 |
-
|
1066 |
-
|
1067 |
-
|
1068 |
-
|
1069 |
-
|
1070 |
-
|
1071 |
-
|
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');
|
1106 |
setTimeout(() => {
|
1107 |
-
gameMessage.classList.remove('visible');
|
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 |
-
|
1133 |
-
|
1134 |
-
|
1135 |
-
|
1136 |
-
|
1137 |
-
|
1138 |
-
|
1139 |
-
|
1140 |
-
|
1141 |
-
|
1142 |
-
|
1143 |
-
|
1144 |
-
|
1145 |
-
|
1146 |
-
|
1147 |
-
|
1148 |
-
|
1149 |
-
|
1150 |
-
|
|
|
|
|
|
|
1151 |
}
|
1152 |
|
1153 |
function updateScore() {
|
1154 |
-
if (scoreDisplay) {
|
1155 |
-
scoreDisplay.textContent = `Score: ${score}`; // Simplified score display
|
1156 |
-
}
|
1157 |
}
|
1158 |
|
1159 |
-
// --- Drawing Functions (
|
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;
|
1171 |
-
ctx.shadowOffsetX = 5;
|
1172 |
-
|
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 |
-
|
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";
|
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";
|
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 |
-
|
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 |
-
|
1237 |
-
|
1238 |
-
|
1239 |
-
|
1240 |
-
|
1241 |
-
|
1242 |
-
|
1243 |
-
|
1244 |
-
|
1245 |
-
|
1246 |
-
|
1247 |
-
|
1248 |
-
|
1249 |
-
|
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 |
-
|
1280 |
-
const
|
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
|
1289 |
-
const
|
1290 |
-
const
|
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 |
-
|
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.
|
1322 |
-
ctx.
|
1323 |
-
|
1324 |
-
|
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 |
-
|
1405 |
-
ctx.
|
1406 |
-
|
1407 |
-
|
1408 |
-
//
|
1409 |
-
|
1410 |
-
|
1411 |
-
|
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 |
-
//
|
|
|
|
|
1492 |
let totalForce = { x: 0, y: 0 };
|
1493 |
let totalTorque = 0;
|
1494 |
-
|
1495 |
-
|
1496 |
-
const
|
1497 |
-
|
1498 |
-
|
1499 |
-
|
1500 |
-
|
1501 |
-
|
1502 |
-
|
1503 |
-
|
1504 |
-
|
1505 |
-
|
1506 |
-
|
1507 |
-
|
1508 |
-
|
1509 |
-
const
|
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 |
-
//
|
1530 |
-
|
1531 |
-
|
1532 |
-
|
1533 |
-
|
1534 |
-
|
1535 |
-
|
1536 |
-
|
1537 |
-
|
1538 |
-
|
1539 |
-
|
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 |
-
//
|
1559 |
-
|
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 |
-
//
|
1576 |
-
|
1577 |
-
|
|
|
1578 |
|
1579 |
-
// Update Turbo Timer
|
1580 |
-
if (
|
1581 |
-
|
1582 |
-
if (
|
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 |
-
|
1601 |
-
|
1602 |
-
|
1603 |
-
|
1604 |
-
|
1605 |
-
|
1606 |
-
|
1607 |
-
|
1608 |
-
|
1609 |
-
|
1610 |
-
|
1611 |
-
|
1612 |
-
|
1613 |
-
|
1614 |
-
|
|
|
|
|
|
|
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 |
-
|
1629 |
-
|
1630 |
-
|
1631 |
-
{ x:
|
1632 |
-
{ x:
|
1633 |
-
{ x:
|
|
|
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 });
|
1643 |
obj.axes.push(normal);
|
1644 |
}
|
1645 |
}
|
1646 |
|
1647 |
-
// Project polygon vertices onto an axis
|
1648 |
function projectPolygon(vertices, axis) {
|
1649 |
-
|
1650 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
1665 |
-
|
1666 |
-
|
|
|
|
|
|
|
|
|
1667 |
|
1668 |
-
const axes = [...objA.axes, ...objB.axes];
|
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 |
-
|
1680 |
-
}
|
1681 |
-
|
1682 |
-
// Track minimum overlap and corresponding axis
|
1683 |
-
if (overlap < minOverlap) {
|
1684 |
-
minOverlap = overlap;
|
1685 |
-
collisionNormal = axis;
|
1686 |
-
}
|
1687 |
}
|
1688 |
|
1689 |
-
//
|
1690 |
-
|
1691 |
-
const
|
1692 |
-
|
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 |
-
|
1704 |
-
|
1705 |
-
|
1706 |
-
|
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 |
-
|
1717 |
-
|
1718 |
-
|
|
|
|
|
|
|
1719 |
|
1720 |
|
1721 |
-
//
|
1722 |
-
|
1723 |
-
|
1724 |
-
|
1725 |
-
|
1726 |
-
|
1727 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
1728 |
|
1729 |
-
//
|
|
|
1730 |
const rA = vecSub(collisionPoint, {x: objA.x, y: objA.y});
|
1731 |
const rB = vecSub(collisionPoint, {x: objB.x, y: objB.y});
|
1732 |
-
|
1733 |
-
|
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 |
-
|
1748 |
-
if (velocityAlongNormal > 0) return;
|
1749 |
-
|
1750 |
-
const restitution = COLLISION_RESTITUTION; // Bounciness
|
1751 |
|
1752 |
-
|
1753 |
const rACrossN = vecCross2D(rA, normal);
|
1754 |
const rBCrossN = vecCross2D(rB, normal);
|
1755 |
-
|
1756 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1757 |
|
1758 |
let j = -(1 + restitution) * velocityAlongNormal;
|
1759 |
-
j /=
|
1760 |
|
1761 |
-
//
|
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 |
-
//
|
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 |
-
|
1780 |
-
|
1781 |
-
|
1782 |
-
|
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 |
-
|
1790 |
-
|
1791 |
-
|
1792 |
-
|
1793 |
-
|
1794 |
-
|
1795 |
-
|
1796 |
-
|
1797 |
-
|
1798 |
-
|
1799 |
-
|
1800 |
-
|
1801 |
-
|
1802 |
-
if (
|
1803 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
1817 |
-
|
1818 |
-
|
1819 |
-
|
1820 |
-
|
1821 |
-
|
1822 |
-
|
1823 |
-
|
1824 |
-
|
1825 |
-
|
1826 |
-
|
1827 |
-
|
1828 |
-
|
1829 |
-
|
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 |
-
|
1839 |
-
|
1840 |
-
const
|
1841 |
-
|
1842 |
-
|
|
|
|
|
1843 |
}
|
1844 |
-
}
|
1845 |
|
1846 |
-
|
1847 |
-
|
1848 |
-
|
1849 |
-
|
1850 |
-
|
1851 |
-
|
1852 |
-
|
1853 |
-
|
|
|
1854 |
}
|
1855 |
}
|
1856 |
-
}
|
1857 |
|
1858 |
-
|
1859 |
-
|
1860 |
-
|
1861 |
-
|
1862 |
-
|
1863 |
-
|
1864 |
-
|
1865 |
-
|
1866 |
-
|
1867 |
-
|
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 |
-
|
1887 |
-
|
1888 |
-
return;
|
1889 |
-
}
|
1890 |
|
1891 |
-
// Calculate delta time
|
1892 |
const now = performance.now();
|
1893 |
-
|
|
|
1894 |
lastTimestamp = now;
|
1895 |
-
|
1896 |
-
|
1897 |
-
|
1898 |
-
|
1899 |
-
if (gameActive && !isPaused) {
|
1900 |
-
|
1901 |
-
|
1902 |
-
|
1903 |
-
|
1904 |
-
|
1905 |
-
|
1906 |
-
|
1907 |
-
|
1908 |
-
|
1909 |
-
|
1910 |
-
}
|
1911 |
-
*/
|
1912 |
-
// --- Variable timestep (simpler for now) ---
|
1913 |
-
applyForces(deltaTime);
|
1914 |
-
updateObstaclesPhysics(deltaTime);
|
1915 |
-
checkAllCollisions();
|
1916 |
|
1917 |
} else {
|
1918 |
-
//
|
1919 |
-
//
|
1920 |
}
|
1921 |
|
1922 |
|
1923 |
// --- DRAW ---
|
1924 |
if (ctx && canvas) {
|
1925 |
-
|
1926 |
-
|
1927 |
-
|
1928 |
-
|
1929 |
-
|
1930 |
-
|
1931 |
-
|
1932 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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>
|