<!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: #f4f6f8;
--panel-bg: #ffffff;
--text-color: #2c3e50;
--border-color: #e0e6ed;
--accent-color: #3498db;
--grid-bg: #dfe6e9;
--shadow: 0 4px 12px rgba(0,0,0,0.05);
}
[data-theme="dark"] {
--bg-color: #121212;
--panel-bg: #1e1e1e;
--text-color: #e0e0e0;
--border-color: #333333;
--accent-color: #bb86fc;
--grid-bg: #000000;
--shadow: 0 4px 12px rgba(0,0,0,0.5);
}
[data-theme="military"] {
--bg-color: #2b3a42;
--panel-bg: #3d4f5f;
--text-color: #dbeeff;
--border-color: #576f82;
--accent-color: #ff7e00;
--grid-bg: #1a252b;
--shadow: 0 4px 12px rgba(0,0,0,0.3);
}
* { margin: 0; padding: 0; box-sizing: border-box; transition: background 0.3s, color 0.3s, border 0.3s; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--bg-color);
color: var(--text-color);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
}
header {
padding: 15px 0;
font-size: 1.5rem;
font-weight: 700;
letter-spacing: 1px;
display: flex;
align-items: center;
gap: 10px;
}
.main-layout {
display: flex;
gap: 20px;
padding: 20px;
height: calc(100vh - 70px);
width: 100%;
max-width: 1400px;
}
.sidebar {
width: 320px;
display: flex;
flex-direction: column;
gap: 15px;
flex-shrink: 0;
}
.panel {
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
box-shadow: var(--shadow);
}
.canvas-wrapper {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background: var(--panel-bg);
border-radius: 12px;
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
position: relative;
overflow: hidden;
}
canvas {
image-rendering: pixelated; /* 关键:保持像素锐利,防止模糊交融感 */
max-width: 95%;
max-height: 95%;
border: 1px solid var(--border-color);
}
/* 控件样式 */
.control-row { margin-bottom: 15px; }
.control-row label { display: block; font-size: 0.85rem; margin-bottom: 6px; opacity: 0.8; }
button {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
background: var(--accent-color);
color: white;
font-weight: 600;
cursor: pointer;
margin-bottom: 8px;
font-size: 0.95rem;
}
button:hover { filter: brightness(1.1); }
button.secondary { background: transparent; border: 1px solid var(--border-color); color: var(--text-color); }
button.secondary:hover { background: var(--border-color); }
select, input[type="range"] {
width: 100%;
padding: 8px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: var(--bg-color);
color: var(--text-color);
}
/* 状态卡片 */
.faction-card {
padding: 12px;
border-radius: 8px;
margin-bottom: 10px;
border-left: 4px solid transparent;
background: var(--bg-color);
font-size: 0.85rem;
}
.faction-header { display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 4px; }
.faction-stats { display: flex; gap: 10px; opacity: 0.8; font-size: 0.75rem; }
.bar-bg { height: 4px; background: var(--border-color); border-radius: 2px; margin-top: 6px; overflow: hidden; }
.bar-fill { height: 100%; transition: width 0.5s ease; }
.theme-selector { display: flex; gap: 5px; }
.theme-dot {
width: 24px; height: 24px; border-radius: 50%; cursor: pointer;
border: 2px solid transparent; transition: transform 0.2s;
}
.theme-dot:hover { transform: scale(1.2); }
.theme-dot.active { border-color: var(--text-color); }
#statusText {
position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7); color: white; padding: 6px 16px;
border-radius: 20px; font-size: 0.8rem; pointer-events: none;
opacity: 0; transition: opacity 0.3s;
}
#statusText.visible { opacity: 1; }
</style>
</head>
<body data-theme="dark">
<header>⚔️ 粒子领土战争模拟器</header>
<div class="main-layout">
<div class="sidebar">
<div class="panel">
<div class="control-row">
<label>界面主题</label>
<div class="theme-selector">
<div class="theme-dot" style="background:#f4f6f8" onclick="setTheme('light')" title="明亮"></div>
<div class="theme-dot active" style="background:#121212" onclick="setTheme('dark')" title="暗黑"></div>
<div class="theme-dot" style="background:#2b3a42" onclick="setTheme('military')" title="军事"></div>
</div>
</div>
<div class="control-row">
<label>模拟速度 (越低越慢)</label>
<input type="range" id="speedRange" min="1" max="30" value="5">
</div>
<div class="control-row">
<label>地图分辨率</label>
<select id="gridSizeSelect">
<option value="60">60x60 (快速)</option>
<option value="100" selected>100x100 (标准)</option>
<option value="150">150x150 (精细)</option>
</select>
</div>
<button id="btnStart" onclick="toggleSimulation()">▶ 开始战争</button>
<button class="secondary" onclick="resetSimulation()">↺ 重新生成局势</button>
</div>
<div class="panel" style="flex:1; overflow-y:auto;">
<label style="display:block; margin-bottom:10px; font-weight:bold;">实时战报</label>
<div id="factionsContainer"></div>
</div>
</div>
<div class="canvas-wrapper">
<div id="statusText">等待指令...</div>
<canvas id="battleCanvas"></canvas>
</div>
</div>
<script>
/**
* 核心配置与状态
*/
const CONFIG = {
colors: ['#E74C3C', '#3498DB', '#2ECC71', '#F1C40F'], // 红, 蓝, 绿, 黄
names: ['赤焰帝国', '苍蓝联邦', '翠森同盟', '金辉神权'],
baseStats: { atk: 50, def: 50 },
variance: 0.6, // 随机波动幅度,越大越悬殊
initRadius: 4, // 初始领土半径
};
let state = {
gridSize: 100,
grid: [], // 存储每个格子的所属阵营ID (-1为空)
factions: [], // 存储阵营详细数据
running: false,
lastTime: 0,
tickAccumulator: 0,
borders: new Set(), // 存储所有边界格子的索引,用于高效扩张
winner: null
};
const canvas = document.getElementById('battleCanvas');
const ctx = canvas.getContext('2d', { alpha: false }); // 优化性能
let cellPixelSize = 1;
/**
* 初始化与重置
*/
function init() {
resizeCanvas();
resetSimulation();
window.addEventListener('resize', () => { resizeCanvas(); draw(); });
requestAnimationFrame(loop);
}
function resizeCanvas() {
const wrapper = document.querySelector('.canvas-wrapper');
const size = Math.min(wrapper.clientWidth, wrapper.clientHeight) - 40;
canvas.width = size;
canvas.height = size;
cellPixelSize = size / state.gridSize;
ctx.imageSmoothingEnabled = false; // 关键:关闭平滑,保证实心像素感
}
function resetSimulation() {
state.running = false;
state.winner = null;
state.gridSize = parseInt(document.getElementById('gridSizeSelect').value);
state.tickAccumulator = 0;
// 1. 生成悬殊的随机属性
state.factions = CONFIG.colors.map((color, i) => {
// 使用幂函数拉大差距,让强者更强,弱者更弱
const atkMod = Math.pow(Math.random(), 1.5) * CONFIG.variance + (1 - CONFIG.variance/2);
const defMod = Math.pow(Math.random(), 1.5) * CONFIG.variance + (1 - CONFIG.variance/2);
return {
id: i,
name: CONFIG.names[i],
color: color,
atk: Math.floor(CONFIG.baseStats.atk * atkMod * (0.8 + Math.random()*0.4)),
def: Math.floor(CONFIG.baseStats.def * defMod * (0.8 + Math.random()*0.4)),
cells: 0,
alive: true
};
});
// 2. 初始化网格与出生点
state.grid = new Int8Array(state.gridSize * state.gridSize).fill(-1);
state.borders.clear();
const corners = [
{x: 0, y: 0},
{x: state.gridSize-1, y: 0},
{x: 0, y: state.gridSize-1},
{x: state.gridSize-1, y: state.gridSize-1}
];
corners.forEach((pos, fid) => {
for(let dy = -CONFIG.initRadius; dy <= CONFIG.initRadius; dy++) {
for(let dx = -CONFIG.initRadius; dx <= CONFIG.initRadius; dx++) {
if(dx*dx + dy*dy <= CONFIG.initRadius*CONFIG.initRadius) {
setCell(pos.x + dx, pos.y + dy, fid);
}
}
}
});
updateUI();
draw();
showStatus("局势已生成,等待开战");
document.getElementById('btnStart').textContent = "▶ 开始战争";
}
/**
* 核心模拟循环
*/
function loop(timestamp) {
if (!state.lastTime) state.lastTime = timestamp;
const delta = timestamp - state.lastTime;
state.lastTime = timestamp;
if (state.running && !state.winner) {
// 速度控制:数值越小,每次tick需要的累积时间越长
const speedVal = parseInt(document.getElementById('speedRange').value);
const tickInterval = 200 / speedVal;
state.tickAccumulator += delta;
while (state.tickAccumulator >= tickInterval) {
simulateTick();
state.tickAccumulator -= tickInterval;
}
}
draw();
requestAnimationFrame(loop);
}
/**
* 单步模拟逻辑(仅处理边界)
*/
function simulateTick() {
if (state.borders.size === 0) return;
// 将Set转为数组以便遍历(避免在迭代中修改Set导致问题)
const currentBorders = Array.from(state.borders);
const dirs = [[0,1], [0,-1], [1,0], [-1,0]];
const size = state.gridSize;
// 每tick只处理一部分边界,模拟"缓慢推进"的感觉,同时减少瞬间计算量
const processCount = Math.max(10, Math.floor(currentBorders.length * 0.1));
for (let i = 0; i < processCount; i++) {
const idx = currentBorders[Math.floor(Math.random() * currentBorders.length)];
const x = idx % size;
const y = Math.floor(idx / size);
const ownerId = state.grid[idx];
if (ownerId === -1) continue; // 已被占领,跳过
// 随机选一个方向尝试扩张
const dir = dirs[Math.floor(Math.random() * 4)];
const nx = x + dir[0];
const ny = y + dir[1];
if (nx >= 0 && nx < size && ny >= 0 && ny < size) {
const nIdx = ny * size + nx;
const targetId = state.grid[nIdx];
if (targetId === -1) {
// 空地:直接占领
setCell(nx, ny, ownerId);
} else if (targetId !== ownerId) {
// 敌对:战斗判定
const attacker = state.factions[ownerId];
const defender = state.factions[targetId];
// 战斗公式:攻击力 vs 防御力 + 随机扰动
const dmg = attacker.atk * (0.8 + Math.random() * 0.4);
const resist = defender.def * (0.8 + Math.random() * 0.4);
if (dmg > resist) {
setCell(nx, ny, ownerId);
}
}
}
}
checkWinCondition();
if (state.running) updateUI(); // 节流更新UI
}
/**
* 辅助:设置格子并维护边界集合
*/
function setCell(x, y, factionId) {
const size = state.gridSize;
if (x < 0 || x >= size || y < 0 || y >= size) return;
const idx = y * size + x;
const oldId = state.grid[idx];
if (oldId === factionId) return;
state.grid[idx] = factionId;
// 更新计数
if (oldId !== -1) state.factions[oldId].cells--;
state.factions[factionId].cells++;
// 维护边界集合:新占领的格子一定是新的潜在边界
state.borders.add(idx);
// 检查该格子的邻居,如果邻居也是同色且被包围,则从边界移除(优化性能)
// 简化版:我们只在tick开始时清理无效边界,或者容忍少量冗余边界
// 为了极致性能,这里做一个简单检查:如果该格子四周都是己方,则它不再是边界
const dirs = [[0,1], [0,-1], [1,0], [-1,0]];
let isBorder = false;
for (const d of dirs) {
const nx = x + d[0], ny = y + d[1];
if (nx < 0 || nx >= size || ny < 0 || ny >= size) { isBorder = true; break; }
if (state.grid[ny * size + nx] !== factionId) { isBorder = true; break; }
}
if (!isBorder) state.borders.delete(idx);
}
function checkWinCondition() {
const alive = state.factions.filter(f => f.cells > 0);
if (alive.length === 1) {
state.winner = alive[0];
state.running = false;
showStatus(`🏆 ${alive[0].name} 统一了大陆!`);
document.getElementById('btnStart').textContent = "▶ 重新开始";
updateUI();
}
}
/**
* 渲染与UI
*/
function draw() {
// 清空背景
const style = getComputedStyle(document.body);
ctx.fillStyle = style.getPropertyValue('--grid-bg');
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 批量绘制同色格子以减少fillStyle切换开销
const size = state.gridSize;
const cs = cellPixelSize;
// 由于格子数量可能上万,逐格drawImage太慢。
// 使用ImageData或直接fillRect。对于100x100,fillRect足够快。
for (let i = 0; i < state.grid.length; i++) {
const fid = state.grid[i];
if (fid !== -1) {
ctx.fillStyle = state.factions[fid].color;
const x = (i % size) * cs;
const y = Math.floor(i / size) * cs;
// +0.5 防止亚像素缝隙
ctx.fillRect(Math.floor(x), Math.floor(y), Math.ceil(cs)+0.5, Math.ceil(cs)+0.5);
}
}
}
function updateUI() {
const container = document.getElementById('factionsContainer');
const total = state.gridSize * state.gridSize;
// 按领土排序
const sorted = [...state.factions].sort((a,b) => b.cells - a.cells);
container.innerHTML = sorted.map(f => {
const pct = ((f.cells / total) * 100).toFixed(1);
const opacity = f.cells > 0 ? 1 : 0.4;
return `
<div class="faction-card" style="border-left-color:${f.color}; opacity:${opacity}">
<div class="faction-header">
<span>${f.name}</span>
<span>${pct}%</span>
</div>
<div class="faction-stats">
<span>⚔️ ${f.atk}</span>
<span>🛡️ ${f.def}</span>
<span>🟩 ${f.cells}</span>
</div>
<div class="bar-bg"><div class="bar-fill" style="width:${pct}%; background:${f.color}"></div></div>
</div>`;
}).join('');
}
function toggleSimulation() {
if (state.winner) {
resetSimulation();
return;
}
state.running = !state.running;
const btn = document.getElementById('btnStart');
btn.textContent = state.running ? "⏸ 暂停战争" : "▶ 继续战争";
showStatus(state.running ? "战争进行中..." : "战争已暂停");
}
function setTheme(name) {
document.body.setAttribute('data-theme', name);
document.querySelectorAll('.theme-dot').forEach(d => d.classList.remove('active'));
event.target.classList.add('active');
draw(); // 主题切换后重绘以更新背景色
}
function showStatus(msg) {
const el = document.getElementById('statusText');
el.textContent = msg;
el.classList.add('visible');
clearTimeout(el._timer);
el._timer = setTimeout(() => el.classList.remove('visible'), 2000);
}
// 启动
init();
</script>
</body>
</html>
index.html
index.html