未命名 dVsDDWedit icon

创建者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
index.html
            
            <!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>
        
编辑器加载中
预览
控制台