added sys digger
This commit is contained in:
84
sys_digger/atom.js
Normal file
84
sys_digger/atom.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
sys_digger/constants.js
Normal file
50
sys_digger/constants.js
Normal file
@@ -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
|
||||||
227
sys_digger/game.js
Normal file
227
sys_digger/game.js
Normal file
@@ -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 = `<td colspan="3" style="color:#555;text-align:center;padding:12px">No scores yet</td>`;
|
||||||
|
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 = `
|
||||||
|
<td class="rank-num">${medal}</td>
|
||||||
|
<td style="text-align:left">${escapeHtml(entry.name)}</td>
|
||||||
|
<td style="text-align:right">${entry.score}</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return str.replace(/&/g, '&').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);
|
||||||
238
sys_digger/index.html
Normal file
238
sys_digger/index.html
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Sys Digger</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
background: #0a0a1a;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
canvas { display: block; cursor: crosshair; }
|
||||||
|
|
||||||
|
#score {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
color: #00ff88;
|
||||||
|
font-size: 24px;
|
||||||
|
text-shadow: 0 0 10px #00ff88;
|
||||||
|
}
|
||||||
|
#highscore {
|
||||||
|
position: absolute;
|
||||||
|
top: 52px;
|
||||||
|
right: 20px;
|
||||||
|
color: #ffcc00;
|
||||||
|
font-size: 16px;
|
||||||
|
text-shadow: 0 0 8px #ffcc00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cystinstein-area {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
width: 160px;
|
||||||
|
border: 2px solid #ff4444;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
#cystinstein-label {
|
||||||
|
color: #ff4444;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-shadow: 0 0 8px #ff4444;
|
||||||
|
}
|
||||||
|
#cystinstein-canvas { display: block; margin: 0 auto; }
|
||||||
|
#docks-count {
|
||||||
|
color: #ff8888;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Game-over overlay ── */
|
||||||
|
#overlay {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.90);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
#overlay.active { display: flex; }
|
||||||
|
|
||||||
|
#overlay h1 {
|
||||||
|
font-size: 44px;
|
||||||
|
color: #ff4444;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-shadow: 0 0 30px #ff4444;
|
||||||
|
animation: flicker 1s infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes flicker {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0.75; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#final-score {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #00ff88;
|
||||||
|
text-shadow: 0 0 8px #00ff88;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
#new-record {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #ffcc00;
|
||||||
|
text-shadow: 0 0 12px #ffcc00;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
animation: flicker 0.6s infinite alternate;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#new-record.visible { display: block; }
|
||||||
|
|
||||||
|
/* ── Name form ── */
|
||||||
|
#name-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
#player-name {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #00ff88;
|
||||||
|
border-radius: 6px;
|
||||||
|
outline: none;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
#player-name:focus { border-color: #00ffcc; }
|
||||||
|
#submit-score-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #00ff88;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
#submit-score-btn:hover { background: #00cc66; }
|
||||||
|
|
||||||
|
/* ── Leaderboard ── */
|
||||||
|
#leaderboard {
|
||||||
|
width: 340px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
#leaderboard h2 {
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
color: #ffcc00;
|
||||||
|
text-shadow: 0 0 8px #ffcc00;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
#leaderboard table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
#leaderboard th {
|
||||||
|
color: #888;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
#leaderboard td {
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: #ccc;
|
||||||
|
border-bottom: 1px solid #1a1a2e;
|
||||||
|
}
|
||||||
|
#leaderboard tr.rank-1 td { color: #ffd700; }
|
||||||
|
#leaderboard tr.rank-2 td { color: #c0c0c0; }
|
||||||
|
#leaderboard tr.rank-3 td { color: #cd7f32; }
|
||||||
|
#leaderboard tr.highlight td {
|
||||||
|
color: #00ff88;
|
||||||
|
text-shadow: 0 0 6px #00ff88;
|
||||||
|
background: rgba(0, 255, 136, 0.06);
|
||||||
|
}
|
||||||
|
.rank-num { width: 32px; text-align: center; color: #555; }
|
||||||
|
|
||||||
|
#restart-btn {
|
||||||
|
padding: 14px 40px;
|
||||||
|
background: #00ff88;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
#restart-btn:hover { background: #00cc66; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="gameCanvas"></canvas>
|
||||||
|
|
||||||
|
<div id="cystinstein-area">
|
||||||
|
<div id="cystinstein-label">CYSTINSTEIN</div>
|
||||||
|
<canvas id="cystinstein-canvas" width="130" height="130"></canvas>
|
||||||
|
<div id="docks-count">0 / 8 docks</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="score">Score: 0</div>
|
||||||
|
<div id="highscore">Best: —</div>
|
||||||
|
|
||||||
|
<div id="overlay">
|
||||||
|
<h1>CYSTINSTEIN FORMED!</h1>
|
||||||
|
<p id="final-score"></p>
|
||||||
|
<p id="new-record">★ NEW HIGH SCORE ★</p>
|
||||||
|
|
||||||
|
<div id="name-form">
|
||||||
|
<input id="player-name" type="text" maxlength="20" placeholder="Your name" autocomplete="off" />
|
||||||
|
<button id="submit-score-btn" onclick="submitScore()">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="leaderboard">
|
||||||
|
<h2>LEADERBOARD</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="rank-num">#</th>
|
||||||
|
<th style="text-align:left">Name</th>
|
||||||
|
<th style="text-align:right">Score</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="leaderboard-rows"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="restart-btn" onclick="restartGame()">Play Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="constants.js"></script>
|
||||||
|
<script src="atom.js"></script>
|
||||||
|
<script src="render.js"></script>
|
||||||
|
<script src="game.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
sys_digger/readme.md
Normal file
8
sys_digger/readme.md
Normal file
@@ -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.
|
||||||
210
sys_digger/render.js
Normal file
210
sys_digger/render.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user