Files
MRCC/prototype/redcircuit.html
T
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

692 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RedCircuit · 红石回路 - 原型 v0.1</title>
<style>
:root {
--bg: #0E1116; --card: #161B22; --border: #2A3441; --primary: #E83229;
--fg: #E8EAED; --muted: #6B7280; --surface: #1C2230;
--success: #3FB950; --warning: #D29922; --error: #F85149; --info: #58A6FF;
--grid-dot: rgba(139,148,158,0.10);
}
* { margin:0; padding:0; box-sizing:border-box; }
body { background:var(--bg); color:var(--fg); font-family:'Segoe UI',system-ui,sans-serif; overflow:hidden; height:100vh; display:flex; flex-direction:column; }
/* Topbar */
.topbar { height:48px; background:var(--surface); border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 16px; gap:12px; flex-shrink:0; }
.topbar-logo { font-weight:900; color:var(--primary); font-size:16px; letter-spacing:1px; }
.topbar-ver { font-size:11px; color:var(--muted); }
.topbar-spacer { flex:1; }
.btn { background:var(--card); border:1px solid var(--border); color:var(--fg); padding:6px 14px; border-radius:4px; cursor:pointer; font-size:12px; transition:all .15s; }
.btn:hover { border-color:var(--primary); color:var(--primary); }
.btn.active { background:var(--primary); border-color:var(--primary); color:#fff; }
.btn-group { display:flex; gap:4px; }
/* Main layout */
.main { display:flex; flex:1; overflow:hidden; }
/* Sidebar */
.sidebar { width:180px; background:var(--card); border-right:1px solid var(--border); padding:12px; overflow-y:auto; flex-shrink:0; }
.sidebar-title { font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:1px; margin-bottom:8px; }
.comp-item { display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:6px; cursor:pointer; margin-bottom:2px; border:1px solid transparent; transition:all .12s; }
.comp-item:hover { background:var(--surface); }
.comp-item.selected { background:var(--surface); border-color:var(--primary); }
.comp-icon { width:28px; height:28px; border-radius:4px; display:flex; align-items:center; justify-content:center; font-size:14px; flex-shrink:0; }
.comp-name { font-size:12px; }
.comp-cost { font-size:10px; color:var(--muted); margin-left:auto; }
.comp-sep { height:1px; background:var(--border); margin:8px 0; }
/* Canvas area */
.canvas-area { flex:1; position:relative; overflow:hidden; background:var(--bg); }
canvas { display:block; cursor:crosshair; }
/* Status bar */
.statusbar { height:28px; background:var(--surface); border-top:1px solid var(--border); display:flex; align-items:center; padding:0 12px; gap:16px; font-size:11px; color:var(--muted); flex-shrink:0; }
.status-item { display:flex; align-items:center; gap:4px; }
.status-val { color:var(--fg); font-weight:600; }
/* Tool mode */
.tool-mode { display:flex; gap:2px; background:var(--card); border:1px solid var(--border); border-radius:4px; padding:2px; }
.tool-mode button { background:transparent; border:none; color:var(--muted); padding:4px 10px; border-radius:3px; cursor:pointer; font-size:11px; }
.tool-mode button.active { background:var(--primary); color:#fff; }
/* Hover tooltip */
.tooltip { position:absolute; background:var(--card); border:1px solid var(--border); border-radius:4px; padding:6px 10px; font-size:11px; pointer-events:none; z-index:100; display:none; white-space:nowrap; }
.tooltip strong { color:var(--primary); }
</style>
</head>
<body>
<div class="topbar">
<span class="topbar-logo">RedCircuit</span>
<span class="topbar-ver">原型 v0.1</span>
<div class="btn-group">
<button class="btn" id="btnRun">▶ 运行</button>
<button class="btn" id="btnStep">⏭ 单步</button>
<button class="btn" id="btnClear">🗑 清空</button>
</div>
<div class="btn-group">
<button class="btn" id="btnSave">💾 保存</button>
<button class="btn" id="btnLoad">📂 加载</button>
</div>
<div class="btn-group">
<button class="btn" id="btnDemo">✨ 示例电路</button>
</div>
<div class="topbar-spacer"></div>
<div class="tool-mode">
<button class="active" id="modePlace">放置</button>
<button id="modeDelete">删除</button>
</div>
</div>
<div class="main">
<div class="sidebar" id="sidebar">
<div class="sidebar-title">电源 POWER</div>
<div class="comp-item" data-type="lever"><div class="comp-icon" style="background:#8B5E3C">🔘</div><span class="comp-name">拉杆</span><span class="comp-cost">30</span></div>
<div class="comp-item" data-type="block"><div class="comp-icon" style="background:#B91C1C"></div><span class="comp-name">红石块</span><span class="comp-cost">100</span></div>
<div class="comp-item" data-type="torch"><div class="comp-icon" style="background:#F87171">🔥</div><span class="comp-name">红石火把</span><span class="comp-cost">50</span></div>
<div class="comp-item" data-type="button"><div class="comp-icon" style="background:#4A6B8A">🔲</div><span class="comp-name">按钮</span><span class="comp-cost">40</span></div>
<div class="comp-sep"></div>
<div class="sidebar-title">传输 WIRE</div>
<div class="comp-item selected" data-type="dust"><div class="comp-icon" style="background:#7F1D1D">粉尘</div><span class="comp-name">红石粉</span><span class="comp-cost">5</span></div>
<div class="comp-item" data-type="insulated"><div class="comp-icon" style="background:#1E3A5F">线段</div><span class="comp-name">绝缘导线</span><span class="comp-cost">15</span></div>
<div class="comp-item" data-type="repeater"><div class="comp-icon" style="background:#92400E">▶▶</div><span class="comp-name">中继器</span><span class="comp-cost">80</span></div>
<div class="comp-sep"></div>
<div class="sidebar-title">输出 OUTPUT</div>
<div class="comp-item" data-type="lamp"><div class="comp-icon" style="background:#374151">💡</div><span class="comp-name">红石灯</span><span class="comp-cost">60</span></div>
</div>
<div class="canvas-area">
<canvas id="canvas"></canvas>
<div class="tooltip" id="tooltip"></div>
</div>
</div>
<div class="statusbar">
<div class="status-item">坐标: <span class="status-val" id="stCoord">0, 0</span></div>
<div class="status-item">元件: <span class="status-val" id="stCount">0</span></div>
<div class="status-item">成本: <span class="status-val" id="stCost">0</span></div>
<div class="status-item">信号: <span class="status-val" id="stSignal">-</span></div>
<div class="status-item">状态: <span class="status-val" id="stState">已暂停</span></div>
<div style="flex:1"></div>
<div class="status-item">左键放置 · 右键删除 · 滚轮旋转 · 空格运行/暂停</div>
</div>
<script>
// ==================== 配置 ====================
const GRID_W = 48, GRID_H = 32, CELL = 20;
const MAX_SIGNAL = 15;
// 元件定义
const COMPONENTS = {
lever: { name:'拉杆', cost:30, power:true, signal:15, interactive:true },
block: { name:'红石块', cost:100, power:true, signal:15, interactive:false },
torch: { name:'红石火把', cost:50, power:true, signal:15, interactive:false, inverter:true },
button: { name:'按钮', cost:40, power:true, signal:15, interactive:true, momentary:true },
dust: { name:'红石粉', cost:5, wire:true, cross:true },
insulated:{ name:'绝缘导线', cost:15, wire:true, cross:false },
repeater: { name:'中继器', cost:80, wire:true, repeater:true },
lamp: { name:'红石灯', cost:60, output:true },
};
// ==================== 游戏状态 ====================
let grid = []; // grid[y][x] = { type, rotation, active, signal, powered }
let selectedType = 'dust';
let toolMode = 'place'; // place / delete
let isRunning = false;
let mouseGX = -1, mouseGY = -1;
let canvas, ctx;
function initGrid() {
grid = [];
for (let y = 0; y < GRID_H; y++) {
const row = [];
for (let x = 0; x < GRID_W; x++) row.push(null);
grid.push(row);
}
}
// ==================== 仿真引擎 ====================
function simulate() {
// 1. 重置信号
for (let y = 0; y < GRID_H; y++)
for (let x = 0; x < GRID_W; x++)
if (grid[y][x]) { grid[y][x].signal = 0; grid[y][x].powered = false; }
// 2. 收集电源 (排除火把,火把需要先检查反相)
const sources = [];
for (let y = 0; y < GRID_H; y++) {
for (let x = 0; x < GRID_W; x++) {
const c = grid[y][x];
if (!c) continue;
const def = COMPONENTS[c.type];
if (def.power && !def.inverter) {
const isActive = c.type === 'button' ? c.active : (c.type === 'lever' ? c.active : true);
if (isActive) sources.push({ x, y, strength: def.signal });
}
}
}
// 3. 检查火把反相: 火把附着方块有信号时熄灭
for (let y = 0; y < GRID_H; y++) {
for (let x = 0; x < GRID_W; x++) {
const c = grid[y][x];
if (!c || c.type !== 'torch') continue;
// 火把检查下方方块是否被信号驱动 (简化: 检查相邻4格是否有信号)
let attachedPowered = false;
for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) {
const nx = x+dx, ny = y+dy;
if (nx<0||nx>=GRID_W||ny<0||ny>=GRID_H) continue;
const n = grid[ny][nx];
if (n && n.signal > 0 && n.type !== 'torch') { attachedPowered = true; break; }
}
if (!attachedPowered) sources.push({ x, y, strength: MAX_SIGNAL });
}
}
// 4. BFS 传播 (优先级队列, 按强度降序)
const visited = Array.from({length:GRID_H}, () => new Array(GRID_W).fill(0));
const queue = sources.slice().sort((a,b) => b.strength - a.strength);
while (queue.length > 0) {
const { x, y, strength } = queue.shift();
if (strength <= 0) continue;
if (visited[y][x] >= strength) continue;
visited[y][x] = strength;
const c = grid[y][x];
if (c) {
c.signal = Math.max(c.signal, strength);
const def = COMPONENTS[c.type];
if (def.output) c.powered = strength >= 1;
// 中继器: 输出满强度到前方
if (def.repeater) {
const [dx, dy] = getDir(c.rotation);
const nx = x+dx, ny = y+dy;
if (nx>=0 && nx<GRID_W && ny>=0 && ny<GRID_H) {
queue.push({ x:nx, y:ny, strength: MAX_SIGNAL });
}
continue; // 中继器不向其他方向传播
}
// 火把不传播粉尘信号
if (def.inverter) {
for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) {
const nx = x+dx, ny = y+dy;
if (nx<0||nx>=GRID_W||ny<0||ny>=GRID_H) continue;
const n = grid[ny][nx];
if (n && COMPONENTS[n.type].wire) {
queue.push({ x:nx, y:ny, strength: MAX_SIGNAL - 1 });
}
}
continue;
}
}
// 向邻居传播 (导线衰减1, 其他不传播)
for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) {
const nx = x+dx, ny = y+dy;
if (nx<0||nx>=GRID_W||ny<0||ny>=GRID_H) continue;
const n = grid[ny][nx];
if (!n) continue;
const ndef = COMPONENTS[n.type];
// 绝缘导线不与交叉导线串扰 (简化: 绝缘导线只传播到同类型或输出)
if (n.type === 'insulated') {
// 绝缘导线传播到所有相邻导线和输出
if (ndef.wire || ndef.output) {
queue.push({ x:nx, y:ny, strength: strength - 1 });
}
} else if (ndef.wire || ndef.output) {
// 红石粉交叉: 检查是否允许传播
if (n.type === 'dust' || n.type === 'repeater') {
queue.push({ x:nx, y:ny, strength: strength - 1 });
} else {
queue.push({ x:nx, y:ny, strength: strength - 1 });
}
}
}
}
}
function getDir(rot) {
// 0=右, 1=下, 2=左, 3=上
return [[1,0],[0,1],[-1,0],[0,-1]][rot % 4];
}
// ==================== 渲染 ====================
function render() {
const w = canvas.width, h = canvas.height;
ctx.fillStyle = '#0E1116';
ctx.fillRect(0, 0, w, h);
// 网格点
ctx.fillStyle = 'rgba(139,148,158,0.10)';
for (let y = 0; y <= GRID_H; y++)
for (let x = 0; x <= GRID_W; x++)
ctx.fillRect(x*CELL-0.5, y*CELL-0.5, 1, 1);
// 元件
for (let y = 0; y < GRID_H; y++) {
for (let x = 0; x < GRID_W; x++) {
const c = grid[y][x];
if (!c) continue;
drawComponent(x, y, c);
}
}
// 鼠标预览
if (mouseGX >= 0 && mouseGX < GRID_W && mouseGY >= 0 && mouseGY < GRID_H) {
ctx.strokeStyle = toolMode === 'delete' ? 'rgba(248,81,73,0.6)' : 'rgba(232,50,41,0.6)';
ctx.lineWidth = 2;
ctx.strokeRect(mouseGX*CELL+1, mouseGY*CELL+1, CELL-2, CELL-2);
if (toolMode === 'place' && !grid[mouseGY][mouseGX]) {
ctx.globalAlpha = 0.35;
drawComponent(mouseGX, mouseGY, { type: selectedType, rotation: 0, signal: 0, active: false, powered: false });
ctx.globalAlpha = 1;
}
}
}
function drawComponent(x, y, c) {
const px = x * CELL, py = y * CELL;
const def = COMPONENTS[c.type];
const sig = c.signal || 0;
// 信号强度 -> 颜色亮度
const glow = sig / MAX_SIGNAL;
const r = Math.round(232 * (0.3 + 0.7 * glow));
const g = Math.round(50 * (0.3 + 0.7 * glow));
const b = Math.round(41 * (0.3 + 0.7 * glow));
ctx.save();
switch (c.type) {
case 'lever':
ctx.fillStyle = '#8B5E3C';
roundRect(px+2, py+2, CELL-4, CELL-4, 3); ctx.fill();
ctx.fillStyle = c.active ? `rgb(${r},${g},${b})` : '#555';
ctx.beginPath();
ctx.arc(px+CELL/2, py+CELL/2, 4, 0, Math.PI*2); ctx.fill();
if (c.active) { ctx.shadowColor = `rgb(${r},${g},${b})`; ctx.shadowBlur = 8; ctx.fill(); }
break;
case 'block':
ctx.fillStyle = c.signal > 0 ? `rgb(${r},${g},${b})` : '#7F1D1D';
roundRect(px+1, py+1, CELL-2, CELL-2, 2); ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.3)'; ctx.lineWidth = 1;
ctx.strokeRect(px+3, py+3, CELL-6, CELL-6);
break;
case 'torch':
ctx.fillStyle = '#4A2C20';
roundRect(px+4, py+6, CELL-8, CELL-10, 2); ctx.fill();
const torchOn = c.signal > 0 || !isAttachedPowered(x, y);
if (torchOn) {
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.shadowColor = `rgb(${r},${g},${b})`; ctx.shadowBlur = 6;
ctx.beginPath();
ctx.arc(px+CELL/2, py+5, 3.5, 0, Math.PI*2); ctx.fill();
} else {
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(px+CELL/2, py+5, 3.5, 0, Math.PI*2); ctx.fill();
}
break;
case 'button':
ctx.fillStyle = '#4A6B8A';
roundRect(px+2, py+2, CELL-4, CELL-4, 3); ctx.fill();
ctx.fillStyle = c.active ? `rgb(${r},${g},${b})` : '#6B8AB0';
roundRect(px+6, py+6, CELL-12, CELL-12, 2); ctx.fill();
break;
case 'dust':
if (sig > 0) {
ctx.shadowColor = `rgba(232,50,41,${0.15+0.35*glow})`;
ctx.shadowBlur = 6;
}
ctx.fillStyle = sig > 0 ? `rgb(${r},${g},${b})` : '#7F1D1D';
// 画十字线
ctx.fillRect(px+CELL/2-1, py+3, 2, CELL-6);
ctx.fillRect(px+3, py+CELL/2-1, CELL-6, 2);
// 中心点
ctx.beginPath(); ctx.arc(px+CELL/2, py+CELL/2, 2.5, 0, Math.PI*2); ctx.fill();
break;
case 'insulated':
ctx.fillStyle = sig > 0 ? `rgb(${r*0.7},${g*0.7+60},${b*0.7+60})` : '#1E3A5F';
ctx.fillRect(px+CELL/2-1.5, py+4, 3, CELL-8);
ctx.fillRect(px+4, py+CELL/2-1.5, CELL-8, 3);
ctx.strokeStyle = '#2A4A6F'; ctx.lineWidth = 0.5;
ctx.strokeRect(px+4, py+4, CELL-8, CELL-8);
break;
case 'repeater':
ctx.fillStyle = '#92400E';
roundRect(px+1, py+4, CELL-2, CELL-8, 2); ctx.fill();
// 方向箭头
const [dx, dy] = getDir(c.rotation || 0);
ctx.fillStyle = sig > 0 ? `rgb(${r},${g},${b})` : '#FCD34D';
ctx.beginPath();
const cx = px+CELL/2, cy = py+CELL/2;
ctx.moveTo(cx+dx*4, cy+dy*4);
ctx.lineTo(cx-dx*3+dy*3, cy-dy*3-dx*3);
ctx.lineTo(cx-dx*3-dy*3, cy-dy*3+dx*3);
ctx.closePath(); ctx.fill();
break;
case 'lamp':
if (c.powered) {
ctx.shadowColor = 'rgba(232,50,41,0.5)'; ctx.shadowBlur = 12;
ctx.fillStyle = `rgb(${r},${g},${b})`;
} else {
ctx.fillStyle = '#374151';
}
roundRect(px+2, py+2, CELL-4, CELL-4, 3); ctx.fill();
ctx.shadowBlur = 0;
// 灯丝
ctx.strokeStyle = c.powered ? '#FFD700' : '#1F2937'; ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(px+6, py+CELL/2); ctx.lineTo(px+CELL-6, py+CELL/2);
ctx.moveTo(px+CELL/2, py+6); ctx.lineTo(px+CELL/2, py+CELL-6);
ctx.stroke();
break;
}
ctx.restore();
}
function isAttachedPowered(x, y) {
for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) {
const nx = x+dx, ny = y+dy;
if (nx<0||nx>=GRID_W||ny<0||ny>=GRID_H) continue;
const n = grid[ny][nx];
if (n && n.signal > 0 && n.type !== 'torch') return true;
}
return false;
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x+r, y);
ctx.arcTo(x+w, y, x+w, y+h, r);
ctx.arcTo(x+w, y+h, x, y+h, r);
ctx.arcTo(x, y+h, x, y, r);
ctx.arcTo(x, y, x+w, y, r);
ctx.closePath();
}
// ==================== 输入处理 ====================
function getMouseGrid(e) {
const rect = canvas.getBoundingClientRect();
const sx = canvas.width / rect.width;
const sy = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * sx;
const my = (e.clientY - rect.top) * sy;
return { x: Math.floor(mx / CELL), y: Math.floor(my / CELL) };
}
function placeComponent(x, y) {
if (x < 0 || x >= GRID_W || y < 0 || y >= GRID_H) return;
if (grid[y][x]) return;
grid[y][x] = { type: selectedType, rotation: 0, active: false, signal: 0, powered: false };
updateStatus();
if (isRunning) simulate();
render();
}
function deleteComponent(x, y) {
if (x < 0 || x >= GRID_W || y < 0 || y >= GRID_H) return;
if (!grid[y][x]) return;
grid[y][x] = null;
updateStatus();
if (isRunning) simulate();
render();
}
function toggleComponent(x, y) {
if (x < 0 || x >= GRID_W || y < 0 || y >= GRID_H) return;
const c = grid[y][x];
if (!c) return;
const def = COMPONENTS[c.type];
if (def.interactive) {
c.active = !c.active;
if (def.momentary) {
c.active = true;
setTimeout(() => { c.active = false; if (isRunning) { simulate(); render(); } }, 800);
}
if (isRunning) simulate();
render();
}
}
// ==================== UI 更新 ====================
function updateStatus() {
let count = 0, cost = 0;
for (let y = 0; y < GRID_H; y++)
for (let x = 0; x < GRID_W; x++) {
const c = grid[y][x];
if (c) { count++; cost += COMPONENTS[c.type].cost; }
}
document.getElementById('stCount').textContent = count;
document.getElementById('stCost').textContent = cost;
}
function updateHoverInfo(x, y) {
document.getElementById('stCoord').textContent = `${x}, ${y}`;
const c = grid[y] && grid[y][x];
if (c) {
document.getElementById('stSignal').textContent = c.signal || 0;
} else {
document.getElementById('stSignal').textContent = '-';
}
}
// ==================== 示例电路 ====================
function loadDemo() {
initGrid();
// 电源 -> 红石粉 -> 灯 (基础链路)
grid[8][5] = { type:'lever', rotation:0, active:true, signal:0, powered:false };
for (let x = 6; x <= 18; x++)
grid[8][x] = { type:'dust', rotation:0, active:false, signal:0, powered:false };
grid[8][19] = { type:'lamp', rotation:0, active:false, signal:0, powered:false };
// 中继器恢复信号
grid[14][5] = { type:'lever', rotation:0, active:true, signal:0, powered:false };
for (let x = 6; x <= 12; x++)
grid[14][x] = { type:'dust', rotation:0, active:false, signal:0, powered:false };
grid[14][13] = { type:'repeater', rotation:0, active:false, signal:0, powered:false };
for (let x = 14; x <= 25; x++)
grid[14][x] = { type:'dust', rotation:0, active:false, signal:0, powered:false };
grid[14][26] = { type:'lamp', rotation:0, active:false, signal:0, powered:false };
// 火把反相 (NOT门)
grid[20][5] = { type:'block', rotation:0, active:false, signal:0, powered:false };
grid[20][6] = { type:'torch', rotation:0, active:false, signal:0, powered:false };
for (let x = 7; x <= 15; x++)
grid[20][x] = { type:'dust', rotation:0, active:false, signal:0, powered:false };
grid[20][16] = { type:'lamp', rotation:0, active:false, signal:0, powered:false };
// 按钮 -> 短脉冲 -> 灯
grid[26][5] = { type:'button', rotation:0, active:false, signal:0, powered:false };
for (let x = 6; x <= 12; x++)
grid[26][x] = { type:'dust', rotation:0, active:false, signal:0, powered:false };
grid[26][13] = { type:'lamp', rotation:0, active:false, signal:0, powered:false };
updateStatus();
if (isRunning) simulate();
render();
}
// ==================== 保存/加载 ====================
function saveCircuit() {
const data = { w: GRID_W, h: GRID_H, components: [] };
for (let y = 0; y < GRID_H; y++)
for (let x = 0; x < GRID_W; x++) {
const c = grid[y][x];
if (c) data.components.push({ x, y, ...c });
}
localStorage.setItem('redcircuit_save', JSON.stringify(data));
flashBtn('btnSave', '已保存!');
}
function loadCircuit() {
const raw = localStorage.getItem('redcircuit_save');
if (!raw) { flashBtn('btnLoad', '无存档'); return; }
try {
const data = JSON.parse(raw);
initGrid();
for (const c of data.components) {
if (c.x < GRID_W && c.y < GRID_H)
grid[c.y][c.x] = { type: c.type, rotation: c.rotation||0, active: c.active||false, signal: 0, powered: false };
}
updateStatus();
if (isRunning) simulate();
render();
flashBtn('btnLoad', '已加载!');
} catch(e) { flashBtn('btnLoad', '加载失败'); }
}
function flashBtn(id, text) {
const btn = document.getElementById(id);
const orig = btn.textContent;
btn.textContent = text;
setTimeout(() => btn.textContent = orig, 1200);
}
// ==================== 初始化 ====================
function init() {
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
canvas.width = GRID_W * CELL;
canvas.height = GRID_H * CELL;
initGrid();
// 侧边栏选择
document.querySelectorAll('.comp-item').forEach(el => {
el.addEventListener('click', () => {
document.querySelectorAll('.comp-item').forEach(e => e.classList.remove('selected'));
el.classList.add('selected');
selectedType = el.dataset.type;
});
});
// 工具模式
document.getElementById('modePlace').addEventListener('click', () => {
toolMode = 'place';
document.getElementById('modePlace').classList.add('active');
document.getElementById('modeDelete').classList.remove('active');
});
document.getElementById('modeDelete').addEventListener('click', () => {
toolMode = 'delete';
document.getElementById('modeDelete').classList.add('active');
document.getElementById('modePlace').classList.remove('active');
});
// 按钮
document.getElementById('btnRun').addEventListener('click', () => {
isRunning = !isRunning;
const btn = document.getElementById('btnRun');
btn.textContent = isRunning ? '⏸ 暂停' : '▶ 运行';
btn.classList.toggle('active', isRunning);
document.getElementById('stState').textContent = isRunning ? '运行中' : '已暂停';
if (isRunning) simulate();
render();
});
document.getElementById('btnStep').addEventListener('click', () => {
simulate(); render();
});
document.getElementById('btnClear').addEventListener('click', () => {
initGrid(); updateStatus(); render();
});
document.getElementById('btnSave').addEventListener('click', saveCircuit);
document.getElementById('btnLoad').addEventListener('click', loadCircuit);
document.getElementById('btnDemo').addEventListener('click', loadDemo);
// Canvas 鼠标事件
canvas.addEventListener('mousemove', (e) => {
const { x, y } = getMouseGrid(e);
mouseGX = x; mouseGY = y;
updateHoverInfo(x, y);
render();
});
canvas.addEventListener('mouseleave', () => {
mouseGX = -1; mouseGY = -1;
render();
});
canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
const { x, y } = getMouseGrid(e);
deleteComponent(x, y);
});
canvas.addEventListener('mousedown', (e) => {
e.preventDefault();
const { x, y } = getMouseGrid(e);
if (e.button === 0) { // 左键
if (e.shiftKey || e.ctrlKey) {
toggleComponent(x, y); // Shift+点击 = 切换开关
} else {
if (toolMode === 'delete') deleteComponent(x, y);
else {
const c = grid[y] && grid[y][x];
if (c && COMPONENTS[c.type].interactive) toggleComponent(x, y);
else placeComponent(x, y);
}
}
}
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const { x, y } = getMouseGrid(e);
const c = grid[y] && grid[y][x];
if (c) {
c.rotation = ((c.rotation || 0) + (e.deltaY > 0 ? 1 : 3)) % 4;
if (isRunning) simulate();
render();
}
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
document.getElementById('btnRun').click();
} else if (e.code === 'KeyC') {
document.getElementById('btnClear').click();
} else if (e.code === 'KeyD') {
document.getElementById('btnDemo').click();
}
});
// 自适应 canvas 大小
function resizeCanvas() {
const area = document.querySelector('.canvas-area');
const scale = Math.min(area.clientWidth / canvas.width, area.clientHeight / canvas.height, 1.5);
canvas.style.width = (canvas.width * scale) + 'px';
canvas.style.height = (canvas.height * scale) + 'px';
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// 加载示例
loadDemo();
}
init();
</script>
</body>
</html>