项目文件夹

文件
Christina Pan af5af0c445 Add score decay over time feature
Implemented score decay mechanics where cells gradually lose score over time.
Larger cells decay faster based on their size, adding a strategic element to gameplay.
Added comprehensive unit tests for the feature.
2025-05-27 14:43:37 -07:00

214 行
6.6 KiB
JavaScript

import { gameState } from './gameState.js';
import { getSize, calculateCenterOfMass } from './utils.js';
import { WORLD_SIZE, COLORS, FOOD_SIZE, MIN_SPLIT_SCORE } from './config.js';
// Helper function to convert hex color to RGB
function hexToRgb(hex) {
// Remove the hash if present
hex = hex.replace(/^#/, '');
// Parse the hex values
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return `${r}, ${g}, ${b}`;
}
let canvas, ctx, minimapCanvas, minimapCtx, scoreElement, leaderboardContent;
export function initRenderer(canvasElements) {
canvas = canvasElements.gameCanvas;
ctx = canvas.getContext('2d');
minimapCanvas = canvasElements.minimapCanvas;
minimapCtx = minimapCanvas.getContext('2d');
scoreElement = canvasElements.scoreElement;
leaderboardContent = canvasElements.leaderboardContent;
// Initial canvas setup
resizeCanvas();
}
export function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function drawCircle(x, y, value, color, isFood) {
const size = isFood ? value : getSize(value);
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
}
function drawCellWithName(x, y, score, color, name) {
const size = getSize(score);
// Draw cell with decay effect
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
// Add pulsing effect when score is decaying
if (score < MIN_SPLIT_SCORE) {
const pulseIntensity = 0.2 * Math.sin(Date.now() / 200); // Pulsing every 200ms
const alpha = Math.max(0.6, 1 - pulseIntensity);
ctx.fillStyle = color.startsWith('rgba') ? color : `rgba(${hexToRgb(color)}, ${alpha})`;
} else {
ctx.fillStyle = color;
}
ctx.fill();
// Draw name
if (size > 20) { // Only draw name if cell is big enough
ctx.save();
// Calculate font size based on cell size
const fontSize = Math.max(12, Math.min(20, size / 2));
ctx.font = `bold ${fontSize}px Arial`;
ctx.fillStyle = 'white';
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Draw text stroke (outline)
ctx.strokeText(name, x, y);
// Draw text fill
ctx.fillText(name, x, y);
ctx.restore();
}
}
export function drawGame() {
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Update camera to follow player's center of mass
const centerOfMass = calculateCenterOfMass(gameState.playerCells);
gameState.camera.x = centerOfMass.x - canvas.width / 2;
gameState.camera.y = centerOfMass.y - canvas.height / 2;
// Draw food
gameState.food.forEach(food => {
const screenX = food.x - gameState.camera.x;
const screenY = food.y - gameState.camera.y;
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);
}
});
// Draw AI players
gameState.aiPlayers.forEach(ai => {
const screenX = ai.x - gameState.camera.x;
const screenY = ai.y - gameState.camera.y;
const size = getSize(ai.score);
if (screenX >= -size && screenX <= canvas.width + size &&
screenY >= -size && screenY <= canvas.height + size) {
drawCellWithName(screenX, screenY, ai.score, ai.color, ai.name);
}
});
// Draw player cells
gameState.playerCells.forEach(cell => {
const screenX = cell.x - gameState.camera.x;
const screenY = cell.y - gameState.camera.y;
const size = getSize(cell.score);
if (screenX >= -size && screenX <= canvas.width + size &&
screenY >= -size && screenY <= canvas.height + size) {
drawCellWithName(screenX, screenY, cell.score, COLORS.PLAYER, gameState.playerName);
}
});
// Update score display with 1 decimal place when below split threshold
const totalScore = gameState.playerCells.reduce((sum, cell) => sum + cell.score, 0);
const formattedScore = totalScore < MIN_SPLIT_SCORE * 2 ?
totalScore.toFixed(1) :
Math.floor(totalScore);
scoreElement.textContent = `Score: ${formattedScore}`;
}
export function drawMinimap() {
if (!minimapCtx) return;
const MINIMAP_SIZE = 150;
const scale = MINIMAP_SIZE / WORLD_SIZE;
minimapCtx.fillStyle = 'rgba(0, 0, 0, 0.7)';
minimapCtx.fillRect(0, 0, MINIMAP_SIZE, MINIMAP_SIZE);
const viewWidth = canvas.width * scale;
const viewHeight = canvas.height * scale;
const viewX = gameState.camera.x * scale;
const viewY = gameState.camera.y * scale;
minimapCtx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
minimapCtx.strokeRect(viewX, viewY, viewWidth, viewHeight);
// Draw AI players on minimap
gameState.aiPlayers.forEach(ai => {
minimapCtx.beginPath();
minimapCtx.arc(
ai.x * scale,
ai.y * scale,
2,
0,
Math.PI * 2
);
minimapCtx.fillStyle = COLORS.MINIMAP.OTHER;
minimapCtx.fill();
});
// Draw player cells on minimap
gameState.playerCells.forEach(cell => {
minimapCtx.beginPath();
minimapCtx.arc(
cell.x * scale,
cell.y * scale,
3,
0,
Math.PI * 2
);
minimapCtx.fillStyle = COLORS.MINIMAP.PLAYER;
minimapCtx.fill();
});
}
export function updateLeaderboard() {
if (!leaderboardContent) return;
const playerTotalScore = gameState.playerCells.reduce((sum, cell) => sum + cell.score, 0);
// Combine player score with AI scores
const allPlayers = [
{
name: gameState.playerName,
score: playerTotalScore,
isPlayer: true
},
...gameState.aiPlayers.map(ai => ({
name: ai.name,
score: ai.score,
isPlayer: false
}))
];
allPlayers.sort((a, b) => b.score - a.score);
leaderboardContent.innerHTML = allPlayers
.slice(0, 5)
.map((player, index) => `
<div class="leaderboard-item">
<span class="${player.isPlayer ? 'player-name' : ''}">${index + 1}. ${player.name}</span>
<span>${Math.floor(player.score)}</span>
</div>
`)
.join('');
}