313 lines
10 KiB
JavaScript
313 lines
10 KiB
JavaScript
import { gameState, mouse } from './gameState.js';
|
|
import { getSize, getRandomPosition, calculateCenterOfMass, getDistance } from './utils.js';
|
|
import {
|
|
WORLD_SIZE,
|
|
FOOD_COUNT,
|
|
AI_COUNT,
|
|
MIN_SPLIT_SCORE,
|
|
SPLIT_VELOCITY,
|
|
MAX_PLAYER_CELLS,
|
|
AI_STARTING_SCORE,
|
|
MERGE_COOLDOWN,
|
|
MERGE_DISTANCE,
|
|
MERGE_FORCE,
|
|
MERGE_START_FORCE
|
|
} from './config.js';
|
|
|
|
const AI_NAMES = [
|
|
'Cursor',
|
|
'Zed',
|
|
'VSCode',
|
|
'Visual Studio',
|
|
'Eclipse',
|
|
'JetBrains',
|
|
'XCode',
|
|
'Sublime',
|
|
'Neovim',
|
|
'Emacs'
|
|
];
|
|
|
|
// Function to get an unused AI name
|
|
function getUnusedAIName() {
|
|
const usedNames = new Set(gameState.aiPlayers.map(ai => ai.name));
|
|
return AI_NAMES.find(name => !usedNames.has(name)) || AI_NAMES[0];
|
|
}
|
|
|
|
function updateCellMerging() {
|
|
const now = Date.now();
|
|
const cellsToMerge = [];
|
|
|
|
// First pass: calculate merging forces and identify mergeable cells
|
|
for (let i = 0; i < gameState.playerCells.length; i++) {
|
|
const cell1 = gameState.playerCells[i];
|
|
|
|
// Skip if cell is already marked for merging
|
|
if (cellsToMerge.includes(i)) continue;
|
|
|
|
for (let j = i + 1; j < gameState.playerCells.length; j++) {
|
|
const cell2 = gameState.playerCells[j];
|
|
|
|
// Skip if cell is already marked for merging
|
|
if (cellsToMerge.includes(j)) continue;
|
|
|
|
const distance = getDistance(cell1, cell2);
|
|
const cell1Size = getSize(cell1.score);
|
|
const cell2Size = getSize(cell2.score);
|
|
const minMergeDistance = (cell1Size + cell2Size) * MERGE_DISTANCE;
|
|
const minDistance = cell1Size + cell2Size; // Minimum distance before repulsion
|
|
|
|
// Calculate time since split
|
|
const timeSinceSplit1 = now - (cell1.splitTime || 0);
|
|
const timeSinceSplit2 = now - (cell2.splitTime || 0);
|
|
const canMerge = timeSinceSplit1 > MERGE_COOLDOWN && timeSinceSplit2 > MERGE_COOLDOWN;
|
|
|
|
if (distance < minMergeDistance && canMerge) {
|
|
// Mark cells for merging only if they're very close
|
|
if (distance < minDistance * 0.5) {
|
|
cellsToMerge.push(i, j);
|
|
} else {
|
|
// Strong attraction force when close to merging
|
|
const dx = cell2.x - cell1.x;
|
|
const dy = cell2.y - cell1.y;
|
|
const force = MERGE_FORCE;
|
|
const factor = force / Math.max(1, distance);
|
|
|
|
cell1.velocityX += dx * factor;
|
|
cell1.velocityY += dy * factor;
|
|
cell2.velocityX -= dx * factor;
|
|
cell2.velocityY -= dy * factor;
|
|
}
|
|
} else {
|
|
// Calculate repulsion when too close
|
|
if (distance < minDistance) {
|
|
const repulsionStrength = 0.3; // Adjust this to control repulsion strength
|
|
const repulsionFactor = (minDistance - distance) / minDistance * repulsionStrength;
|
|
const dx = cell2.x - cell1.x;
|
|
const dy = cell2.y - cell1.y;
|
|
|
|
// Apply repulsion
|
|
cell1.velocityX -= dx * repulsionFactor;
|
|
cell1.velocityY -= dy * repulsionFactor;
|
|
cell2.velocityX += dx * repulsionFactor;
|
|
cell2.velocityY += dy * repulsionFactor;
|
|
}
|
|
|
|
// Apply attraction force if not too close
|
|
if (distance > minDistance) {
|
|
const dx = cell2.x - cell1.x;
|
|
const dy = cell2.y - cell1.y;
|
|
const force = canMerge ? MERGE_FORCE : MERGE_START_FORCE;
|
|
const factor = force / Math.max(1, distance);
|
|
|
|
cell1.velocityX += dx * factor;
|
|
cell1.velocityY += dy * factor;
|
|
cell2.velocityX -= dx * factor;
|
|
cell2.velocityY -= dy * factor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Second pass: merge cells
|
|
if (cellsToMerge.length > 0) {
|
|
// Sort indices in descending order to remove from end first
|
|
cellsToMerge.sort((a, b) => b - a);
|
|
|
|
// Get unique indices
|
|
const uniqueIndices = [...new Set(cellsToMerge)];
|
|
|
|
// Group cells to merge
|
|
const groups = [];
|
|
let currentGroup = [uniqueIndices[0]];
|
|
|
|
for (let i = 1; i < uniqueIndices.length; i++) {
|
|
const current = uniqueIndices[i];
|
|
const prev = currentGroup[currentGroup.length - 1];
|
|
|
|
if (prev - current === 1) {
|
|
currentGroup.push(current);
|
|
} else {
|
|
groups.push(currentGroup);
|
|
currentGroup = [current];
|
|
}
|
|
}
|
|
groups.push(currentGroup);
|
|
|
|
// Merge each group
|
|
groups.forEach(group => {
|
|
const cells = group.map(index => gameState.playerCells[index]);
|
|
|
|
// Calculate total score and weighted position
|
|
const totalScore = cells.reduce((sum, cell) => sum + cell.score, 0);
|
|
const weightedX = cells.reduce((sum, cell) => sum + cell.x * cell.score, 0) / totalScore;
|
|
const weightedY = cells.reduce((sum, cell) => sum + cell.y * cell.score, 0) / totalScore;
|
|
|
|
// Calculate average velocity weighted by mass
|
|
const avgVelocityX = cells.reduce((sum, cell) => sum + cell.velocityX * cell.score, 0) / totalScore;
|
|
const avgVelocityY = cells.reduce((sum, cell) => sum + cell.velocityY * cell.score, 0) / totalScore;
|
|
|
|
// Remove old cells (in reverse order to maintain correct indices)
|
|
group.sort((a, b) => b - a).forEach(index => {
|
|
gameState.playerCells.splice(index, 1);
|
|
});
|
|
|
|
// Add merged cell with combined score
|
|
gameState.playerCells.push({
|
|
x: weightedX,
|
|
y: weightedY,
|
|
score: totalScore, // This is the sum of all merged cell scores
|
|
velocityX: avgVelocityX,
|
|
velocityY: avgVelocityY,
|
|
splitTime: 0 // Reset split time for merged cell
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
export function updatePlayer() {
|
|
const dx = mouse.x - window.innerWidth / 2;
|
|
const dy = mouse.y - window.innerHeight / 2;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance > 0) {
|
|
const direction = {
|
|
x: dx / distance,
|
|
y: dy / distance
|
|
};
|
|
|
|
// Update each cell
|
|
gameState.playerCells.forEach(cell => {
|
|
// Base speed is inversely proportional to cell size
|
|
const speed = 5 / (getSize(cell.score) / 20);
|
|
|
|
// Update velocity (with inertia)
|
|
cell.velocityX = cell.velocityX * 0.9 + direction.x * speed * 0.1;
|
|
cell.velocityY = cell.velocityY * 0.9 + direction.y * speed * 0.1;
|
|
|
|
// 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));
|
|
});
|
|
}
|
|
|
|
// Handle cell merging
|
|
updateCellMerging();
|
|
}
|
|
|
|
export function splitPlayerCell(cell) {
|
|
if (cell.score < MIN_SPLIT_SCORE ||
|
|
gameState.playerCells.length >= MAX_PLAYER_CELLS) {
|
|
return;
|
|
}
|
|
|
|
// Calculate split 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);
|
|
|
|
if (distance === 0) return;
|
|
|
|
const direction = {
|
|
x: dx / distance,
|
|
y: dy / distance
|
|
};
|
|
|
|
const now = Date.now();
|
|
|
|
// Create new cell
|
|
const newCell = {
|
|
x: cell.x,
|
|
y: cell.y,
|
|
score: cell.score / 2,
|
|
velocityX: direction.x * SPLIT_VELOCITY,
|
|
velocityY: direction.y * SPLIT_VELOCITY,
|
|
splitTime: now
|
|
};
|
|
|
|
// Update original cell
|
|
cell.score /= 2;
|
|
cell.velocityX = -direction.x * SPLIT_VELOCITY * 0.5;
|
|
cell.velocityY = -direction.y * SPLIT_VELOCITY * 0.5;
|
|
cell.splitTime = now;
|
|
|
|
// Add new cell
|
|
gameState.playerCells.push(newCell);
|
|
}
|
|
|
|
export function handlePlayerSplit() {
|
|
// Split each cell that's large enough
|
|
const cellsToSplit = gameState.playerCells.filter(cell =>
|
|
cell.score >= MIN_SPLIT_SCORE &&
|
|
gameState.playerCells.length < MAX_PLAYER_CELLS
|
|
);
|
|
|
|
cellsToSplit.forEach(cell => splitPlayerCell(cell));
|
|
}
|
|
|
|
export function updateAI() {
|
|
gameState.aiPlayers.forEach(ai => {
|
|
if (Math.random() < 0.02) {
|
|
ai.direction = Math.random() * Math.PI * 2;
|
|
}
|
|
|
|
const speed = 5 / (getSize(ai.score) / 20);
|
|
ai.x += Math.cos(ai.direction) * speed;
|
|
ai.y += Math.sin(ai.direction) * speed;
|
|
|
|
ai.x = Math.max(0, Math.min(WORLD_SIZE, ai.x));
|
|
ai.y = Math.max(0, Math.min(WORLD_SIZE, ai.y));
|
|
});
|
|
}
|
|
|
|
export function initEntities() {
|
|
// Clear existing entities
|
|
gameState.food = [];
|
|
gameState.aiPlayers = [];
|
|
|
|
console.log('Initializing entities...');
|
|
|
|
// Initialize food
|
|
for (let i = 0; i < FOOD_COUNT; i++) {
|
|
const pos = getRandomPosition();
|
|
gameState.food.push({
|
|
x: pos.x,
|
|
y: pos.y,
|
|
color: `hsl(${Math.random() * 360}, 50%, 50%)`
|
|
});
|
|
}
|
|
|
|
// Initialize AI players
|
|
for (let i = 0; i < AI_COUNT; i++) {
|
|
const pos = getRandomPosition();
|
|
const ai = {
|
|
x: pos.x,
|
|
y: pos.y,
|
|
score: AI_STARTING_SCORE,
|
|
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
|
|
direction: Math.random() * Math.PI * 2,
|
|
name: getUnusedAIName()
|
|
};
|
|
gameState.aiPlayers.push(ai);
|
|
}
|
|
|
|
console.log('Entities initialized:', {
|
|
foodCount: gameState.food.length,
|
|
aiCount: gameState.aiPlayers.length,
|
|
playerCells: gameState.playerCells.length
|
|
});
|
|
}
|
|
|
|
// Export for use in other modules
|
|
export function respawnAI() {
|
|
const pos = getRandomPosition();
|
|
const name = getUnusedAIName();
|
|
|
|
return {
|
|
x: pos.x,
|
|
y: pos.y,
|
|
score: AI_STARTING_SCORE,
|
|
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
|
|
direction: Math.random() * Math.PI * 2,
|
|
name: name
|
|
};
|
|
} |