sy_digger
This commit is contained in:
@@ -12,7 +12,10 @@
|
|||||||
|
|
||||||
<a href="/games/Cyst_Kid/index.html">Cyst Kid</a>
|
<a href="/games/Cyst_Kid/index.html">Cyst Kid</a>
|
||||||
<br><br>
|
<br><br>
|
||||||
<a href="/games/test/index.html">Kidney Lab</a>
|
<a href="/games/kidney_lab/index.html">Kidney Lab</a>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<a href="/games/sys_digger/index.html">Sys Digger</a>
|
||||||
<br><br>
|
<br><br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
59
sys_digger/CLAUDE.md
Normal file
59
sys_digger/CLAUDE.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Running the game
|
||||||
|
|
||||||
|
Open `index.html` directly in a browser — no build step, no server required.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
There are no tests, no linter, and no package manager.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Pure vanilla JS/HTML/CSS game. No frameworks, no modules — all files are loaded as classic `<script>` tags and share a single global scope. **Load order matters**:
|
||||||
|
|
||||||
|
```
|
||||||
|
constants.js → atom.js → render.js → game.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### File responsibilities
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `constants.js` | Single source of truth for every tunable value (speeds, counts, radii, timing). Edit here first when tweaking gameplay feel. |
|
||||||
|
| `atom.js` | `IncomingAtom` class — construction, per-frame movement, docking logic, self-draw. Reads globals from `game.js` (`canvas`, `CX`, `CY`, `anchorPoints`, `docks`, `gameOver`) and calls `burst()`, `updateCystinstein()`, `triggerGameOver()` from `render.js`/`game.js`. |
|
||||||
|
| `render.js` | All canvas drawing: background, central molecule, anchor points, particles, click effects, and the Cystinstein side-panel. Also owns `burst()` (particle spawner) and `updateCystinstein()`. Reads `ctx`, `cysCtx`, `CX`, `CY`, `anchorPoints`, `particles`, `clickEffects`, `docks` from `game.js`. |
|
||||||
|
| `game.js` | Game state, `requestAnimationFrame` loop, input handling, leaderboard (localStorage), screen transitions (intro → game → game-over → intro). Declares all shared mutable globals consumed by the other files. |
|
||||||
|
| `index.html` | CSS + HTML structure only. Houses the intro screen, attract screen, game-over overlay, leaderboard table, and the kidney-shaped Cystinstein side panel (inline SVG border). |
|
||||||
|
|
||||||
|
### Global state (declared in `game.js`, used everywhere)
|
||||||
|
|
||||||
|
`canvas`, `ctx`, `cysCanvas`, `cysCtx`, `CX`, `CY`, `score`, `docks`, `gameOver`, `gameStarted`, `incomingAtoms`, `anchorPoints`, `particles`, `clickEffects`, `stars`.
|
||||||
|
|
||||||
|
### Screen flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Boot → Intro ──(30 s idle)──→ Attract (highscore)
|
||||||
|
←─(any key/click)─
|
||||||
|
│
|
||||||
|
└─(START clicked)──→ Game loop
|
||||||
|
│
|
||||||
|
(6 docks)──→ Game-over overlay
|
||||||
|
│
|
||||||
|
(Play Again)──→ Intro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Leaderboard persistence
|
||||||
|
|
||||||
|
Stored in `localStorage` under key `sysdigger_leaderboard` as a JSON array of `{name, score}` objects, capped at 10 entries, sorted descending by score.
|
||||||
|
|
||||||
|
### Key design constraints
|
||||||
|
|
||||||
|
- Anchor points activate (turn red) only when the incoming atom **enters the visible viewport**, not at spawn time — see `IncomingAtom.update()` in `atom.js`.
|
||||||
|
- Difficulty ramps automatically: `spawnInterval` decreases by `SPAWN_DIFFICULTY_STEP` each spawn, floored at `SPAWN_INTERVAL_MIN`.
|
||||||
|
- The custom cursor is a data-URL SVG pill embedded directly in CSS on the `canvas` selector.
|
||||||
|
- The Cystinstein side panel uses an inline SVG with a bezier kidney path for its border; the interior atom-cluster is drawn on a separate `<canvas id="cystinstein-canvas">` by `updateCystinstein()` in `render.js`.
|
||||||
@@ -189,11 +189,67 @@ function restartGame() {
|
|||||||
document.getElementById('name-form').style.display = 'flex';
|
document.getElementById('name-form').style.display = 'flex';
|
||||||
document.getElementById('overlay').classList.remove('active');
|
document.getElementById('overlay').classList.remove('active');
|
||||||
document.getElementById('intro').classList.remove('hidden');
|
document.getElementById('intro').classList.remove('hidden');
|
||||||
|
startIdleTimer();
|
||||||
initAnchors();
|
initAnchors();
|
||||||
updateCystinstein();
|
updateCystinstein();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Attract mode ────────────────────────────────────────────────────────────
|
||||||
|
const ATTRACT_DELAY = 30_000;
|
||||||
|
let idleTimer = null;
|
||||||
|
|
||||||
|
function startIdleTimer() {
|
||||||
|
clearTimeout(idleTimer);
|
||||||
|
idleTimer = setTimeout(showAttract, ATTRACT_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAttract() {
|
||||||
|
document.getElementById('intro').classList.add('hidden');
|
||||||
|
renderAttractLeaderboard();
|
||||||
|
document.getElementById('attract').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showIntro() {
|
||||||
|
document.getElementById('attract').classList.add('hidden');
|
||||||
|
document.getElementById('intro').classList.remove('hidden');
|
||||||
|
startIdleTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAttractLeaderboard() {
|
||||||
|
const board = loadLeaderboard();
|
||||||
|
const tbody = document.getElementById('attract-rows');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if (board.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="3" style="color:#555;text-align:center;padding:16px">No scores yet — be the first!</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
board.forEach((entry, i) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
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 dismissAttract() {
|
||||||
|
if (!document.getElementById('attract').classList.contains('hidden')) {
|
||||||
|
showIntro();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', dismissAttract);
|
||||||
|
document.addEventListener('click', dismissAttract);
|
||||||
|
|
||||||
function startGame() {
|
function startGame() {
|
||||||
|
clearTimeout(idleTimer);
|
||||||
gameStarted = true;
|
gameStarted = true;
|
||||||
document.getElementById('intro').classList.add('hidden');
|
document.getElementById('intro').classList.add('hidden');
|
||||||
}
|
}
|
||||||
@@ -229,10 +285,10 @@ function gameLoop(ts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Boot ────────────────────────────────────────────────────────────────────
|
// ─── Boot ────────────────────────────────────────────────────────────────────
|
||||||
// Fill dynamic value in intro text
|
|
||||||
document.getElementById('intro').innerHTML =
|
document.getElementById('intro').innerHTML =
|
||||||
document.getElementById('intro').innerHTML.replace('${MAX_DOCKS}', MAX_DOCKS);
|
document.getElementById('intro').innerHTML.replace('${MAX_DOCKS}', MAX_DOCKS);
|
||||||
updateHudBest(topScore());
|
updateHudBest(topScore());
|
||||||
|
startIdleTimer();
|
||||||
initAnchors();
|
initAnchors();
|
||||||
updateCystinstein();
|
updateCystinstein();
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
|
|||||||
@@ -137,6 +137,62 @@
|
|||||||
}
|
}
|
||||||
#start-btn:hover { background: #00cc66; transform: scale(1.04); }
|
#start-btn:hover { background: #00cc66; transform: scale(1.04); }
|
||||||
|
|
||||||
|
/* ── Attract / highscore screen ── */
|
||||||
|
#attract {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.92);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
#attract.hidden { display: none; }
|
||||||
|
|
||||||
|
#attract h2 {
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
color: #ffcc00;
|
||||||
|
text-shadow: 0 0 12px #ffcc00;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
#attract .attract-game-title {
|
||||||
|
font-size: 44px;
|
||||||
|
letter-spacing: 6px;
|
||||||
|
color: #00ff88;
|
||||||
|
text-shadow: 0 0 20px #00ff88;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
animation: flicker 2s infinite alternate;
|
||||||
|
}
|
||||||
|
#attract-table {
|
||||||
|
width: 360px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
#attract-table th {
|
||||||
|
color: #555;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
#attract-table td { padding: 8px 10px; color: #ccc; border-bottom: 1px solid #1a1a2e; }
|
||||||
|
#attract-table tr.rank-1 td { color: #ffd700; font-size: 18px; }
|
||||||
|
#attract-table tr.rank-2 td { color: #c0c0c0; }
|
||||||
|
#attract-table tr.rank-3 td { color: #cd7f32; }
|
||||||
|
.attract-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #444;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
animation: flicker 1.2s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Game-over overlay ── */
|
/* ── Game-over overlay ── */
|
||||||
#overlay {
|
#overlay {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -309,6 +365,22 @@
|
|||||||
<button id="start-btn" onclick="startGame()">START</button>
|
<button id="start-btn" onclick="startGame()">START</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="attract" class="hidden">
|
||||||
|
<h2>HIGH SCORES</h2>
|
||||||
|
<div class="attract-game-title">SYS DIGGER</div>
|
||||||
|
<table id="attract-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="attract-rows"></tbody>
|
||||||
|
</table>
|
||||||
|
<div class="attract-hint">— Press any key to continue —</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="overlay">
|
<div id="overlay">
|
||||||
<h1>CYSTINSTEIN FORMED!</h1>
|
<h1>CYSTINSTEIN FORMED!</h1>
|
||||||
<p id="final-score"></p>
|
<p id="final-score"></p>
|
||||||
|
|||||||
Reference in New Issue
Block a user