<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>粒子大战模拟器</title>
<style>
:root {
--bg-color: #f5f5f5;
--panel-bg: #ffffff;
--text-color: #333333;
--border-color: #dddddd;
--btn-bg: #4a90e2;
--btn-hover: #357abd;
--grid-bg: #e8e8e8;
--shadow: 0 2px 8px rgba(0,0,0,0.1);
}
[data-theme="dark"] {
--bg-color: #1a1a2e;
--panel-bg: #16213e;
--text-color: #e0e0e0;
--border-color: #3a3a5c;
--btn-bg: #0f3460;
--btn-hover: #1a5276;
--grid-bg: #0a0a1a;
--shadow: 0 2px 8px rgba(0,0,0,0.4);
}
[data-theme="green"] {
--bg-color: #f0f7f0;
--panel-bg: #e8f5e9;
--text-color: #2e7d32;
--border-color: #a5d6a7;
--btn-bg: #4caf50;
--btn-hover: #388e3c;
--grid-bg: #c8e6c9;
--shadow: 0 2px 8px rgba(76,175,80,0.2);
}
[data-theme="sepia"] {
--bg-color: #f4ecd8;
--panel-bg: #f9f1e7;
--text-color: #5c4033;
--border-color: #c4a882;
--btn-bg: #8b6914;
--btn-hover: #6d5210;
--grid-bg: #e8dcc8;
--shadow: 0 2px 8px rgba(139,105,20,0.2);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg-color);
color: var(--text-color);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
transition: all 0.3s ease;
}
h1 {
font-size: 1.8em;
margin-bottom: 20px;
font-weight: 600;
}
.container {
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
max-width: 1200px;
}
.panel {
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
transition: all 0.3s ease;
}
.control-panel {
min-width: 280px;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
margin-bottom: 5px;
font-size: 0.9em;
font-weight: 500;
}
button {
padding: 10px 20px;
border: none;
border-radius: 5px;
background: var(--btn-bg);
color: white;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
margin: 3px;
}
button:hover {
background: var(--btn-hover);
transform: translateY(-1px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
select, input {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-color);
color: var(--text-color);
font-size: 14px;
width: 100%;
transition: all 0.3s ease;
}
.grid-container {
display: flex;
flex-direction: column;
align-items: center;
}
#battleGrid {
border: 2px solid var(--border-color);
border-radius: 4px;
image-rendering: pixelated;
box-shadow: var(--shadow);
}
.stats-panel {
min-width: 280px;
}
.particle-info {
margin-bottom: 12px;
padding: 10px;
border-radius: 5px;
border: 1px solid var(--border-color);
font-size: 0.85em;
}
.particle-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
font-weight: 600;
}
.color-indicator {
width: 16px;
height: 16px;
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.2);
}
.stat-row {
display: flex;
justify-content: space-between;
margin: 3px 0;
}
.territory-bar {
width: 100%;
height: 6px;
background: var(--border-color);
border-radius: 3px;
margin-top: 5px;
overflow: hidden;
}
.territory-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.speed-control {
display: flex;
align-items: center;
gap: 10px;
}
.speed-control input[type="range"] {
flex: 1;
}
.theme-buttons {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.theme-btn {
padding: 6px 12px;
font-size: 12px;
}
.status {
text-align: center;
margin-top: 10px;
font-size: 0.9em;
padding: 8px;
background: var(--bg-color);
border-radius: 4px;
border: 1px solid var(--border-color);
}
</style>
</head>
<body>
<h1>⚔️ 粒子大战模拟器</h1>
<div class="container">
<div class="panel control-panel">
<div class="control-group">
<label>主题设置</label>
<div class="theme-buttons">
<button class="theme-btn" onclick="setTheme('default')">默认</button>
<button class="theme-btn" onclick="setTheme('dark')">暗色</button>
<button class="theme-btn" onclick="setTheme('green')">护眼</button>
<button class="theme-btn" onclick="setTheme('sepia')">复古</button>
</div>
</div>
<div class="control-group">
<label>网格大小</label>
<select id="gridSize">
<option value="50">50 × 50</option>
<option value="80" selected>80 × 80</option>
<option value="100">100 × 100</option>
<option value="150">150 × 150</option>
</select>
</div>
<div class="control-group">
<label>扩张速度</label>
<div class="speed-control">
<input type="range" id="speed" min="1" max="100" value="50">
<span id="speedValue">50</span>
</div>
</div>
<div class="control-group">
<label>初始领土大小</label>
<input type="number" id="initSize" value="5" min="2" max="20">
</div>
<div class="control-group">
<button id="startBtn" onclick="startBattle()">🚀 开始大战</button>
<button id="pauseBtn" onclick="pauseBattle()" disabled>⏸ 暂停</button>
<button id="resetBtn" onclick="resetBattle()">🔄 重置</button>
</div>
<div class="status" id="status">准备就绪,点击"开始大战"开始模拟</div>
</div>
<div class="grid-container">
<div class="panel" style="padding: 10px;">
<canvas id="battleGrid"></canvas>
</div>
</div>
<div class="panel stats-panel">
<div class="control-group">
<label>粒子状态</label>
</div>
<div id="particleStats"></div>
</div>
</div>
<script>
// 粒子类型定义
const PARTICLE_TYPES = [
{
name: "烈焰军团",
color: "#e74c3c",
attack: 8,
defense: 5,
expansionRate: 1.2
},
{
name: "寒冰部落",
color: "#3498db",
attack: 6,
defense: 8,
expansionRate: 1.0
},
{
name: "自然之灵",
color: "#27ae60",
attack: 7,
defense: 7,
expansionRate: 1.1
},
{
name: "暗影教团",
color: "#8e44ad",
attack: 9,
defense: 4,
expansionRate: 1.3
}
];
let grid = [];
let gridSize = 80;
let canvas, ctx;
let cellSize;
let isRunning = false;
let animationId = null;
let battleSpeed = 50;
let totalCells = 0;
let tickCount = 0;
// 初始化
function init() {
canvas = document.getElementById('battleGrid');
ctx = canvas.getContext('2d');
updateGridSize();
updateStats();
document.getElementById('gridSize').addEventListener('change', updateGridSize);
document.getElementById('speed').addEventListener('input', function() {
battleSpeed = parseInt(this.value);
document.getElementById('speedValue').textContent = battleSpeed;
});
}
function updateGridSize() {
gridSize = parseInt(document.getElementById('gridSize').value);
// 计算画布大小(限制最大显示尺寸为600px)
const maxSize = 600;
cellSize = Math.floor(maxSize / gridSize);
canvas.width = gridSize * cellSize;
canvas.height = gridSize * cellSize;
if (!isRunning) {
resetBattle();
}
}
function setTheme(theme) {
document.body.setAttribute('data-theme', theme);
}
function resetBattle() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
isRunning = false;
tickCount = 0;
// 初始化网格
grid = Array(gridSize).fill(null).map(() => Array(gridSize).fill(-1));
// 设置初始领土
const initSize = parseInt(document.getElementById('initSize').value);
const corners = [
{x: 0, y: 0},
{x: gridSize - initSize, y: 0},
{x: 0, y: gridSize - initSize},
{x: gridSize - initSize, y: gridSize - initSize}
];
PARTICLE_TYPES.forEach((particle, index) => {
const corner = corners[index];
for (let i = 0; i < initSize; i++) {
for (let j = 0; j < initSize; j++) {
grid[corner.x + i][corner.y + j] = index;
}
}
});
renderGrid();
updateStats();
document.getElementById('startBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
document.getElementById('status').textContent = '准备就绪,点击"开始大战"开始模拟';
}
function startBattle() {
if (!isRunning) {
isRunning = true;
document.getElementById('startBtn').disabled = true;
document.getElementById('pauseBtn').disabled = false;
document.getElementById('status').textContent = '战斗进行中...';
gameLoop();
}
}
function pauseBattle() {
isRunning = false;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
document.getElementById('startBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
document.getElementById('status').textContent = '战斗已暂停';
}
function gameLoop() {
if (!isRunning) return;
const speedFactor = battleSpeed / 50;
const ticksPerFrame = Math.ceil(speedFactor * 2);
for (let i = 0; i < ticksPerFrame; i++) {
tick();
tickCount++;
}
renderGrid();
updateStats();
// 检查是否结束
if (checkBattleEnd()) {
isRunning = false;
document.getElementById('startBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
return;
}
animationId = requestAnimationFrame(gameLoop);
}
function tick() {
const newGrid = grid.map(row => [...row]);
const directions = [[0,1],[0,-1],[1,0],[-1,0]];
// 收集所有边界细胞
const borderCells = [];
for (let x = 0; x < gridSize; x++) {
for (let y = 0; y < gridSize; y++) {
if (grid[x][y] !== -1) {
// 检查相邻的空细胞或敌方细胞
for (const [dx, dy] of directions) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
const neighbor = grid[nx][ny];
if (neighbor === -1 || (neighbor !== grid[x][y] && neighbor !== -1)) {
borderCells.push({
owner: grid[x][y],
fromX: x,
fromY: y,
toX: nx,
toY: ny,
target: neighbor
});
}
}
}
}
}
}
// 随机打乱边界细胞处理顺序
shuffleArray(borderCells);
// 处理扩张和战斗
for (const cell of borderCells) {
const currentOwner = newGrid[cell.toX][cell.toY];
if (currentOwner === -1) {
// 空地扩张
const particle = PARTICLE_TYPES[cell.owner];
if (Math.random() < particle.expansionRate * 0.3) {
newGrid[cell.toX][cell.toY] = cell.owner;
}
} else if (currentOwner !== cell.owner) {
// 战斗判定
const attacker = PARTICLE_TYPES[cell.owner];
const defender = PARTICLE_TYPES[currentOwner];
const attackPower = attacker.attack * (0.7 + Math.random() * 0.6);
const defensePower = defender.defense * (0.7 + Math.random() * 0.6);
if (attackPower > defensePower) {
newGrid[cell.toX][cell.toY] = cell.owner;
}
}
}
grid = newGrid;
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
function renderGrid() {
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--grid-bg');
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let x = 0; x < gridSize; x++) {
for (let y = 0; y < gridSize; y++) {
if (grid[x][y] !== -1) {
ctx.fillStyle = PARTICLE_TYPES[grid[x][y]].color;
ctx.fillRect(y * cellSize, x * cellSize, cellSize, cellSize);
}
}
}
}
function updateStats() {
const counts = Array(PARTICLE_TYPES.length).fill(0);
for (let x = 0; x < gridSize; x++) {
for (let y = 0; y < gridSize; y++) {
if (grid[x][y] !== -1) {
counts[grid[x][y]]++;
}
}
}
totalCells = gridSize * gridSize;
const statsContainer = document.getElementById('particleStats');
statsContainer.innerHTML = PARTICLE_TYPES.map((particle, index) => {
const percentage = ((counts[index] / totalCells) * 100).toFixed(1);
const isAlive = counts[index] > 0;
return `
<div class="particle-info" style="opacity: ${isAlive ? 1 : 0.4}">
<div class="particle-header">
<div class="color-indicator" style="background: ${particle.color}"></div>
<span>${particle.name}</span>
</div>
<div class="stat-row">
<span>⚔️ 战力: ${particle.attack}</span>
<span>🛡️ 防御: ${particle.defense}</span>
</div>
<div class="stat-row">
<span>领土: ${counts[index]}</span>
<span>占比: ${percentage}%</span>
</div>
<div class="territory-bar">
<div class="territory-fill" style="width: ${percentage}%; background: ${particle.color}"></div>
</div>
</div>
`;
}).join('');
}
function checkBattleEnd() {
const activeTypes = new Set();
for (let x = 0; x < gridSize; x++) {
for (let y = 0; y < gridSize; y++) {
if (grid[x][y] !== -1) {
activeTypes.add(grid[x][y]);
}
}
}
if (activeTypes.size === 0) return true;
// 检查是否全部占领且只剩一个势力
if (activeTypes.size === 1) {
const winner = Array.from(activeTypes)[0];
document.getElementById('status').textContent =
`🏆 战斗结束!${PARTICLE_TYPES[winner].name}获得胜利!`;
return true;
}
return false;
}
// 页面加载时初始化
window.addEventListener('load', init);
</script>
</body>
</html>
index.html
index.html