项目文件夹

文件
2025-06-27 15:46:46 -07:00

559 行
22 KiB
Plaintext

diff --git a/static/js/collisions.js b/static/js/collisions.js
index 70cda7e..4bc83d7 100644
--- a/static/js/collisions.js
+++ b/static/js/collisions.js
@@ -2,6 +2,7 @@ import { gameState } from './gameState.js';
import { getDistance, getSize, getRandomPosition, findSafeSpawnLocation } from './utils.js';
import { FOOD_SIZE, FOOD_SCORE, COLLISION_THRESHOLD, FOOD_COUNT, AI_COUNT, STARTING_SCORE, WORLD_SIZE } from './config.js';
import { respawnAI } from './entities.js';
+import { doTrianglesOverlap, createTriangle } from './triangleCollision.js';
export function handleFoodCollisions() {
// Player cells eating food
@@ -11,8 +12,15 @@ export function handleFoodCollisions() {
const playerSize = getSize(playerCell.score);
if (distance < playerSize + FOOD_SIZE) {
- playerCell.score += FOOD_SCORE;
- return false;
+ // Create triangles for more precise collision detection
+ const playerTriangle = createTriangle(playerCell.x, playerCell.y, playerSize, playerCell.rotation || 0);
+ const foodTriangle = createTriangle(food.x, food.y, FOOD_SIZE, food.rotation || 0);
+
+ // Check for triangle overlap
+ if (doTrianglesOverlap(playerTriangle, foodTriangle)) {
+ playerCell.score += FOOD_SCORE;
+ return false;
+ }
}
return true;
});
@@ -25,8 +33,15 @@ export function handleFoodCollisions() {
const aiSize = getSize(ai.score);
if (distance < aiSize + FOOD_SIZE) {
- ai.score += FOOD_SCORE;
- return false;
+ // Create triangles for more precise collision detection
+ const aiTriangle = createTriangle(ai.x, ai.y, aiSize, ai.rotation || 0);
+ const foodTriangle = createTriangle(food.x, food.y, FOOD_SIZE, food.rotation || 0);
+
+ // Check for triangle overlap
+ if (doTrianglesOverlap(aiTriangle, foodTriangle)) {
+ ai.score += FOOD_SCORE;
+ return false;
+ }
}
return true;
});
@@ -45,22 +60,31 @@ export function handlePlayerAICollisions() {
if (aiIndicesToRemove.has(aiIndex)) return;
if (playerCellsToRemove.has(playerCellIndex)) return;
+ // First do a quick circle-based distance check for performance
const distance = getDistance(playerCell, ai);
const playerSize = getSize(playerCell.score);
const aiSize = getSize(ai.score);
const minDistance = playerSize + aiSize;
- if (distance < minDistance) {
- // Player cell is bigger
- if (playerSize > aiSize * COLLISION_THRESHOLD) {
- const currentGain = scoreGains.get(playerCellIndex) || 0;
- scoreGains.set(playerCellIndex, currentGain + ai.score + 100);
- aiIndicesToRemove.add(aiIndex);
- }
- // AI is bigger
- else if (aiSize > playerSize * COLLISION_THRESHOLD) {
- ai.score += playerCell.score + 100;
- playerCellsToRemove.add(playerCellIndex);
+ // Only perform triangle collision if circles are close enough
+ if (distance < minDistance * 1.5) {
+ // Create triangles for more precise collision detection
+ const playerTriangle = createTriangle(playerCell.x, playerCell.y, playerSize, playerCell.rotation || 0);
+ const aiTriangle = createTriangle(ai.x, ai.y, aiSize, ai.rotation || 0);
+
+ // Check for triangle overlap
+ if (doTrianglesOverlap(playerTriangle, aiTriangle)) {
+ // Player cell is bigger
+ if (playerSize > aiSize * COLLISION_THRESHOLD) {
+ const currentGain = scoreGains.get(playerCellIndex) || 0;
+ scoreGains.set(playerCellIndex, currentGain + ai.score + 100);
+ aiIndicesToRemove.add(aiIndex);
+ }
+ // AI is bigger
+ else if (aiSize > playerSize * COLLISION_THRESHOLD) {
+ ai.score += playerCell.score + 100;
+ playerCellsToRemove.add(playerCellIndex);
+ }
}
}
});
@@ -115,16 +139,24 @@ export function handleAIAICollisions() {
const ai2Size = getSize(ai2.score);
const minDistance = ai1Size + ai2Size;
- if (distance < minDistance) {
- if (ai1Size > ai2Size * COLLISION_THRESHOLD) {
- const currentGain = scoreGains.get(i) || 0;
- scoreGains.set(i, currentGain + ai2.score + 100);
- aisToRemove.add(j);
- } else if (ai2Size > ai1Size * COLLISION_THRESHOLD) {
- const currentGain = scoreGains.get(j) || 0;
- scoreGains.set(j, currentGain + ai1.score + 100);
- aisToRemove.add(i);
- break;
+ // Only perform triangle collision if circles are close enough
+ if (distance < minDistance * 1.5) {
+ // Create triangles for more precise collision detection
+ const ai1Triangle = createTriangle(ai1.x, ai1.y, ai1Size, ai1.rotation || 0);
+ const ai2Triangle = createTriangle(ai2.x, ai2.y, ai2Size, ai2.rotation || 0);
+
+ // Check for triangle overlap
+ if (doTrianglesOverlap(ai1Triangle, ai2Triangle)) {
+ if (ai1Size > ai2Size * COLLISION_THRESHOLD) {
+ const currentGain = scoreGains.get(i) || 0;
+ scoreGains.set(i, currentGain + ai2.score + 100);
+ aisToRemove.add(j);
+ } else if (ai2Size > ai1Size * COLLISION_THRESHOLD) {
+ const currentGain = scoreGains.get(j) || 0;
+ scoreGains.set(j, currentGain + ai1.score + 100);
+ aisToRemove.add(i);
+ break;
+ }
}
}
}
@@ -150,7 +182,8 @@ export function respawnEntities() {
gameState.food.push({
x: pos.x,
y: pos.y,
- color: `hsl(${Math.random() * 360}, 50%, 50%)`
+ color: `hsl(${Math.random() * 360}, 50%, 50%)`,
+ rotation: Math.random() * Math.PI * 2 // Add random rotation
});
}
@@ -160,6 +193,10 @@ export function respawnEntities() {
const newAI = respawnAI();
newAI.x = safePos.x;
newAI.y = safePos.y;
+ // Ensure AI has rotation property
+ if (!newAI.hasOwnProperty('rotation')) {
+ newAI.rotation = Math.random() * Math.PI * 2;
+ }
gameState.aiPlayers.push(newAI);
}
@@ -171,7 +208,8 @@ export function respawnEntities() {
y: safePos.y,
score: STARTING_SCORE,
velocityX: 0,
- velocityY: 0
+ velocityY: 0,
+ rotation: Math.random() * Math.PI * 2 // Add initial rotation
});
}
}
\ No newline at end of file
diff --git a/static/js/entities.js b/static/js/entities.js
index 01441f2..96b75bd 100644
--- a/static/js/entities.js
+++ b/static/js/entities.js
@@ -165,6 +165,7 @@ function updateCellMerging() {
}
export function updatePlayer() {
+ // Calculate direction towards mouse
const dx = mouse.x - window.innerWidth / 2;
const dy = mouse.y - window.innerHeight / 2;
const distance = Math.sqrt(dx * dx + dy * dy);
@@ -187,6 +188,11 @@ export function updatePlayer() {
// Update position
cell.x = Math.max(0, Math.min(WORLD_SIZE, cell.x + cell.velocityX));
cell.y = Math.max(0, Math.min(WORLD_SIZE, cell.y + cell.velocityY));
+
+ // Update rotation based on movement direction
+ if (Math.abs(cell.velocityX) > 0.01 || Math.abs(cell.velocityY) > 0.01) {
+ cell.rotation = Math.atan2(cell.velocityY, cell.velocityX);
+ }
});
}
@@ -221,6 +227,7 @@ export function splitPlayerCell(cell) {
score: cell.score / 2,
velocityX: direction.x * SPLIT_VELOCITY,
velocityY: direction.y * SPLIT_VELOCITY,
+ rotation: Math.atan2(direction.y, direction.x), // Set rotation based on split direction
splitTime: now
};
@@ -228,6 +235,7 @@ export function splitPlayerCell(cell) {
cell.score /= 2;
cell.velocityX = -direction.x * SPLIT_VELOCITY * 0.5;
cell.velocityY = -direction.y * SPLIT_VELOCITY * 0.5;
+ cell.rotation = Math.atan2(-direction.y, -direction.x); // Update rotation based on new direction
cell.splitTime = now;
// Add new cell
@@ -256,6 +264,9 @@ export function updateAI() {
ai.x = Math.max(0, Math.min(WORLD_SIZE, ai.x));
ai.y = Math.max(0, Math.min(WORLD_SIZE, ai.y));
+
+ // Update rotation to match movement direction
+ ai.rotation = ai.direction;
});
}
@@ -272,7 +283,21 @@ export function initEntities() {
gameState.food.push({
x: pos.x,
y: pos.y,
- color: `hsl(${Math.random() * 360}, 50%, 50%)`
+ color: `hsl(${Math.random() * 360}, 50%, 50%)`,
+ rotation: Math.random() * Math.PI * 2 // Random rotation for triangular food
+ });
+ }
+
+ // Ensure player has at least one cell
+ if (gameState.playerCells.length === 0) {
+ const safePos = findSafeSpawnLocation(gameState);
+ gameState.playerCells.push({
+ x: safePos.x,
+ y: safePos.y,
+ score: STARTING_SCORE,
+ velocityX: 0,
+ velocityY: 0,
+ rotation: 0 // Initial rotation for triangle rendering
});
}
@@ -285,6 +310,7 @@ export function initEntities() {
score: AI_STARTING_SCORE,
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
direction: Math.random() * Math.PI * 2,
+ rotation: Math.random() * Math.PI * 2, // Initial rotation for triangle rendering
name: getUnusedAIName()
};
gameState.aiPlayers.push(ai);
@@ -301,6 +327,7 @@ export function initEntities() {
export function respawnAI() {
const pos = getRandomPosition();
const name = getUnusedAIName();
+ const rotation = Math.random() * Math.PI * 2;
return {
x: pos.x,
@@ -308,6 +335,7 @@ export function respawnAI() {
score: AI_STARTING_SCORE,
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
direction: Math.random() * Math.PI * 2,
+ rotation: rotation, // Add rotation for triangle rendering
name: name
};
}
\ No newline at end of file
diff --git a/static/js/renderer.js b/static/js/renderer.js
index fc0853d..b01d9cb 100644
--- a/static/js/renderer.js
+++ b/static/js/renderer.js
@@ -1,6 +1,7 @@
import { gameState } from './gameState.js';
import { getSize, calculateCenterOfMass } from './utils.js';
import { WORLD_SIZE, COLORS, FOOD_SIZE } from './config.js';
+import { createTriangle } from './triangleCollision.js';
let canvas, ctx, minimapCanvas, minimapCtx, scoreElement, leaderboardContent;
@@ -29,14 +30,30 @@ function drawCircle(x, y, value, color, isFood) {
ctx.fill();
}
-function drawCellWithName(x, y, score, color, name) {
- const size = getSize(score);
+function drawTriangle(x, y, size, color, rotation = 0) {
+ // Create a triangle shape
+ const triangle = createTriangle(x, y, size, rotation);
- // Draw cell
ctx.beginPath();
- ctx.arc(x, y, size, 0, Math.PI * 2);
+ ctx.moveTo(triangle.x1, triangle.y1);
+ ctx.lineTo(triangle.x2, triangle.y2);
+ ctx.lineTo(triangle.x3, triangle.y3);
+ ctx.closePath();
+
ctx.fillStyle = color;
ctx.fill();
+
+ // Add a subtle stroke for better visibility
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
+ ctx.lineWidth = 1;
+ ctx.stroke();
+}
+
+function drawCellWithName(x, y, score, color, name, rotation = 0) {
+ const size = getSize(score);
+
+ // Draw cell as a triangle
+ drawTriangle(x, y, size, color, rotation);
// Draw name
if (size > 20) { // Only draw name if cell is big enough
@@ -77,7 +94,7 @@ export function drawGame() {
if (screenX >= -FOOD_SIZE && screenX <= canvas.width + FOOD_SIZE &&
screenY >= -FOOD_SIZE && screenY <= canvas.height + FOOD_SIZE) {
- drawCircle(screenX, screenY, FOOD_SIZE, food.color, true);
+ drawTriangle(screenX, screenY, FOOD_SIZE, food.color, food.rotation);
}
});
@@ -89,7 +106,7 @@ export function drawGame() {
if (screenX >= -size && screenX <= canvas.width + size &&
screenY >= -size && screenY <= canvas.height + size) {
- drawCellWithName(screenX, screenY, ai.score, ai.color, ai.name);
+ drawCellWithName(screenX, screenY, ai.score, ai.color, ai.name, ai.rotation);
}
});
@@ -101,7 +118,7 @@ export function drawGame() {
if (screenX >= -size && screenX <= canvas.width + size &&
screenY >= -size && screenY <= canvas.height + size) {
- drawCellWithName(screenX, screenY, cell.score, COLORS.PLAYER, gameState.playerName);
+ drawCellWithName(screenX, screenY, cell.score, COLORS.PLAYER, gameState.playerName, cell.rotation);
}
});
diff --git a/static/js/triangleCollision.js b/static/js/triangleCollision.js
new file mode 100644
index 0000000..334075b
--- /dev/null
+++ b/static/js/triangleCollision.js
@@ -0,0 +1,222 @@
+/**
+ * Triangle Collision Detection Module
+ * Implements algorithms for detecting overlap between triangles
+ */
+
+/**
+ * Represents a triangle with three vertices
+ * @typedef {Object} Triangle
+ * @property {number} x1 - x-coordinate of first vertex
+ * @property {number} y1 - y-coordinate of first vertex
+ * @property {number} x2 - x-coordinate of second vertex
+ * @property {number} y2 - y-coordinate of second vertex
+ * @property {number} x3 - x-coordinate of third vertex
+ * @property {number} y3 - y-coordinate of third vertex
+ */
+
+/**
+ * Determines if a point is inside a triangle using barycentric coordinates
+ * @param {number} px - x-coordinate of the point
+ * @param {number} py - y-coordinate of the point
+ * @param {Triangle} triangle - The triangle to check against
+ * @returns {boolean} - True if the point is inside the triangle
+ */
+export function isPointInTriangle(px, py, triangle) {
+ const { x1, y1, x2, y2, x3, y3 } = triangle;
+
+ // Calculate barycentric coordinates
+ const denominator = ((y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3));
+
+ // Avoid division by zero
+ if (Math.abs(denominator) < 0.0001) return false;
+
+ const a = ((y2 - y3) * (px - x3) + (x3 - x2) * (py - y3)) / denominator;
+ const b = ((y3 - y1) * (px - x3) + (x1 - x3) * (py - y3)) / denominator;
+ const c = 1 - a - b;
+
+ // Point is inside if all barycentric coordinates are between 0 and 1
+ return a >= 0 && a <= 1 && b >= 0 && b <= 1 && c >= 0 && c <= 1;
+}
+
+/**
+ * Checks if a line segment intersects with another line segment
+ * @param {number} x1 - x-coordinate of first point of first line
+ * @param {number} y1 - y-coordinate of first point of first line
+ * @param {number} x2 - x-coordinate of second point of first line
+ * @param {number} y2 - y-coordinate of second point of first line
+ * @param {number} x3 - x-coordinate of first point of second line
+ * @param {number} y3 - y-coordinate of first point of second line
+ * @param {number} x4 - x-coordinate of second point of second line
+ * @param {number} y4 - y-coordinate of second point of second line
+ * @returns {boolean} - True if the line segments intersect
+ */
+export function doLinesIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
+ // Special case: Check if the lines are actually the same line segment
+ if ((x1 === x3 && y1 === y3 && x2 === x4 && y2 === y4) ||
+ (x1 === x4 && y1 === y4 && x2 === x3 && y2 === y3)) {
+ return true;
+ }
+
+ // Check for the case where one of the endpoints of one line is on the other line
+ // This is a more robust approach for detecting intersections
+
+ // Function to check if point (px,py) is on line segment (x1,y1)-(x2,y2)
+ function isPointOnLine(px, py, lx1, ly1, lx2, ly2) {
+ // Check if point is within the bounding box of the line segment
+ if (px < Math.min(lx1, lx2) - 0.0001 || px > Math.max(lx1, lx2) + 0.0001 ||
+ py < Math.min(ly1, ly2) - 0.0001 || py > Math.max(ly1, ly2) + 0.0001) {
+ return false;
+ }
+
+ // Check if point is on the line using cross product
+ const cross = (py - ly1) * (lx2 - lx1) - (px - lx1) * (ly2 - ly1);
+ return Math.abs(cross) < 0.0001;
+ }
+
+ // Check if any endpoint of one line is on the other line
+ if (isPointOnLine(x1, y1, x3, y3, x4, y4) ||
+ isPointOnLine(x2, y2, x3, y3, x4, y4) ||
+ isPointOnLine(x3, y3, x1, y1, x2, y2) ||
+ isPointOnLine(x4, y4, x1, y1, x2, y2)) {
+ return true;
+ }
+
+ // Calculate the direction of the lines
+ const d1x = x2 - x1;
+ const d1y = y2 - y1;
+ const d2x = x4 - x3;
+ const d2y = y4 - y3;
+
+ // Calculate the determinant
+ const denominator = d1y * d2x - d1x * d2y;
+
+ // Lines are parallel if denominator is close to 0
+ if (Math.abs(denominator) < 0.0001) return false;
+
+ // Calculate the parameters for the intersection point
+ const d3x = x1 - x3;
+ const d3y = y1 - y3;
+
+ const t1 = (d2x * d3y - d2y * d3x) / denominator;
+ const t2 = (d1x * d3y - d1y * d3x) / denominator;
+
+ // Check if the intersection point is within both line segments
+ return t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1;
+}
+
+/**
+ * Checks if two triangles overlap using vertex inclusion and edge intersection tests
+ * @param {Triangle} triangle1 - First triangle
+ * @param {Triangle} triangle2 - Second triangle
+ * @returns {boolean} - True if the triangles overlap
+ */
+export function doTrianglesOverlap(triangle1, triangle2) {
+ // Null check for triangles
+ if (!triangle1 || !triangle2) return false;
+
+ // Ensure all properties exist
+ const t1Props = ['x1', 'y1', 'x2', 'y2', 'x3', 'y3'];
+ const t2Props = ['x1', 'y1', 'x2', 'y2', 'x3', 'y3'];
+
+ for (const prop of t1Props) {
+ if (triangle1[prop] === undefined) return false;
+ }
+
+ for (const prop of t2Props) {
+ if (triangle2[prop] === undefined) return false;
+ }
+
+ // Check if any vertex of triangle1 is inside triangle2
+ if (isPointInTriangle(triangle1.x1, triangle1.y1, triangle2) ||
+ isPointInTriangle(triangle1.x2, triangle1.y2, triangle2) ||
+ isPointInTriangle(triangle1.x3, triangle1.y3, triangle2)) {
+ return true;
+ }
+
+ // Check if any vertex of triangle2 is inside triangle1
+ if (isPointInTriangle(triangle2.x1, triangle2.y1, triangle1) ||
+ isPointInTriangle(triangle2.x2, triangle2.y2, triangle1) ||
+ isPointInTriangle(triangle2.x3, triangle2.y3, triangle1)) {
+ return true;
+ }
+
+ // Check if any edge of triangle1 intersects with any edge of triangle2
+ const edges1 = [
+ [triangle1.x1, triangle1.y1, triangle1.x2, triangle1.y2],
+ [triangle1.x2, triangle1.y2, triangle1.x3, triangle1.y3],
+ [triangle1.x3, triangle1.y3, triangle1.x1, triangle1.y1]
+ ];
+
+ const edges2 = [
+ [triangle2.x1, triangle2.y1, triangle2.x2, triangle2.y2],
+ [triangle2.x2, triangle2.y2, triangle2.x3, triangle2.y3],
+ [triangle2.x3, triangle2.y3, triangle2.x1, triangle2.y1]
+ ];
+
+ for (const [x1, y1, x2, y2] of edges1) {
+ for (const [x3, y3, x4, y4] of edges2) {
+ if (doLinesIntersect(x1, y1, x2, y2, x3, y3, x4, y4)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Creates a triangle with the given center point and size
+ * @param {number} x - x-coordinate of the center
+ * @param {number} y - y-coordinate of the center
+ * @param {number} size - Size of the triangle (distance from center to vertices)
+ * @param {number} rotation - Rotation angle in radians
+ * @returns {Triangle} - A triangle object
+ */
+export function createTriangle(x, y, size, rotation = 0) {
+ // Create an equilateral triangle with a slightly larger size for better collision detection
+ // Using 1.1 * size to make the collision area slightly larger than the visual triangle
+ const collisionSize = size * 1.1; // Slightly larger collision area
+
+ const angle1 = rotation;
+ const angle2 = rotation + (2 * Math.PI / 3);
+ const angle3 = rotation + (4 * Math.PI / 3);
+
+ return {
+ x1: x + collisionSize * Math.cos(angle1),
+ y1: y + collisionSize * Math.sin(angle1),
+ x2: x + collisionSize * Math.cos(angle2),
+ y2: y + collisionSize * Math.sin(angle2),
+ x3: x + collisionSize * Math.cos(angle3),
+ y3: y + collisionSize * Math.sin(angle3)
+ };
+}
+
+/**
+ * Calculates the area of a triangle
+ * @param {Triangle} triangle - The triangle
+ * @returns {number} - The area of the triangle
+ */
+export function calculateTriangleArea(triangle) {
+ const { x1, y1, x2, y2, x3, y3 } = triangle;
+ return Math.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2);
+}
+
+/**
+ * Checks if a point is inside a convex polygon
+ * @param {number} x - x-coordinate of the point
+ * @param {number} y - y-coordinate of the point
+ * @param {Array<{x: number, y: number}>} polygon - Array of polygon vertices
+ * @returns {boolean} - True if the point is inside the polygon
+ */
+export function isPointInPolygon(x, y, polygon) {
+ let inside = false;
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
+ const xi = polygon[i].x, yi = polygon[i].y;
+ const xj = polygon[j].x, yj = polygon[j].y;
+
+ const intersect = ((yi > y) !== (yj > y)) &&
+ (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
+ if (intersect) inside = !inside;
+ }
+ return inside;
+}