Run details
- Model
- opencode-go/mimo-v2.6-pro
- Route
- opencode CLI
- Reasoning
- provider default
- Date
- 2 Oct 2026
- Time
- 13 min 51 s
- Size
- 56 kB
- Tokens
- 55,850 in, 48,431 out
- List price
- $0.066 ($0.435 in / $0.87 out per M)
- Paid
- Not billed per run (CLI plan)
Exact prompt
Make a Breakout game I can play right now with the keyboard or the mouse. Give it several levels, a score and some juice. It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Save it as index.html in the current directory, then stop. Do not ask questions.
Source
56 kB 1,747 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>BREAKOUT // CIRCUIT</title>
<style>
:root{
--canvas:#080B10;
--surface-1:#101722;
--surface-2:#172331;
--line:#2A3B4A;
--ink:#EAF7F5;
--ink-muted:#83939A;
--voltage:#62F5D0;
--voltage-dim:#2A806F;
--warning:#FFB454;
--danger:#FF6B7A;
--font-display:"Arial Narrow","Helvetica Neue",Arial,sans-serif;
--font-mono:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
--stage-w:min(72vw, 760px, calc(100vw - 64px), calc((100vh - 168px) * 4 / 3));
}
*{box-sizing:border-box;}
[hidden]{display:none !important;}
html,body{height:100%;}
body{
margin:0;
background:var(--canvas);
color:var(--ink);
font-family:var(--font-display);
overflow:hidden;
min-height:100vh;
-webkit-font-smoothing:antialiased;
-webkit-tap-highlight-color:transparent;
}
.shell{display:flex;flex-direction:column;min-height:100vh;}
/* ---------- utility strip ---------- */
.util{
flex:0 0 auto;height:32px;display:flex;align-items:center;gap:16px;
padding:0 32px;border-bottom:1px solid var(--line);
}
.wordmark{
font-family:var(--font-display);font-size:14px;font-weight:700;
letter-spacing:.14em;text-transform:uppercase;color:var(--ink);white-space:nowrap;
}
.wordmark span{color:var(--voltage);}
.util-right{margin-left:auto;display:flex;align-items:center;gap:16px;}
.status{display:flex;align-items:center;gap:8px;}
.status-dot{width:4px;height:4px;background:var(--voltage-dim);}
.status-dot.on{background:var(--voltage);box-shadow:0 0 6px var(--voltage);}
.status-text{
font-family:var(--font-mono);font-size:11px;font-weight:700;
letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);
}
.status-text.live{color:var(--ink);}
/* ---------- buttons ---------- */
.btn{
appearance:none;background:transparent;border:1px solid var(--line);border-radius:0;
color:var(--ink);font-family:var(--font-display);font-size:14px;font-weight:700;
letter-spacing:.08em;text-transform:uppercase;line-height:1;
padding:4px 8px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;
transition:border-color .12s linear,color .12s linear,background-color .12s linear;
white-space:nowrap;
}
.btn:hover{border-color:var(--voltage);color:var(--voltage);background:var(--surface-1);}
.btn:focus-visible{outline:2px solid var(--voltage);outline-offset:2px;}
.btn[aria-pressed="true"]{border-color:var(--voltage);color:var(--voltage);background:var(--surface-1);}
.btn svg{display:block;flex:0 0 auto;}
/* ---------- body / rail / stage ---------- */
.body-row{flex:1 1 auto;display:flex;min-height:0;}
.rail{
flex:0 0 56px;width:56px;background:var(--surface-1);border-right:1px solid var(--line);
display:flex;flex-direction:column;justify-content:center;gap:28px;padding:16px 0;
}
.rail-group{display:flex;flex-direction:column;align-items:center;gap:8px;}
.rail-label{
font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.12em;
text-transform:uppercase;color:var(--ink-muted);
writing-mode:vertical-rl;transform:rotate(180deg);
}
.rail-value{
font-family:var(--font-mono);font-size:14px;font-weight:700;letter-spacing:.04em;
color:var(--ink);font-variant-numeric:tabular-nums;
}
.rail-value.accent{color:var(--voltage);}
.lives{display:flex;flex-direction:column;gap:6px;align-items:center;}
.life-pip{width:6px;height:6px;background:var(--voltage);}
.life-pip.off{background:var(--line);}
.main{
flex:1 1 auto;min-width:0;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:0;padding:16px 0;
}
.stage-wrap{
width:var(--stage-w);
}
.stage{
position:relative;width:100%;aspect-ratio:4/3;background:var(--canvas);
border:2px solid var(--line);
}
.stage::after{
content:"";position:absolute;inset:0;border:1px solid var(--voltage);
opacity:.22;pointer-events:none;
transition:opacity .26s linear;
}
.stage.hot::after{opacity:.8;}
.stage.danger-frame::after{opacity:.8;border-color:var(--danger);}
canvas{
display:block;width:100%;height:100%;cursor:crosshair;
touch-action:none;
}
/* ---------- overlays ---------- */
.overlay{
position:absolute;inset:0;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:0;
background:rgba(8,11,16,0);padding:16px;text-align:center;
}
.overlay[hidden]{display:none;}
.overlay.dim{background:rgba(8,11,16,.58);}
.overlay.help{background:var(--surface-1);border:2px solid var(--voltage);inset:12px;}
.mark{margin-bottom:16px;}
.ov-title{
font-family:var(--font-display);font-size:64px;font-weight:700;line-height:.95;
letter-spacing:-.03em;text-transform:uppercase;color:var(--ink);margin:0;
}
.ov-title.sm{font-size:32px;line-height:1;letter-spacing:-.02em;}
.ov-sub{
font-family:var(--font-display);font-size:18px;font-weight:700;letter-spacing:.22em;
text-transform:uppercase;color:var(--voltage);margin:8px 0 0;
}
.ov-score{
font-family:var(--font-mono);font-size:18px;font-weight:700;letter-spacing:.08em;
color:var(--voltage);margin:12px 0 0;font-variant-numeric:tabular-nums;
}
.ov-prompt{
font-family:var(--font-display);font-size:16px;font-weight:500;line-height:1.4;
color:var(--ink-muted);margin:24px 0 0;max-width:34ch;
}
.ov-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:16px;}
.ov-legend{
display:flex;flex-wrap:wrap;gap:8px 16px;justify-content:center;margin-top:24px;
font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.12em;
text-transform:uppercase;color:var(--ink-muted);
}
.ov-legend b{color:var(--ink);font-weight:700;}
.ov-grid{
display:grid;grid-template-columns:auto auto;gap:10px 20px;
margin:20px 0 0;text-align:left;
}
.ov-grid dt{
font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.12em;
text-transform:uppercase;color:var(--voltage);margin:0;
}
.ov-grid dd{
font-family:var(--font-display);font-size:16px;font-weight:500;line-height:1.4;
color:var(--ink);margin:0;
}
.countdown{
font-family:var(--font-mono);font-size:18px;font-weight:700;letter-spacing:.12em;
color:var(--ink-muted);margin-top:16px;text-transform:uppercase;
}
/* ---------- action prompt inside stage ---------- */
.prompt{
position:absolute;left:0;right:0;bottom:0;padding:8px 12px;
font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.12em;
text-transform:uppercase;color:var(--voltage-dim);text-align:center;
pointer-events:none;
}
.prompt[hidden]{display:none;}
/* ---------- control legend ---------- */
.legend{
flex:0 0 auto;height:48px;width:var(--stage-w);
display:flex;align-items:center;justify-content:center;gap:24px;
border-top:1px solid var(--line);margin-top:0;
}
.legend-keys{
display:flex;align-items:center;gap:16px;
font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.12em;
text-transform:uppercase;color:var(--ink-muted);
}
.legend-keys span{display:inline-flex;align-items:center;gap:6px;}
.legend-keys b{color:var(--ink);font-weight:700;}
.legend-btns{margin-left:auto;display:flex;gap:8px;}
.sr-only{
position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
clip:rect(0 0 0 0);white-space:nowrap;border:0;
}
/* ---------- tablet ---------- */
@media (max-width:1023px){
:root{ --stage-w:min(72vw, 620px, calc(100vw - 48px), calc((100vh - 168px) * 4 / 3)); }
.util{padding:0 20px;}
.rail{
flex:0 0 48px;width:48px;
}
.rail-label{font-size:9px;}
.rail-value{font-size:12px;}
.ov-title{font-size:52px;}
}
/* ---------- mobile ---------- */
@media (max-width:599px){
:root{ --stage-w:calc(100vw - 40px); }
.util{height:32px;padding:0 12px;gap:10px;}
.wordmark{font-size:11px;letter-spacing:.1em;}
.body-row{flex-direction:column;}
.rail{
flex:0 0 48px;width:100%;height:48px;flex-direction:row;
justify-content:space-around;align-items:center;gap:0;padding:0 12px;
border-right:0;border-bottom:1px solid var(--line);
}
.rail-group{flex-direction:row;align-items:center;gap:8px;}
.rail-label{writing-mode:horizontal-tb;transform:none;}
.lives{flex-direction:row;}
.main{padding:12px 0 0;justify-content:flex-start;}
.stage-wrap{width:var(--stage-w);}
.legend{
width:var(--stage-w);height:auto;flex-direction:column;
gap:10px;padding:12px 0 16px;
}
.legend-keys{flex-wrap:wrap;justify-content:center;gap:8px 12px;}
.legend-btns{margin-left:0;width:100%;justify-content:center;}
.legend-btns .btn{min-height:44px;min-width:88px;justify-content:center;font-size:12px;}
.util-right .btn{min-height:28px;}
.ov-title{font-size:42px;}
.ov-title.sm{font-size:26px;}
.ov-sub{font-size:14px;letter-spacing:.18em;}
.ov-prompt{font-size:14px;margin-top:16px;}
.ov-grid{gap:8px 14px;}
.ov-grid dd{font-size:14px;}
.overlay.help{inset:8px;}
.prompt{font-size:10px;padding:6px;}
}
@media (prefers-reduced-motion: reduce){
.stage::after{transition:none;}
}
</style>
</head>
<body>
<div class="shell">
<header class="util">
<div class="wordmark">BREAKOUT <span>//</span> CIRCUIT</div>
<div class="util-right">
<div class="status" role="status" aria-live="off">
<div class="status-dot on" id="statusDot"></div>
<div class="status-text live" id="statusText">READY</div>
</div>
<button class="btn" id="helpBtn" type="button" aria-pressed="false">HELP</button>
</div>
</header>
<div class="body-row">
<aside class="rail" aria-label="Telemetry">
<div class="rail-group">
<div class="rail-label">Score</div>
<div class="rail-value" id="railScore">0</div>
</div>
<div class="rail-group">
<div class="rail-label">Lives</div>
<div class="lives" id="railLives" aria-hidden="true">
<div class="life-pip"></div>
<div class="life-pip"></div>
<div class="life-pip"></div>
</div>
</div>
<div class="rail-group">
<div class="rail-label">Level</div>
<div class="rail-value accent" id="railLevel">1</div>
</div>
</aside>
<main class="main">
<div class="stage-wrap">
<div class="stage" id="stage">
<canvas id="game" role="img" aria-label="Breakout game board. Use Left and Right Arrow or A and D to move the paddle."></canvas>
<div class="overlay" id="overlay">
<div class="mark" id="mark">
<svg width="96" height="96" viewBox="0 0 96 96" aria-hidden="true">
<circle cx="58" cy="48" r="30" fill="none" stroke="#62F5D0" stroke-width="4"
stroke-linecap="square" stroke-dasharray="158 30" stroke-dashoffset="22"
transform="rotate(-40 58 48)"/>
<path d="M22 48 L42 48" stroke="#2A806F" stroke-width="2" stroke-linecap="square"/>
<circle cx="18" cy="48" r="6" fill="#62F5D0"/>
</svg>
</div>
<h1 class="ov-title" id="ovTitle">BREAKOUT</h1>
<p class="ov-sub" id="ovSub">CIRCUIT</p>
<p class="ov-score" id="ovScore" hidden></p>
<p class="ov-prompt" id="ovPrompt">Press Enter or click to launch</p>
<div class="ov-actions" id="ovActions">
<button class="btn" id="startBtn" type="button">LAUNCH</button>
<button class="btn" id="resetBtn2" type="button">RESET</button>
</div>
<div class="ov-legend" id="ovLegend">
<span><b>ARROWS / A D</b> MOVE</span>
<span><b>SPACE</b> LAUNCH</span>
<span><b>MOUSE</b> STEER</span>
<span><b>P / ESC</b> PAUSE</span>
<span><b>R</b> RESET</span>
</div>
<div class="countdown" id="ovCountdown" hidden></div>
</div>
<div class="overlay help" id="helpOverlay" hidden>
<h2 class="ov-title sm">CIRCUIT MANUAL</h2>
<p class="ov-sub">CONTROLS</p>
<dl class="ov-grid">
<dt>Move</dt><dd>Arrow Left / Arrow Right, or A and D</dd>
<dt>Pointer</dt><dd>Move the mouse over the board to steer</dd>
<dt>Launch</dt><dd>Space or Enter, or click the board</dd>
<dt>Pause</dt><dd>P or Escape, or the Pause button</dd>
<dt>Reset</dt><dd>R or the Reset button</dd>
<dt>Objective</dt><dd>Clear every destructible brick across 5 circuit levels</dd>
<dt>Scoring</dt><dd>10 points per brick, up to a ×5 chain, plus 100 per surviving life</dd>
</dl>
<div class="ov-actions">
<button class="btn" id="closeHelpBtn" type="button">CLOSE HELP</button>
</div>
</div>
<div class="prompt" id="prompt">SPACE TO LAUNCH</div>
</div>
</div>
<div class="legend">
<div class="legend-keys">
<span>
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
<path d="M10 3 L5 8 L10 13" fill="none" stroke="#83939A" stroke-width="1.5" stroke-linecap="square"/>
</svg>
<b>ARROWS / A D</b> MOVE
</span>
<span>
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
<path d="M6 3 L11 8 L6 13" fill="none" stroke="#83939A" stroke-width="1.5" stroke-linecap="square"/>
</svg>
<b>SPACE</b> LAUNCH
</span>
<span>
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
<circle cx="8" cy="8" r="5.5" fill="none" stroke="#83939A" stroke-width="1.5"/>
<path d="M8 5 L8 9" stroke="#83939A" stroke-width="1.5" stroke-linecap="square"/>
</svg>
<b>MOUSE</b> STEER
</span>
</div>
<div class="legend-btns">
<button class="btn" id="pauseBtn" type="button" aria-pressed="false">
<svg width="10" height="12" viewBox="0 0 10 12" aria-hidden="true">
<rect x="1" y="1" width="2.5" height="10" fill="currentColor"/>
<rect x="6.5" y="1" width="2.5" height="10" fill="currentColor"/>
</svg>
PAUSE
</button>
<button class="btn" id="resetBtn" type="button">
<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
<path d="M10 6 A4 4 0 1 1 6 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/>
<path d="M6 0.5 L6 3.5 L9 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/>
</svg>
RESET
</button>
<button class="btn" id="soundBtn" type="button" aria-pressed="false">SOUND ON</button>
</div>
</div>
</main>
</div>
</div>
<div class="sr-only" aria-live="polite" id="announce"></div>
<script>
(function(){
'use strict';
/* ============================================================
CONSTANTS
============================================================ */
var W = 960, H = 720;
var PADDLE_W = 210, PADDLE_H = 12, PADDLE_Y = 640, PADDLE_CLAMP_PAD = 8;
var BALL_R = 6;
var COLS = 14, ROWS = 7;
var BRICK_W = 38, BRICK_H = 38, BRICK_GAP = 8;
var FIELD_W = COLS * BRICK_W + (COLS - 1) * BRICK_GAP;
var FIELD_X = (W - FIELD_W) / 2;
var FIELD_Y = 40;
var C = {
canvas:'#080B10', surface1:'#101722', surface2:'#172331', line:'#2A3B4A',
ink:'#EAF7F5', inkMuted:'#83939A', voltage:'#62F5D0', voltageDim:'#2A806F',
warning:'#FFB454', danger:'#FF6B7A'
};
var HP_COLOR = [null, '#62F5D0', '#A7F86B', '#FFD166', '#FF9B54'];
var HP_CORE = [null, '#62F5D0', '#A7F86B', '#FFD166', '#FF9B54'];
var STATE = {
READY:'READY', RUNNING:'RUNNING', PAUSED:'PAUSED',
LEVEL_COMPLETE:'LEVEL_COMPLETE', GAME_OVER:'GAME_OVER'
};
/* ============================================================
DOM
============================================================ */
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
var stage = document.getElementById('stage');
var overlay = document.getElementById('overlay');
var helpOverlay = document.getElementById('helpOverlay');
var mark = document.getElementById('mark');
var ovTitle = document.getElementById('ovTitle');
var ovSub = document.getElementById('ovSub');
var ovScore = document.getElementById('ovScore');
var ovPrompt = document.getElementById('ovPrompt');
var ovActions = document.getElementById('ovActions');
var ovLegend = document.getElementById('ovLegend');
var ovCountdown = document.getElementById('ovCountdown');
var promptEl = document.getElementById('prompt');
var statusDot = document.getElementById('statusDot');
var statusText = document.getElementById('statusText');
var railScore = document.getElementById('railScore');
var railLevel = document.getElementById('railLevel');
var railLives = document.getElementById('railLives');
var announce = document.getElementById('announce');
var helpBtn = document.getElementById('helpBtn');
var closeHelpBtn = document.getElementById('closeHelpBtn');
var startBtn = document.getElementById('startBtn');
var resetBtn = document.getElementById('resetBtn');
var resetBtn2 = document.getElementById('resetBtn2');
var pauseBtn = document.getElementById('pauseBtn');
var soundBtn = document.getElementById('soundBtn');
/* ============================================================
PREFERENCES
============================================================ */
var reduceMotion = false;
try {
var mq = window.matchMedia('(prefers-reduced-motion: reduce)');
reduceMotion = mq.matches;
if (mq.addEventListener) mq.addEventListener('change', function(e){ reduceMotion = e.matches; });
else if (mq.addListener) mq.addListener(function(e){ reduceMotion = e.matches; });
} catch (e) {}
/* ============================================================
AUDIO
============================================================ */
var audioCtx = null;
var muted = false;
function initAudio(){
if (muted) return;
try {
if (!audioCtx) {
var AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
audioCtx = new AC();
}
if (audioCtx.state === 'suspended') audioCtx.resume();
} catch (e) {}
}
function tone(freq, dur, type, vol, slideTo){
if (muted || !audioCtx) return;
try {
var t0 = audioCtx.currentTime;
var osc = audioCtx.createOscillator();
var g = audioCtx.createGain();
osc.type = type || 'square';
osc.frequency.setValueAtTime(freq, t0);
if (slideTo) osc.frequency.linearRampToValueAtTime(slideTo, t0 + dur);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(vol || 0.05, t0 + 0.006);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
osc.connect(g); g.connect(audioCtx.destination);
osc.start(t0); osc.stop(t0 + dur + 0.02);
} catch (e) {}
}
var sfx = {
wall: function(){ tone(220, 0.035, 'square', 0.035, 190); },
paddle: function(){ tone(160, 0.055, 'triangle', 0.06, 140); },
brick: function(combo){
tone(400 + Math.min(combo,5) * 70, 0.045, 'square', 0.05);
},
unbreakable: function(){ tone(110, 0.05, 'sawtooth', 0.035, 90); },
lifeLost: function(){ tone(300, 0.16, 'sawtooth', 0.05, 120); },
level: function(){
[320,400,500,640].forEach(function(f,i){
setTimeout(function(){ tone(f, 0.12, 'square', 0.05); }, i * 90);
});
},
gameOver: function(){
[300,240,180,120].forEach(function(f,i){
setTimeout(function(){ tone(f, 0.2, 'sawtooth', 0.05); }, i * 130);
});
},
launch: function(){ tone(340, 0.07, 'square', 0.045, 520); }
};
/* ============================================================
LEVELS — 14 columns x 7 rows. 0 empty, 1..4 hp, 9 unbreakable
============================================================ */
function grid(){
var a = [], r, c;
for (r = 0; r < ROWS; r++){ a.push(new Array(COLS).fill(0)); }
return a;
}
function level1(){
var g = grid();
for (var r = 0; r < ROWS; r++){
for (var c = 0; c < COLS; c++){
g[r][c] = r < 2 ? 1 : (r < 4 ? 2 : 3);
}
}
g[2][6] = 0;
return g;
}
function level2(){
var g = grid();
for (var r = 0; r < ROWS; r++){
for (var c = 0; c < COLS; c++){
if ((c === 6 || c === 7) && r >= 1 && r <= 4) continue;
var filled = ((r + c) % 2 === 0) || (c % 2 === 1 && r % 2 === 1);
if (!filled) continue;
g[r][c] = r < 3 ? 1 : (r < 5 ? 2 : 3);
}
}
for (var i = 0; i < COLS; i += 2){ g[0][i] = 1; }
return g;
}
function level3(){
var g = grid();
for (var r = 0; r < ROWS; r++){
for (var c = 0; c < COLS; c++){
var d = Math.abs(c - 6.5) / 7 + Math.abs(r - 3) / 3.6;
if (d <= 1.02 && ((r + c) % 2 === 0)){
g[r][c] = r < 2 ? 1 : (r < 5 ? 2 : 3);
}
}
}
g[2][6] = 9;
g[4][6] = 9;
g[3][5] = 2;
g[3][8] = 2;
return g;
}
function level4(){
return [
[3,3,3,3,3,3,3,3,3,3,3,3,3,3],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[2,2,2,2,2,2,2,2,2,2,2,2,2,2],
[9,9,9,9,9,9,0,0,9,9,9,9,9,9],
[2,2,2,2,2,2,0,0,2,2,2,2,2,2],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0]
];
}
function level5(){
var g = grid();
for (var r = 0; r < ROWS; r++){
for (var c = 0; c < COLS; c++){
var dist = Math.abs(c - 6.5);
if (dist <= 6.6 - r * 0.95){
g[r][c] = dist <= 1.6 ? 4 : (dist <= 3.2 ? 3 : (dist <= 4.8 ? 2 : 1));
}
}
}
g[3][0] = 9; g[3][13] = 9; g[5][0] = 9; g[5][13] = 9;
return g;
}
var LEVELS = [
{ name:'TRAINING GRID', speed:360, build:level1, moveRow:-1, moveAmp:0 },
{ name:'ALTERNATE COLUMNS', speed:400, build:level2, moveRow:-1, moveAmp:0 },
{ name:'DIAMOND CORE', speed:445, build:level3, moveRow:-1, moveAmp:0 },
{ name:'BAND HAZARD', speed:485, build:level4, moveRow:3, moveAmp:46 },
{ name:'PYRAMID SHIELD', speed:545, build:level5, moveRow:-1, moveAmp:0 }
];
/* ============================================================
GAME STATE
============================================================ */
var state = STATE.READY;
var stateBeforeHelp = STATE.READY;
var helpOpen = false;
var levelIndex = 0;
var score = 0;
var scoreShown = 0;
var scoreRollT = 0;
var scoreRollFrom = 0;
var scoreRollTo = 0;
var lives = 3;
var combo = 0;
var maxCombo = 0;
var paddle = { x: W/2 - PADDLE_W/2, y: PADDLE_Y, w: PADDLE_W, h: PADDLE_H, vx:0, squeeze:0, overshoot:0, sheen:0 };
var ball = {
x: W/2, y: PADDLE_Y - BALL_R - 2, r: BALL_R,
vx:0, vy:0, speed:360, launched:false, trail:[]
};
var bricks = [];
var particles = [];
var rings = [];
var debris = [];
var levelStartT = 0;
var levelElapsed = 0;
var completeT = 0;
var countdownVal = 3;
var shake = 0;
var hotFlash = 0;
var dangerFlash = 0;
var bobT = 0;
var now = 0;
/* ============================================================
CANVAS RESIZE
============================================================ */
function resize(){
var rect = canvas.getBoundingClientRect();
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var cw = Math.max(1, Math.round(rect.width * dpr));
var ch = Math.max(1, Math.round(rect.height * dpr));
if (canvas.width !== cw || canvas.height !== ch){
canvas.width = cw;
canvas.height = ch;
}
ctx.setTransform(cw / W, 0, 0, ch / H, 0, 0);
}
/* ============================================================
LEVEL SETUP
============================================================ */
function buildBricks(){
var g = LEVELS[levelIndex].build();
bricks = [];
for (var r = 0; r < ROWS; r++){
for (var c = 0; c < COLS; c++){
var v = g[r][c];
if (!v) continue;
bricks.push({
col: c, row: r,
x: FIELD_X + c * (BRICK_W + BRICK_GAP),
y: FIELD_Y + r * (BRICK_H + BRICK_GAP),
w: BRICK_W, h: BRICK_H,
hp: v === 9 ? 1 : v,
maxHp: v === 9 ? 1 : v,
unbreakable: v === 9,
flash: 0,
scale: 1
});
}
}
}
function currentSpeed(){
var base = LEVELS[levelIndex].speed;
var cleared = brickTotalCount() - destructibleLeft();
var bump = Math.min(0.22, cleared * 0.008);
return base * (1 + bump);
}
function brickTotalCount(){
return bricks.length;
}
function destructibleLeft(){
var n = 0;
for (var i = 0; i < bricks.length; i++){
if (!bricks[i].unbreakable) n++;
}
return n;
}
function resetBallOnPaddle(){
ball.launched = false;
ball.trail.length = 0;
ball.x = paddle.x + paddle.w / 2;
ball.y = paddle.y - BALL_R - 2;
ball.vx = 0;
ball.vy = 0;
}
function launchBall(){
if (state !== STATE.READY && state !== STATE.RUNNING) return;
if (ball.launched) return;
initAudio();
var s = currentSpeed();
ball.speed = s;
var angle = (Math.random() * 0.5 - 0.25) + (Math.random() < 0.5 ? -0.22 : 0.22);
ball.vx = s * Math.sin(angle);
ball.vy = -s * Math.cos(angle);
ball.launched = true;
setState(STATE.RUNNING);
sfx.launch();
}
function startLevel(idx, keepScore){
levelIndex = idx;
if (!keepScore){ score = 0; scoreShown = 0; }
combo = 0;
buildBricks();
paddle.x = W/2 - PADDLE_W/2;
paddle.squeeze = 0;
paddle.overshoot = 0;
paddle.sheen = 0;
resetBallOnPaddle();
ball.speed = LEVELS[levelIndex].speed;
particles.length = 0;
rings.length = 0;
debris.length = 0;
levelStartT = now;
levelElapsed = 0;
countdownVal = 3;
setState(STATE.READY);
}
function resetGame(){
lives = 3;
score = 0;
scoreShown = 0;
maxCombo = 0;
shake = 0;
hotFlash = 0.6;
dangerFlash = 0;
startLevel(0, true);
updateHud();
announce_('Game reset. Level 1.');
}
function announce_(msg){
announce.textContent = '';
setTimeout(function(){ announce.textContent = msg; }, 30);
}
/* ============================================================
STATE + HUD
============================================================ */
function setState(s){
state = s;
statusText.textContent = s === STATE.LEVEL_COMPLETE ? 'LEVEL CLEAR' :
s === STATE.GAME_OVER ? 'GAME OVER' :
s === STATE.RUNNING ? 'RUNNING' :
s === STATE.PAUSED ? 'PAUSED' : 'READY';
statusText.classList.toggle('live', s === STATE.RUNNING);
statusDot.classList.toggle('on', s === STATE.RUNNING || s === STATE.READY);
pauseBtn.setAttribute('aria-pressed', s === STATE.PAUSED ? 'true' : 'false');
pauseBtn.lastChild.nodeValue = s === STATE.PAUSED ? ' RESUME' : ' PAUSE';
var showOverlay = (s === STATE.READY || s === STATE.LEVEL_COMPLETE || s === STATE.GAME_OVER);
overlay.hidden = !showOverlay;
if (s === STATE.READY){
mark.hidden = false;
ovTitle.className = 'ov-title';
ovTitle.textContent = 'BREAKOUT';
ovSub.hidden = false;
ovSub.textContent = 'CIRCUIT';
ovScore.hidden = true;
ovPrompt.hidden = false;
ovPrompt.textContent = 'Press Enter or click to launch';
ovActions.hidden = false;
ovLegend.hidden = false;
ovCountdown.hidden = true;
overlay.className = 'overlay';
promptEl.hidden = false;
promptEl.textContent = 'SPACE TO LAUNCH';
} else if (s === STATE.LEVEL_COMPLETE){
mark.hidden = true;
ovTitle.className = 'ov-title sm';
ovTitle.textContent = 'LEVEL ' + (levelIndex + 1) + ' CLEAR';
ovSub.hidden = true;
ovScore.hidden = false;
ovPrompt.hidden = true;
ovActions.hidden = true;
ovLegend.hidden = true;
ovCountdown.hidden = false;
overlay.className = 'overlay dim';
promptEl.hidden = true;
} else if (s === STATE.GAME_OVER){
mark.hidden = true;
ovTitle.className = 'ov-title sm';
ovTitle.textContent = 'CIRCUIT LOST';
ovSub.hidden = true;
ovScore.hidden = false;
ovScore.textContent = 'SCORE ' + fmt(score);
ovPrompt.hidden = false;
ovPrompt.textContent = 'Press Enter or click to restart';
ovActions.hidden = false;
ovLegend.hidden = false;
ovCountdown.hidden = true;
overlay.className = 'overlay dim';
promptEl.hidden = true;
} else if (s === STATE.RUNNING){
promptEl.hidden = true;
} else if (s === STATE.PAUSED){
promptEl.hidden = false;
promptEl.textContent = 'PAUSED — PRESS P OR ESC TO RESUME';
}
updateHud();
}
function fmt(n){
n = Math.max(0, Math.round(n));
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function updateHud(){
railScore.textContent = fmt(scoreShown);
railLevel.textContent = String(levelIndex + 1);
var pips = railLives.children;
for (var i = 0; i < pips.length; i++){
pips[i].className = 'life-pip' + (i < lives ? '' : ' off');
}
}
/* ============================================================
INPUT
============================================================ */
var keys = { left:false, right:false };
var mouseActive = false;
var mouseGameX = W / 2;
function stageToGame(clientX){
var rect = canvas.getBoundingClientRect();
var t = (clientX - rect.left) / Math.max(1, rect.width);
return t * W;
}
window.addEventListener('keydown', function(e){
var k = e.key;
if (k === 'ArrowLeft' || k === 'a' || k === 'A'){ keys.left = true; mouseActive = false; e.preventDefault(); }
else if (k === 'ArrowRight' || k === 'd' || k === 'D'){ keys.right = true; mouseActive = false; e.preventDefault(); }
else if (k === ' ' || k === 'Spacebar'){
e.preventDefault();
initAudio();
if (state === STATE.READY){ launchBall(); }
else if (state === STATE.RUNNING && !ball.launched){ launchBall(); }
else if (state === STATE.PAUSED){ togglePause(); }
else if (state === STATE.GAME_OVER){ resetGame(); }
}
else if (k === 'Enter'){
e.preventDefault();
initAudio();
if (state === STATE.READY){ launchBall(); }
else if (state === STATE.GAME_OVER){ resetGame(); }
else if (state === STATE.PAUSED){ togglePause(); }
}
else if (k === 'p' || k === 'P' || k === 'Escape'){
e.preventDefault();
if (helpOpen){ closeHelp(); return; }
togglePause();
}
else if (k === 'r' || k === 'R'){
e.preventDefault();
initAudio();
resetGame();
}
else if (k === 'h' || k === 'H'){
e.preventDefault();
toggleHelp();
}
});
window.addEventListener('keyup', function(e){
var k = e.key;
if (k === 'ArrowLeft' || k === 'a' || k === 'A') keys.left = false;
else if (k === 'ArrowRight' || k === 'd' || k === 'D') keys.right = false;
});
canvas.addEventListener('pointermove', function(e){
mouseGameX = stageToGame(e.clientX);
mouseActive = true;
});
canvas.addEventListener('pointerdown', function(e){
initAudio();
mouseGameX = stageToGame(e.clientX);
mouseActive = true;
if (e.pointerType === 'touch') e.preventDefault();
handlePrimaryAction();
});
canvas.addEventListener('pointerleave', function(){
mouseActive = false;
});
canvas.addEventListener('touchmove', function(e){
if (e.touches && e.touches.length){
mouseGameX = stageToGame(e.touches[0].clientX);
mouseActive = true;
e.preventDefault();
}
}, { passive:false });
function handlePrimaryAction(){
if (helpOpen){ closeHelp(); return; }
if (state === STATE.READY){ launchBall(); }
else if (state === STATE.RUNNING && !ball.launched){ launchBall(); }
else if (state === STATE.GAME_OVER){ resetGame(); }
else if (state === STATE.PAUSED){ togglePause(); }
}
function togglePause(){
if (helpOpen) return;
if (state === STATE.RUNNING || state === STATE.READY){
stateBeforeHelp = state;
setState(STATE.PAUSED);
} else if (state === STATE.PAUSED){
setState(stateBeforeHelp === STATE.READY ? STATE.READY : STATE.RUNNING);
}
}
function toggleHelp(){
if (helpOpen){ closeHelp(); return; }
helpOpen = true;
stateBeforeHelp = state;
helpOverlay.hidden = false;
helpBtn.setAttribute('aria-pressed', 'true');
helpBtn.textContent = 'CLOSE HELP';
if (state === STATE.RUNNING || state === STATE.READY){
setState(STATE.PAUSED);
}
closeHelpBtn.focus();
}
function closeHelp(){
helpOpen = false;
helpOverlay.hidden = true;
helpBtn.setAttribute('aria-pressed', 'false');
helpBtn.textContent = 'HELP';
if (state === STATE.PAUSED){
setState(stateBeforeHelp === STATE.READY ? STATE.READY : STATE.RUNNING);
}
helpBtn.focus();
}
helpBtn.addEventListener('click', function(){ initAudio(); toggleHelp(); });
closeHelpBtn.addEventListener('click', function(){ closeHelp(); });
startBtn.addEventListener('click', function(){
initAudio();
if (state === STATE.GAME_OVER){ resetGame(); }
else { launchBall(); }
});
resetBtn2.addEventListener('click', function(){ initAudio(); resetGame(); });
resetBtn.addEventListener('click', function(){ initAudio(); resetGame(); });
pauseBtn.addEventListener('click', function(){ initAudio(); togglePause(); });
soundBtn.addEventListener('click', function(){
muted = !muted;
soundBtn.textContent = muted ? 'SOUND OFF' : 'SOUND ON';
soundBtn.setAttribute('aria-pressed', muted ? 'true' : 'false');
if (!muted) initAudio();
});
window.addEventListener('resize', function(){ resize(); });
window.addEventListener('orientationchange', function(){ setTimeout(resize, 120); });
document.addEventListener('visibilitychange', function(){
if (document.hidden && (state === STATE.RUNNING || state === STATE.READY)){
stateBeforeHelp = state;
setState(STATE.PAUSED);
}
});
/* ============================================================
PARTICLES
============================================================ */
function maxParticles(){ return reduceMotion ? 8 : 90; }
function spawnSparks(x, y, dirX, dirY, color, count){
if (reduceMotion) count = Math.min(count, 8);
for (var i = 0; i < count; i++){
if (particles.length >= maxParticles()) particles.shift();
var spread = (Math.random() - 0.5) * 1.5;
var ang = Math.atan2(dirY, dirX) + spread;
var sp = 90 + Math.random() * 260;
particles.push({
x:x, y:y,
vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp,
life:0, max:0.22 + Math.random() * 0.12,
color: color, size: 1 + Math.random() * 2
});
}
}
function spawnBurst(x, y, colors, count, power){
if (reduceMotion) count = Math.min(count, 8);
for (var i = 0; i < count; i++){
if (particles.length >= maxParticles()) particles.shift();
var ang = Math.random() * Math.PI * 2;
var sp = (0.35 + Math.random()) * power;
particles.push({
x:x, y:y,
vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp,
life:0, max: 0.3 + Math.random() * 0.4,
color: colors[i % colors.length], size: 1.2 + Math.random() * 2.4
});
}
}
function addRing(x, y, color, r0, r1, life){
if (rings.length > 24) rings.shift();
rings.push({ x:x, y:y, color:color, r0:r0, r1:r1, life:0, max:life });
}
/* ============================================================
PHYSICS
============================================================ */
function updatePaddle(dt){
var dir = 0;
if (keys.left) dir -= 1;
if (keys.right) dir += 1;
if (dir !== 0){
paddle.vx = dir * 640;
paddle.x += paddle.vx * dt;
} else {
paddle.vx *= Math.pow(0.001, dt);
}
if (mouseActive){
var target = mouseGameX - paddle.w / 2;
paddle.x += (target - paddle.x) * Math.min(1, dt * 18);
}
var minX = PADDLE_CLAMP_PAD;
var maxX = W - paddle.w - PADDLE_CLAMP_PAD;
if (paddle.x < minX){ paddle.x = minX; paddle.vx = 0; }
if (paddle.x > maxX){ paddle.x = maxX; paddle.vx = 0; }
paddle.squeeze = Math.max(0, paddle.squeeze - dt / 0.08);
paddle.overshoot = Math.max(0, paddle.overshoot - dt / 0.12);
paddle.sheen = Math.max(0, paddle.sheen - dt / 0.22);
}
function updateBall(dt){
if (!ball.launched){
ball.x = paddle.x + paddle.w / 2;
ball.y = paddle.y - BALL_R - 2;
return;
}
var steps = Math.max(1, Math.ceil(dt / 0.008));
var step = dt / steps;
for (var s = 0; s < steps; s++){
ball.x += ball.vx * step;
ball.y += ball.vy * step;
// walls
if (ball.x - ball.r < 0){
ball.x = ball.r;
ball.vx = Math.abs(ball.vx);
sfx.wall();
addRing(ball.r, ball.y, C.voltage, 4, 16, 0.18);
} else if (ball.x + ball.r > W){
ball.x = W - ball.r;
ball.vx = -Math.abs(ball.vx);
sfx.wall();
addRing(W - ball.r, ball.y, C.voltage, 4, 16, 0.18);
}
if (ball.y - ball.r < 0){
ball.y = ball.r;
ball.vy = Math.abs(ball.vy);
sfx.wall();
addRing(ball.x, ball.r, C.voltage, 4, 16, 0.18);
}
// paddle
if (ball.vy > 0 &&
ball.y + ball.r >= paddle.y &&
ball.y - ball.r <= paddle.y + paddle.h &&
ball.x + ball.r >= paddle.x &&
ball.x - ball.r <= paddle.x + paddle.w){
ball.y = paddle.y - ball.r - 0.5;
var hit = (ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
hit = Math.max(-1, Math.min(1, hit));
var ang = hit * 1.12;
var sp = currentSpeed();
ball.speed = sp;
ball.vx = sp * Math.sin(ang);
ball.vy = -sp * Math.cos(ang);
if (Math.abs(ball.vy) < sp * 0.28){
ball.vy = -sp * 0.28 * (ball.vy > 0 ? 1 : -1);
var k = Math.sqrt(Math.max(0, sp*sp - ball.vy*ball.vy));
ball.vx = (hit >= 0 ? 1 : -1) * k;
}
combo = 0;
paddle.squeeze = 1;
paddle.overshoot = 1;
paddle.sheen = 1;
sfx.paddle();
spawnSparks(ball.x, paddle.y, ball.vx, ball.vy, C.voltage, 8);
addRing(ball.x, paddle.y, C.voltage, 6, 26, 0.2);
}
// bricks
collideBricks();
}
// trail
if (!reduceMotion){
ball.trail.push({ x:ball.x, y:ball.y });
if (ball.trail.length > 10) ball.trail.shift();
}
// life lost
if (ball.y - ball.r > H + 20){
loseLife();
}
}
function collideBricks(){
for (var i = 0; i < bricks.length; i++){
var b = bricks[i];
var cx = Math.max(b.x, Math.min(ball.x, b.x + b.w));
var cy = Math.max(b.y, Math.min(ball.y, b.y + b.h));
var dx = ball.x - cx;
var dy = ball.y - cy;
if (dx * dx + dy * dy > ball.r * ball.r) continue;
// determine reflection axis by penetration
var overlapX = (ball.r + b.w / 2) - Math.abs(ball.x - (b.x + b.w / 2));
var overlapY = (ball.r + b.h / 2) - Math.abs(ball.y - (b.y + b.h / 2));
if (overlapX < overlapY){
if (ball.x < b.x + b.w / 2) ball.vx = -Math.abs(ball.vx);
else ball.vx = Math.abs(ball.vx);
ball.x += (ball.x < b.x + b.w / 2 ? -1 : 1) * Math.min(overlapX, 4);
} else {
if (ball.y < b.y + b.h / 2) ball.vy = -Math.abs(ball.vy);
else ball.vy = Math.abs(ball.vy);
ball.y += (ball.y < b.y + b.h / 2 ? -1 : 1) * Math.min(overlapY, 4);
}
hitBrick(b, i);
break;
}
}
function hitBrick(b, index){
var color = b.unbreakable ? C.warning : HP_COLOR[Math.min(b.maxHp, 4)];
if (b.unbreakable){
b.flash = 1;
sfx.unbreakable();
spawnSparks(ball.x, ball.y, -ball.vx, -ball.vy, C.warning, 6);
addRing(ball.x, ball.y, C.warning, 5, 18, 0.16);
shake = Math.max(shake, reduceMotion ? 0 : 3);
return;
}
b.hp -= 1;
b.flash = 1;
b.scale = 0.78;
combo = Math.min(5, combo + 1);
if (combo > maxCombo) maxCombo = combo;
var pts = 10 * Math.max(1, combo);
score += pts;
sfx.brick(combo);
spawnSparks(ball.x, ball.y, ball.vx, ball.vy, color, 10);
addRing(ball.x, ball.y, color, 6, 24, 0.2);
spawnBurst(ball.x, ball.y, [color, C.ink, HP_COLOR[b.hp] || color], 5, 120);
if (b.hp <= 0){
bricks.splice(index, 1);
spawnBurst(ball.x, ball.y, [color, C.ink, C.voltage], 12, 240);
addRing(ball.x, ball.y, color, 8, 40, 0.28);
shake = Math.max(shake, reduceMotion ? 0 : 2.2);
if (destructibleLeft() === 0) triggerLevelComplete();
} else {
shake = Math.max(shake, reduceMotion ? 0 : 1.2);
}
updateHud();
}
function loseLife(){
lives -= 1;
combo = 0;
dangerFlash = 1;
shake = reduceMotion ? 0 : 10;
sfx.lifeLost();
spawnBurst(ball.x, Math.min(H - 10, ball.y), [C.danger, C.warning, C.ink], 18, 320);
addRing(ball.x, Math.min(H - 10, ball.y), C.danger, 10, 70, 0.36);
if (lives <= 0){
lives = 0;
updateHud();
triggerGameOver();
} else {
resetBallOnPaddle();
setState(STATE.READY);
announce_('Life lost. ' + lives + ' remaining.');
}
updateHud();
}
function triggerLevelComplete(){
stateBeforeHelp = STATE.RUNNING;
completeT = 0;
countdownVal = 3;
// life bonus
score += 100 * lives;
// collapse remaining bricks toward the ball, then burst
var cx = ball.x, cy = ball.y;
for (var i = 0; i < bricks.length; i++){
var b = bricks[i];
debris.push({
x:b.x + b.w/2, y:b.y + b.h/2,
tx:cx, ty:cy,
w:b.w, h:b.h,
color: b.unbreakable ? C.line : HP_COLOR[Math.min(b.maxHp,4)],
t:0, delay: (b.row * 0.03) + (b.col * 0.008)
});
}
bricks.length = 0;
var burstColors = [C.voltage, C.ink, HP_COLOR[2], HP_COLOR[3], HP_COLOR[4], C.warning];
spawnBurst(cx, cy, burstColors, reduceMotion ? 8 : 34, 420);
addRing(cx, cy, C.voltage, 10, 120, 0.5);
addRing(cx, cy, C.ink, 4, 70, 0.36);
shake = reduceMotion ? 0 : 6;
hotFlash = 1;
scoreRollFrom = scoreShown;
scoreRollTo = score;
scoreRollT = 0;
sfx.level();
setState(STATE.LEVEL_COMPLETE);
announce_('Level ' + (levelIndex + 1) + ' clear. Score ' + fmt(score) + '.');
}
function triggerGameOver(){
stateBeforeHelp = STATE.RUNNING;
setState(STATE.GAME_OVER);
sfx.gameOver();
announce_('Game over. Final score ' + fmt(score) + '.');
}
/* ============================================================
LEVEL COMPLETE SEQUENCE
============================================================ */
function updateLevelComplete(dt){
completeT += dt;
for (var i = 0; i < debris.length; i++){
var d = debris[i];
if (completeT > d.delay) d.t = Math.min(1, d.t + dt / 0.42);
}
if (scoreRollT < 0.52){
scoreRollT = Math.min(0.52, scoreRollT + dt);
var p = scoreRollT / 0.52;
var e = 1 - Math.pow(1 - p, 3);
scoreShown = scoreRollFrom + (scoreRollTo - scoreRollFrom) * e;
updateHud();
} else {
scoreShown = scoreRollTo;
}
var since = completeT;
if (since < 3.2){
var cd = Math.max(1, Math.ceil(3 - (since - 0.55)));
if (cd !== countdownVal){
countdownVal = cd;
}
ovCountdown.textContent = 'NEXT LEVEL IN ' + countdownVal;
}
if (completeT > 3.25){
if (levelIndex + 1 < LEVELS.length){
levelIndex += 1;
startLevel(levelIndex, true);
hotFlash = 1;
announce_('Level ' + (levelIndex + 1) + ': ' + LEVELS[levelIndex].name + '.');
} else {
// circuit complete -> loop back with a speed crown
levelIndex = 0;
startLevel(0, true);
hotFlash = 1;
announce_('Circuit complete. Running level 1 again.');
}
}
}
/* ============================================================
RENDER
============================================================ */
function draw(nowT){
resize();
ctx.save();
if (shake > 0.05 && !reduceMotion){
var mag = shake;
ctx.translate((Math.random() - 0.5) * mag, (Math.random() - 0.5) * mag);
}
// field background
ctx.fillStyle = C.canvas;
ctx.fillRect(-20, -20, W + 40, H + 40);
drawScanline();
drawBricks(nowT);
drawDebris();
drawParticles();
drawRings();
drawTrail();
drawPaddle();
drawBall();
drawFieldRules();
if (dangerFlash > 0){
ctx.fillStyle = 'rgba(255,107,122,' + (dangerFlash * 0.14) + ')';
ctx.fillRect(0, 0, W, H);
}
if (hotFlash > 0){
ctx.strokeStyle = 'rgba(98,245,208,' + (hotFlash * 0.8) + ')';
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
}
if (state === STATE.PAUSED || helpOpen){
ctx.fillStyle = 'rgba(8,11,16,0.58)';
ctx.fillRect(0, 0, W, H);
}
ctx.restore();
}
function drawScanline(){
// very quiet structural grid inside the arena
ctx.save();
ctx.strokeStyle = 'rgba(42,59,74,0.22)';
ctx.lineWidth = 1;
for (var x = 0; x <= W; x += 48){
ctx.beginPath();
ctx.moveTo(x + 0.5, 0);
ctx.lineTo(x + 0.5, H);
ctx.stroke();
}
for (var y = 0; y <= H; y += 48){
ctx.beginPath();
ctx.moveTo(0, y + 0.5);
ctx.lineTo(W, y + 0.5);
ctx.stroke();
}
ctx.restore();
}
function drawFieldRules(){
ctx.save();
// paddle zone rule
ctx.strokeStyle = 'rgba(42,59,74,0.9)';
ctx.lineWidth = 1;
ctx.setLineDash([6, 8]);
ctx.beginPath();
ctx.moveTo(24, PADDLE_Y - 26);
ctx.lineTo(W - 24, PADDLE_Y - 26);
ctx.stroke();
ctx.setLineDash([]);
// corner ticks
ctx.strokeStyle = 'rgba(98,245,208,0.22)';
ctx.lineWidth = 2;
var t = 16;
var pts = [[24,24,1,1],[W-24,24,-1,1],[24,H-24,1,-1],[W-24,H-24,-1,-1]];
for (var i = 0; i < pts.length; i++){
var p = pts[i];
ctx.beginPath();
ctx.moveTo(p[0], p[1]);
ctx.lineTo(p[0] + t * p[2], p[1]);
ctx.moveTo(p[0], p[1]);
ctx.lineTo(p[0], p[1] + t * p[3]);
ctx.stroke();
}
ctx.restore();
}
function drawBricks(nowT){
var elapsed = nowT - levelStartT;
for (var i = 0; i < bricks.length; i++){
var b = bricks[i];
// moving hazard row
var ox = 0;
var lv = LEVELS[levelIndex];
if (lv.moveRow === b.row && lv.moveAmp > 0 && !reduceMotion){
ox = Math.sin(nowT * 0.7 + b.row) * lv.moveAmp;
} else if (lv.moveRow === b.row && lv.moveAmp > 0){
ox = Math.sin(nowT * 0.7 + b.row) * lv.moveAmp;
}
var bx = b.x + ox;
// spawn draw-in
var appear = 1;
if (elapsed < 0.42 && state === STATE.READY){
appear = Math.max(0, Math.min(1, (elapsed - b.row * 0.024 - b.col * 0.0037) / 0.05));
}
var flash = b.flash;
var scaleX = b.scale;
var scaleY = b.scale * (2 - b.scale);
ctx.save();
var cx = bx + b.w / 2;
var cy = b.y + b.h / 2;
ctx.translate(cx, cy);
ctx.scale(Math.max(0.02, scaleX * appear), Math.max(0.02, scaleY * appear));
ctx.translate(-b.w / 2, -b.h / 2);
if (b.unbreakable){
ctx.fillStyle = C.surface2;
ctx.fillRect(0, 0, b.w, b.h);
// diagonal hatch
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, b.w, b.h);
ctx.clip();
ctx.strokeStyle = C.inkMuted;
ctx.lineWidth = 1;
for (var k = -b.h; k < b.w + b.h; k += 8){
ctx.beginPath();
ctx.moveTo(k, 0);
ctx.lineTo(k + b.h, b.h);
ctx.stroke();
}
ctx.restore();
ctx.strokeStyle = flash > 0 ? C.ink : C.warning;
ctx.lineWidth = 1 + flash * 1.5;
ctx.strokeRect(0.5, 0.5, b.w - 1, b.h - 1);
} else {
var col = HP_COLOR[Math.min(b.maxHp, 4)];
if (b.hp < b.maxHp && b.maxHp > 1) col = HP_COLOR[Math.min(b.hp, 4)];
var glow = flash;
ctx.fillStyle = C.surface2;
ctx.fillRect(0, 0, b.w, b.h);
if (glow > 0){
ctx.fillStyle = 'rgba(234,247,245,' + (glow * 0.16) + ')';
ctx.fillRect(0, 0, b.w, b.h);
}
ctx.strokeStyle = glow > 0 ? mixHex(col, '#FFFFFF', glow * 0.7) : col;
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, b.w - 2, b.h - 2);
// center core
var coreSize = 4 + (b.hp - 1) * 1.2;
ctx.fillStyle = glow > 0 ? mixHex(col, '#FFFFFF', glow * 0.6) : HP_CORE[Math.min(b.maxHp, 4)];
ctx.fillRect(b.w / 2 - coreSize / 2, b.h / 2 - coreSize / 2, coreSize, coreSize);
// top edge accent
ctx.fillStyle = col;
ctx.globalAlpha = 0.55;
ctx.fillRect(1, 1, b.w - 2, 2);
ctx.globalAlpha = 1;
}
ctx.restore();
b.flash = Math.max(0, b.flash - 0.09);
b.scale += (1 - b.scale) * 0.22;
}
}
function drawDebris(){
for (var i = 0; i < debris.length; i++){
var d = debris[i];
var e = d.t < 1 ? (1 - Math.pow(1 - d.t, 3)) : 1;
var x = d.x + (d.tx - d.x) * e;
var y = d.y + (d.ty - d.y) * e;
var s = (1 - e * 0.85) * (d.w / 38);
ctx.save();
ctx.globalAlpha = 1 - e * 0.9;
ctx.translate(x, y);
ctx.scale(Math.max(0.02, s), Math.max(0.02, s));
ctx.strokeStyle = d.color;
ctx.lineWidth = 2;
ctx.strokeRect(-19, -19, 38, 38);
ctx.restore();
}
}
function drawParticles(){
for (var i = particles.length - 1; i >= 0; i--){
var p = particles[i];
var t = p.life / p.max;
if (t >= 1){ particles.splice(i, 1); continue; }
ctx.globalAlpha = 1 - t;
ctx.fillStyle = p.color;
var sz = p.size * (1 - t * 0.5);
ctx.fillRect(p.x - sz / 2, p.y - sz / 2, sz, sz);
}
ctx.globalAlpha = 1;
}
function drawRings(){
for (var i = rings.length - 1; i >= 0; i--){
var r = rings[i];
var t = r.life / r.max;
if (t >= 1){ rings.splice(i, 1); continue; }
var rad = r.r0 + (r.r1 - r.r0) * (1 - Math.pow(1 - t, 2));
ctx.globalAlpha = (1 - t) * 0.8;
ctx.strokeStyle = r.color;
ctx.lineWidth = 1 + (1 - t) * 1.5;
ctx.beginPath();
ctx.arc(r.x, r.y, rad, 0, Math.PI * 2);
ctx.stroke();
}
ctx.globalAlpha = 1;
}
function drawTrail(){
if (ball.trail.length < 2) return;
ctx.save();
for (var i = 0; i < ball.trail.length; i++){
var p = ball.trail[i];
var t = i / ball.trail.length;
ctx.globalAlpha = t * 0.28;
ctx.fillStyle = C.voltage;
var r = BALL_R * (0.35 + t * 0.6);
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
ctx.globalAlpha = 1;
}
function drawPaddle(){
var w = paddle.w;
var h = paddle.h;
var x = paddle.x;
var y = paddle.y;
// squeeze on contact, overshoot rebound
var squeezeAmt = paddle.squeeze;
var over = paddle.overshoot;
var dw = -6 * squeezeAmt + 10 * over * (1 - squeezeAmt);
var dwClamped = Math.max(-8, Math.min(12, dw));
var cx = x + w / 2;
x = cx - (w + dwClamped) / 2;
w = w + dwClamped;
ctx.save();
// glow under
ctx.fillStyle = 'rgba(98,245,208,0.16)';
ctx.fillRect(x - 6, y - 4, w + 12, h + 10);
ctx.fillStyle = C.voltage;
ctx.fillRect(x, y, w, h);
// seams
ctx.fillStyle = C.canvas;
ctx.fillRect(x + w * 0.16, y, 2, h);
ctx.fillRect(x + w * 0.84 - 2, y, 2, h);
// top highlight
ctx.fillStyle = 'rgba(234,247,245,0.75)';
ctx.fillRect(x, y, w, 2);
// sheen sweep
if (paddle.sheen > 0 && !reduceMotion){
var t = 1 - paddle.sheen;
var sx = x - 30 + t * (w + 60);
var grad = ctx.createLinearGradient(sx - 26, 0, sx + 26, 0);
grad.addColorStop(0, 'rgba(234,247,245,0)');
grad.addColorStop(0.5, 'rgba(234,247,245,' + (paddle.sheen * 0.55) + ')');
grad.addColorStop(1, 'rgba(234,247,245,0)');
ctx.fillStyle = grad;
ctx.fillRect(x, y, w, h);
}
// end caps
ctx.fillStyle = C.voltageDim;
ctx.fillRect(x, y, 3, h);
ctx.fillRect(x + w - 3, y, 3, h);
ctx.restore();
}
function drawBall(){
// trail
if (!reduceMotion && ball.launched && ball.trail.length){
ctx.save();
for (var i = 0; i < ball.trail.length; i++){
var p = ball.trail[i];
var t = i / ball.trail.length;
ctx.globalAlpha = t * 0.22;
ctx.fillStyle = C.voltage;
var r = BALL_R * (0.3 + t * 0.7);
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
ctx.globalAlpha = 1;
}
// soft glow
var g = ctx.createRadialGradient(ball.x, ball.y, 1, ball.x, ball.y, BALL_R * 3.2);
g.addColorStop(0, 'rgba(98,245,208,0.5)');
g.addColorStop(1, 'rgba(98,245,208,0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(ball.x, ball.y, BALL_R * 3.2, 0, Math.PI * 2);
ctx.fill();
// core
ctx.fillStyle = C.voltage;
ctx.beginPath();
ctx.arc(ball.x, ball.y, BALL_R, 0, Math.PI * 2);
ctx.fill();
// highlight
ctx.fillStyle = '#FFFFFF';
ctx.globalAlpha = 0.9;
ctx.beginPath();
ctx.arc(ball.x - 2, ball.y - 2, 2.1, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
/* ============================================================
COLOR HELPERS
============================================================ */
function hexToRgb(h){
h = h.replace('#','');
if (h.length === 3) h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2];
return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)];
}
function mixHex(a, b, t){
t = Math.max(0, Math.min(1, t));
var A = hexToRgb(a), B = hexToRgb(b);
return 'rgb(' + Math.round(A[0]+(B[0]-A[0])*t) + ',' +
Math.round(A[1]+(B[1]-A[1])*t) + ',' +
Math.round(A[2]+(B[2]-A[2])*t) + ')';
}
/* ============================================================
MAIN LOOP
============================================================ */
var lastT = 0;
function frame(ts){
if (!lastT) lastT = ts;
var dt = Math.min(0.033, (ts - lastT) / 1000);
lastT = ts;
now = ts / 1000;
if (state === STATE.RUNNING){
levelElapsed += dt;
updatePaddle(dt);
updateBall(dt);
} else if (state === STATE.READY){
bobT += dt;
updatePaddle(dt);
updateBall(dt);
} else if (state === STATE.LEVEL_COMPLETE){
updateLevelComplete(dt);
updatePaddle(dt);
} else if (state === STATE.PAUSED){
// frozen
} else if (state === STATE.GAME_OVER){
updatePaddle(dt);
}
// particle + ring integration
for (var i = 0; i < particles.length; i++){
var p = particles[i];
p.life += dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= Math.pow(0.12, dt);
p.vy = p.vy * Math.pow(0.12, dt) + 260 * dt;
}
for (var j = 0; j < rings.length; j++){
rings[j].life += dt;
}
shake = Math.max(0, shake - dt * 26);
hotFlash = Math.max(0, hotFlash - dt * 1.8);
dangerFlash = Math.max(0, dangerFlash - dt * 1.4);
// score smoothing outside of the explicit roll
if (state !== STATE.LEVEL_COMPLETE){
if (Math.abs(scoreShown - score) > 0.5){
scoreShown += (score - scoreShown) * Math.min(1, dt * 9);
updateHud();
} else if (scoreShown !== score){
scoreShown = score;
updateHud();
}
}
draw(now);
requestAnimationFrame(frame);
}
/* ============================================================
BOOT
============================================================ */
resize();
startLevel(0, true);
setState(STATE.READY);
requestAnimationFrame(frame);
// expose a tiny hook for automated verification
window.__BREAKOUT__ = {
state: function(){ return state; },
score: function(){ return score; },
level: function(){ return levelIndex + 1; },
lives: function(){ return lives; },
bricks: function(){ return bricks.length; },
destructible: function(){ return destructibleLeft(); },
ball: function(){ return ball; },
paddle: function(){ return paddle; },
version: '1.0.0'
};
})();
</script>
</body>
</html>

