Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Redstone Sandbox 2.0</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| background: #1a1a1a; | |
| font-family: 'Segoe UI', sans-serif; | |
| overflow: hidden; | |
| color: #fff; | |
| } | |
| #gameContainer { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| background: radial-gradient(circle at center, #2a2a2a 0%, #1a1a1a 100%); | |
| } | |
| #grid { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: rgba(0,0,0,0.3); | |
| padding: 2px; | |
| border-radius: 8px; | |
| box-shadow: 0 0 50px rgba(0,0,0,0.5); | |
| } | |
| .grid-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| pointer-events: none; | |
| border-radius: 8px; | |
| } | |
| .cell { | |
| width: 40px; | |
| height: 40px; | |
| border: 1px solid #2a2a2a; | |
| position: absolute; | |
| transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 10px; | |
| cursor: pointer; | |
| box-shadow: inset 0 0 10px rgba(0,0,0,0.2); | |
| } | |
| .cell:hover { | |
| box-shadow: inset 0 0 15px rgba(255,255,255,0.1); | |
| } | |
| .cell[data-power] { | |
| transform: scale(1.05); | |
| z-index: 1; | |
| } | |
| .toolbar { | |
| position: fixed; | |
| top: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(20,20,20,0.95); | |
| padding: 15px; | |
| border-radius: 15px; | |
| display: flex; | |
| gap: 12px; | |
| box-shadow: 0 5px 25px rgba(0,0,0,0.3); | |
| z-index: 100; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .tool { | |
| width: 50px; | |
| height: 50px; | |
| border: 2px solid #444; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 22px; | |
| background: rgba(255,255,255,0.05); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| } | |
| .tool:hover { | |
| transform: translateY(-2px); | |
| background: rgba(255,255,255,0.1); | |
| box-shadow: 0 0 20px rgba(255,85,85,0.3); | |
| } | |
| .tool.active { | |
| border-color: #f55; | |
| background: rgba(255,85,85,0.2); | |
| transform: scale(1.1); | |
| } | |
| .tool-tooltip { | |
| position: absolute; | |
| bottom: -30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0,0,0,0.8); | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| opacity: 0; | |
| transition: 0.3s; | |
| pointer-events: none; | |
| white-space: nowrap; | |
| } | |
| .tool:hover .tool-tooltip { | |
| opacity: 1; | |
| bottom: -25px; | |
| } | |
| .button { | |
| background: #f55; | |
| border: none; | |
| border-radius: 8px; | |
| color: white; | |
| cursor: pointer; | |
| font-size: 16px; | |
| padding: 10px 20px; | |
| transition: 0.3s; | |
| } | |
| .button:hover { | |
| transform: scale(1.1); | |
| } | |
| .button.pause { | |
| background: #55f; | |
| } | |
| .button.pause.active { | |
| background: #f55; | |
| } | |
| .save-btn, .load-btn { | |
| background: #444; | |
| margin: 0 5px; | |
| } | |
| .save-btn:hover, .load-btn:hover { | |
| background: #555; | |
| } | |
| .wire { | |
| background: #300; | |
| transition: background-color 0.3s; | |
| } | |
| .wire.powered { | |
| background: #f00; | |
| } | |
| .wire[data-power="1"] { background: #400; } | |
| .wire[data-power="2"] { background: #500; } | |
| .wire[data-power="3"] { background: #600; } | |
| .wire[data-power="4"] { background: #700; } | |
| .wire[data-power="5"] { background: #800; } | |
| .wire[data-power="6"] { background: #900; } | |
| .wire[data-power="7"] { background: #a00; } | |
| .wire[data-power="8"] { background: #b00; } | |
| .wire[data-power="9"] { background: #c00; } | |
| .wire[data-power="10"] { background: #d00; } | |
| .wire[data-power="11"] { background: #e00; } | |
| .wire[data-power="12"] { background: #f00; } | |
| .wire[data-power="13"] { background: #f11; } | |
| .wire[data-power="14"] { background: #f22; } | |
| .wire[data-power="15"] { background: #f33; } | |
| .torch { | |
| background: #320; | |
| box-shadow: 0 0 10px #320; | |
| } | |
| .torch.powered { | |
| background: #fa0; | |
| box-shadow: 0 0 20px #fa0; | |
| animation: torch-glow 1s infinite alternate; | |
| } | |
| .block { | |
| background: #444; | |
| box-shadow: inset 0 0 10px rgba(0,0,0,0.5); | |
| } | |
| .spreader { | |
| background: #030; | |
| transition: all 0.3s; | |
| } | |
| .spreader.powered { | |
| background: #0f0; | |
| box-shadow: 0 0 20px #0f0; | |
| animation: spreader-pulse 1s infinite; | |
| } | |
| .lamp { | |
| background: #330; | |
| transition: all 0.3s; | |
| } | |
| .lamp.powered { | |
| background: #ff0; | |
| box-shadow: 0 0 30px #ff0; | |
| animation: lamp-shine 2s infinite alternate; | |
| } | |
| .timer { | |
| background: #303; | |
| transition: all 0.3s; | |
| } | |
| .timer.powered { | |
| background: #f0f; | |
| box-shadow: 0 0 20px #f0f; | |
| } | |
| .power-level { | |
| position: absolute; | |
| top: 2px; | |
| right: 2px; | |
| font-size: 10px; | |
| padding: 2px 4px; | |
| background: rgba(0,0,0,0.5); | |
| border-radius: 4px; | |
| pointer-events: none; | |
| } | |
| .timer-value { | |
| position: absolute; | |
| bottom: 2px; | |
| left: 2px; | |
| font-size: 10px; | |
| color: rgba(255,255,255,0.8); | |
| } | |
| @keyframes torch-glow { | |
| from { box-shadow: 0 0 10px #fa0; } | |
| to { box-shadow: 0 0 30px #fa0; } | |
| } | |
| @keyframes spreader-pulse { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.1); } | |
| 100% { transform: scale(1); } | |
| } | |
| @keyframes lamp-shine { | |
| from { box-shadow: 0 0 20px #ff0; } | |
| to { box-shadow: 0 0 40px #ff0; } | |
| } | |
| @keyframes power-pulse { | |
| 0% { box-shadow: 0 0 10px currentColor; } | |
| 50% { box-shadow: 0 0 20px currentColor; } | |
| 100% { box-shadow: 0 0 10px currentColor; } | |
| } | |
| .powered { | |
| animation: power-pulse 2s infinite; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="gameContainer"> | |
| <div class="toolbar"> | |
| <div class="tool active" data-tool="wire" data-name="Wire">⚡ | |
| <div class="tool-tooltip">Wire</div> | |
| </div> | |
| <div class="tool" data-tool="torch" data-name="Torch">🔥 | |
| <div class="tool-tooltip">Torch</div> | |
| </div> | |
| <div class="tool" data-tool="block" data-name="Block">⬛ | |
| <div class="tool-tooltip">Block</div> | |
| </div> | |
| <div class="tool" data-tool="spreader" data-name="Spreader">➕ | |
| <div class="tool-tooltip">Spreader</div> | |
| </div> | |
| <div class="tool" data-tool="lamp" data-name="Lamp">💡 | |
| <div class="tool-tooltip">Lamp</div> | |
| </div> | |
| <div class="tool" data-tool="timer" data-name="Timer">⏲️ | |
| <div class="tool-tooltip">Timer</div> | |
| </div> | |
| <button class="button reset">Reset</button> | |
| <button class="button pause">Pause</button> | |
| <button class="button save-btn">Save</button> | |
| <button class="button load-btn">Load</button> | |
| </div> | |
| <div id="grid"> | |
| <div class="grid-overlay"></div> | |
| </div> | |
| </div> | |
| <script> | |
| class RedstoneSandbox { | |
| constructor() { | |
| this.gridSize = 20; | |
| this.cellSize = 40; | |
| this.grid = {}; | |
| this.selectedTool = 'wire'; | |
| this.timers = new Set(); | |
| this.isPaused = false; | |
| this.gameLoopInterval = null; | |
| this.undoStack = []; | |
| this.redoStack = []; | |
| this.init(); | |
| } | |
| init() { | |
| this.setupGrid(); | |
| this.setupTools(); | |
| this.setupButtons(); | |
| this.bindEvents(); | |
| this.addTooltips(); | |
| this.setupSaveLoad(); | |
| this.setupKeyboardShortcuts(); | |
| this.startGameLoop(); | |
| } | |
| setupGrid() { | |
| const grid = document.getElementById('grid'); | |
| grid.style.width = `${this.gridSize * this.cellSize + 2}px`; | |
| grid.style.height = `${this.gridSize * this.cellSize + 2}px`; | |
| for(let y = 0; y < this.gridSize; y++) { | |
| for(let x = 0; x < this.gridSize; x++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'cell'; | |
| cell.style.left = `${x * this.cellSize}px`; | |
| cell.style.top = `${y * this.cellSize}px`; | |
| cell.dataset.x = x; | |
| cell.dataset.y = y; | |
| const powerLevel = document.createElement('div'); | |
| powerLevel.className = 'power-level'; | |
| const timerValue = document.createElement('div'); | |
| timerValue.className = 'timer-value'; | |
| cell.appendChild(powerLevel); | |
| cell.appendChild(timerValue); | |
| grid.appendChild(cell); | |
| this.grid[`${x},${y}`] = { | |
| element: cell, | |
| powerDisplay: powerLevel, | |
| timerDisplay: timerValue, | |
| type: null, | |
| powered: false, | |
| powerLevel: 0, | |
| timerInterval: 1, | |
| lastUpdate: Date.now() | |
| }; | |
| } | |
| } | |
| } | |
| addTooltips() { | |
| document.querySelectorAll('.tool').forEach(tool => { | |
| const tooltip = document.createElement('div'); | |
| tooltip.className = 'tool-tooltip'; | |
| tooltip.textContent = tool.dataset.name; | |
| tool.appendChild(tooltip); | |
| }); | |
| } | |
| setupSaveLoad() { | |
| document.querySelector('.save-btn').addEventListener('click', () => { | |
| const saveData = {}; | |
| Object.entries(this.grid).forEach(([coord, cell]) => { | |
| if(cell.type) { | |
| saveData[coord] = { | |
| type: cell.type, | |
| powered: cell.powered, | |
| powerLevel: cell.powerLevel, | |
| timerInterval: cell.timerInterval | |
| }; | |
| } | |
| }); | |
| localStorage.setItem('redstoneSave', JSON.stringify(saveData)); | |
| this.showNotification('Design saved successfully!'); | |
| }); | |
| document.querySelector('.load-btn').addEventListener('click', () => { | |
| const saveData = JSON.parse(localStorage.getItem('redstoneSave') || '{}'); | |
| this.reset(); | |
| Object.entries(saveData).forEach(([coord, data]) => { | |
| const [x,y] = coord.split(','); | |
| const cell = this.grid[coord]; | |
| cell.type = data.type; | |
| cell.powered = data.powered; | |
| cell.powerLevel = data.powerLevel; | |
| cell.timerInterval = data.timerInterval; | |
| cell.element.className = `cell ${data.type}`; | |
| if(data.powered) cell.element.classList.add('powered'); | |
| if(data.type === 'timer') { | |
| this.timers.add(coord); | |
| cell.timerDisplay.textContent = `${data.timerInterval}s`; | |
| } | |
| }); | |
| this.updatePower(); | |
| this.showNotification('Design loaded successfully!'); | |
| }); | |
| } | |
| showNotification(message) { | |
| const notification = document.createElement('div'); | |
| notification.style.cssText = ` | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0,0,0,0.8); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| z-index: 1000; | |
| animation: fadeInOut 2s forwards; | |
| `; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| setTimeout(() => notification.remove(), 2000); | |
| } | |
| setupKeyboardShortcuts() { | |
| document.addEventListener('keydown', (e) => { | |
| if(e.ctrlKey && e.key === 'z') { | |
| this.undo(); | |
| } else if(e.ctrlKey && e.key === 'y') { | |
| this.redo(); | |
| } else if(e.key === ' ') { | |
| this.isPaused = !this.isPaused; | |
| document.querySelector('.pause').classList.toggle('active'); | |
| } | |
| }); | |
| } | |
| saveState() { | |
| const state = {}; | |
| Object.entries(this.grid).forEach(([coord, cell]) => { | |
| if(cell.type) { | |
| state[coord] = { | |
| type: cell.type, | |
| powered: cell.powered, | |
| powerLevel: cell.powerLevel, | |
| timerInterval: cell.timerInterval | |
| }; | |
| } | |
| }); | |
| this.undoStack.push(state); | |
| this.redoStack = []; | |
| if(this.undoStack.length > 50) this.undoStack.shift(); | |
| } | |
| undo() { | |
| if(this.undoStack.length === 0) return; | |
| const currentState = {}; | |
| Object.entries(this.grid).forEach(([coord, cell]) => { | |
| if(cell.type) { | |
| currentState[coord] = { | |
| type: cell.type, | |
| powered: cell.powered, | |
| powerLevel: cell.powerLevel, | |
| timerInterval: cell.timerInterval | |
| }; | |
| } | |
| }); | |
| this.redoStack.push(currentState); | |
| const previousState = this.undoStack.pop(); | |
| this.reset(); | |
| this.loadState(previousState); | |
| } | |
| redo() { | |
| if(this.redoStack.length === 0) return; | |
| const state = this.redoStack.pop(); | |
| this.reset(); | |
| this.loadState(state); | |
| this.undoStack.push(state); | |
| } | |
| loadState(state) { | |
| Object.entries(state).forEach(([coord, data]) => { | |
| const cell = this.grid[coord]; | |
| cell.type = data.type; | |
| cell.powered = data.powered; | |
| cell.powerLevel = data.powerLevel; | |
| cell.timerInterval = data.timerInterval; | |
| cell.element.className = `cell ${data.type}`; | |
| if(data.powered) cell.element.classList.add('powered'); | |
| if(data.type === 'timer') { | |
| this.timers.add(coord); | |
| cell.timerDisplay.textContent = `${data.timerInterval}s`; | |
| } | |
| }); | |
| this.updatePower(); | |
| } | |
| setupTools() { | |
| const tools = document.querySelectorAll('.tool'); | |
| tools.forEach(tool => { | |
| tool.addEventListener('click', () => { | |
| tools.forEach(t => t.classList.remove('active')); | |
| tool.classList.add('active'); | |
| this.selectedTool = tool.dataset.tool; | |
| }); | |
| }); | |
| } | |
| setupButtons() { | |
| document.querySelector('.reset').addEventListener('click', () => { | |
| this.reset(); | |
| }); | |
| const pauseBtn = document.querySelector('.pause'); | |
| pauseBtn.addEventListener('click', () => { | |
| this.isPaused = !this.isPaused; | |
| pauseBtn.classList.toggle('active'); | |
| pauseBtn.textContent = this.isPaused ? 'Resume' : 'Pause'; | |
| }); | |
| } | |
| reset() { | |
| Object.values(this.grid).forEach(cell => { | |
| cell.element.className = 'cell'; | |
| cell.type = null; | |
| cell.powered = false; | |
| cell.powerLevel = 0; | |
| cell.powerDisplay.textContent = ''; | |
| cell.timerDisplay.textContent = ''; | |
| }); | |
| this.timers.clear(); | |
| } | |
| bindEvents() { | |
| const grid = document.getElementById('grid'); | |
| grid.addEventListener('contextmenu', e => { | |
| e.preventDefault(); | |
| if(e.target.classList.contains('cell')) { | |
| const x = parseInt(e.target.dataset.x); | |
| const y = parseInt(e.target.dataset.y); | |
| this.deleteComponent(x, y); | |
| this.saveState(); | |
| } | |
| }); | |
| grid.addEventListener('click', e => { | |
| if(e.target.classList.contains('cell')) { | |
| const x = parseInt(e.target.dataset.x); | |
| const y = parseInt(e.target.dataset.y); | |
| const cell = this.grid[`${x},${y}`]; | |
| if(cell.type === 'torch') { | |
| this.toggleTorch(cell); | |
| } else if(cell.type === 'timer') { | |
| this.adjustTimer(cell); | |
| } else { | |
| this.placeComponent(x, y); | |
| } | |
| this.saveState(); | |
| } | |
| }); | |
| let isDragging = false; | |
| grid.addEventListener('mousedown', () => isDragging = false); | |
| grid.addEventListener('mousemove', () => isDragging = true); | |
| grid.addEventListener('mouseup', e => { | |
| if(!isDragging && e.target.classList.contains('cell')) { | |
| const x = parseInt(e.target.dataset.x); | |
| const y = parseInt(e.target.dataset.y); | |
| this.placeComponent(x, y); | |
| this.saveState(); | |
| } | |
| }); | |
| } | |
| deleteComponent(x, y) { | |
| const cell = this.grid[`${x},${y}`]; | |
| if(cell.type === 'timer') { | |
| this.timers.delete(`${x},${y}`); | |
| } | |
| cell.element.className = 'cell'; | |
| cell.type = null; | |
| cell.powered = false; | |
| cell.powerLevel = 0; | |
| cell.powerDisplay.textContent = ''; | |
| cell.timerDisplay.textContent = ''; | |
| this.updatePower(); | |
| } | |
| toggleTorch(cell) { | |
| cell.powered = !cell.powered; | |
| cell.powerLevel = cell.powered ? 15 : 0; | |
| cell.element.classList.toggle('powered'); | |
| this.updatePower(); | |
| } | |
| adjustTimer(cell) { | |
| cell.timerInterval = (cell.timerInterval % 15) + 1; | |
| cell.timerDisplay.textContent = `${cell.timerInterval}s`; | |
| } | |
| placeComponent(x, y) { | |
| const cell = this.grid[`${x},${y}`]; | |
| if(cell.type === this.selectedTool) return; | |
| cell.element.className = 'cell'; | |
| cell.element.classList.add(this.selectedTool); | |
| cell.type = this.selectedTool; | |
| if(this.selectedTool === 'torch') { | |
| cell.powered = true; | |
| cell.powerLevel = 15; | |
| cell.element.classList.add('powered'); | |
| } else if(this.selectedTool === 'spreader') { | |
| cell.powered = false; | |
| cell.powerLevel = 0; | |
| } | |
| if(this.selectedTool === 'timer') { | |
| cell.timerInterval = 1; | |
| cell.timerDisplay.textContent = '1s'; | |
| this.timers.add(`${x},${y}`); | |
| } | |
| this.updatePower(); | |
| } | |
| updatePower() { | |
| if(this.isPaused) return; | |
| Object.values(this.grid).forEach(cell => { | |
| if(cell.type && !['torch', 'timer'].includes(cell.type)) { | |
| cell.powered = false; | |
| cell.powerLevel = 0; | |
| cell.element.classList.remove('powered'); | |
| } | |
| }); | |
| for(let i = 15; i > 0; i--) { | |
| Object.entries(this.grid).forEach(([coord, cell]) => { | |
| if(!cell.powered) return; | |
| const [x, y] = coord.split(',').map(Number); | |
| if(cell.type === 'wire') { | |
| this.powerNearby(x, y, cell.powerLevel - 1); | |
| } else if(['torch', 'timer'].includes(cell.type)) { | |
| this.powerNearby(x, y, 15); | |
| } else if(cell.type === 'spreader' && cell.powered) { | |
| this.spreadPower(x, y); | |
| } | |
| }); | |
| } | |
| Object.values(this.grid).forEach(cell => { | |
| if(cell.powerLevel > 0) { | |
| cell.element.classList.add('powered'); | |
| cell.element.dataset.power = cell.powerLevel; | |
| cell.powerDisplay.textContent = cell.powerLevel; | |
| } else { | |
| cell.element.classList.remove('powered'); | |
| delete cell.element.dataset.power; | |
| cell.powerDisplay.textContent = ''; | |
| } | |
| }); | |
| } | |
| powerNearby(x, y, power) { | |
| if(power <= 0) return; | |
| [[x+1,y], [x-1,y], [x,y+1], [x,y-1]].forEach(([nx, ny]) => { | |
| const neighbor = this.grid[`${nx},${ny}`]; | |
| if(neighbor && neighbor.type && neighbor.type !== 'block' && neighbor.type !== 'timer' && neighbor.type !== 'torch' && power > neighbor.powerLevel) { | |
| neighbor.powered = true; | |
| neighbor.powerLevel = power; | |
| } | |
| }); | |
| } | |
| spreadPower(x, y) { | |
| [[x+1,y], [x-1,y], [x,y+1], [x,y-1]].forEach(([nx, ny]) => { | |
| const neighbor = this.grid[`${nx},${ny}`]; | |
| if(neighbor && neighbor.type && neighbor.type !== 'block' && neighbor.type !== 'timer' && neighbor.type !== 'torch' && neighbor.powerLevel === 0) { | |
| neighbor.powered = true; | |
| neighbor.powerLevel = 15; | |
| } | |
| }); | |
| } | |
| startGameLoop() { | |
| this.gameLoopInterval = setInterval(() => { | |
| if(this.isPaused) return; | |
| const now = Date.now(); | |
| this.timers.forEach(coord => { | |
| const cell = this.grid[coord]; | |
| if(now - cell.lastUpdate >= cell.timerInterval * 1000) { | |
| cell.powered = !cell.powered; | |
| cell.powerLevel = cell.powered ? 15 : 0; | |
| cell.lastUpdate = now; | |
| this.updatePower(); | |
| } | |
| }); | |
| }, 50); | |
| } | |
| } | |
| new RedstoneSandbox(); | |
| </script> | |
| </body> | |
| </html> |