diff --git a/Games/Relay_Rift/README.md b/Games/Relay_Rift/README.md new file mode 100644 index 000000000..a67727a65 --- /dev/null +++ b/Games/Relay_Rift/README.md @@ -0,0 +1,59 @@ +# Relay Rift + +**Relay Rift** is a production-grade signal-routing puzzle game built for [GameZone](https://github.com/kunjgit/GameZone). Players rotate conductive wire tiles to route power from a source node through the grid to every receiver — before the move budget runs out. + +## ✨ Features + +- **30 hand-crafted levels** — grids grow from 3×3 to 7×7 with 1–6 receivers per level +- **Mathematically verified solvable** — every puzzle is built from a proven solution path +- **Varied path shapes** — L, U, zigzag, snake, spiral, W, staircase, diagonal wave, and more +- **Progressive difficulty** — budgets tighten and grid complexity increases level-by-level +- **Smart Hint system** — highlights the next tile and tells you exactly how many rotations it needs +- **"How to Play" tutorial** — 5-step illustrated modal on first visit, reopenable via `?` +- **Full keyboard support** — Arrow keys to navigate, Space/Enter to rotate, `H` hint, `R` reset +- **Score system** — bonus points for efficiency, persistent best score via `localStorage` +- **Particle explosion** on level completion +- **No build step** — pure HTML, CSS, and vanilla JavaScript + +## 🎮 How to Play + +1. **Click** (or press `Space`) a tile to rotate it 90° clockwise +2. Connect wires so signal flows from the **cyan source** node through the grid +3. Light up every **red receiver** before using all your moves +4. Press **`H`** for a hint or **`R`** to reset the level + +### Keyboard Shortcuts + +| Key | Action | +|-----|--------| +| `Click` / `Space` | Rotate focused tile | +| `Arrow keys` | Navigate between tiles | +| `H` | Show hint | +| `R` | Reset level | +| `?` | Re-open tutorial | + +## 🗂️ File Structure + +``` +Games/Relay_Rift/ +├── index.html — game layout, HUD, win overlay, how-to-play modal +├── style.css — cyberpunk dark theme, responsive grid, tile animations +└── script.js — 30 levels, BFS power propagation, hint engine, tutorial +``` + +## 🔧 Technical Notes + +- **BFS power routing** — signal propagates from source via connected wire ports each click +- **Tile encoding** — `L`=straight, `C`=corner, `T`=tee, `X`=cross, `S`=source, `R`=receiver; digit = rotation (0–3) +- **Verified solvability** — each level's solution path is traced manually; path tiles are scrambled exactly 1 step back, guaranteeing a minimum-click solution exists +- **Google Fonts** — Orbitron (display) + DM Mono (body) +- **Responsive** — adapts from 320px mobile to 1240px desktop + +## 🚀 Running Locally + +Open `Games/Relay_Rift/index.html` directly in any modern browser, or serve the repository root: + +```bash +python -m http.server 3000 +# then open http://localhost:3000/Games/Relay_Rift/ +``` diff --git a/Games/Relay_Rift/index.html b/Games/Relay_Rift/index.html new file mode 100644 index 000000000..6529986bf --- /dev/null +++ b/Games/Relay_Rift/index.html @@ -0,0 +1,221 @@ + + + + + + + Relay Rift · Signal Routing Puzzle + + + + + + + + + + + + + + + + +
+ +
+ + + GameZone + +
+ Signal Routing Puzzle + RELAY RIFT +
+ +
+ + +
+
+ Level + 1 +
+
+ Progress + 1 / 30 +
+
+ Moves + 0 / 0 +
+
+ Score + 0 +
+
+ Best + 0 +
+
+ + +
+
+

Relay grid

+
+

