Catena — videogioco completo in un file HTML

Gioco 8x8 a gemme colorate: trascina per formare catene
di 3+ gemme dello stesso colore e fare punti.

- Griglia 8x8, 6 colori, input mouse e touch
- Timer 90 secondi, punteggio e record in sessione
- Animazioni: scale-out (rimozione), drop (caduta), shake (mescola)
- Pulsante Mescola: 50pt + 10s per rimescolare le gemme
- Responsive, funziona da desktop e mobile
- Nessuna libreria esterna
This commit is contained in:
Catena Developer committed 2026-06-22 18:18:25 +02:00
commit 68bed620a0
3 files changed
+899

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# OS files
Thumbs.db
.DS_Store
Desktop.ini
# Editor files
*.swp
*.swo
*~
.vscode/
.idea/
+888
View File
@@ -0,0 +1,888 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Catena</title>
<style>
/* ===== RESET & BASE ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%; height: 100%;
overflow: hidden;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: #0d1b2a;
color: #fff;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
/* ===== SCREENS ===== */
.screen {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease;
}
.screen.hidden {
display: none;
opacity: 0;
}
/* ===== START SCREEN ===== */
#start-screen h1 {
font-size: clamp(3rem, 12vw, 5rem);
font-weight: 800;
letter-spacing: 0.05em;
margin-bottom: 0.5em;
background: linear-gradient(135deg, #ff6b6b, #ffd93d, #6bcb77, #4d96ff, #9b59b6, #ff8c32);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
#start-screen p {
font-size: clamp(1rem, 3vw, 1.3rem);
text-align: center;
line-height: 1.6;
color: #c0d0e0;
margin-bottom: 2em;
max-width: 28em;
padding: 0 1em;
}
#start-screen button, #gameover-screen button {
font-size: clamp(1.1rem, 3vw, 1.3rem);
font-weight: 700;
padding: 0.8em 2.5em;
border: none;
border-radius: 50px;
background: linear-gradient(135deg, #4d96ff, #2a6fd4);
color: #fff;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
box-shadow: 0 4px 20px rgba(77,150,255,0.4);
}
#start-screen button:hover, #gameover-screen button:hover {
transform: scale(1.05);
box-shadow: 0 6px 30px rgba(77,150,255,0.6);
}
#start-screen button:active, #gameover-screen button:active {
transform: scale(0.97);
}
/* ===== GAME SCREEN ===== */
#game-screen {
justify-content: flex-start;
padding-top: env(safe-area-inset-top, 0);
}
#header {
width: 100%;
max-width: 560px;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px 6px;
flex-shrink: 0;
}
#score {
font-size: clamp(1rem, 4vw, 1.3rem);
font-weight: 700;
white-space: nowrap;
min-width: 4em;
color: #ffd93d;
}
#high-score {
font-size: clamp(0.75rem, 3vw, 0.9rem);
font-weight: 600;
white-space: nowrap;
color: #8aaccc;
}
#timer-container {
flex: 1;
height: 14px;
background: rgba(255,255,255,0.12);
border-radius: 7px;
overflow: hidden;
position: relative;
}
#timer-bar {
width: 100%;
height: 100%;
border-radius: 7px;
background: linear-gradient(90deg, #6bcb77, #ffd93d 50%, #ff6b6b);
transition: width 0.3s linear;
}
#timer-bar.urgent {
background: linear-gradient(90deg, #ff6b6b, #ff4444);
animation: pulseBar 0.6s ease-in-out infinite;
}
@keyframes pulseBar {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* ===== BOARD ===== */
#board-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 6px 10px 10px;
min-height: 0;
width: 100%;
}
#board-wrapper {
position: relative;
width: min(92vw, calc(100vh - 100px), 500px);
aspect-ratio: 1;
}
#board {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
place-items: center;
touch-action: none;
}
/* ===== GEMS ===== */
.gem {
width: 86%;
height: 86%;
border-radius: 50%;
position: relative;
cursor: pointer;
transition: transform 0.15s, opacity 0.15s, box-shadow 0.1s;
will-change: transform, opacity;
}
.gem::after {
content: '';
position: absolute;
top: 10%;
left: 14%;
width: 38%;
height: 30%;
border-radius: 50%;
background: radial-gradient(circle at 30% 40%, rgba(255,255,255,0.5), transparent 70%);
pointer-events: none;
}
.gem.selected {
box-shadow: 0 0 0 3px #fff, 0 0 12px rgba(255,255,255,0.5);
transform: scale(1.08);
z-index: 2;
}
.gem.removing {
transform: scale(0);
opacity: 0;
transition: transform 0.2s ease-in, opacity 0.2s ease-in;
}
.gem.drop {
animation: gemDrop 0.3s ease-out;
}
@keyframes gemDrop {
from { transform: translateY(-40px); opacity: 0.4; }
to { transform: translateY(0); opacity: 1; }
}
/* Gem colors — radial gradient for 3D effect */
.gem-0 { background: radial-gradient(circle at 35% 32%, #ff9e9e, #ff6b6b 55%, #d94444); }
.gem-1 { background: radial-gradient(circle at 35% 32%, #ffe680, #ffd93d 55%, #d4ad00); }
.gem-2 { background: radial-gradient(circle at 35% 32%, #9edfa6, #6bcb77 55%, #48a854); }
.gem-3 { background: radial-gradient(circle at 35% 32%, #80b5ff, #4d96ff 55%, #2a6fd4); }
.gem-4 { background: radial-gradient(circle at 35% 32%, #c08ad6, #9b59b6 55%, #7d3c98); }
.gem-5 { background: radial-gradient(circle at 35% 32%, #ffb070, #ff8c32 55%, #d46a1a); }
/* ===== SVG LINE ===== */
#line-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
#path-line-glow {
stroke: rgba(255,255,255,0.25);
stroke-width: 10;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
#path-line {
stroke: #fff;
stroke-width: 3.5;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
/* ===== COMBO ===== */
#combo {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
font-size: clamp(2rem, 8vw, 3.5rem);
font-weight: 900;
color: #ffd93d;
text-shadow: 0 0 30px rgba(255,217,61,0.6), 0 4px 12px rgba(0,0,0,0.5);
pointer-events: none;
z-index: 100;
opacity: 0;
transition: none;
}
#combo.show {
animation: comboIn 0.8s ease-out forwards;
}
@keyframes comboIn {
0% { transform: translate(-50%, -50%) scale(0.3); opacity: 0; }
25% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; }
50% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
100% { transform: translate(-50%, -80%) scale(1); opacity: 0; }
}
/* ===== SHUFFLE BUTTON ===== */
#toolbar {
width: 100%;
max-width: 560px;
display: flex;
justify-content: center;
padding: 2px 12px 4px;
flex-shrink: 0;
}
.shuffle-btn {
font-size: clamp(0.8rem, 2.5vw, 0.95rem);
font-weight: 600;
padding: 0.35em 1em;
border: 1.5px solid rgba(255,255,255,0.25);
border-radius: 16px;
background: rgba(255,255,255,0.08);
color: #89a;
cursor: pointer;
transition: background 0.2s, color 0.2s, border-color 0.2s;
white-space: nowrap;
}
.shuffle-btn:disabled {
opacity: 0.6;
cursor: default;
}
.shuffle-btn:hover:not(:disabled) {
background: rgba(255,255,255,0.18);
color: #dde;
border-color: rgba(255,255,255,0.45);
}
.shuffle-btn:active:not(:disabled) {
transform: scale(0.96);
}
/* ===== SHAKE ANIMATION ===== */
@keyframes screenShake {
0% { transform: translate(0, 0); }
10% { transform: translate(-6px, 4px); }
20% { transform: translate(5px, -3px); }
30% { transform: translate(-4px, 5px); }
40% { transform: translate(3px, -2px); }
50% { transform: translate(-5px, 3px); }
60% { transform: translate(2px, -4px); }
70% { transform: translate(-3px, 2px); }
80% { transform: translate(4px, -5px); }
90% { transform: translate(-2px, 3px); }
100% { transform: translate(0, 0); }
}
#game-screen.shaking {
animation: screenShake 0.45s ease-in-out;
}
/* ===== GAME OVER ===== */
#gameover-screen h1 {
font-size: clamp(2.5rem, 10vw, 4rem);
margin-bottom: 0.4em;
color: #ff6b6b;
}
#gameover-screen p {
font-size: clamp(1.1rem, 4vw, 1.5rem);
margin-bottom: 0.4em;
color: #c0d0e0;
}
#gameover-screen .final-score {
font-size: clamp(2rem, 7vw, 3rem);
font-weight: 800;
color: #ffd93d;
}
#gameover-screen .final-record {
font-size: clamp(1rem, 3.5vw, 1.3rem);
color: #8aaccc;
margin-bottom: 1.5em;
}
#gameover-screen .btn-group {
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: center;
}
#gameover-screen .btn-group button:last-child {
background: rgba(255,255,255,0.12);
box-shadow: none;
}
#gameover-screen .btn-group button:last-child:hover {
background: rgba(255,255,255,0.2);
}
/* ===== RESPONSIVE ===== */
@media (max-height: 500px) {
#board-wrapper { width: min(85vw, calc(100vh - 70px), 400px); }
#header { padding: 4px 10px; gap: 8px; }
#timer-container { height: 10px; }
#score { font-size: 0.9rem; }
}
@media (min-width: 800px) {
#board-wrapper { width: min(70vh, 70vw, 500px); }
}
/* ===== UTILITY ===== */
.hidden { display: none !important; }
</style>
</head>
<body>
<!-- START SCREEN -->
<div id="start-screen" class="screen">
<h1>Catena</h1>
<p>Trascina su gemme dello stesso colore.<br>Almeno 3 per fare punti.</p>
<button id="btn-start">Inizia</button>
</div>
<!-- GAME SCREEN -->
<div id="game-screen" class="screen hidden">
<div id="header">
<div id="score">0</div>
<div id="timer-container"><div id="timer-bar"></div></div>
<div id="high-score">Record: 0</div>
</div>
<div id="toolbar">
<button id="btn-shuffle" class="shuffle-btn" disabled>⟳ Mescola (50pt + 10s)</button>
</div>
<div id="board-container">
<div id="board-wrapper">
<div id="board"></div>
<svg id="line-svg" viewBox="0 0 100 100">
<polyline id="path-line-glow" points=""/>
<polyline id="path-line" points=""/>
</svg>
</div>
</div>
<div id="combo">Combo!</div>
</div>
<!-- GAME OVER SCREEN -->
<div id="gameover-screen" class="screen hidden">
<h1>Game Over</h1>
<p class="final-score" id="final-score">0</p>
<p>Punteggio finale</p>
<p class="final-record" id="final-record">Record: 0</p>
<div class="btn-group">
<button id="btn-restart">Rigioca</button>
<button id="btn-home">Menu</button>
</div>
</div>
<script>
/* ===================================================================
CATENA — Gioco di gemme a percorsi
=================================================================== */
// ===== CONFIG =====
const ROWS = 8;
const COLS = 8;
const NUM_COLORS = 6;
const MIN_CHAIN = 3;
const GAME_TIME = 90;
const POINTS = { 3: 10, 4: 15, 5: 25, 6: 40 };
// ===== STATE =====
let grid = []; // ROWS x COLS: colorIndex (0-5)
let score = 0;
let highScore = 0;
let timeLeft = GAME_TIME;
let currentPath = []; // [{row, col}, ...]
let isPlaying = false;
let isDragging = false;
let isAnimating = false;
let timerId = null;
// ===== DOM REFS =====
const $ = id => document.getElementById(id);
const startScreen = $('start-screen');
const gameScreen = $('game-screen');
const gameoverScreen = $('gameover-screen');
const boardEl = $('board');
const boardWrapper = $('board-wrapper');
const scoreEl = $('score');
const highScoreEl = $('high-score');
const timerBar = $('timer-bar');
const finalScore = $('final-score');
const finalRecord = $('final-record');
const comboEl = $('combo');
const lineSvg = $('line-svg');
const pathLine = $('path-line');
const pathLineGlow = $('path-line-glow');
// ===== GRID =====
function createGrid() {
const data = [];
for (let r = 0; r < ROWS; r++) {
data[r] = [];
for (let c = 0; c < COLS; c++) {
data[r][c] = Math.floor(Math.random() * NUM_COLORS);
}
}
return data;
}
function renderBoard() {
let html = '';
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const ci = grid[r][c];
const cls = ci !== null ? `gem gem-${ci}` : 'gem gem-empty';
html += `<div class="${cls}" data-row="${r}" data-col="${c}"></div>`;
}
}
boardEl.innerHTML = html;
updateSVGViewBox();
}
function updateSVGViewBox() {
const rect = boardEl.getBoundingClientRect();
const w = rect.width, h = rect.height;
if (w > 0 && h > 0) {
lineSvg.setAttribute('viewBox', `0 0 ${w} ${h}`);
}
}
// ===== PATH LINE =====
function updatePathLine() {
if (currentPath.length < 2) {
pathLine.setAttribute('points', '');
pathLineGlow.setAttribute('points', '');
return;
}
const rect = boardEl.getBoundingClientRect();
const cellW = rect.width / COLS;
const cellH = rect.height / ROWS;
const pts = currentPath.map(p => {
const x = p.col * cellW + cellW / 2;
const y = p.row * cellH + cellH / 2;
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
pathLine.setAttribute('points', pts);
pathLineGlow.setAttribute('points', pts);
}
function clearLine() {
pathLine.setAttribute('points', '');
pathLineGlow.setAttribute('points', '');
}
// ===== SELECTION VISUALS =====
function updateSelection() {
// Remove selected class from all gems
const allGems = boardEl.querySelectorAll('.gem.selected');
for (const g of allGems) g.classList.remove('selected');
// Add selected class to current path gems
for (const p of currentPath) {
const gem = boardEl.querySelector(`[data-row="${p.row}"][data-col="${p.col}"]`);
if (gem) gem.classList.add('selected');
}
updatePathLine();
}
function clearSelection() {
const allGems = boardEl.querySelectorAll('.gem.selected');
for (const g of allGems) g.classList.remove('selected');
currentPath = [];
clearLine();
}
// ===== INPUT =====
function getGridCell(clientX, clientY) {
const rect = boardEl.getBoundingClientRect();
const x = clientX - rect.left;
const y = clientY - rect.top;
if (x < 0 || x >= rect.width || y < 0 || y >= rect.height) return null;
const cellW = rect.width / COLS;
const cellH = rect.height / ROWS;
const col = Math.floor(x / cellW);
const row = Math.floor(y / cellH);
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) return null;
return { row, col };
}
function isAdjacent(r1, c1, r2, c2) {
return Math.abs(r1 - r2) + Math.abs(c1 - c2) === 1;
}
function isInPath(row, col) {
return currentPath.some(p => p.row === row && p.col === col);
}
function canExtendPath(row, col) {
if (currentPath.length === 0) return true;
const last = currentPath[currentPath.length - 1];
if (!isAdjacent(last.row, last.col, row, col)) return false;
if (isInPath(row, col)) return false;
if (grid[row][col] === null) return false;
const firstColor = grid[currentPath[0].row][currentPath[0].col];
return grid[row][col] === firstColor;
}
// ===== INPUT =====
function startDrag(clientX, clientY) {
console.log('startDrag', clientX, clientY, 'isPlaying:', isPlaying, 'isAnimating:', isAnimating);
if (!isPlaying || isAnimating) return;
const cell = getGridCell(clientX, clientY);
console.log(' cell:', cell);
if (!cell) return;
if (grid[cell.row][cell.col] === null) return;
console.log(' color index:', grid[cell.row][cell.col]);
isDragging = true;
currentPath = [cell];
updateSelection();
}
function continueDrag(clientX, clientY) {
console.log('continueDrag', clientX, clientY, 'isDragging:', isDragging);
if (!isPlaying || !isDragging || isAnimating) return;
const cell = getGridCell(clientX, clientY);
console.log(' cell:', cell);
if (!cell) return;
const last = currentPath[currentPath.length - 1];
if (last && last.row === cell.row && last.col === cell.col) return;
const canExtend = canExtendPath(cell.row, cell.col);
console.log(' canExtendPath:', canExtend, 'last:', last, 'grid[last]', last ? grid[last.row][last.col] : '?', 'grid[cell]', grid[cell.row][cell.col]);
if (canExtend) {
currentPath.push(cell);
updateSelection();
console.log(' -> PATH EXTENDED! path length:', currentPath.length);
}
}
function endDrag() {
console.log('endDrag, isDragging:', isDragging);
if (isDragging) {
isDragging = false;
processPath();
} else {
clearSelection();
}
}
function bindInput() {
// ----- Mouse (window-level) -----
window.addEventListener('mousedown', e => {
console.log('MOUSE DOWN', e.clientX, e.clientY, 'target:', e.target.id || e.target.className);
if (!boardEl.contains(e.target)) { console.log(' -> outside board'); return; }
startDrag(e.clientX, e.clientY);
e.preventDefault();
});
window.addEventListener('mousemove', e => {
if (isDragging) {
continueDrag(e.clientX, e.clientY);
e.preventDefault();
}
});
window.addEventListener('mouseup', e => {
if (isDragging) {
endDrag();
e.preventDefault();
}
});
// ----- Touch (window-level) -----
window.addEventListener('touchstart', e => {
console.log('TOUCH START', 'touches:', e.touches.length);
const touch = e.touches[0];
if (!touch || !boardEl.contains(e.target)) { console.log(' -> ignored'); return; }
startDrag(touch.clientX, touch.clientY);
e.preventDefault();
}, { passive: false });
window.addEventListener('touchmove', e => {
if (isDragging) {
const touch = e.touches[0];
if (touch) { continueDrag(touch.clientX, touch.clientY); }
e.preventDefault();
}
}, { passive: false });
window.addEventListener('touchend', e => {
if (isDragging) {
endDrag();
e.preventDefault();
}
}, { passive: false });
window.addEventListener('touchcancel', () => {
if (!isAnimating) clearSelection();
isDragging = false;
});
}
// ===== GAME LOGIC =====
function processPath() {
if (currentPath.length < MIN_CHAIN) {
clearSelection();
return;
}
const len = currentPath.length;
const ppg = len <= 6 ? POINTS[len] : POINTS[6]; // 6+ uses 40 pts/gem
const totalPoints = len * ppg;
// Update score
score += totalPoints;
updateScoreDisplay();
// Show combo for 6+
if (len >= 6) showCombo();
// Animate removal
isAnimating = true;
const pathCopy = currentPath.slice();
clearLine();
for (const p of pathCopy) {
const gem = boardEl.querySelector(`[data-row="${p.row}"][data-col="${p.col}"]`);
if (gem) gem.classList.add('removing');
}
setTimeout(() => {
if (!isPlaying) { isAnimating = false; return; } // Game ended during animation
// Remove gems from grid
for (const p of pathCopy) {
grid[p.row][p.col] = null;
}
// Gravity & fill
applyGravity();
// Re-render
renderBoard();
// Drop animation
const allGems = boardEl.querySelectorAll('.gem:not(.gem-empty)');
for (const g of allGems) g.classList.add('drop');
// Clean up
currentPath = [];
isAnimating = false;
updateSVGViewBox();
}, 220);
}
function applyGravity() {
for (let c = 0; c < COLS; c++) {
let writeRow = ROWS - 1;
for (let r = ROWS - 1; r >= 0; r--) {
if (grid[r][c] !== null) {
grid[writeRow][c] = grid[r][c];
if (writeRow !== r) grid[r][c] = null;
writeRow--;
}
}
// Fill top empty cells with new random gems
for (let r = writeRow; r >= 0; r--) {
grid[r][c] = Math.floor(Math.random() * NUM_COLORS);
}
}
}
// ===== COMBO =====
let comboTimeout = null;
function showCombo() {
if (comboTimeout) clearTimeout(comboTimeout);
comboEl.classList.remove('show');
// Force reflow
void comboEl.offsetWidth;
comboEl.classList.add('show');
comboTimeout = setTimeout(() => comboEl.classList.remove('show'), 900);
}
// ===== SCORE =====
function updateScoreDisplay() {
scoreEl.textContent = score;
if (score > highScore) {
highScore = score;
highScoreEl.textContent = 'Record: ' + highScore;
}
if (isPlaying) updateShuffleBtn();
}
// ===== TIMER =====
function startTimer() {
timeLeft = GAME_TIME;
updateTimerDisplay();
timerId = setInterval(() => {
timeLeft--;
updateTimerDisplay();
if (timeLeft <= 5) {
timerBar.classList.add('urgent');
}
if (timeLeft <= 0) {
endGame();
}
}, 1000);
}
function updateTimerDisplay() {
const pct = Math.max(0, (timeLeft / GAME_TIME) * 100);
timerBar.style.width = pct + '%';
if (isPlaying) updateShuffleBtn();
}
function stopTimer() {
if (timerId) {
clearInterval(timerId);
timerId = null;
}
timerBar.classList.remove('urgent');
}
// ===== SCREENS =====
function showScreen(screen) {
[startScreen, gameScreen, gameoverScreen].forEach(s => s.classList.add('hidden'));
screen.classList.remove('hidden');
}
function startGame() {
// Reset state
stopTimer();
grid = createGrid();
score = 0;
currentPath = [];
isPlaying = true;
isDragging = false;
isAnimating = false;
timerBar.classList.remove('urgent');
// Update displays
scoreEl.textContent = '0';
highScoreEl.textContent = 'Record: ' + highScore;
updateTimerDisplay();
// Render
renderBoard();
clearLine();
// Show game screen
showScreen(gameScreen);
updateShuffleBtn();
// Start timer after a brief delay to avoid accidental pointer events
// Also need to update SVG viewBox after rendering
requestAnimationFrame(() => {
updateSVGViewBox();
startTimer();
});
}
function endGame() {
isPlaying = false;
isDragging = false;
isAnimating = false;
stopTimer();
if (score > highScore) {
highScore = score;
}
finalScore.textContent = score;
finalRecord.textContent = 'Record: ' + highScore;
showScreen(gameoverScreen);
}
function goHome() {
isPlaying = false;
isDragging = false;
stopTimer();
showScreen(startScreen);
}
// ===== SHUFFLE =====
function shuffleBoard() {
if (!isPlaying || isAnimating) return;
if (score < 50 || timeLeft < 10) return;
// Deduct cost
score -= 50;
timeLeft -= 10;
updateScoreDisplay();
updateTimerDisplay();
updateShuffleBtn();
// Collect all gem colours and Fisher-Yates shuffle
const all = [];
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
all.push(grid[r][c]);
for (let i = all.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[all[i], all[j]] = [all[j], all[i]];
}
// Refill grid
let idx = 0;
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
grid[r][c] = all[idx++];
// Screen shake
gameScreen.classList.add('shaking');
setTimeout(() => gameScreen.classList.remove('shaking'), 500);
// Re-render
clearSelection();
renderBoard();
}
function updateShuffleBtn() {
const btn = $('btn-shuffle');
if (!btn) return;
const canAfford = score >= 50 && timeLeft >= 10;
btn.disabled = !canAfford;
btn.textContent = canAfford ? '⟳ Mescola (50pt + 10s)' : '⟳ Mescola (50pt + 10s)';
}
// ===== RESIZE =====
let resizeTimeout = null;
function onResize() {
if (resizeTimeout) cancelAnimationFrame(resizeTimeout);
resizeTimeout = requestAnimationFrame(() => {
if (isPlaying) {
updateSVGViewBox();
updatePathLine();
}
});
}
// ===== INIT =====
function init() {
console.log('Catena init OK');
bindInput();
window.addEventListener('resize', onResize);
$('btn-start').addEventListener('click', startGame);
$('btn-restart').addEventListener('click', startGame);
$('btn-home').addEventListener('click', goHome);
$('btn-shuffle').addEventListener('click', shuffleBoard);
showScreen(startScreen);
}
// Start — safe for both early and late script execution
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 779 KiB