Files
2026-07-13 12:35:44 +08:00

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