+
+ + +
+
+ + + + diff --git a/Games/Relay_Rift/script.js b/Games/Relay_Rift/script.js new file mode 100644 index 000000000..aa90724c2 --- /dev/null +++ b/Games/Relay_Rift/script.js @@ -0,0 +1,533 @@ +"use strict"; + +// ── DOM refs ────────────────────────────────────────────── +const boardEl = document.getElementById("board"); +const messageEl = document.getElementById("message"); +const levelLabel = document.getElementById("levelLabel"); +const progressLabel = document.getElementById("progressLabel"); +const movesLabel = document.getElementById("movesLabel"); +const scoreLabel = document.getElementById("scoreLabel"); +const bestLabel = document.getElementById("bestLabel"); +const receiverList = document.getElementById("receiverList"); +const objectiveText = document.getElementById("objectiveText"); +const hintBtn = document.getElementById("hintButton"); +const resetBtn = document.getElementById("resetButton"); +const nextBtn = document.getElementById("nextButton"); +const toastEl = document.getElementById("toast"); +const winOverlay = document.getElementById("winOverlay"); +const winNextBtn = document.getElementById("winNextBtn"); +const winRetryBtn = document.getElementById("winRetryBtn"); +const winScoreText = document.getElementById("winScoreText"); +const particleCanvas= document.getElementById("particleCanvas"); +const tutOverlay = document.getElementById("tutorialOverlay"); +const tutPrev = document.getElementById("tutPrev"); +const tutNext = document.getElementById("tutNext"); +const tutSkip = document.getElementById("tutSkip"); +const tutDotsEl = document.getElementById("tutDots"); +const startGameBtn = document.getElementById("startGameBtn"); +const helpBtn = document.getElementById("helpBtn"); + +// ── Direction helpers ───────────────────────────────────── +const DIRS = { n:[-1,0], e:[0,1], s:[1,0], w:[0,-1] }; +const OPPOSITE = { n:"s", s:"n", e:"w", w:"e" }; +const DIR_ORDER = ["n","e","s","w"]; + +const SHAPE_PORTS = { + straight: ["n","s"], + corner: ["n","e"], + tee: ["n","e","s"], + cross: ["n","e","s","w"], +}; + +// ── SVG wire builder ────────────────────────────────────── +function buildWireSVG(shape) { + const h = 8, cx = 50, cy = 50; + const portLines = { + n:`M${cx-h},${cy} L${cx-h},0 L${cx+h},0 L${cx+h},${cy} Z`, + s:`M${cx-h},${cy} L${cx-h},100 L${cx+h},100 L${cx+h},${cy} Z`, + e:`M${cx},${cy-h} L100,${cy-h} L100,${cy+h} L${cx},${cy+h} Z`, + w:`M${cx},${cy-h} L0,${cy-h} L0,${cy+h} L${cx},${cy+h} Z`, + }; + const ports = SHAPE_PORTS[shape] || SHAPE_PORTS.cross; + const paths = ports.map(p=>``).join(""); + return ``; +} + +// ══════════════════════════════════════════════════════════ +// LEVELS (30 verified-solvable puzzles) +// +// Design contract: +// • Every PATH tile is scrambled exactly 1 step back from its +// solution rotation: puzzle = (solution - 1 + 4) % 4 +// • Filler tiles = "C1" (corner [e,s]) – never on signal path +// • X = cross (all ports, rotation-independent, no scrambling) +// • Source S0 and Receivers R0 never scrambled +// +// Shape key L=straight C=corner T=tee X=cross S=source R=receiver +// Puzzle→Solution mappings: +// L0→L1(horiz) L3→L0(vert) +// C0→C1 C1→C2 C2→C3 C3→C0 +// T0→T1 +// ══════════════════════════════════════════════════════════ +const LEVELS = [ + // ─── 3×3 · Levels 1-6 ──────────────────────────────── + { size:3, budget:5, objective:"Basic L-route — connect source to receiver in 3 rotations.", + tiles:["S0","L0","C1","C1","C1","L3","C1","C1","R0"] }, + // Spiral: S→E,E,S,W,W,S,E,E→R (all 9 cells on path) + { size:3, budget:10, objective:"Spiral — signal winds through every tile on the board.", + tiles:["S0","L0","C1","C0","L0","C2","C3","L0","R0"] }, + // Zigzag: S→S,E,N,E,S,S→R + { size:3, budget:7, objective:"Zigzag — the path doubles back before the receiver.", + tiles:["S0","C0","C1","C3","C2","L3","C1","C1","R0"] }, + // U-shape: S→S,S,E,E,N,N→R + { size:3, budget:7, objective:"U-turn — route the signal all the way around.", + tiles:["S0","C1","R0","L3","C1","L3","C3","L0","C2"] }, + // V-split: 2 RX at (0,2) and (2,2) + { size:3, budget:7, objective:"V-split — power two receivers from one source.", + tiles:["S0","L0","R0","L3","C1","C1","C3","L0","R0"] }, + // Fork: RX at (1,2) and (2,0) + { size:3, budget:7, objective:"Fork — one branch right, one branch down.", + tiles:["S0","C1","C1","L3","C3","R0","R0","C1","C1"] }, + + // ─── 4×4 · Levels 7-12 ─────────────────────────────── + // Cross: RX at (0,3) top-right and (3,0) bottom-left + { size:4, budget:7, objective:"Cross — route power along both axes simultaneously.", + tiles:["S0","L0","L0","R0","L3","C1","C1","C1","L3","C1","C1","C1","R0","C1","C1","C1"] }, + // Long L: S→E,E,E(C1corner)→S,S,S→R + { size:4, budget:8, objective:"Long L — three steps right, three steps down.", + tiles:["S0","L0","L0","C1","C1","C1","C1","L3","C1","C1","C1","L3","C1","C1","C1","R0"] }, + // U-route: S→S,S,S,E,E,E,N,N,N→R at (0,3) + { size:4, budget:11, objective:"U-route — down the left, across the bottom, back up the right.", + tiles:["S0","C1","C1","R0","L3","C1","C1","L3","L3","C1","C1","L3","C3","L0","L0","C2"] }, + // Zigzag: S→E,E,S,W,W,S,E,E,S,E→R + { size:4, budget:12, objective:"Zigzag — alternates direction every row across the grid.", + tiles:["S0","L0","C1","C1","C0","L0","C2","C1","C3","L0","C1","C1","C1","C1","C3","R0"] }, + // W-shape: 2 RX at (3,1) and (3,3) + { size:4, budget:11, objective:"W-shape — two inner branches reach separate receivers.", + tiles:["S0","L0","L0","C1","C3","C1","C1","L3","C1","L3","C1","L3","C1","R0","C1","R0"] }, + // Staircase: diagonal + left column; 2 RX at (3,0) and (3,3) + { size:4, budget:10, objective:"Staircase — diagonal path plus a vertical trunk.", + tiles:["S0","C1","C1","C1","L3","C3","C1","C1","L3","C1","C3","C1","R0","C1","C1","R0"] }, + + // ─── 5×5 · Levels 13-18 ────────────────────────────── + // Zigzag across 3 rows, 1 RX at (4,4) + { size:5, budget:14, objective:"Zigzag descent — weave across three rows to the corner.", + tiles:["S0","L0","C1","C1","C1","C1","C1","C3","L0","C1","C1","C1","C0","L0","C2","C1","C1","C3","L0","C1","C1","C1","C1","C1","R0"] }, + // Rectangle edges: 2 RX at (2,4) and (4,2) + { size:5, budget:14, objective:"Rectangle edges — trace two perpendicular borders.", + tiles:["S0","L0","L0","L0","C1","L3","C1","C1","C1","L3","L3","C1","C1","C1","R0","L3","C1","C1","C1","C1","C3","L0","R0","C1","C1"] }, + // Full snake: 5 rows, 2 RX at (0,4) and (4,0) + { size:5, budget:22, objective:"Snake — signal winds left and right through all five rows.", + tiles:["S0","L0","L0","L0","R0","C0","L0","L0","L0","C2","C3","L0","L0","L0","C1","C0","L0","L0","L0","C2","R0","C1","C1","C1","C1"] }, + // Perimeter: 3 RX at (0,4),(4,0),(4,4) + { size:5, budget:13, objective:"Perimeter — run signal around the full border of the grid.", + tiles:["S0","L0","L0","L0","R0","L3","C1","C1","C1","L3","L3","C1","C1","C1","L3","L3","C1","C1","C1","L3","R0","C1","C1","C1","R0"] }, + // W-shape: 3 RX at (2,2),(4,0),(4,4) + { size:5, budget:13, objective:"W-shape — two inner branches plus the outer frame.", + tiles:["S0","C1","C1","C1","C1","L3","L3","C1","C1","C1","L3","C3","R0","C1","C1","L3","C1","C1","C1","C1","R0","L0","L0","L0","R0"] }, + // Diagonal wave: 3 RX at (0,4),(4,0),(4,4) + { size:5, budget:20, objective:"Diagonal wave — staircase path to right, branches to bottom.", + tiles:["S0","C1","C1","C1","R0","L3","C3","C1","L3","L3","L3","C1","C3","C1","L3","L3","C1","C1","C3","C2","R0","L0","L0","L0","R0"] }, + + // ─── 6×6 · Levels 19-24 ────────────────────────────── + // Perimeter: 3 RX at (0,5),(5,0),(5,5) + { size:6, budget:16, objective:"Perimeter 6×6 — signal hugs three edges of the large grid.", + tiles:["S0","L0","L0","L0","L0","R0","L3","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","L3","R0","C1","C1","C1","C1","R0"] }, + // Diagonal descent + branches: 3 RX at (0,5),(5,0),(5,5) + { size:6, budget:20, objective:"Diagonal descent — path steps diagonally to the corner.", + tiles:["S0","L0","L0","L0","L0","R0","C3","C1","C1","C1","C1","L3","C1","C3","C1","C1","C1","L3","C1","C1","C3","C1","C1","L3","C1","C1","C1","C3","C1","L3","R0","L0","L0","L0","C2","R0"] }, + // Full 6-row snake: 4 RX at (0,5),(2,5),(4,5),(5,0) + { size:6, budget:36, objective:"Full snake — signal weaves through all six rows of the grid.", + tiles:["S0","L0","L0","L0","L0","R0","C0","L0","L0","L0","L0","C2","C3","L0","L0","L0","L0","R0","C0","L0","L0","L0","L0","C2","C3","L0","L0","L0","L0","R0","R0","L0","L0","L0","L0","C2"] }, + // T-junction branch: 4 RX at (0,5),(3,3),(5,0),(5,5) + { size:6, budget:21, objective:"T-branch — a T-junction splits signal east and south.", + tiles:["S0","T0","L0","L0","L0","R0","L3","L3","C1","C1","C1","L3","L3","L3","C1","C1","C1","L3","L3","C3","L0","R0","C1","L3","L3","C1","C1","C1","C1","L3","R0","C1","C1","C1","C1","R0"] }, + // Perimeter + mid-right RX: 4 RX at (0,5),(3,5),(5,0),(5,5) + { size:6, budget:19, objective:"Border + midpoint — full perimeter with a waypoint receiver.", + tiles:["S0","L0","L0","L0","L0","R0","L3","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","R0","L3","C1","C1","C1","C1","L3","R0","L0","L0","L0","L0","R0"] }, + // X-cross branch: 4 RX at (0,5),(2,2),(5,0),(5,5) + { size:6, budget:16, objective:"X-branch — a cross junction splits the signal mid-grid.", + tiles:["S0","L0","L0","L0","L0","R0","L3","C1","C1","C1","C1","C1","X0","L0","R0","C1","C1","C1","L3","C1","C1","C1","C1","C1","L3","C1","C1","C1","C1","C1","R0","L0","L0","L0","L0","R0"] }, + + // ─── 7×7 · Levels 25-30 ────────────────────────────── + // Perimeter + mid-right: 4 RX at (0,6),(3,6),(6,0),(6,6) + { size:7, budget:23, objective:"Frame + waypoint — four receivers around the 7×7 grid.", + tiles:["S0","L0","L0","L0","L0","L0","R0","L3","C1","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","C1","R0","L3","C1","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","C1","L3","R0","C1","C1","C1","C1","C1","R0"] }, + // T-junction + full frame: 4 RX at (0,6),(3,3),(6,0),(6,6) + { size:7, budget:28, objective:"T-branch 7×7 — inner junction branches to center and corners.", + tiles:["S0","T0","L0","L0","L0","L0","R0","L3","L3","C1","C1","C1","C1","L3","L3","L3","C1","C1","C1","C1","L3","L3","C3","L0","R0","C1","C1","L3","L3","C1","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","C1","L3","R0","L0","L0","L0","L0","L0","R0"] }, + // Full 7-row snake: 5 RX at (0,6),(2,6),(4,6),(6,0),(6,6) + { size:7, budget:48, objective:"Full snake — signal winds through all seven rows. Stay focused!", + tiles:["S0","L0","L0","L0","L0","L0","R0","C0","L0","L0","L0","L0","L0","C2","C3","L0","L0","L0","L0","L0","R0","C0","L0","L0","L0","L0","L0","C2","C3","L0","L0","L0","L0","L0","R0","C0","L0","L0","L0","L0","L0","C2","R0","L0","L0","L0","L0","L0","R0"] }, + // Multi-RX columns: 5 RX at (0,6),(2,6),(4,6),(6,0),(6,6) + { size:7, budget:22, objective:"Multi-column — power five receivers across three columns.", + tiles:["S0","L0","L0","L0","L0","L0","R0","L3","C1","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","C1","R0","L3","C1","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","C1","R0","L3","C1","C1","C1","C1","C1","L3","R0","L0","L0","L0","L0","L0","R0"] }, + // X-cross + columns: 5 RX at (0,6),(3,3),(3,6),(6,0),(6,6) + { size:7, budget:24, objective:"Five-way spread — cross junction + column receivers.", + tiles:["S0","L0","L0","L0","L0","L0","R0","L3","C1","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","C1","L3","X0","L0","L0","R0","C1","C1","R0","L3","C1","C1","C1","C1","C1","L3","L3","C1","C1","C1","C1","C1","L3","R0","L0","L0","L0","L0","L0","R0"] }, + // Ultimate: 7-row snake + center cross + 6 RX + { size:7, budget:46, objective:"FINAL — 6 receivers, full snake + cross junction. Route them all!", + tiles:["S0","L0","L0","L0","L0","L0","R0","C0","L0","L0","L0","L0","L0","C2","C3","L0","L0","L0","L0","L0","R0","X0","L0","L0","R0","L0","L0","C2","C3","L0","L0","L0","L0","L0","R0","C0","L0","L0","L0","L0","L0","C2","R0","L0","L0","L0","L0","L0","R0"] }, +]; + +const TOTAL_LEVELS = LEVELS.length; + +// ── State ───────────────────────────────────────────────── +const state = { + levelIndex: 0, + moves: 0, + score: 0, + solved: false, + tiles: [], + powered: new Set(), +}; +const STORAGE_KEY = "relay-rift-v3-best"; +const SEEN_KEY = "relay-rift-seen-tutorial"; + +// ── Tile parsing ────────────────────────────────────────── +const TYPE_MAP = { S:"source", R:"receiver", L:"straight", C:"corner", T:"tee", X:"cross" }; + +function parseTile(code, index) { + return { id:index, type:TYPE_MAP[code[0]], rotation:Number(code.slice(1)), initialRotation:Number(code.slice(1)) }; +} +function getShapeName(tile) { + return (tile.type==="source"||tile.type==="receiver") ? "cross" : tile.type; +} +function rotatedPorts(tile) { + return SHAPE_PORTS[getShapeName(tile)].map(d => DIR_ORDER[(DIR_ORDER.indexOf(d)+tile.rotation)%4]); +} + +// ── Toast ───────────────────────────────────────────────── +let toastTimer = null; +function showToast(msg) { + clearTimeout(toastTimer); + toastEl.textContent = msg; + toastEl.hidden = false; + toastEl.classList.remove("visible"); + void toastEl.offsetWidth; + toastEl.classList.add("visible"); + toastTimer = setTimeout(()=>{ + toastEl.classList.remove("visible"); + setTimeout(()=>{ toastEl.hidden = true; }, 250); + }, 2400); +} +function setMessage(text) { messageEl.textContent = text; } + +// ── Level loading ───────────────────────────────────────── +function loadLevel(index) { + const level = LEVELS[index]; + state.levelIndex = index; + state.moves = 0; + state.solved = false; + state.tiles = level.tiles.map(parseTile); + state.powered = new Set(); + objectiveText.textContent = level.objective; + boardEl.style.gridTemplateColumns = `repeat(${level.size}, 1fr)`; + nextBtn.disabled = true; + winOverlay.hidden = true; + renderBoard(); + updatePower(); + updateHud(); + setMessage("Rotate tiles to route the signal to all receivers."); +} + +// ── Render board ────────────────────────────────────────── +function renderBoard() { + boardEl.innerHTML = ""; + state.tiles.forEach(tile => { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "tile"; + if (tile.type==="source") btn.classList.add("source"); + if (tile.type==="receiver") btn.classList.add("receiver"); + btn.dataset.id = tile.id; + btn.setAttribute("role","gridcell"); + btn.innerHTML = buildWireSVG(getShapeName(tile)) + `
`; + applyRotationToSvg(btn, tile.rotation); + btn.addEventListener("click", () => rotateTile(tile.id)); + btn.addEventListener("keydown", e => handleTileKey(e, tile.id)); + boardEl.appendChild(btn); + }); +} + +function applyRotationToSvg(tileEl, rotation) { + const svg = tileEl.querySelector(".tile-svg"); + if (svg) svg.style.transform = `rotate(${rotation*90}deg)`; +} + +// ── Keyboard nav ────────────────────────────────────────── +function handleTileKey(event, id) { + const size = LEVELS[state.levelIndex].size; + if (event.key==="Enter"||event.key===" ") { event.preventDefault(); rotateTile(id); return; } + const moves = { ArrowRight:1, ArrowLeft:-1, ArrowDown:size, ArrowUp:-size }; + if (!(event.key in moves)) return; + event.preventDefault(); + const next = id + moves[event.key]; + const horiz = event.key==="ArrowRight"||event.key==="ArrowLeft"; + const sameRow = Math.floor(id/size)===Math.floor(next/size); + if (next>=0 && nextbudget) setMessage("Over budget. Reset for a clean run."); +} + +// ── Power propagation (BFS) ─────────────────────────────── +function updatePower() { + const level = LEVELS[state.levelIndex]; + const srcIdx = state.tiles.findIndex(t=>t.type==="source"); + if (srcIdx<0) return; + const energized = new Set([srcIdx]); + const queue = [srcIdx]; + while (queue.length) { + const cur = queue.shift(); + const tile = state.tiles[cur]; + const row = Math.floor(cur/level.size), col = cur%level.size; + rotatedPorts(tile).forEach(dir => { + const [dr,dc] = DIRS[dir]; + const nr=row+dr, nc=col+dc; + if (nr<0||nc<0||nr>=level.size||nc>=level.size) return; + const nIdx = nr*level.size+nc; + if (!rotatedPorts(state.tiles[nIdx]).includes(OPPOSITE[dir])) return; + if (!energized.has(nIdx)) { energized.add(nIdx); queue.push(nIdx); } + }); + } + state.powered = energized; + paintPower(); + updateReceivers(); + checkWin(); +} + +function paintPower() { + boardEl.querySelectorAll(".tile").forEach((el,i) => { + const on = state.powered.has(i); + el.classList.toggle("powered", on); + el.classList.toggle("solved", on && state.tiles[i].type==="receiver"); + }); +} + +function updateReceivers() { + receiverList.innerHTML = ""; + state.tiles.filter(t=>t.type==="receiver").forEach((rx,i) => { + const div = document.createElement("div"); + div.className = "receiver-pill"; + div.textContent = `RX-${String(i+1).padStart(2,"0")}`; + div.classList.toggle("online", state.powered.has(rx.id)); + receiverList.appendChild(div); + }); +} + +// ── Win check ───────────────────────────────────────────── +function checkWin() { + const receivers = state.tiles.filter(t=>t.type==="receiver"); + if (!receivers.every(t=>state.powered.has(t.id))||state.solved) return; + state.solved = true; + const level = LEVELS[state.levelIndex]; + const bonus = Math.max(0,level.budget-state.moves)*20; + const lvlScore = 300+bonus+receivers.length*100; + state.score += lvlScore; + saveBest(); + updateHud(); + const isLast = state.levelIndex===TOTAL_LEVELS-1; + winScoreText.textContent = `+${lvlScore} pts`; + winNextBtn.disabled = isLast; + winNextBtn.textContent = isLast ? "🏆 All Clear!" : "Next Level →"; + winOverlay.hidden = false; + setMessage(isLast ? "All 30 levels complete! You are the Relay Master." : `Level ${state.levelIndex+1} clear!`); + nextBtn.disabled = isLast; + launchParticles(); +} + +function saveBest() { + const prev = Number(localStorage.getItem(STORAGE_KEY)||0); + if (state.score>prev) localStorage.setItem(STORAGE_KEY, String(state.score)); +} + +// ── HUD ─────────────────────────────────────────────────── +function updateHud() { + const level = LEVELS[state.levelIndex]; + const over = state.moves>level.budget; + levelLabel.textContent = String(state.levelIndex+1); + progressLabel.textContent= `${state.levelIndex+1} / ${TOTAL_LEVELS}`; + movesLabel.textContent = `${state.moves} / ${level.budget}`; + movesLabel.style.color = over ? "var(--red)" : ""; + scoreLabel.textContent = String(state.score); + bestLabel.textContent = localStorage.getItem(STORAGE_KEY)||"0"; +} + +// ── Reset ───────────────────────────────────────────────── +function resetLevel() { + state.tiles.forEach(t=>{ t.rotation=t.initialRotation; }); + state.moves = 0; + state.solved = false; + nextBtn.disabled = true; + winOverlay.hidden = true; + boardEl.querySelectorAll(".tile").forEach((el,i)=>applyRotationToSvg(el,state.tiles[i].rotation)); + updatePower(); + updateHud(); + setMessage("Grid reset."); +} + +function goNext() { + if (state.levelIndex=level.size||nc>=level.size) continue; + const ni=nr*level.size+nc; + if (!state.powered.has(ni)) continue; + if (!rotatedPorts(state.tiles[ni]).includes(dir)) continue; + const needed=OPPOSITE[dir]; + const tile=state.tiles[i]; + for (let r=1; r<=4; r++) { + const testPorts = SHAPE_PORTS[getShapeName(tile)].map( + d=>DIR_ORDER[(DIR_ORDER.indexOf(d)+(tile.rotation+r))%4] + ); + if (testPorts.includes(needed)) { + bestTarget=i; rotationsNeeded=(r===4)?0:r; break outer; + } + } + } + } + if (bestTarget===null) { + const rx=state.tiles.find(t=>t.type==="receiver"&&!state.powered.has(t.id)); + if (rx) { bestTarget=rx.id; rotationsNeeded=0; } + } + if (bestTarget===null) return; + + const el=boardEl.querySelector(`[data-id="${bestTarget}"]`); + if (!el) return; + el.classList.remove("hint"); void el.offsetWidth; el.classList.add("hint"); + el.addEventListener("animationend",()=>el.classList.remove("hint"),{once:true}); + + const msg = rotationsNeeded===1 ? "Highlighted tile needs 1 more rotation" + : rotationsNeeded>1 ? `Highlighted tile needs ${rotationsNeeded} rotations` + : "Highlighted tile is a receiver — trace the path to it"; + showToast(msg); +} + +// ── Particles ───────────────────────────────────────────── +function launchParticles() { + const ctx=particleCanvas.getContext("2d"); + particleCanvas.width=window.innerWidth; + particleCanvas.height=window.innerHeight; + const particles=Array.from({length:80},()=>({ + x:particleCanvas.width/2+(Math.random()-.5)*120, + y:particleCanvas.height/2+(Math.random()-.5)*120, + vx:(Math.random()-.5)*14, vy:(Math.random()-.8)*14, + r:2+Math.random()*4, alpha:1, + color:Math.random()>.5?"#00f0c8":"#ffb830", + decay:0.016+Math.random()*0.012, + })); + let raf; + function draw() { + ctx.clearRect(0,0,particleCanvas.width,particleCanvas.height); + let alive=false; + particles.forEach(p=>{ + p.x+=p.vx; p.y+=p.vy; p.vy+=0.38; p.alpha-=p.decay; + if (p.alpha<=0) return; + alive=true; + ctx.globalAlpha=p.alpha; ctx.fillStyle=p.color; + ctx.shadowColor=p.color; ctx.shadowBlur=8; + ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill(); + }); + ctx.globalAlpha=1; ctx.shadowBlur=0; + if (alive) raf=requestAnimationFrame(draw); + } + raf=requestAnimationFrame(draw); +} + +// ══════════════════════════════════════════════════════════ +// TUTORIAL / HOW-TO-PLAY SYSTEM +// ══════════════════════════════════════════════════════════ +const TOTAL_STEPS = 5; +let currentStep = 1; + +function buildDots() { + tutDotsEl.innerHTML=""; + for (let i=1; i<=TOTAL_STEPS; i++) { + const d=document.createElement("button"); + d.type="button"; + d.className="tut-dot"+(i===currentStep?" active":""); + d.setAttribute("aria-label",`Step ${i}`); + d.addEventListener("click",()=>goToStep(i)); + tutDotsEl.appendChild(d); + } +} + +function goToStep(n) { + currentStep=Math.max(1,Math.min(TOTAL_STEPS,n)); + document.querySelectorAll(".tut-step").forEach(s=>{ + s.classList.toggle("active",Number(s.dataset.step)===currentStep); + }); + buildDots(); + // Show/hide Next vs Start button + const isLast = currentStep===TOTAL_STEPS; + tutNext.style.display = isLast ? "none" : ""; + tutPrev.style.display = currentStep===1 ? "none" : ""; +} + +function closeTutorial() { + tutOverlay.hidden=true; + localStorage.setItem(SEEN_KEY,"1"); +} + +function openTutorial() { + tutOverlay.hidden=false; + goToStep(1); +} + +tutNext.addEventListener("click", ()=>goToStep(currentStep+1)); +tutPrev.addEventListener("click", ()=>goToStep(currentStep-1)); +tutSkip.addEventListener("click", closeTutorial); +startGameBtn.addEventListener("click", closeTutorial); +helpBtn.addEventListener("click", openTutorial); + +// ── Event wiring ────────────────────────────────────────── +hintBtn.addEventListener("click", revealHint); +resetBtn.addEventListener("click", resetLevel); +nextBtn.addEventListener("click", goNext); +winNextBtn.addEventListener("click", ()=>{ winOverlay.hidden=true; goNext(); }); +winRetryBtn.addEventListener("click", ()=>{ winOverlay.hidden=true; resetLevel(); }); + +window.addEventListener("keydown", e => { + if (!tutOverlay.hidden||!winOverlay.hidden) return; + if (e.key.toLowerCase()==="r") resetLevel(); + if (e.key.toLowerCase()==="h") revealHint(); +}); + +// ── Boot ────────────────────────────────────────────────── +buildDots(); +loadLevel(0); + +// Show tutorial on first visit +if (!localStorage.getItem(SEEN_KEY)) { + openTutorial(); +} else { + tutOverlay.hidden = true; +} diff --git a/Games/Relay_Rift/style.css b/Games/Relay_Rift/style.css new file mode 100644 index 000000000..6ccc3d374 --- /dev/null +++ b/Games/Relay_Rift/style.css @@ -0,0 +1,835 @@ +/* ============================================================ + RELAY RIFT — style.css + Dark cyberpunk / neon-circuit aesthetic + ============================================================ */ + +/* ── Design tokens ── */ +:root { + --bg: #07090d; + --bg-panel: #0d1117; + --bg-raised: #111820; + --border: rgba(0, 240, 200, 0.14); + --border-hi: rgba(0, 240, 200, 0.36); + + --teal: #00f0c8; + --teal-dim: rgba(0, 240, 200, 0.18); + --teal-glow: rgba(0, 240, 200, 0.55); + --amber: #ffb830; + --amber-dim: rgba(255, 184, 48, 0.18); + --red: #ff4d5e; + --green: #39e88f; + + --ink: #e8f0ef; + --ink-dim: #6b8080; + --ink-muted: rgba(232, 240, 239, 0.45); + + --tile-off: #121c24; + --tile-on: #0d2822; + --tile-src: #0a2030; + --tile-rx-on: #062418; + --tile-rx-off: #220a0e; + + --font-display: 'Orbitron', 'Courier New', monospace; + --font-mono: 'DM Mono', 'Courier New', monospace; + + --radius: 8px; + --radius-lg: 14px; + --gap: 14px; + --transition: 200ms ease; +} + +/* ── Reset ── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { font-size: 16px; } + +body { + min-height: 100svh; + background: var(--bg); + color: var(--ink); + font-family: var(--font-mono); + /* subtle scanline texture */ + background-image: + repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0,240,200,0.018) 2px, + rgba(0,240,200,0.018) 4px + ); + overflow-x: hidden; +} + +/* ── Screen reader only ── */ +.sr-only { + position: absolute; + width: 1px; height: 1px; + padding: 0; overflow: hidden; + clip: rect(0,0,0,0); + white-space: nowrap; + border: 0; +} + +/* ────────────────────────────────────────────────────────── + SHELL +────────────────────────────────────────────────────────── */ +.shell { + width: min(1240px, 100%); + min-height: 100svh; + margin: 0 auto; + padding: 0 clamp(10px, 3vw, 28px) 40px; + display: flex; + flex-direction: column; + gap: 14px; +} + +/* ────────────────────────────────────────────────────────── + TOP BAR +────────────────────────────────────────────────────────── */ +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 0 10px; + border-bottom: 1px solid var(--border); +} + +.back-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--ink-dim); + text-decoration: none; + font: 500 0.78rem/1 var(--font-mono); + letter-spacing: 0.06em; + text-transform: uppercase; + transition: color var(--transition); +} +.back-link:hover { color: var(--teal); } + +.topbar-brand { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; +} +.brand-kicker { + font: 500 0.62rem/1 var(--font-mono); + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ink-muted); +} +.brand-name { + font: 900 1.3rem/1 var(--font-display); + letter-spacing: 0.12em; + background: linear-gradient(90deg, var(--teal), var(--amber)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ────────────────────────────────────────────────────────── + HUD +────────────────────────────────────────────────────────── */ +.hud { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; +} + +.hud-cell { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 10px 14px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.hud-label { + font: 500 0.66rem/1 var(--font-mono); + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--ink-dim); +} + +.hud-value { + font: 700 1.5rem/1 var(--font-display); + color: var(--ink); +} +.hud-value.accent { color: var(--teal); } +.hud-value.dim { color: var(--ink-dim); } + +/* ────────────────────────────────────────────────────────── + GAME AREA +────────────────────────────────────────────────────────── */ +.game-area { + flex: 1; + display: grid; + grid-template-columns: 1fr 260px; + gap: var(--gap); + align-items: start; +} + +/* ────────────────────────────────────────────────────────── + BOARD PANEL +────────────────────────────────────────────────────────── */ +.board-panel { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: clamp(12px, 2.5vw, 26px); + display: flex; + flex-direction: column; + gap: 12px; + box-shadow: inset 0 0 60px rgba(0, 240, 200, 0.03); + overflow: hidden; + min-width: 0; +} + +/* ─── The Grid ─── */ +.board { + display: grid; + gap: 8px; + width: min(100%, 520px); + max-width: 100%; + margin: 0 auto; + aspect-ratio: 1; +} + +/* ─── Tile ─── */ +.tile { + position: relative; + width: 100%; min-width: 0; min-height: 0; + padding: 0; + aspect-ratio: 1; + border: 1px solid rgba(0, 240, 200, 0.18); + border-radius: var(--radius); + background: var(--tile-off); + cursor: pointer; + overflow: hidden; + /* NO rotation on the whole tile — only the SVG inside rotates */ + transition: background var(--transition), border-color var(--transition), box-shadow var(--transition); +} + +.tile:focus-visible { + outline: 2px solid var(--teal); + outline-offset: 2px; + z-index: 2; +} + +/* Powered tile */ +.tile.powered { + background: var(--tile-on); + border-color: rgba(0, 240, 200, 0.42); + box-shadow: 0 0 18px rgba(0, 240, 200, 0.22), inset 0 0 12px rgba(0, 240, 200, 0.08); +} + +/* Source tile */ +.tile.source { + background: var(--tile-src); + border-color: rgba(0, 240, 200, 0.6); + box-shadow: 0 0 24px rgba(0, 240, 200, 0.4), inset 0 0 18px rgba(0, 240, 200, 0.12); +} + +/* Receiver offline */ +.tile.receiver:not(.solved) { + background: var(--tile-rx-off); + border-color: rgba(255, 77, 94, 0.36); +} + +/* Receiver online */ +.tile.receiver.solved { + background: var(--tile-rx-on); + border-color: rgba(57, 232, 143, 0.54); + box-shadow: 0 0 22px rgba(57, 232, 143, 0.32), inset 0 0 14px rgba(57, 232, 143, 0.1); +} + +/* Hint pulse */ +.tile.hint { + animation: hintPulse 600ms ease-in-out 4; +} +@keyframes hintPulse { + 0%, 100% { box-shadow: 0 0 0 rgba(255,184,48,0); } + 50% { box-shadow: 0 0 0 4px rgba(255,184,48,0.55); } +} + +/* ─── SVG Wire inside tile ─── */ +.tile-svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + /* The SVG rotates with the tile's logical rotation */ + transition: transform 180ms cubic-bezier(0.22, 0.61, 0.36, 1); +} + +/* Powered wire colour */ +.tile.powered .wire-path { stroke: var(--teal); filter: url(#wireGlow); } +.tile.source .wire-path { stroke: var(--teal); filter: url(#wireGlow); } +.tile.solved .wire-path { stroke: var(--green); filter: url(#wireGlow); } + +/* Node (center circle) — does NOT rotate with the SVG */ +.tile-node { + position: absolute; + left: 50%; top: 50%; + transform: translate(-50%, -50%); + width: 28%; + aspect-ratio: 1; + border-radius: 50%; + border: 2px solid rgba(0, 240, 200, 0.35); + background: var(--bg-panel); + pointer-events: none; + z-index: 1; + transition: background var(--transition), border-color var(--transition), box-shadow var(--transition); +} +.tile.powered .tile-node { background: #082823; border-color: var(--teal); box-shadow: 0 0 10px var(--teal-glow); } +.tile.source .tile-node { background: var(--teal); border-color: var(--teal); box-shadow: 0 0 16px var(--teal-glow); } +.tile.receiver:not(.solved) .tile-node { background: rgba(255,77,94,0.25); border-color: var(--red); } +.tile.receiver.solved .tile-node { background: rgba(57,232,143,0.25); border-color: var(--green); box-shadow: 0 0 12px rgba(57,232,143,0.6); } + +/* ─── Message bar ─── */ +.message { + min-height: 1.4rem; + font: 500 0.72rem/1.4 var(--font-mono); + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--teal); + text-align: center; + opacity: 0.8; +} + +/* ────────────────────────────────────────────────────────── + SIDE CONSOLE +────────────────────────────────────────────────────────── */ +.console { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.console-section { display: flex; flex-direction: column; gap: 8px; } + +.console-label { + font: 700 0.62rem/1 var(--font-mono); + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--ink-dim); + padding-bottom: 4px; + border-bottom: 1px solid var(--border); +} + +.console-body { + font: 400 0.82rem/1.5 var(--font-mono); + color: var(--ink-muted); +} + +/* Receiver pills */ +.receiver-list { display: flex; flex-direction: column; gap: 6px; } + +.receiver-pill { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 7px 10px; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-raised); + font: 500 0.72rem/1 var(--font-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-dim); + transition: border-color var(--transition), background var(--transition); +} +.receiver-pill::after { + content: '● OFFLINE'; + color: var(--red); + font-size: 0.65rem; + letter-spacing: 0.1em; +} +.receiver-pill.online { + border-color: rgba(57,232,143,0.35); + background: rgba(57,232,143,0.06); + color: var(--green); +} +.receiver-pill.online::after { + content: '● ONLINE'; + color: var(--green); +} + +/* Buttons */ +.btn-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} +.btn-grid button:last-child { grid-column: 1 / -1; } + +button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 38px; + padding: 0 14px; + border: 1px solid var(--border-hi); + border-radius: var(--radius); + background: var(--bg-raised); + color: var(--ink); + font: 600 0.72rem/1 var(--font-mono); + letter-spacing: 0.1em; + text-transform: uppercase; + cursor: pointer; + transition: background var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition); +} +button:hover:not(:disabled) { + background: var(--teal-dim); + border-color: var(--teal); + color: var(--teal); + box-shadow: 0 0 12px rgba(0,240,200,0.2); +} +button:focus-visible { + outline: 2px solid var(--teal); + outline-offset: 2px; +} +button:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +/* Key legend */ +.keys-section { display: none; } /* hidden on mobile — shown on wider screens */ +.key-legend { + display: flex; + flex-direction: column; + gap: 5px; + font: 400 0.68rem/1.5 var(--font-mono); + color: var(--ink-muted); +} +kbd { + display: inline-block; + padding: 1px 5px; + border: 1px solid var(--border-hi); + border-radius: 4px; + font: 500 0.65rem/1.4 var(--font-mono); + color: var(--teal); + background: var(--bg-raised); +} + +/* ────────────────────────────────────────────────────────── + TOAST +────────────────────────────────────────────────────────── */ +.toast { + position: fixed; + left: 50%; + bottom: 24px; + z-index: 50; + transform: translateX(-50%) translateY(10px); + max-width: min(420px, calc(100% - 32px)); + padding: 10px 20px; + border: 1px solid var(--border-hi); + border-radius: 100px; + background: var(--bg-raised); + color: var(--teal); + font: 600 0.74rem/1.3 var(--font-mono); + letter-spacing: 0.1em; + text-transform: uppercase; + text-align: center; + box-shadow: 0 0 30px rgba(0,240,200,0.2); + opacity: 0; + transition: opacity 200ms ease, transform 200ms ease; +} +.toast.visible { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* ────────────────────────────────────────────────────────── + WIN OVERLAY +────────────────────────────────────────────────────────── */ +.win-overlay { + position: fixed; + inset: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + background: rgba(7, 9, 13, 0.88); + backdrop-filter: blur(4px); + animation: fadeIn 350ms ease both; +} +.win-overlay[hidden] { display: none; } + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.particle-canvas { + position: absolute; + inset: 0; + width: 100%; height: 100%; + pointer-events: none; +} + +.win-card { + position: relative; + z-index: 1; + background: var(--bg-panel); + border: 1px solid var(--border-hi); + border-radius: var(--radius-lg); + padding: clamp(28px, 5vw, 56px) clamp(24px, 6vw, 72px); + text-align: center; + box-shadow: + 0 0 0 1px rgba(0,240,200,0.06), + 0 0 80px rgba(0,240,200,0.18), + 0 40px 100px rgba(0,0,0,0.6); + animation: cardPop 400ms cubic-bezier(0.22, 0.61, 0.36, 1) both; +} + +@keyframes cardPop { + from { transform: scale(0.8); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.win-kicker { + font: 700 0.7rem/1 var(--font-mono); + letter-spacing: 0.24em; + text-transform: uppercase; + color: var(--teal); + margin-bottom: 10px; +} +.win-title { + font: 900 2.8rem/1 var(--font-display); + letter-spacing: 0.08em; + text-transform: uppercase; + background: linear-gradient(135deg, var(--teal), var(--amber)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 16px; +} +.win-score { + font: 700 1.4rem/1 var(--font-display); + color: var(--amber); + margin-bottom: 28px; +} +.win-actions { + display: flex; + gap: 12px; + justify-content: center; +} +.win-actions button { + min-width: 120px; +} +#winNextBtn { + background: var(--teal-dim); + border-color: var(--teal); + color: var(--teal); +} +#winNextBtn:hover { + background: var(--teal); + color: var(--bg); +} + +/* ────────────────────────────────────────────────────────── + RESPONSIVE — tablet (≤ 860px) +────────────────────────────────────────────────────────── */ +@media (max-width: 860px) { + .game-area { + grid-template-columns: 1fr; + } + .console { + flex-direction: row; + flex-wrap: wrap; + gap: 12px; + } + .console-section { + flex: 1 1 140px; + min-width: 0; + } + .keys-section { display: none !important; } +} + +/* ────────────────────────────────────────────────────────── + RESPONSIVE — mobile (≤ 600px) +────────────────────────────────────────────────────────── */ +@media (min-width: 600px) { + .keys-section { display: flex; } +} + +@media (max-width: 600px) { + .hud { + grid-template-columns: repeat(2, 1fr); + } + .topbar-brand .brand-name { + font-size: 1rem; + } + .board { + gap: 5px; + } + .console { + flex-direction: column; + } +} + +/* ── Help button (topbar) ── */ +.help-btn { + width: 32px; height: 32px; + border-radius: 50%; + background: transparent; + border: 1px solid var(--border-hi); + color: var(--teal); + font: 700 1rem/1 var(--font-display); + cursor: pointer; + transition: background var(--transition), box-shadow var(--transition); +} +.help-btn:hover { + background: var(--teal-dim); + box-shadow: 0 0 10px var(--teal-glow); +} + +/* ── HUD label variants ── */ +.hud-value.dim { color: var(--ink-dim); } +.hud-value.accent{ color: var(--teal); } + +/* ══════════════════════════════════════════════════════════ + TUTORIAL / HOW-TO-PLAY MODAL +══════════════════════════════════════════════════════════ */ +.tutorial-overlay { + position: fixed; inset: 0; + background: rgba(7,9,13,0.88); + display: flex; align-items: center; justify-content: center; + z-index: 1000; + padding: 16px; + backdrop-filter: blur(6px); +} +.tutorial-overlay[hidden] { display: none; } + +.tutorial-card { + background: var(--bg-panel); + border: 1px solid var(--border-hi); + border-radius: var(--radius-lg); + padding: clamp(20px, 4vw, 40px); + width: min(520px, 100%); + position: relative; + box-shadow: 0 0 60px rgba(0,240,200,0.12); + display: flex; + flex-direction: column; + gap: 0; +} + +/* Steps */ +.tut-step { display: none; flex-direction: column; gap: 14px; } +.tut-step.active { display: flex; } + +.tut-icon { font-size: 2.4rem; text-align: center; } + +.tut-title { + font: 900 1.5rem/1.2 var(--font-display); + text-align: center; + color: var(--ink); + letter-spacing: 0.04em; +} +.tut-title span { color: var(--teal); } + +.tut-body { + font: 400 0.9rem/1.6 var(--font-mono); + color: var(--ink-dim); + text-align: center; +} +.tut-body strong { color: var(--ink); } + +.tut-caption { + font: 400 0.75rem/1.4 var(--font-mono); + color: var(--ink-muted); + text-align: center; +} +.tut-caption-sm { + font: 400 0.78rem/1.5 var(--font-mono); + color: var(--ink-muted); + text-align: center; + margin-top: 8px; +} + +/* Visual demo area */ +.tut-visual { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +/* Demo grid tiles */ +.tut-grid-demo { + flex-direction: row; + justify-content: center; + align-items: center; + gap: 8px; +} + +.demo-tile { + width: 64px; height: 64px; + border-radius: 8px; + background: #121c24; + border: 1px solid rgba(0,240,200,0.2); + display: flex; align-items: center; justify-content: center; + position: relative; + flex-shrink: 0; +} +.demo-tile.sm { width: 48px; height: 48px; } +.demo-tile.dim { opacity: 0.5; } +.demo-tile.lit { border-color: var(--teal); box-shadow: 0 0 12px rgba(0,240,200,0.3); background: #0d2822; } +.demo-tile.source-tile { background: #0a2030; border-color: var(--teal); box-shadow: 0 0 16px rgba(0,240,200,0.4); } +.demo-tile.rx-tile { background: #220a0e; border-color: rgba(255,77,94,0.5); } + +.demo-node { + width: 14px; height: 14px; + border-radius: 50%; + background: #2a3a3a; + border: 2px solid rgba(0,240,200,0.3); + position: absolute; z-index: 2; +} +.demo-node.src { background: var(--teal); border-color: var(--teal); box-shadow: 0 0 8px var(--teal); } +.demo-node.rx { background: rgba(255,77,94,0.5); border-color: #ff4d5e; } +.demo-node.powered-node { background: #082823; border-color: var(--teal); box-shadow: 0 0 8px rgba(0,240,200,0.5); } + +/* Wire drawings */ +.demo-wire { position: absolute; inset: 0; } +.demo-wire::before, .demo-wire::after { content:''; position:absolute; background: rgba(0,240,200,0.35); border-radius:3px; } +.demo-wire.demo-horiz::before { top:50%;left:0;right:0;height:10px; transform:translateY(-50%); } +.demo-wire.demo-vert::before { left:50%;top:0;bottom:0;width:10px; transform:translateX(-50%); } +.demo-wire.demo-cross::before { top:50%;left:0;right:0;height:10px; transform:translateY(-50%); } +.demo-wire.demo-cross::after { left:50%;top:0;bottom:0;width:10px; transform:translateX(-50%); } +.demo-wire.demo-corner-ne::before { left:50%;top:0;height:50%;width:10px; transform:translateX(-50%); } +.demo-wire.demo-corner-ne::after { top:50%;left:50%;right:0;height:10px; transform:translateY(-50%); } +.demo-wire.demo-tee::before { left:50%;top:0;bottom:50%;width:10px; transform:translateX(-50%); background:rgba(0,240,200,0.35); } +.demo-wire.demo-tee::after { top:50%;left:0;right:0;height:10px; transform:translateY(-50%); } + +.demo-arrow { + color: var(--teal); + font-size: 1.2rem; + font-weight: 700; +} + +/* Rotate demo */ +.tut-rotate-demo { gap: 12px; } +.rotate-seq { display:flex; align-items:center; gap:12px; } +.rot-arrow { font: 500 0.75rem/1 var(--font-mono); color: var(--ink-muted); white-space: nowrap; } + +/* Tile type grid */ +.tut-tile-grid { display:flex; flex-direction:column; gap:10px; } +.tile-type-row { display:flex; align-items:center; gap:14px; } +.tile-type-info { font: 400 0.82rem/1.5 var(--font-mono); color: var(--ink-dim); } +.tile-type-info strong { color: var(--ink); } + +/* Win demo */ +.win-demo { gap: 12px; } +.win-demo-row { display:flex; gap:8px; flex-wrap:wrap; justify-content:center; } +.win-arrow { font: 500 0.85rem/1 var(--font-mono); color: var(--ink-muted); text-align:center; } +.pill { + font: 500 0.72rem/1 var(--font-mono); + letter-spacing: 0.08em; + padding: 5px 10px; + border-radius: 20px; +} +.offline-pill { background: rgba(255,77,94,0.12); border:1px solid rgba(255,77,94,0.35); color: #ff4d5e; } +.online-pill { background: rgba(57,232,143,0.12); border:1px solid rgba(57,232,143,0.35); color: #39e88f; } + +/* Tips list */ +.tut-tips { + list-style: none; + display: flex; flex-direction: column; gap: 10px; +} +.tut-tips li { + font: 400 0.84rem/1.5 var(--font-mono); + color: var(--ink-dim); + padding: 10px 14px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); +} +.tut-tips li strong { color: var(--ink); } + +/* Start button */ +.tut-start-btn { + width: 100%; + padding: 14px; + border-radius: var(--radius); + background: var(--teal); + color: #050c10; + font: 700 1rem/1 var(--font-display); + letter-spacing: 0.1em; + border: none; + cursor: pointer; + margin-top: 8px; + transition: opacity var(--transition), box-shadow var(--transition); +} +.tut-start-btn:hover { opacity: 0.88; box-shadow: 0 0 20px var(--teal-glow); } + +/* Navigation */ +.tut-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid var(--border); +} +.tut-dots { display:flex; gap:8px; align-items:center; } +.tut-dot { + width: 8px; height: 8px; + border-radius: 50%; + background: var(--border-hi); + transition: background var(--transition), transform var(--transition); + cursor: pointer; +} +.tut-dot.active { background: var(--teal); transform: scale(1.3); } + +.tut-nav-btns { display:flex; gap:8px; } +.tut-btn { + padding: 8px 16px; + border-radius: var(--radius); + border: 1px solid var(--border-hi); + background: transparent; + color: var(--ink-dim); + font: 500 0.78rem/1 var(--font-mono); + cursor: pointer; + transition: all var(--transition); +} +.tut-btn:hover { background: var(--teal-dim); color: var(--teal); } +.tut-btn.primary { background: var(--teal-dim); color: var(--teal); border-color: var(--teal); } +.tut-btn.primary:hover { background: var(--teal); color: #050c10; } + +.tut-skip { + position: absolute; + top: 12px; right: 14px; + background: none; + border: none; + color: var(--ink-muted); + font: 400 0.72rem/1 var(--font-mono); + cursor: pointer; + transition: color var(--transition); +} +.tut-skip:hover { color: var(--ink); } + +/* kbd in tutorial */ +.tut-tips kbd, .tut-body kbd { + display: inline-block; + padding: 2px 6px; + border-radius: 4px; + background: var(--bg-raised); + border: 1px solid var(--border-hi); + color: var(--teal); + font: 500 0.78em/1.4 var(--font-mono); + vertical-align: middle; +} diff --git a/assets/images/Relay_Rift.svg b/assets/images/Relay_Rift.svg new file mode 100644 index 000000000..ca4fe948b --- /dev/null +++ b/assets/images/Relay_Rift.svg @@ -0,0 +1,177 @@ + + Relay Rift — signal routing puzzle game thumbnail + Dark cyberpunk circuit board with glowing teal signal paths leading from a generator to receivers. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SIGNAL ROUTING PUZZLE + + + RELAY + RIFT + + + + + + + 3 Levels · Move Budget · Score System + Keyboard + Touch · Best Score Saved + + + + PLAY NOW + + diff --git a/assets/js/gamesData.json b/assets/js/gamesData.json index 07dfe9fdd..45cff3898 100644 --- a/assets/js/gamesData.json +++ b/assets/js/gamesData.json @@ -3250,9 +3250,14 @@ "gameUrl": "Random_Choice_Picker", "thumbnailUrl": "Drop_Dash_Game.png" }, - "648":{ + "650":{ "gameTitle" : "Drummer Kit", "gameUrl": "Drummer_Kit", "thumbnailUrl": "Drummer_Kit.png" + }, + "651":{ + "gameTitle" : "Relay Rift", + "gameUrl": "Relay_Rift", + "thumbnailUrl": "Relay_Rift.svg" } }