CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
692 lines
25 KiB
HTML
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>
|