diff --git a/sys_digger/atom.js b/sys_digger/atom.js
new file mode 100644
index 0000000..668b123
--- /dev/null
+++ b/sys_digger/atom.js
@@ -0,0 +1,84 @@
+class IncomingAtom {
+ constructor(anchorIndex) {
+ this.anchorIndex = anchorIndex;
+ const ap = anchorPoints[anchorIndex];
+ const far = Math.max(canvas.width, canvas.height) * 0.9;
+ this.x = CX + Math.cos(ap.angle) * far;
+ this.y = CY + Math.sin(ap.angle) * far;
+ this.tx = ap.x;
+ this.ty = ap.y;
+ this.speed = ATOM_SPEED_MIN + Math.random() * (ATOM_SPEED_MAX - ATOM_SPEED_MIN);
+ this.radius = ATOM_RADIUS_MIN + Math.random() * (ATOM_RADIUS_MAX - ATOM_RADIUS_MIN);
+ this.hue = ATOM_HUE_MIN + Math.random() * ATOM_HUE_RANGE;
+ this.dead = false;
+ this.eAngle = Math.random() * Math.PI * 2;
+ this.eSpeed = ATOM_ELECTRON_SPEED_MIN + Math.random() * (ATOM_ELECTRON_SPEED_MAX - ATOM_ELECTRON_SPEED_MIN);
+ // ap.active is set once the atom enters the visible screen (see update)
+ }
+
+ update() {
+ if (this.dead || gameOver) return;
+
+ // Activate the anchor the moment the atom becomes visible
+ const ap = anchorPoints[this.anchorIndex];
+ if (!ap.active &&
+ this.x >= 0 && this.x <= canvas.width &&
+ this.y >= 0 && this.y <= canvas.height) {
+ ap.active = true;
+ }
+
+ const dx = this.tx - this.x;
+ const dy = this.ty - this.y;
+ const dist = Math.hypot(dx, dy);
+
+ if (dist < this.speed) {
+ this.dead = true;
+ anchorPoints[this.anchorIndex].active = false;
+ docks++;
+ document.getElementById('docks-count').textContent = `${docks} / ${MAX_DOCKS} docks`;
+ updateCystinstein();
+ burst(this.tx, this.ty, '#ff4444');
+ if (docks >= MAX_DOCKS) triggerGameOver();
+ } else {
+ this.x += (dx / dist) * this.speed;
+ this.y += (dy / dist) * this.speed;
+ this.eAngle += this.eSpeed;
+ }
+ }
+
+ draw() {
+ if (this.dead) return;
+ const c = `hsl(${this.hue}, 80%, 60%)`;
+
+ const g = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius * 2.5);
+ g.addColorStop(0, `hsla(${this.hue}, 80%, 60%, 0.4)`);
+ g.addColorStop(1, 'transparent');
+ ctx.fillStyle = g;
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.radius * 2.5, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.fillStyle = c;
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.radius * 0.55, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.save();
+ ctx.translate(this.x, this.y);
+ ctx.strokeStyle = `hsla(${this.hue}, 80%, 70%, 0.35)`;
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.ellipse(0, 0, this.radius, this.radius * 0.4, this.eAngle * 0.25, 0, Math.PI * 2);
+ ctx.stroke();
+
+ ctx.fillStyle = '#fff';
+ ctx.beginPath();
+ ctx.arc(
+ Math.cos(this.eAngle) * this.radius,
+ Math.sin(this.eAngle) * (this.radius * 0.4),
+ 3, 0, Math.PI * 2
+ );
+ ctx.fill();
+ ctx.restore();
+ }
+}
diff --git a/sys_digger/constants.js b/sys_digger/constants.js
new file mode 100644
index 0000000..9fd27af
--- /dev/null
+++ b/sys_digger/constants.js
@@ -0,0 +1,50 @@
+// ─── Game rules ──────────────────────────────────────────────────────────────
+const MAX_DOCKS = 8; // failed docks before game over
+const NUM_ANCHORS = 6; // anchor points around the central molecule
+const ANCHOR_RADIUS = 120; // px distance from center to anchor points
+const CLICK_RADIUS = 38; // px hit radius for clicking an anchor
+
+// ─── Spawn timing ────────────────────────────────────────────────────────────
+const SPAWN_INTERVAL_START = 2500; // ms between spawns at the start
+const SPAWN_INTERVAL_MIN = 700; // ms floor (fastest difficulty)
+const SPAWN_DIFFICULTY_STEP = 18; // ms reduction per spawn
+
+// ─── Incoming atom ───────────────────────────────────────────────────────────
+const ATOM_SPEED_MIN = 1.4; // px/frame minimum
+const ATOM_SPEED_MAX = 3.2; // px/frame maximum (min + random range)
+const ATOM_RADIUS_MIN = 16; // px nucleus radius minimum
+const ATOM_RADIUS_MAX = 24; // px nucleus radius maximum (min + random range)
+const ATOM_HUE_MIN = 180; // hsl hue range start (blue-cyan)
+const ATOM_HUE_RANGE = 60; // hsl hue range width
+const ATOM_ELECTRON_SPEED_MIN = 0.05;
+const ATOM_ELECTRON_SPEED_MAX = 0.08;
+
+// ─── Particles ───────────────────────────────────────────────────────────────
+const BURST_COUNT = 20; // particles per burst
+const BURST_SPEED_MIN = 2;
+const BURST_SPEED_MAX = 7;
+const PARTICLE_DECAY_MIN = 0.022;
+const PARTICLE_DECAY_MAX = 0.047;
+const PARTICLE_RADIUS_MIN = 2;
+const PARTICLE_RADIUS_MAX = 6;
+
+// ─── Anchor block flash ───────────────────────────────────────────────────────
+const BLOCK_TIMER_FRAMES = 20; // frames the green flash lasts after a block
+
+// ─── Stars ───────────────────────────────────────────────────────────────────
+const STAR_COUNT = 160;
+const STAR_ALPHA_MIN = 0.15;
+const STAR_ALPHA_RANGE = 0.55;
+const STAR_RADIUS_MAX = 1.5;
+
+// ─── Central nucleus ─────────────────────────────────────────────────────────
+const NUCLEUS_RADIUS = 52; // px
+const NUCLEUS_ORBITS = 3; // electron orbits to draw
+
+// ─── Cystinstein indicator ────────────────────────────────────────────────────
+const CYS_CENTER_X = 65; // px within the 130×130 side canvas
+const CYS_CENTER_Y = 65;
+const CYS_CORE_RADIUS = 14; // central atom radius
+const CYS_DOCK_RADIUS = 28; // distance from center to docked atoms
+const CYS_ATOM_RADIUS = 9; // docked atom radius
+const CYS_RING_RADIUS = 48; // final danger ring radius
diff --git a/sys_digger/game.js b/sys_digger/game.js
new file mode 100644
index 0000000..3c2e7d3
--- /dev/null
+++ b/sys_digger/game.js
@@ -0,0 +1,227 @@
+const canvas = document.getElementById('gameCanvas');
+const ctx = canvas.getContext('2d');
+const cysCanvas = document.getElementById('cystinstein-canvas');
+const cysCtx = cysCanvas.getContext('2d');
+
+canvas.width = window.innerWidth;
+canvas.height = window.innerHeight;
+
+const CX = canvas.width / 2;
+const CY = canvas.height / 2;
+
+// ─── Leaderboard ─────────────────────────────────────────────────────────────
+const LEADERBOARD_KEY = 'sysdigger_leaderboard';
+const LEADERBOARD_MAX = 10;
+
+function loadLeaderboard() {
+ try {
+ return JSON.parse(localStorage.getItem(LEADERBOARD_KEY)) || [];
+ } catch {
+ return [];
+ }
+}
+
+function saveLeaderboard(board) {
+ localStorage.setItem(LEADERBOARD_KEY, JSON.stringify(board));
+}
+
+function addEntry(name, score) {
+ const board = loadLeaderboard();
+ board.push({ name: name.trim() || 'Anonymous', score });
+ board.sort((a, b) => b.score - a.score);
+ const trimmed = board.slice(0, LEADERBOARD_MAX);
+ saveLeaderboard(trimmed);
+ return trimmed;
+}
+
+function renderLeaderboard(highlightEntry) {
+ const board = loadLeaderboard();
+ const tbody = document.getElementById('leaderboard-rows');
+ tbody.innerHTML = '';
+
+ if (board.length === 0) {
+ const tr = document.createElement('tr');
+ tr.innerHTML = `
No scores yet | `;
+ tbody.appendChild(tr);
+ return;
+ }
+
+ board.forEach((entry, i) => {
+ const tr = document.createElement('tr');
+ const isHighlight = highlightEntry &&
+ entry.name === highlightEntry.name &&
+ entry.score === highlightEntry.score;
+
+ if (isHighlight) tr.classList.add('highlight');
+ else if (i === 0) tr.classList.add('rank-1');
+ else if (i === 1) tr.classList.add('rank-2');
+ else if (i === 2) tr.classList.add('rank-3');
+
+ const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : i + 1;
+ tr.innerHTML = `
+ ${medal} |
+ ${escapeHtml(entry.name)} |
+ ${entry.score} | `;
+ tbody.appendChild(tr);
+ });
+}
+
+function escapeHtml(str) {
+ return str.replace(/&/g, '&').replace(//g, '>');
+}
+
+function topScore() {
+ const board = loadLeaderboard();
+ return board.length > 0 ? board[0].score : null;
+}
+
+// ─── State ───────────────────────────────────────────────────────────────────
+let score = 0;
+let docks = 0;
+let gameOver = false;
+let incomingAtoms = [];
+let anchorPoints = [];
+let particles = [];
+let clickEffects = [];
+let lastSpawn = 0;
+let spawnInterval = SPAWN_INTERVAL_START;
+
+const stars = Array.from({ length: STAR_COUNT }, () => ({
+ x: Math.random() * canvas.width,
+ y: Math.random() * canvas.height,
+ r: Math.random() * STAR_RADIUS_MAX,
+ a: STAR_ALPHA_MIN + Math.random() * STAR_ALPHA_RANGE
+}));
+
+// ─── Anchors ─────────────────────────────────────────────────────────────────
+function initAnchors() {
+ anchorPoints = [];
+ for (let i = 0; i < NUM_ANCHORS; i++) {
+ const angle = (i / NUM_ANCHORS) * Math.PI * 2 - Math.PI / 2;
+ anchorPoints.push({
+ x: CX + Math.cos(angle) * ANCHOR_RADIUS,
+ y: CY + Math.sin(angle) * ANCHOR_RADIUS,
+ angle,
+ active: false,
+ blockTimer: 0,
+ pulsePhase: Math.random() * Math.PI * 2
+ });
+ }
+}
+
+// ─── Spawn ───────────────────────────────────────────────────────────────────
+function spawnAtom() {
+ const available = anchorPoints.map((_, i) => i).filter(i => !anchorPoints[i].active);
+ if (available.length === 0) return;
+ const idx = available[Math.floor(Math.random() * available.length)];
+ incomingAtoms.push(new IncomingAtom(idx));
+}
+
+// ─── Input ───────────────────────────────────────────────────────────────────
+canvas.addEventListener('click', e => {
+ if (gameOver) return;
+ const rect = canvas.getBoundingClientRect();
+ const mx = e.clientX - rect.left;
+ const my = e.clientY - rect.top;
+ let hit = false;
+
+ anchorPoints.forEach((ap, i) => {
+ if (!ap.active) return;
+ if (Math.hypot(mx - ap.x, my - ap.y) > CLICK_RADIUS) return;
+
+ const atom = incomingAtoms.find(a => a.anchorIndex === i && !a.dead);
+ if (atom) {
+ atom.dead = true;
+ ap.active = false;
+ ap.blockTimer = BLOCK_TIMER_FRAMES;
+ score++;
+ document.getElementById('score').textContent = `Score: ${score}`;
+ burst(ap.x, ap.y, '#00ff88');
+ clickEffects.push({ x: ap.x, y: ap.y, radius: 5, life: 1 });
+ hit = true;
+ }
+ });
+
+ if (!hit) clickEffects.push({ x: mx, y: my, radius: 5, life: 0.5 });
+});
+
+// ─── Game flow ───────────────────────────────────────────────────────────────
+function triggerGameOver() {
+ gameOver = true;
+ const best = topScore();
+ const isNewRecord = best === null || score > best;
+
+ document.getElementById('final-score').textContent = `Score: ${score}`;
+ document.getElementById('new-record').classList.toggle('visible', isNewRecord);
+
+ const input = document.getElementById('player-name');
+ input.value = '';
+ setTimeout(() => input.focus(), 50);
+
+ renderLeaderboard(null);
+ document.getElementById('overlay').classList.add('active');
+}
+
+function submitScore() {
+ const name = document.getElementById('player-name').value.trim() || 'Anonymous';
+ const entry = { name, score };
+ addEntry(name, score);
+ renderLeaderboard(entry);
+
+ const best = topScore();
+ updateHudBest(best);
+
+ document.getElementById('name-form').style.display = 'none';
+}
+
+function restartGame() {
+ score = docks = 0;
+ gameOver = false;
+ incomingAtoms = [];
+ particles = [];
+ clickEffects = [];
+ lastSpawn = 0;
+ spawnInterval = SPAWN_INTERVAL_START;
+ document.getElementById('score').textContent = 'Score: 0';
+ document.getElementById('docks-count').textContent = '0 / 8 docks';
+ document.getElementById('name-form').style.display = 'flex';
+ document.getElementById('overlay').classList.remove('active');
+ initAnchors();
+ updateCystinstein();
+}
+
+function updateHudBest(best) {
+ document.getElementById('highscore').textContent = best !== null ? `Best: ${best}` : 'Best: —';
+}
+
+// ─── Enter key on name input ──────────────────────────────────────────────────
+document.getElementById('player-name').addEventListener('keydown', e => {
+ if (e.key === 'Enter') submitScore();
+});
+
+// ─── Main loop ───────────────────────────────────────────────────────────────
+function gameLoop(ts) {
+ drawBackground();
+
+ if (!gameOver && ts - lastSpawn > spawnInterval) {
+ spawnAtom();
+ lastSpawn = ts;
+ spawnInterval = Math.max(SPAWN_INTERVAL_MIN, spawnInterval - SPAWN_DIFFICULTY_STEP);
+ }
+
+ incomingAtoms = incomingAtoms.filter(a => !a.dead);
+ incomingAtoms.forEach(a => { a.update(); a.draw(); });
+
+ drawCentralMolecule();
+ drawAnchorPoints();
+ drawParticles();
+ drawClickEffects();
+
+ requestAnimationFrame(gameLoop);
+}
+
+// ─── Boot ────────────────────────────────────────────────────────────────────
+updateHudBest(topScore());
+initAnchors();
+updateCystinstein();
+requestAnimationFrame(gameLoop);
diff --git a/sys_digger/index.html b/sys_digger/index.html
new file mode 100644
index 0000000..58d648a
--- /dev/null
+++ b/sys_digger/index.html
@@ -0,0 +1,238 @@
+
+
+
+
+
+ Sys Digger
+
+
+
+
+
+
+
CYSTINSTEIN
+
+
0 / 8 docks
+
+
+Score: 0
+Best: —
+
+
+
CYSTINSTEIN FORMED!
+
+
★ NEW HIGH SCORE ★
+
+
+
+
+
+
+
+
LEADERBOARD
+
+
+
+ | # |
+ Name |
+ Score |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sys_digger/readme.md b/sys_digger/readme.md
new file mode 100644
index 0000000..b7686b6
--- /dev/null
+++ b/sys_digger/readme.md
@@ -0,0 +1,8 @@
+Game Sys Digger
+
+We are going to develop an html game called Sys Digger. The game will be loosly based on the game whac-a-mole.
+In this game the user has to protect an atom from growing to a Cystinstein. For this the user has to hit on the end of the atom where other atom's can connect to form a molekul.
+The user has to prevent this so the atom doesn't grow into a Cystinstein. When to many connections are made the user looses the game.
+
+In the center of the game the molekul is shown. This molekul has a set of anker points where other morlekuls can dock onto to create the Cystinstein. The user has to prevent the other modules from docking by hitting the anker point. For every successfull prevented dock the user gets a point.
+In the left upper corner a small area where the growing of the Cystinstein is shown. When the user is to late to preent the dock the Cystinstein in this area grows. When the user is uanble to prevent 8 docks the user looses the game and Cystinstein is at his max size.
diff --git a/sys_digger/render.js b/sys_digger/render.js
new file mode 100644
index 0000000..f86e55f
--- /dev/null
+++ b/sys_digger/render.js
@@ -0,0 +1,210 @@
+function burst(x, y, color) {
+ for (let i = 0; i < BURST_COUNT; i++) {
+ const a = Math.random() * Math.PI * 2;
+ const s = BURST_SPEED_MIN + Math.random() * (BURST_SPEED_MAX - BURST_SPEED_MIN);
+ particles.push({
+ x, y,
+ vx: Math.cos(a) * s, vy: Math.sin(a) * s,
+ life: 1,
+ decay: PARTICLE_DECAY_MIN + Math.random() * (PARTICLE_DECAY_MAX - PARTICLE_DECAY_MIN),
+ r: PARTICLE_RADIUS_MIN + Math.random() * (PARTICLE_RADIUS_MAX - PARTICLE_RADIUS_MIN),
+ color
+ });
+ }
+}
+
+function drawBackground() {
+ ctx.fillStyle = '#0a0a1a';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ stars.forEach(s => {
+ ctx.fillStyle = `rgba(255,255,255,${s.a})`;
+ ctx.beginPath();
+ ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
+ ctx.fill();
+ });
+}
+
+function drawCentralMolecule() {
+ const t = Date.now() / 1000;
+
+ const amb = ctx.createRadialGradient(CX, CY, 10, CX, CY, ANCHOR_RADIUS + 50);
+ amb.addColorStop(0, 'rgba(0,180,255,0.12)');
+ amb.addColorStop(1, 'transparent');
+ ctx.fillStyle = amb;
+ ctx.beginPath();
+ ctx.arc(CX, CY, ANCHOR_RADIUS + 50, 0, Math.PI * 2);
+ ctx.fill();
+
+ anchorPoints.forEach(ap => {
+ ctx.beginPath();
+ ctx.moveTo(CX, CY);
+ ctx.lineTo(ap.x, ap.y);
+ ctx.strokeStyle = ap.active ? 'rgba(255,80,80,0.45)' : 'rgba(0,200,255,0.18)';
+ ctx.lineWidth = ap.active ? 2 : 1;
+ ctx.stroke();
+ });
+
+ const ng = ctx.createRadialGradient(CX, CY, 0, CX, CY, NUCLEUS_RADIUS);
+ ng.addColorStop(0, '#00ffcc');
+ ng.addColorStop(0.55, '#0088ff');
+ ng.addColorStop(1, 'rgba(0,80,220,0.3)');
+ ctx.fillStyle = ng;
+ ctx.beginPath();
+ ctx.arc(CX, CY, NUCLEUS_RADIUS, 0, Math.PI * 2);
+ ctx.fill();
+
+ const pulse = Math.sin(t * 2) * 0.3 + 0.7;
+ ctx.strokeStyle = `rgba(0,255,200,${pulse * 0.6})`;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.arc(CX, CY, NUCLEUS_RADIUS + Math.sin(t * 2) * 5, 0, Math.PI * 2);
+ ctx.stroke();
+
+ ctx.fillStyle = 'rgba(255,255,255,0.55)';
+ ctx.beginPath();
+ ctx.arc(CX - 16, CY - 16, 8, 0, Math.PI * 2);
+ ctx.fill();
+
+ for (let i = 0; i < NUCLEUS_ORBITS; i++) {
+ const orbitAngle = t * (0.45 + i * 0.3) + (i * Math.PI * 2 / 3);
+ const rx = 72 + i * 10;
+ const ry = 28 + i * 8;
+ const tilt = i * Math.PI / 3;
+
+ ctx.save();
+ ctx.translate(CX, CY);
+ ctx.rotate(tilt);
+ ctx.strokeStyle = 'rgba(0,200,255,0.13)';
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
+ ctx.stroke();
+
+ ctx.fillStyle = '#88eeff';
+ ctx.beginPath();
+ ctx.arc(Math.cos(orbitAngle) * rx, Math.sin(orbitAngle) * ry, 4, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+ }
+}
+
+function drawAnchorPoints() {
+ const t = Date.now() / 1000;
+ anchorPoints.forEach(ap => {
+ const pulse = Math.sin(t * 3 + ap.pulsePhase) * 0.5 + 0.5;
+
+ if (ap.blockTimer > 0) {
+ ap.blockTimer--;
+ ctx.strokeStyle = `rgba(0,255,100,${ap.blockTimer / BLOCK_TIMER_FRAMES})`;
+ ctx.lineWidth = 3;
+ ctx.beginPath();
+ ctx.arc(ap.x, ap.y, 20 + (BLOCK_TIMER_FRAMES - ap.blockTimer), 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.fillStyle = 'rgba(0,255,100,0.55)';
+ ctx.beginPath();
+ ctx.arc(ap.x, ap.y, 13, 0, Math.PI * 2);
+ ctx.fill();
+ } else if (ap.active) {
+ ctx.strokeStyle = `rgba(255,70,70,${0.5 + pulse * 0.5})`;
+ ctx.lineWidth = 2.5;
+ ctx.beginPath();
+ ctx.arc(ap.x, ap.y, 18 + pulse * 6, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.fillStyle = 'rgba(255,70,70,0.35)';
+ ctx.beginPath();
+ ctx.arc(ap.x, ap.y, 15, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.strokeStyle = 'rgba(255,200,200,0.85)';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(ap.x - 9, ap.y); ctx.lineTo(ap.x + 9, ap.y);
+ ctx.moveTo(ap.x, ap.y - 9); ctx.lineTo(ap.x, ap.y + 9);
+ ctx.stroke();
+ } else {
+ ctx.strokeStyle = `rgba(0,200,255,${0.18 + pulse * 0.28})`;
+ ctx.lineWidth = 1.5;
+ ctx.beginPath();
+ ctx.arc(ap.x, ap.y, 13, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.fillStyle = 'rgba(0,200,255,0.08)';
+ ctx.beginPath();
+ ctx.arc(ap.x, ap.y, 13, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ });
+}
+
+function drawParticles() {
+ particles = particles.filter(p => p.life > 0);
+ particles.forEach(p => {
+ p.x += p.vx; p.y += p.vy;
+ p.vx *= 0.94; p.vy *= 0.94;
+ p.life -= p.decay;
+ ctx.globalAlpha = p.life;
+ ctx.fillStyle = p.color;
+ ctx.beginPath();
+ ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.globalAlpha = 1;
+ });
+}
+
+function drawClickEffects() {
+ clickEffects = clickEffects.filter(e => e.life > 0);
+ clickEffects.forEach(e => {
+ e.radius += 3;
+ e.life -= 0.06;
+ ctx.globalAlpha = e.life;
+ ctx.strokeStyle = '#00ff88';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.globalAlpha = 1;
+ });
+}
+
+function updateCystinstein() {
+ cysCtx.clearRect(0, 0, cysCanvas.width, cysCanvas.height);
+
+ cysCtx.fillStyle = '#00aaff';
+ cysCtx.shadowColor = '#00aaff';
+ cysCtx.shadowBlur = docks > 0 ? 6 : 0;
+ cysCtx.beginPath();
+ cysCtx.arc(CYS_CENTER_X, CYS_CENTER_Y, CYS_CORE_RADIUS, 0, Math.PI * 2);
+ cysCtx.fill();
+ cysCtx.shadowBlur = 0;
+
+ for (let i = 0; i < docks && i < MAX_DOCKS; i++) {
+ const angle = (i / MAX_DOCKS) * Math.PI * 2 - Math.PI / 2;
+ const ax = CYS_CENTER_X + Math.cos(angle) * CYS_DOCK_RADIUS;
+ const ay = CYS_CENTER_Y + Math.sin(angle) * CYS_DOCK_RADIUS;
+ const danger = i / (MAX_DOCKS - 1);
+
+ cysCtx.strokeStyle = `rgba(255,${Math.floor(150 - danger * 150)},50,0.8)`;
+ cysCtx.lineWidth = 2;
+ cysCtx.beginPath();
+ cysCtx.moveTo(CYS_CENTER_X, CYS_CENTER_Y);
+ cysCtx.lineTo(ax, ay);
+ cysCtx.stroke();
+
+ cysCtx.fillStyle = `rgb(${Math.floor(200 + danger * 55)},${Math.floor(100 - danger * 100)},50)`;
+ cysCtx.shadowColor = '#ff4444';
+ cysCtx.shadowBlur = danger > 0.7 ? 8 : 0;
+ cysCtx.beginPath();
+ cysCtx.arc(ax, ay, CYS_ATOM_RADIUS, 0, Math.PI * 2);
+ cysCtx.fill();
+ cysCtx.shadowBlur = 0;
+ }
+
+ if (docks >= MAX_DOCKS) {
+ cysCtx.strokeStyle = 'rgba(255,0,0,0.9)';
+ cysCtx.lineWidth = 3;
+ cysCtx.shadowColor = '#ff0000';
+ cysCtx.shadowBlur = 15;
+ cysCtx.beginPath();
+ cysCtx.arc(CYS_CENTER_X, CYS_CENTER_Y, CYS_RING_RADIUS, 0, Math.PI * 2);
+ cysCtx.stroke();
+ cysCtx.shadowBlur = 0;
+ }
+}