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>
|