1077 lines
37 KiB
HTML
1077 lines
37 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Claude Planning Mode</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
background: #1a1a2e;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100vh;
|
|
font-family: 'Courier New', monospace;
|
|
overflow: hidden;
|
|
gap: 20px;
|
|
}
|
|
.game-side {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
.header {
|
|
color: #a78bfa;
|
|
font-size: 14px;
|
|
letter-spacing: 3px;
|
|
text-transform: uppercase;
|
|
margin-bottom: 10px;
|
|
opacity: 0.7;
|
|
}
|
|
.header .dot {
|
|
display: inline-block;
|
|
width: 8px; height: 8px;
|
|
background: #a78bfa;
|
|
border-radius: 50%;
|
|
margin-right: 8px;
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse { 0%,100%{opacity:0.4} 50%{opacity:1} }
|
|
|
|
/* Tabs */
|
|
.tabs {
|
|
display: flex;
|
|
gap: 2px;
|
|
margin-bottom: 0;
|
|
width: 700px;
|
|
}
|
|
.tab {
|
|
flex: 1;
|
|
padding: 8px 0;
|
|
text-align: center;
|
|
font-size: 12px;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
color: #4b5563;
|
|
background: #12121f;
|
|
border: 1px solid #2d2d4e;
|
|
border-bottom: none;
|
|
border-radius: 8px 8px 0 0;
|
|
cursor: pointer;
|
|
transition: color 0.2s, background 0.2s;
|
|
user-select: none;
|
|
}
|
|
.tab:hover { color: #9ca3af; background: #1a1a30; }
|
|
.tab.active {
|
|
color: #a78bfa;
|
|
background: #16162a;
|
|
border-color: #7c3aed;
|
|
border-bottom: 1px solid #16162a;
|
|
}
|
|
|
|
canvas {
|
|
border-radius: 0 0 12px 12px;
|
|
will-change: transform;
|
|
isolation: isolate;
|
|
border: 1px solid #2d2d4e;
|
|
border-top: none;
|
|
}
|
|
|
|
/* Footer */
|
|
.footer {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
width: 700px;
|
|
margin-top: 8px;
|
|
font-size: 11px;
|
|
}
|
|
.footer a {
|
|
color: #4b5563;
|
|
text-decoration: none;
|
|
letter-spacing: 0.5px;
|
|
transition: color 0.2s;
|
|
}
|
|
.footer a:hover { color: #a78bfa; }
|
|
.footer .sep { color: #2d2d4e; }
|
|
|
|
/* Activity Panel */
|
|
.activity-panel {
|
|
width: 320px;
|
|
height: 420px;
|
|
background: #12121f;
|
|
border: 1px solid #2d2d4e;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
contain: layout style paint;
|
|
}
|
|
.panel-header {
|
|
background: #1e1e35;
|
|
padding: 10px 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
border-bottom: 1px solid #2d2d4e;
|
|
flex-shrink: 0;
|
|
}
|
|
.panel-header .status-dot {
|
|
width: 8px; height: 8px;
|
|
background: #22c55e;
|
|
border-radius: 50%;
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|
}
|
|
.panel-header span { color: #9ca3af; font-size: 12px; letter-spacing: 1px; text-transform: uppercase; }
|
|
.panel-header .count { margin-left: auto; color: #4b5563; font-size: 11px; }
|
|
.activity-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px 0;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #2d2d4e transparent;
|
|
}
|
|
.activity-list::-webkit-scrollbar { width: 4px; }
|
|
.activity-list::-webkit-scrollbar-thumb { background: #2d2d4e; border-radius: 2px; }
|
|
.activity-item {
|
|
padding: 6px 14px;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
animation: slideIn 0.2s ease;
|
|
border-left: 2px solid transparent;
|
|
}
|
|
.activity-item:hover { background: rgba(167,139,250,0.05); }
|
|
.activity-item.new { border-left-color: #a78bfa; }
|
|
@keyframes slideIn { from{opacity:0;transform:translateX(-8px)} to{opacity:1;transform:translateX(0)} }
|
|
.activity-icon { font-size: 14px; width: 20px; text-align: center; flex-shrink: 0; margin-top: 1px; }
|
|
.activity-content { flex: 1; min-width: 0; }
|
|
.activity-tool { color: #a78bfa; font-size: 11px; font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.activity-detail { color: #6b7280; font-size: 11px; margin-top: 2px; word-break: break-all; line-height: 1.4; }
|
|
.activity-time { color: #374151; font-size: 10px; flex-shrink: 0; margin-top: 2px; }
|
|
.empty-state {
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
height: 100%; color: #374151; font-size: 12px; text-align: center; padding: 20px; gap: 8px;
|
|
}
|
|
.empty-state .icon { font-size: 24px; opacity: 0.5; }
|
|
|
|
/* Overlay */
|
|
.overlay {
|
|
display: none; position: fixed; top:0;left:0;right:0;bottom:0;
|
|
background: rgba(26,26,46,0.95); z-index: 100;
|
|
flex-direction: column; align-items: center; justify-content: center;
|
|
}
|
|
.overlay.active { display: flex; animation: fadeIn 0.5s ease; }
|
|
@keyframes fadeIn { from{opacity:0} to{opacity:1} }
|
|
.announcement { text-align: center; animation: slideUp 0.6s cubic-bezier(0.16,1,0.3,1); }
|
|
@keyframes slideUp { from{transform:translateY(40px);opacity:0} to{transform:translateY(0);opacity:1} }
|
|
.announcement .check {
|
|
width: 80px; height: 80px; border-radius: 50%;
|
|
background: linear-gradient(135deg,#7c3aed,#a78bfa);
|
|
display: flex; align-items: center; justify-content: center;
|
|
margin: 0 auto 24px; font-size: 40px;
|
|
}
|
|
.announcement h1 { color:#e5e7eb; font-size:36px; font-weight:700; margin-bottom:8px; }
|
|
.announcement p { color:#9ca3af; font-size:16px; }
|
|
.announcement .score-final { margin-top:20px; color:#a78bfa; font-size:14px; letter-spacing:2px; }
|
|
.confetti { position:fixed; width:10px; height:10px; top:-10px; z-index:101; animation: confettiFall linear forwards; }
|
|
@keyframes confettiFall { to{transform:translateY(110vh) rotate(720deg);opacity:0} }
|
|
|
|
/* Pause Modal */
|
|
.pause-overlay {
|
|
display: none; position: fixed; top:0;left:0;right:0;bottom:0;
|
|
background: rgba(26,26,46,0.92); z-index: 90;
|
|
flex-direction: column; align-items: center; justify-content: center;
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
.pause-overlay.active { display: flex; animation: fadeIn 0.4s ease; }
|
|
.pause-modal {
|
|
text-align: center;
|
|
animation: pauseBounce 0.5s cubic-bezier(0.16,1,0.3,1);
|
|
}
|
|
@keyframes pauseBounce { from{transform:scale(0.8) translateY(30px);opacity:0} to{transform:scale(1) translateY(0);opacity:1} }
|
|
.pause-icon {
|
|
width: 100px; height: 100px; border-radius: 50%;
|
|
background: linear-gradient(135deg, #f59e0b, #f97316);
|
|
display: flex; align-items: center; justify-content: center;
|
|
margin: 0 auto 28px;
|
|
font-size: 48px;
|
|
box-shadow: 0 0 40px rgba(245,158,11,0.3);
|
|
animation: pausePulse 2s ease-in-out infinite;
|
|
}
|
|
@keyframes pausePulse { 0%,100%{box-shadow:0 0 40px rgba(245,158,11,0.3)} 50%{box-shadow:0 0 60px rgba(245,158,11,0.5)} }
|
|
.pause-modal h1 {
|
|
color: #f59e0b; font-size: 42px; font-weight: 700;
|
|
margin-bottom: 12px; letter-spacing: 2px;
|
|
}
|
|
.pause-modal p {
|
|
color: #9ca3af; font-size: 16px; margin-bottom: 6px;
|
|
}
|
|
.pause-modal .subtext {
|
|
color: #4b5563; font-size: 13px; letter-spacing: 1px;
|
|
text-transform: uppercase; margin-top: 16px;
|
|
}
|
|
.pause-dots span {
|
|
display: inline-block; width: 8px; height: 8px;
|
|
background: #f59e0b; border-radius: 50%; margin: 0 4px;
|
|
animation: dotBlink 1.4s ease-in-out infinite;
|
|
}
|
|
.pause-dots span:nth-child(2) { animation-delay: 0.2s; }
|
|
.pause-dots span:nth-child(3) { animation-delay: 0.4s; }
|
|
@keyframes dotBlink { 0%,100%{opacity:0.3} 50%{opacity:1} }
|
|
|
|
/* Locked Overlay - Not in Plan Mode */
|
|
.locked-overlay {
|
|
display: none; position: fixed; top:0;left:0;right:0;bottom:0;
|
|
background: rgba(26,26,46,0.97); z-index: 95;
|
|
flex-direction: column; align-items: center; justify-content: center;
|
|
}
|
|
.locked-overlay.active { display: flex; }
|
|
.locked-modal { text-align: center; max-width: 520px; padding: 40px; }
|
|
.locked-icon {
|
|
width: 120px; height: 120px; border-radius: 50%;
|
|
background: linear-gradient(135deg, #3b3b6e, #2d2d4e);
|
|
border: 2px solid #7c3aed;
|
|
display: flex; align-items: center; justify-content: center;
|
|
margin: 0 auto 32px; font-size: 56px;
|
|
box-shadow: 0 0 30px rgba(124,58,237,0.2);
|
|
}
|
|
.locked-modal h1 {
|
|
color: #e5e7eb; font-size: 32px; font-weight: 700;
|
|
margin-bottom: 16px; letter-spacing: 1px;
|
|
}
|
|
.locked-modal p {
|
|
color: #9ca3af; font-size: 16px; line-height: 1.7;
|
|
margin-bottom: 8px;
|
|
}
|
|
.locked-modal .hint {
|
|
color: #d1d5db; font-size: 13px; letter-spacing: 0.5px;
|
|
margin-top: 28px;
|
|
padding: 16px 24px;
|
|
border: 1px solid #3b3b6e;
|
|
border-radius: 10px;
|
|
background: rgba(124,58,237,0.08);
|
|
line-height: 1.6;
|
|
}
|
|
.locked-modal .hint strong {
|
|
color: #a78bfa;
|
|
}
|
|
|
|
/* Scoreboard */
|
|
.scoreboard {
|
|
width: 100%;
|
|
margin-top: 24px;
|
|
border: 1px solid #2d2d4e;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
background: #12121f;
|
|
}
|
|
.scoreboard-title {
|
|
background: #1e1e35;
|
|
padding: 8px 16px;
|
|
font-size: 11px;
|
|
letter-spacing: 1.5px;
|
|
text-transform: uppercase;
|
|
color: #9ca3af;
|
|
border-bottom: 1px solid #2d2d4e;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.scoreboard-title .trophy { font-size: 14px; }
|
|
.scoreboard table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
.scoreboard td {
|
|
padding: 10px 16px;
|
|
font-size: 13px;
|
|
border-bottom: 1px solid #1e1e35;
|
|
}
|
|
.scoreboard tr:last-child td { border-bottom: none; }
|
|
.scoreboard .sb-game {
|
|
color: #d1d5db;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.scoreboard .sb-icon { font-size: 16px; width: 24px; text-align: center; }
|
|
.scoreboard .sb-score {
|
|
text-align: right;
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
letter-spacing: 1px;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.scoreboard .sb-score.has-score { color: #a78bfa; }
|
|
.scoreboard .sb-score.no-score { color: #374151; }
|
|
/* Compact variant for footer area */
|
|
.scoreboard.compact { margin-top: 10px; width: 700px; }
|
|
.scoreboard.compact .scoreboard-title { padding: 6px 14px; }
|
|
.scoreboard.compact td { padding: 7px 14px; font-size: 12px; }
|
|
.scoreboard.compact .sb-score { font-size: 13px; }
|
|
/* Locked variant - wider */
|
|
.locked-modal .scoreboard { max-width: 400px; margin: 24px auto 0; }
|
|
|
|
/* Share button */
|
|
.share-btn {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 8px 0;
|
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
|
color: #e5e7eb;
|
|
border: none;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
cursor: pointer;
|
|
transition: background 0.2s, transform 0.1s;
|
|
}
|
|
.share-btn:hover { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
|
|
.share-btn:active { transform: scale(0.98); }
|
|
|
|
/* Share modal */
|
|
.share-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(26,26,46,0.95);
|
|
z-index: 110;
|
|
align-items: center;
|
|
justify-content: center;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
.share-modal.active { display: flex; animation: fadeIn 0.3s ease; }
|
|
.share-inner {
|
|
background: #12121f;
|
|
border: 1px solid #2d2d4e;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
max-width: 480px;
|
|
width: 90%;
|
|
animation: slideUp 0.4s cubic-bezier(0.16,1,0.3,1);
|
|
}
|
|
.share-header {
|
|
color: #a78bfa;
|
|
font-size: 14px;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.share-close {
|
|
color: #4b5563;
|
|
font-size: 18px;
|
|
cursor: pointer;
|
|
background: none;
|
|
border: none;
|
|
font-family: 'Courier New', monospace;
|
|
padding: 4px 8px;
|
|
transition: color 0.2s;
|
|
}
|
|
.share-close:hover { color: #e5e7eb; }
|
|
.share-text {
|
|
width: 100%;
|
|
background: #1a1a2e;
|
|
border: 1px solid #2d2d4e;
|
|
border-radius: 8px;
|
|
color: #d1d5db;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 13px;
|
|
padding: 16px;
|
|
resize: none;
|
|
line-height: 1.6;
|
|
outline: none;
|
|
}
|
|
.share-text:focus { border-color: #7c3aed; }
|
|
.copy-btn {
|
|
display: block;
|
|
width: 100%;
|
|
margin-top: 12px;
|
|
padding: 10px 0;
|
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
|
color: #e5e7eb;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 13px;
|
|
letter-spacing: 1px;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
.copy-btn:hover { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
|
|
.copy-btn.copied { background: linear-gradient(135deg, #059669, #10b981); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="game-side">
|
|
<div class="header"><span class="dot"></span>Claude is planning...</div>
|
|
<div class="tabs">
|
|
<div class="tab active" data-game="dino">Dino Runner</div>
|
|
<div class="tab" data-game="snake">Snake</div>
|
|
<div class="tab" data-game="flappy">Flappy Bird</div>
|
|
</div>
|
|
<canvas id="game" width="700" height="300"></canvas>
|
|
<div class="footer">
|
|
<a href="https://aitmpl.com" target="_blank">aitmpl.com</a>
|
|
<span class="sep">|</span>
|
|
<a href="https://github.com/daniavila/claude-code-templates" target="_blank">GitHub</a>
|
|
</div>
|
|
<div class="scoreboard compact" id="gameScoreboard"></div>
|
|
</div>
|
|
|
|
<div class="activity-panel">
|
|
<div class="panel-header">
|
|
<div class="status-dot"></div>
|
|
<span>Claude Activity</span>
|
|
<span class="count" id="activityCount">0 ops</span>
|
|
</div>
|
|
<div class="activity-list" id="activityList">
|
|
<div class="empty-state" id="emptyState">
|
|
<div class="icon">◎</div>
|
|
<div>Waiting for Claude...</div>
|
|
<div style="color:#2d2d4e">Tool calls will appear here</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="locked-overlay active" id="lockedOverlay">
|
|
<div class="locked-modal">
|
|
<div class="locked-icon">🔒</div>
|
|
<h1>Plan Mode Game</h1>
|
|
<p>This game can only be played while Claude is in Plan Mode.</p>
|
|
<p>It will unlock automatically when Claude starts planning.</p>
|
|
<div class="hint">Ask Claude to <strong>make a plan</strong> for any task.<br>For example: <strong>"Plan how to refactor the auth module"</strong></div>
|
|
<div class="scoreboard" id="lockedScoreboard"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pause-overlay" id="pauseOverlay">
|
|
<div class="pause-modal">
|
|
<div class="pause-icon">⏸</div>
|
|
<h1>PAUSED</h1>
|
|
<p>Claude needs you to continue</p>
|
|
<p>Check your terminal for input</p>
|
|
<div class="subtext">
|
|
Waiting <span class="pause-dots"><span></span><span></span><span></span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="share-modal" id="shareModal">
|
|
<div class="share-inner">
|
|
<div class="share-header">
|
|
<span>Share Scores</span>
|
|
<button class="share-close" onclick="closeShareModal()">×</button>
|
|
</div>
|
|
<textarea class="share-text" id="shareText" rows="7" readonly></textarea>
|
|
<button class="copy-btn" id="copyBtn" onclick="copyShareText()">Copy to Clipboard</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overlay" id="overlay">
|
|
<div class="announcement">
|
|
<div class="check">✓</div>
|
|
<h1>Claude Termino!</h1>
|
|
<p>Plan mode complete - back to building</p>
|
|
<div class="score-final" id="finalScore"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ========== SHARED ==========
|
|
const canvas = document.getElementById('game');
|
|
const ctx = canvas.getContext('2d');
|
|
const W = canvas.width, H = canvas.height;
|
|
const BG = '#16162a';
|
|
const GROUND_COLOR = '#2d2d4e';
|
|
const BODY_COLOR = '#c4795a';
|
|
const DARK_COLOR = '#a0604a';
|
|
const EYE_COLOR = '#4a3028';
|
|
const PURPLE = '#7c3aed';
|
|
const PURPLE_LIGHT = '#a78bfa';
|
|
const TEXT_COLOR = '#6b7280';
|
|
|
|
let currentGame = 'dino';
|
|
let gameState = 'idle'; // idle, running, dead
|
|
let score = 0;
|
|
let frame = 0;
|
|
let planModeActive = false;
|
|
|
|
// ========== TAB SWITCHING ==========
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
const newGame = tab.dataset.game;
|
|
if (newGame !== currentGame) {
|
|
currentGame = newGame;
|
|
gameState = 'idle';
|
|
score = 0;
|
|
frame = 0;
|
|
games[currentGame].init();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ========== DINO GAME ==========
|
|
const dino = {
|
|
x: 80, y: 0, vy: 0, w: 45, h: 50,
|
|
ducking: false, duckH: 25, legFrame: 0,
|
|
groundY: H - 60, speed: 4
|
|
};
|
|
dino.y = dino.groundY - dino.h;
|
|
let dinoObstacles = [], dinoTimer = 0, groundOffset = 0;
|
|
let dinoHigh = parseInt(localStorage.getItem('dinoHigh') || '0');
|
|
|
|
let clouds = [], stars = [];
|
|
for (let i = 0; i < 3; i++) clouds.push({ x: Math.random()*W, y: 40+Math.random()*80, w: 40+Math.random()*30, speed: 0.5+Math.random()*0.5 });
|
|
for (let i = 0; i < 12; i++) stars.push({ x: Math.random()*W, y: Math.random()*(H-80), size: Math.random()*2+0.5 });
|
|
|
|
function drawBG() {
|
|
ctx.fillStyle = BG; ctx.fillRect(0,0,W,H);
|
|
ctx.fillStyle = 'rgba(167,139,250,0.4)';
|
|
for (const s of stars) ctx.fillRect(s.x, s.y, s.size, s.size);
|
|
ctx.fillStyle = 'rgba(45,45,78,0.6)';
|
|
for (const c of clouds) {
|
|
ctx.fillRect(c.x-c.w/2, c.y-6, c.w, 12);
|
|
ctx.fillRect(c.x-c.w*0.35, c.y-10, c.w*0.5, 8);
|
|
}
|
|
}
|
|
|
|
function drawGround() {
|
|
ctx.strokeStyle = GROUND_COLOR; ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.moveTo(0, dino.groundY); ctx.lineTo(W, dino.groundY); ctx.stroke();
|
|
ctx.fillStyle = GROUND_COLOR;
|
|
for (let i = 0; i < 40; i++) {
|
|
const x = ((i*25 - groundOffset*0.8) % (W+20)) - 10;
|
|
ctx.fillRect(x, dino.groundY+4+(i%2)*6, 2+(i%3)*2, 1);
|
|
}
|
|
}
|
|
|
|
function drawPixelChar(x, y, isDucking, legFrame, isDead) {
|
|
const P = 5;
|
|
function px(c,r,color) { ctx.fillStyle=color||BODY_COLOR; ctx.fillRect(x+c*P,y+r*P,P,P); }
|
|
if (isDucking) {
|
|
for (let c=0;c<10;c++) for (let r=0;r<4;r++) px(c,r);
|
|
px(2,1,EYE_COLOR); px(5,1,EYE_COLOR);
|
|
for (let c=3;c<7;c++) px(c,2,DARK_COLOR); px(3,3,DARK_COLOR); px(6,3,DARK_COLOR);
|
|
if (Math.floor(legFrame)%2===0){px(1,4);px(3,4);px(6,4);px(8,4);}
|
|
else{px(2,4);px(4,4);px(5,4);px(7,4);}
|
|
} else {
|
|
px(1,0);px(2,0);px(6,0);px(7,0);
|
|
for(let r=1;r<=7;r++) for(let c=0;c<9;c++) px(c,r);
|
|
const ec = isDead ? '#ef4444' : EYE_COLOR;
|
|
px(2,3,ec);px(3,3,ec);px(5,3,ec);px(6,3,ec);
|
|
for(let c=2;c<7;c++){px(c,5,DARK_COLOR);px(c,6,DARK_COLOR);}
|
|
px(3,7,DARK_COLOR);px(5,7,DARK_COLOR);
|
|
const onGround = y >= dino.groundY - dino.h;
|
|
if(!onGround){px(1,8);px(3,8);px(5,8);px(7,8);}
|
|
else if(gameState==='running'){
|
|
if(Math.floor(legFrame)%2===0){px(1,8);px(1,9);px(3,8);px(5,8);px(5,9);px(7,8);}
|
|
else{px(1,8);px(3,8);px(3,9);px(5,8);px(7,8);px(7,9);}
|
|
} else {px(1,8);px(1,9);px(3,8);px(3,9);px(5,8);px(5,9);px(7,8);px(7,9);}
|
|
}
|
|
}
|
|
|
|
const dinoGame = {
|
|
init() {
|
|
dino.y = dino.groundY - dino.h; dino.vy = 0; dino.ducking = false;
|
|
dino.speed = 4; dino.legFrame = 0;
|
|
dinoObstacles = []; dinoTimer = 200; groundOffset = 0;
|
|
},
|
|
update() {
|
|
frame++; score += dino.speed * 0.05;
|
|
dino.speed = 4 + Math.floor(score/150)*0.4; if(dino.speed>12) dino.speed=12;
|
|
groundOffset += dino.speed; dino.legFrame += dino.speed*0.06;
|
|
dino.vy += 0.6; dino.y += dino.vy;
|
|
if(dino.y >= dino.groundY-dino.h){dino.y=dino.groundY-dino.h;dino.vy=0;}
|
|
for(const c of clouds){c.x-=c.speed;if(c.x<-c.w)c.x=W+c.w;}
|
|
dinoTimer -= dino.speed;
|
|
if(dinoTimer<=0){
|
|
const v=Math.floor(Math.random()*3);
|
|
const hs=[30,40,50],ws=[20,28,22];
|
|
if(Math.random()<0.6||score<200)
|
|
dinoObstacles.push({type:'cactus',x:W+20,y:dino.groundY-hs[v],w:ws[v],h:hs[v],v});
|
|
else dinoObstacles.push({type:'bird',x:W+20,y:dino.groundY-[60,36,80][v],w:36,h:24});
|
|
const bg=Math.max(90,200-Math.floor(score/50)*5);
|
|
dinoTimer=bg+Math.random()*Math.max(60,100-Math.floor(score/80)*3);
|
|
}
|
|
for(let i=dinoObstacles.length-1;i>=0;i--){dinoObstacles[i].x-=dino.speed;if(dinoObstacles[i].x<-60)dinoObstacles.splice(i,1);}
|
|
const dh=dino.ducking?dino.duckH:dino.h;
|
|
const dy=dino.ducking?dino.groundY-dino.duckH:dino.y;
|
|
const db={x:dino.x+4,y:dy+4,w:dino.ducking?42:37,h:dh-8};
|
|
for(const o of dinoObstacles){
|
|
const ob={x:o.x+4,y:o.y+4,w:o.w-8,h:o.h-8};
|
|
if(db.x<ob.x+ob.w&&db.x+db.w>ob.x&&db.y<ob.y+ob.h&&db.y+db.h>ob.y){
|
|
gameState='dead';if(Math.floor(score)>dinoHigh){dinoHigh=Math.floor(score);localStorage.setItem('dinoHigh',String(dinoHigh));renderScoreboards();}
|
|
}
|
|
}
|
|
},
|
|
render() {
|
|
drawBG(); drawGround();
|
|
for(const o of dinoObstacles){
|
|
if(o.type==='cactus'){
|
|
ctx.fillStyle='#059669';
|
|
ctx.fillRect(o.x+o.w/2-4,o.y,8,o.h);
|
|
if(o.v===1){ctx.fillRect(o.x,o.y+10,o.w/2-4,6);ctx.fillRect(o.x,o.y+4,6,12);ctx.fillRect(o.x+o.w/2+4,o.y+16,o.w/2-4,6);ctx.fillRect(o.x+o.w-6,o.y+10,6,12);}
|
|
else if(o.v===2){ctx.fillRect(o.x+o.w/2+4,o.y+8,10,6);ctx.fillRect(o.x+o.w/2+10,o.y+2,6,12);}
|
|
} else {
|
|
ctx.fillStyle='#ef4444';
|
|
const wy=Math.sin(frame*0.15)*8;
|
|
ctx.fillRect(o.x+4,o.y+8,24,8);ctx.fillRect(o.x+28,o.y+10,8,4);ctx.fillRect(o.x+8,o.y+wy,16,6);
|
|
}
|
|
}
|
|
const dy=dino.ducking?dino.groundY-dino.duckH:dino.y;
|
|
drawPixelChar(dino.x,dy,dino.ducking,dino.legFrame,gameState==='dead');
|
|
ctx.fillStyle=TEXT_COLOR;ctx.font='16px Courier New';ctx.textAlign='right';
|
|
ctx.fillText(String(Math.floor(score)).padStart(5,'0'),W-20,30);
|
|
if(dinoHigh>0){ctx.fillStyle='#4b5563';ctx.fillText('HI '+String(dinoHigh).padStart(5,'0'),W-100,30);}
|
|
},
|
|
onKeyDown(code) {
|
|
if(code==='Space'||code==='ArrowUp'){if(gameState==='idle'||gameState==='dead'){gameState='running';dinoGame.init();score=0;}else if(dino.y>=dino.groundY-dino.h-1)dino.vy=-12;}
|
|
if(code==='ArrowDown'&&gameState==='running')dino.ducking=true;
|
|
},
|
|
onKeyUp(code){if(code==='ArrowDown')dino.ducking=false;}
|
|
};
|
|
|
|
// ========== SNAKE GAME ==========
|
|
const CELL = 20;
|
|
const COLS = Math.floor(W/CELL), ROWS = Math.floor(H/CELL);
|
|
let snake = [], snakeDir = {x:1,y:0}, snakeNextDir = {x:1,y:0}, food = {x:0,y:0};
|
|
let snakeTick = 0, snakeSpeed = 8;
|
|
let snakeHigh = parseInt(localStorage.getItem('snakeHigh') || '0');
|
|
|
|
const snakeGame = {
|
|
init() {
|
|
const cy = Math.floor(ROWS/2);
|
|
snake = [{x:5,y:cy},{x:4,y:cy},{x:3,y:cy}];
|
|
snakeDir = {x:1,y:0}; snakeNextDir = {x:1,y:0};
|
|
snakeSpeed = 8; snakeTick = 0;
|
|
this.spawnFood();
|
|
},
|
|
spawnFood() {
|
|
do { food = {x:Math.floor(Math.random()*COLS),y:Math.floor(Math.random()*ROWS)}; }
|
|
while(snake.some(s=>s.x===food.x&&s.y===food.y));
|
|
},
|
|
update() {
|
|
snakeTick++;
|
|
if(snakeTick < snakeSpeed) return;
|
|
snakeTick = 0;
|
|
snakeDir = {...snakeNextDir};
|
|
const head = {x:snake[0].x+snakeDir.x, y:snake[0].y+snakeDir.y};
|
|
// Wall collision
|
|
if(head.x<0||head.x>=COLS||head.y<0||head.y>=ROWS){
|
|
gameState='dead';
|
|
if(score>snakeHigh){snakeHigh=score;localStorage.setItem('snakeHigh',String(snakeHigh));renderScoreboards();}
|
|
return;
|
|
}
|
|
// Self collision
|
|
if(snake.some(s=>s.x===head.x&&s.y===head.y)){
|
|
gameState='dead';
|
|
if(score>snakeHigh){snakeHigh=score;localStorage.setItem('snakeHigh',String(snakeHigh));renderScoreboards();}
|
|
return;
|
|
}
|
|
snake.unshift(head);
|
|
if(head.x===food.x&&head.y===food.y){
|
|
score++;
|
|
snakeSpeed = Math.max(3, 8 - Math.floor(score/5));
|
|
this.spawnFood();
|
|
} else {
|
|
snake.pop();
|
|
}
|
|
},
|
|
render() {
|
|
ctx.fillStyle = BG; ctx.fillRect(0,0,W,H);
|
|
// Grid lines
|
|
ctx.strokeStyle = '#1e1e35'; ctx.lineWidth = 0.5;
|
|
for(let x=0;x<=W;x+=CELL){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
|
|
for(let y=0;y<=H;y+=CELL){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
|
|
// Food
|
|
ctx.fillStyle = '#ef4444';
|
|
ctx.fillRect(food.x*CELL+3, food.y*CELL+3, CELL-6, CELL-6);
|
|
// Snake
|
|
for(let i=0;i<snake.length;i++){
|
|
const s = snake[i];
|
|
const t = i/snake.length;
|
|
ctx.fillStyle = i===0 ? BODY_COLOR : `rgb(${196-t*40},${121-t*30},${90-t*20})`;
|
|
ctx.fillRect(s.x*CELL+1, s.y*CELL+1, CELL-2, CELL-2);
|
|
// Eyes on head
|
|
if(i===0){
|
|
ctx.fillStyle = EYE_COLOR;
|
|
if(snakeDir.x===1){ctx.fillRect(s.x*CELL+13,s.y*CELL+4,4,4);ctx.fillRect(s.x*CELL+13,s.y*CELL+12,4,4);}
|
|
else if(snakeDir.x===-1){ctx.fillRect(s.x*CELL+3,s.y*CELL+4,4,4);ctx.fillRect(s.x*CELL+3,s.y*CELL+12,4,4);}
|
|
else if(snakeDir.y===1){ctx.fillRect(s.x*CELL+4,s.y*CELL+13,4,4);ctx.fillRect(s.x*CELL+12,s.y*CELL+13,4,4);}
|
|
else{ctx.fillRect(s.x*CELL+4,s.y*CELL+3,4,4);ctx.fillRect(s.x*CELL+12,s.y*CELL+3,4,4);}
|
|
}
|
|
}
|
|
// Score
|
|
ctx.fillStyle=TEXT_COLOR;ctx.font='16px Courier New';ctx.textAlign='right';
|
|
ctx.fillText(String(score).padStart(5,'0'),W-20,30);
|
|
if(snakeHigh>0){ctx.fillStyle='#4b5563';ctx.fillText('HI '+String(snakeHigh).padStart(5,'0'),W-100,30);}
|
|
},
|
|
onKeyDown(code) {
|
|
if(gameState==='idle'||gameState==='dead'){gameState='running';snakeGame.init();score=0;return;}
|
|
if(code==='ArrowUp'&&snakeDir.y!==1) snakeNextDir={x:0,y:-1};
|
|
if(code==='ArrowDown'&&snakeDir.y!==-1) snakeNextDir={x:0,y:1};
|
|
if(code==='ArrowLeft'&&snakeDir.x!==1) snakeNextDir={x:-1,y:0};
|
|
if(code==='ArrowRight'&&snakeDir.x!==-1) snakeNextDir={x:1,y:0};
|
|
},
|
|
onKeyUp(){}
|
|
};
|
|
|
|
// ========== FLAPPY BIRD ==========
|
|
let bird = {x:0,y:0,vy:0,size:20};
|
|
let pipes = [], pipeTimer = 0;
|
|
const PIPE_W = 50, GAP = 110, GRAVITY = 0.35, FLAP = -6.5;
|
|
let flappyHigh = parseInt(localStorage.getItem('flappyHigh') || '0');
|
|
let flappySpeed = 3;
|
|
|
|
const flappyGame = {
|
|
init() {
|
|
bird = {x:120, y:H/2, vy:0, size:20, rot:0};
|
|
pipes = []; pipeTimer = 120; flappySpeed = 3;
|
|
},
|
|
update() {
|
|
frame++;
|
|
bird.vy += GRAVITY;
|
|
bird.y += bird.vy;
|
|
bird.rot = Math.min(bird.vy * 3, 40);
|
|
// Ground/ceiling
|
|
if(bird.y+bird.size > H-10 || bird.y < 0){
|
|
gameState='dead';
|
|
if(score>flappyHigh){flappyHigh=score;localStorage.setItem('flappyHigh',String(flappyHigh));renderScoreboards();}
|
|
return;
|
|
}
|
|
// Pipes
|
|
pipeTimer--;
|
|
if(pipeTimer <= 0){
|
|
const gapY = 60 + Math.random()*(H-140);
|
|
pipes.push({x:W, gapY, scored:false});
|
|
pipeTimer = Math.max(70, 120 - Math.floor(score/3)*3);
|
|
}
|
|
flappySpeed = 3 + Math.floor(score/10)*0.3; if(flappySpeed>6) flappySpeed=6;
|
|
for(let i=pipes.length-1;i>=0;i--){
|
|
pipes[i].x -= flappySpeed;
|
|
// Score
|
|
if(!pipes[i].scored && pipes[i].x+PIPE_W < bird.x){
|
|
pipes[i].scored = true;
|
|
score++;
|
|
}
|
|
// Remove offscreen
|
|
if(pipes[i].x < -PIPE_W) pipes.splice(i,1);
|
|
}
|
|
// Collision
|
|
const bx=bird.x, by=bird.y, bs=bird.size;
|
|
for(const p of pipes){
|
|
if(bx+bs>p.x && bx<p.x+PIPE_W){
|
|
if(by<p.gapY-GAP/2 || by+bs>p.gapY+GAP/2){
|
|
gameState='dead';
|
|
if(score>flappyHigh){flappyHigh=score;localStorage.setItem('flappyHigh',String(flappyHigh));renderScoreboards();}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
render() {
|
|
// Sky gradient
|
|
ctx.fillStyle = BG; ctx.fillRect(0,0,W,H);
|
|
// Stars
|
|
ctx.fillStyle = 'rgba(167,139,250,0.3)';
|
|
for(const s of stars) ctx.fillRect(s.x, s.y, s.size, s.size);
|
|
// Ground
|
|
ctx.fillStyle = GROUND_COLOR;
|
|
ctx.fillRect(0, H-10, W, 10);
|
|
// Pipes
|
|
for(const p of pipes){
|
|
ctx.fillStyle = '#059669';
|
|
// Top pipe
|
|
const topH = p.gapY - GAP/2;
|
|
ctx.fillRect(p.x, 0, PIPE_W, topH);
|
|
ctx.fillRect(p.x-4, topH-20, PIPE_W+8, 20);
|
|
// Bottom pipe
|
|
const botY = p.gapY + GAP/2;
|
|
ctx.fillRect(p.x, botY, PIPE_W, H-10-botY);
|
|
ctx.fillRect(p.x-4, botY, PIPE_W+8, 20);
|
|
// Pipe highlights
|
|
ctx.fillStyle = '#06b680';
|
|
ctx.fillRect(p.x+4, 0, 6, topH-20);
|
|
ctx.fillRect(p.x+4, botY+20, 6, H-10-botY-20);
|
|
}
|
|
// Bird (terracotta mascot style)
|
|
const bx = bird.x, by = bird.y, bs = bird.size;
|
|
ctx.fillStyle = BODY_COLOR;
|
|
ctx.fillRect(bx, by, bs, bs);
|
|
// Wing
|
|
const wingOff = Math.sin(frame*0.2)*3;
|
|
ctx.fillStyle = DARK_COLOR;
|
|
ctx.fillRect(bx-4, by+bs/2+wingOff, 8, 8);
|
|
// Eye
|
|
ctx.fillStyle = EYE_COLOR;
|
|
ctx.fillRect(bx+bs-6, by+4, 4, 4);
|
|
// Beak
|
|
ctx.fillStyle = '#f59e0b';
|
|
ctx.fillRect(bx+bs, by+bs/2-2, 8, 5);
|
|
// Score
|
|
ctx.fillStyle = '#e5e7eb'; ctx.font = 'bold 24px Courier New'; ctx.textAlign = 'center';
|
|
ctx.fillText(String(score), W/2, 40);
|
|
if(flappyHigh>0){ctx.fillStyle='#4b5563';ctx.font='14px Courier New';ctx.fillText('HI '+flappyHigh,W/2,60);}
|
|
},
|
|
onKeyDown(code) {
|
|
if(code==='Space'||code==='ArrowUp'){
|
|
if(gameState==='idle'||gameState==='dead'){gameState='running';flappyGame.init();score=0;}
|
|
else bird.vy=FLAP;
|
|
}
|
|
},
|
|
onKeyUp(){}
|
|
};
|
|
|
|
// ========== GAME MANAGER ==========
|
|
const games = { dino: dinoGame, snake: snakeGame, flappy: flappyGame };
|
|
|
|
function drawIdleText(text) {
|
|
ctx.fillStyle='#e5e7eb'; ctx.font='bold 18px Courier New'; ctx.textAlign='center';
|
|
ctx.fillText(text, W/2, H/2-10);
|
|
}
|
|
function drawDeadText() {
|
|
ctx.fillStyle='#e5e7eb'; ctx.font='bold 20px Courier New'; ctx.textAlign='center';
|
|
ctx.fillText('GAME OVER', W/2, H/2-20);
|
|
ctx.fillStyle=TEXT_COLOR; ctx.font='14px Courier New';
|
|
ctx.fillText('Press SPACE to restart', W/2, H/2+10);
|
|
}
|
|
|
|
function gameLoop() {
|
|
const g = games[currentGame];
|
|
if(gameState==='running') g.update();
|
|
g.render();
|
|
if(gameState==='idle') {
|
|
const hints = {dino:'SPACE to jump / Arrows to play',snake:'Arrow keys to move',flappy:'SPACE to flap'};
|
|
drawIdleText('Press SPACE to start');
|
|
ctx.fillStyle=TEXT_COLOR;ctx.font='12px Courier New';ctx.textAlign='center';
|
|
ctx.fillText(hints[currentGame],W/2,H/2+15);
|
|
}
|
|
if(gameState==='dead') drawDeadText();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// Plan mode lock/unlock
|
|
function unlockGame() {
|
|
if (planModeActive) return;
|
|
planModeActive = true;
|
|
document.getElementById('lockedOverlay').classList.remove('active');
|
|
}
|
|
|
|
function lockGame() {
|
|
planModeActive = false;
|
|
gameState = 'idle';
|
|
score = 0;
|
|
games[currentGame].init();
|
|
document.getElementById('lockedOverlay').classList.add('active');
|
|
}
|
|
|
|
// Input - blocked when not in plan mode
|
|
document.addEventListener('keydown', (e) => {
|
|
if (!planModeActive) return;
|
|
if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault();
|
|
games[currentGame].onKeyDown(e.code);
|
|
});
|
|
document.addEventListener('keyup', (e) => {
|
|
if (!planModeActive) return;
|
|
games[currentGame].onKeyUp(e.code);
|
|
});
|
|
canvas.addEventListener('touchstart', (e) => {
|
|
if (!planModeActive) return;
|
|
e.preventDefault();
|
|
games[currentGame].onKeyDown('Space');
|
|
});
|
|
|
|
// ========== ACTIVITY PANEL ==========
|
|
const TOOL_ICONS = {
|
|
Read:'📄',Glob:'🔍',Grep:'🔎',Bash:'⌨',
|
|
Edit:'✎',Write:'📝',MultiEdit:'✎',WebFetch:'🌐',
|
|
WebSearch:'🔭',Task:'⚙',AskUserQuestion:'❓',
|
|
EnterPlanMode:'📋',ExitPlanMode:'✔',default:'▶'
|
|
};
|
|
let activityCount = 0;
|
|
|
|
function getToolDetail(toolName, input) {
|
|
if(!input) return '';
|
|
try {
|
|
const d = typeof input==='string'?JSON.parse(input):input;
|
|
switch(toolName){
|
|
case 'Read': return shortenPath(d.file_path||'');
|
|
case 'Glob': return d.pattern||'';
|
|
case 'Grep': return `"${d.pattern||''}" ${d.glob?'in '+d.glob:''}`;
|
|
case 'Edit':case 'MultiEdit': return shortenPath(d.file_path||'');
|
|
case 'Write': return shortenPath(d.file_path||'');
|
|
case 'Bash': return truncate(d.command||'',60);
|
|
case 'WebFetch': return truncate(d.url||'',50);
|
|
case 'WebSearch': return d.query||'';
|
|
case 'Task': return truncate(d.description||d.prompt||'',50);
|
|
default: return '';
|
|
}
|
|
} catch{return '';}
|
|
}
|
|
function shortenPath(p){if(!p)return '';const a=p.split('/');return a.length<=3?p:'.../' +a.slice(-2).join('/');}
|
|
function truncate(s,m){return s.length>m?s.slice(0,m)+'...':s;}
|
|
|
|
function addActivity(toolName, detail) {
|
|
const list=document.getElementById('activityList');
|
|
const empty=document.getElementById('emptyState');
|
|
if(empty)empty.remove();
|
|
activityCount++;
|
|
document.getElementById('activityCount').textContent=activityCount+' ops';
|
|
list.querySelectorAll('.new').forEach(el=>el.classList.remove('new'));
|
|
const item=document.createElement('div');
|
|
item.className='activity-item new';
|
|
const icon=TOOL_ICONS[toolName]||TOOL_ICONS.default;
|
|
const time=new Date().toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
item.innerHTML=`<div class="activity-icon">${icon}</div><div class="activity-content"><div class="activity-tool">${toolName}</div>${detail?`<div class="activity-detail">${detail}</div>`:''}</div><div class="activity-time">${time}</div>`;
|
|
list.appendChild(item);
|
|
list.scrollTop=list.scrollHeight;
|
|
while(list.children.length>50)list.removeChild(list.firstChild);
|
|
}
|
|
|
|
// ========== SSE ==========
|
|
let isPaused = false;
|
|
let prePauseState = null;
|
|
|
|
function pauseGame() {
|
|
if (isPaused) return;
|
|
isPaused = true;
|
|
prePauseState = gameState;
|
|
if (gameState === 'running') gameState = 'idle';
|
|
document.getElementById('pauseOverlay').classList.add('active');
|
|
}
|
|
|
|
function resumeGame() {
|
|
if (!isPaused) return;
|
|
isPaused = false;
|
|
document.getElementById('pauseOverlay').classList.remove('active');
|
|
if (prePauseState === 'running') gameState = 'running';
|
|
prePauseState = null;
|
|
}
|
|
|
|
function connectSSE() {
|
|
const es = new EventSource('/events');
|
|
es.addEventListener('done', (e) => {
|
|
resumeGame();
|
|
showAnnouncement(e.data);
|
|
// Lock game after a delay so user can see their score
|
|
setTimeout(lockGame, 8000);
|
|
});
|
|
es.addEventListener('pause', () => pauseGame());
|
|
es.addEventListener('activity', (e) => {
|
|
unlockGame();
|
|
resumeGame();
|
|
try { const d=JSON.parse(e.data); addActivity(d.tool_name||'Unknown',getToolDetail(d.tool_name,d.tool_input)); }
|
|
catch { addActivity('Tool',e.data); }
|
|
});
|
|
es.onerror = () => { es.close(); setTimeout(connectSSE,3000); };
|
|
}
|
|
|
|
function showAnnouncement(message) {
|
|
const ov=document.getElementById('overlay');
|
|
document.getElementById('finalScore').textContent=`YOUR SCORE: ${String(Math.floor(score)).padStart(5,'0')}`;
|
|
ov.querySelector('h1').textContent=message||'Claude Termino!';
|
|
ov.classList.add('active');
|
|
if(gameState==='running') gameState='idle';
|
|
spawnConfetti();
|
|
}
|
|
|
|
function spawnConfetti() {
|
|
const colors=['#7c3aed','#a78bfa','#c4b5fd','#059669','#f59e0b','#ef4444','#6366f1'];
|
|
for(let i=0;i<60;i++){
|
|
setTimeout(()=>{
|
|
const c=document.createElement('div');c.className='confetti';
|
|
c.style.left=Math.random()*100+'vw';
|
|
c.style.background=colors[Math.floor(Math.random()*colors.length)];
|
|
c.style.width=(5+Math.random()*10)+'px';c.style.height=(5+Math.random()*10)+'px';
|
|
c.style.borderRadius=Math.random()>0.5?'50%':'2px';
|
|
c.style.animationDuration=(2+Math.random()*3)+'s';
|
|
document.body.appendChild(c);setTimeout(()=>c.remove(),5000);
|
|
},i*30);
|
|
}
|
|
}
|
|
|
|
// ========== SCOREBOARD ==========
|
|
function renderScoreboards() {
|
|
const scores = [
|
|
{ icon: '🏾', name: 'Dino Runner', key: 'dinoHigh' },
|
|
{ icon: '🐍', name: 'Snake', key: 'snakeHigh' },
|
|
{ icon: '🐦', name: 'Flappy Bird', key: 'flappyHigh' }
|
|
];
|
|
const hasAny = scores.some(s => parseInt(localStorage.getItem(s.key) || '0') > 0);
|
|
const shareBtn = hasAny ? `<button class="share-btn" onclick="shareScores()">Share Scores</button>` : '';
|
|
const html = `
|
|
<div class="scoreboard-title"><span class="trophy">🏆</span> High Scores</div>
|
|
<table>${scores.map(s => {
|
|
const val = parseInt(localStorage.getItem(s.key) || '0');
|
|
const cls = val > 0 ? 'has-score' : 'no-score';
|
|
const display = val > 0 ? String(val).padStart(5, '0') : '---';
|
|
return `<tr><td><div class="sb-game"><span class="sb-icon">${s.icon}</span>${s.name}</div></td><td class="sb-score ${cls}">${display}</td></tr>`;
|
|
}).join('')}</table>${shareBtn}`;
|
|
document.getElementById('lockedScoreboard').innerHTML = html;
|
|
document.getElementById('gameScoreboard').innerHTML = html;
|
|
}
|
|
|
|
// Init
|
|
connectSSE();
|
|
dinoGame.init();
|
|
renderScoreboards();
|
|
gameLoop();
|
|
|
|
// ========== SHARE SCORES ==========
|
|
function shareScores() {
|
|
const dino = parseInt(localStorage.getItem('dinoHigh') || '0');
|
|
const snake = parseInt(localStorage.getItem('snakeHigh') || '0');
|
|
const flappy = parseInt(localStorage.getItem('flappyHigh') || '0');
|
|
fetch('/share', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ dino, snake, flappy })
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
document.getElementById('shareText').value = data.text;
|
|
document.getElementById('copyBtn').textContent = 'Copy to Clipboard';
|
|
document.getElementById('copyBtn').classList.remove('copied');
|
|
document.getElementById('shareModal').classList.add('active');
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function copyShareText() {
|
|
const text = document.getElementById('shareText').value;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const btn = document.getElementById('copyBtn');
|
|
btn.textContent = 'Copied!';
|
|
btn.classList.add('copied');
|
|
setTimeout(() => { btn.textContent = 'Copy to Clipboard'; btn.classList.remove('copied'); }, 2000);
|
|
});
|
|
}
|
|
|
|
function closeShareModal() {
|
|
document.getElementById('shareModal').classList.remove('active');
|
|
}
|
|
|
|
document.getElementById('shareModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'shareModal') closeShareModal();
|
|
});
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') closeShareModal();
|
|
});
|
|
|
|
// Check initial plan mode status
|
|
fetch('/status').then(r => r.json()).then(d => {
|
|
if (d.planActive) unlockGame();
|
|
}).catch(() => {});
|
|
</script>
|
|
</body>
|
|
</html>
|