fix: reactive component props + breakout improvements

Compiler fixes:
- Component props with signal-dependent expressions now wrapped as
  reactive getters (() => expr) at call site
- Component declarations handle fn-typed props via
  { get value() { return props.x(); } } for live reactivity
- Container style: prop wrapped in DS.effect when expr contains .value
- Timer merging: all same-interval 'every' statements grouped into
  single setInterval with one DS.flush()

Breakout game improvements:
- Classic row order: blue top (far), red bottom (near paddle)
- All 5 rows have full collision detection (was only 2)
- Faster ball (4px/frame) and paddle (40px/keypress)
- Score/Lives badges now update in real-time

All 48 examples compile, 136 tests pass
This commit is contained in:
enzotar 2026-02-27 10:53:27 -08:00
parent e8bbfcbcb7
commit 4ac584c81e
2 changed files with 130 additions and 65 deletions

View file

@ -483,9 +483,12 @@ impl JsEmitter {
// Destructure props into local signal-compatible variables
// Props may be raw values, signals, or callback functions
for p in &comp.props {
// Create a signal-like wrapper: if prop is a function, keep as-is; if already a signal, use it; otherwise wrap
// Create a signal-like wrapper:
// - If prop is a function (reactive getter), wrap as { get value() { return props.x(); } }
// - If already a signal object, use it directly
// - Otherwise wrap raw value as a getter
self.emit_line(&format!(
"const {} = (typeof props.{} === 'function') ? props.{} : (props.{} !== undefined && props.{} !== null) ? (typeof props.{} === 'object' && 'value' in props.{} ? props.{} : {{ get value() {{ return props.{}; }} }}) : {{ get value() {{ return \"\"; }} }};",
"const {} = (typeof props.{} === 'function') ? {{ get value() {{ return props.{}(); }} }} : (props.{} !== undefined && props.{} !== null) ? (typeof props.{} === 'object' && 'value' in props.{} ? props.{} : {{ get value() {{ return props.{}; }} }}) : {{ get value() {{ return \"\"; }} }};",
p.name, p.name, p.name, p.name, p.name, p.name, p.name, p.name, p.name
));
}
@ -572,7 +575,14 @@ impl JsEmitter {
}
"style" => {
let js = self.emit_expr(val);
self.emit_line(&format!("{}.style.cssText = {};", node_var, js));
if js.contains(".value") {
self.emit_line(&format!(
"DS.effect(() => {{ {}.style.cssText = {}; }});",
node_var, js
));
} else {
self.emit_line(&format!("{}.style.cssText = {};", node_var, js));
}
}
_ => {
// Layout props (x, y, width, height) or arbitrary style
@ -940,7 +950,13 @@ impl JsEmitter {
let handler_js = self.emit_event_handler_expr(v);
format!("{}: () => {{ {}; DS.flush() }}", k, handler_js)
} else {
format!("{}: {}", k, self.emit_expr(v))
let js = self.emit_expr(v);
// If prop expression references signals, wrap as reactive getter
if js.contains(".value") {
format!("{}: () => {}", k, js)
} else {
format!("{}: {}", k, js)
}
}
})
.collect();

View file

@ -2,6 +2,9 @@
-- Classic brick-breaker game with keyboard controls, sound effects, and streaming
-- Paddle, ball, and bricks are CSS-positioned elements inside a stack container
--
-- Layout: Blue (top/far) → Green → Yellow → Orange → Red (bottom/near paddle)
-- Ball hits red first, then works up — classic Breakout style
--
-- Run:
-- Tab 1: cargo run -p ds-stream (relay)
-- Tab 2: dreamstack dev examples/game-breakout.ds (player)
@ -11,14 +14,19 @@ import { Badge } from "../registry/components/badge"
-- ── State ──
let paddleX = 250
let ballX = 300
let ballY = 350
let bvx = 3
let bvy = -3
let ballY = 320
let bvx = 4
let bvy = -4
let score = 0
let lives = 3
let paused = 0
-- 5 rows × 10 columns = 50 bricks (1 = alive, 0 = destroyed)
-- r0 = red (bottom, closest to paddle) y=200..216
-- r1 = orange y=180..196
-- r2 = yellow y=160..176
-- r3 = green y=140..156
-- r4 = blue (top, farthest) y=120..136
let r0 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
let r1 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
let r2 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
@ -26,7 +34,7 @@ let r3 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
let r4 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
-- ── Keyboard controls ──
on keydown(ev) -> paddleX = if ev.key == "ArrowLeft" then (if paddleX > 0 then paddleX - 30 else paddleX) else (if ev.key == "ArrowRight" then (if paddleX < 520 then paddleX + 30 else paddleX) else paddleX)
on keydown(ev) -> paddleX = if ev.key == "ArrowLeft" then (if paddleX > 0 then paddleX - 40 else paddleX) else (if ev.key == "ArrowRight" then (if paddleX < 520 then paddleX + 40 else paddleX) else paddleX)
on keydown(ev) -> paused = if ev.key == " " then (if paused then 0 else 1) else paused
-- ── Game loop at ~30fps ──
@ -34,58 +42,99 @@ every 33 -> ballX = if paused then ballX else (if lives > 0 then ballX + bvx els
every 33 -> ballY = if paused then ballY else (if lives > 0 then ballY + bvy else ballY)
-- Wall bounces (left/right/ceiling)
every 33 -> bvx = if ballX < 4 then 3 else bvx
every 33 -> bvx = if ballX > 590 then -3 else bvx
every 33 -> bvy = if ballY < 4 then 3 else bvy
every 33 -> bvx = if ballX < 4 then 4 else bvx
every 33 -> bvx = if ballX > 590 then -4 else bvx
every 33 -> bvy = if ballY < 4 then 4 else bvy
-- Ball lost (bottom) — lose a life and reset
every 33 -> lives = if ballY > 410 then lives - 1 else lives
every 33 -> ballX = if ballY > 410 then 300 else ballX
every 33 -> ballY = if ballY > 410 then 350 else ballY
every 33 -> bvx = if ballY > 410 then 3 else bvx
every 33 -> bvy = if ballY > 410 then -3 else bvy
every 33 -> lives = if ballY > 395 then lives - 1 else lives
every 33 -> ballX = if ballY > 395 then 300 else ballX
every 33 -> ballY = if ballY > 395 then 320 else ballY
every 33 -> bvx = if ballY > 395 then 4 else bvx
every 33 -> bvy = if ballY > 395 then -4 else bvy
-- Paddle hit
every 33 -> bvy = if ballY > 365 then (if ballX > paddleX then (if ballX < paddleX + 80 then -3 else bvy) else bvy) else bvy
-- Paddle hit (paddle at y=360)
every 33 -> bvy = if ballY > 348 then (if ballX > paddleX then (if ballX < paddleX + 80 then -4 else bvy) else bvy) else bvy
-- ── Brick collision: row 0 (y=30..48) ──
-- Score + bounce FIRST (before brick is cleared)
every 33 -> score = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if r0[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if r0[floor(ballX / 60)] then 3 else bvy) else bvy) else bvy) else bvy
-- ──────────────────────────────────────────────────────
-- Brick collision for all 5 rows
-- Each row: score+bounce FIRST, then destroy per-brick
-- ──────────────────────────────────────────────────────
-- Then destroy the brick
every 33 -> r0[0] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 0 then (if ballX < 60 then (if r0[0] then 0 else r0[0]) else r0[0]) else r0[0]) else r0[0]) else r0[0]) else r0[0]
every 33 -> r0[1] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 60 then (if ballX < 120 then (if r0[1] then 0 else r0[1]) else r0[1]) else r0[1]) else r0[1]) else r0[1]) else r0[1]
every 33 -> r0[2] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 120 then (if ballX < 180 then (if r0[2] then 0 else r0[2]) else r0[2]) else r0[2]) else r0[2]) else r0[2]) else r0[2]
every 33 -> r0[3] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 180 then (if ballX < 240 then (if r0[3] then 0 else r0[3]) else r0[3]) else r0[3]) else r0[3]) else r0[3]) else r0[3]
every 33 -> r0[4] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 240 then (if ballX < 300 then (if r0[4] then 0 else r0[4]) else r0[4]) else r0[4]) else r0[4]) else r0[4]) else r0[4]
every 33 -> r0[5] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 300 then (if ballX < 360 then (if r0[5] then 0 else r0[5]) else r0[5]) else r0[5]) else r0[5]) else r0[5]) else r0[5]
every 33 -> r0[6] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 360 then (if ballX < 420 then (if r0[6] then 0 else r0[6]) else r0[6]) else r0[6]) else r0[6]) else r0[6]) else r0[6]
every 33 -> r0[7] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 420 then (if ballX < 480 then (if r0[7] then 0 else r0[7]) else r0[7]) else r0[7]) else r0[7]) else r0[7]) else r0[7]
every 33 -> r0[8] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 480 then (if ballX < 540 then (if r0[8] then 0 else r0[8]) else r0[8]) else r0[8]) else r0[8]) else r0[8]) else r0[8]
every 33 -> r0[9] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 then (if ballX > 540 then (if ballX < 600 then (if r0[9] then 0 else r0[9]) else r0[9]) else r0[9]) else r0[9]) else r0[9]) else r0[9]
-- ── Row 0: RED (bottom, y=200..216) — hit first by ball ──
every 33 -> score = if bvy < 0 then (if ballY < 218 then (if ballY > 198 then (if r0[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 218 then (if ballY > 198 then (if r0[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r0[0] = if ballY < 218 then (if ballY > 198 then (if ballX > 0 then (if ballX < 60 then (if r0[0] then 0 else r0[0]) else r0[0]) else r0[0]) else r0[0]) else r0[0]
every 33 -> r0[1] = if ballY < 218 then (if ballY > 198 then (if ballX > 60 then (if ballX < 120 then (if r0[1] then 0 else r0[1]) else r0[1]) else r0[1]) else r0[1]) else r0[1]
every 33 -> r0[2] = if ballY < 218 then (if ballY > 198 then (if ballX > 120 then (if ballX < 180 then (if r0[2] then 0 else r0[2]) else r0[2]) else r0[2]) else r0[2]) else r0[2]
every 33 -> r0[3] = if ballY < 218 then (if ballY > 198 then (if ballX > 180 then (if ballX < 240 then (if r0[3] then 0 else r0[3]) else r0[3]) else r0[3]) else r0[3]) else r0[3]
every 33 -> r0[4] = if ballY < 218 then (if ballY > 198 then (if ballX > 240 then (if ballX < 300 then (if r0[4] then 0 else r0[4]) else r0[4]) else r0[4]) else r0[4]) else r0[4]
every 33 -> r0[5] = if ballY < 218 then (if ballY > 198 then (if ballX > 300 then (if ballX < 360 then (if r0[5] then 0 else r0[5]) else r0[5]) else r0[5]) else r0[5]) else r0[5]
every 33 -> r0[6] = if ballY < 218 then (if ballY > 198 then (if ballX > 360 then (if ballX < 420 then (if r0[6] then 0 else r0[6]) else r0[6]) else r0[6]) else r0[6]) else r0[6]
every 33 -> r0[7] = if ballY < 218 then (if ballY > 198 then (if ballX > 420 then (if ballX < 480 then (if r0[7] then 0 else r0[7]) else r0[7]) else r0[7]) else r0[7]) else r0[7]
every 33 -> r0[8] = if ballY < 218 then (if ballY > 198 then (if ballX > 480 then (if ballX < 540 then (if r0[8] then 0 else r0[8]) else r0[8]) else r0[8]) else r0[8]) else r0[8]
every 33 -> r0[9] = if ballY < 218 then (if ballY > 198 then (if ballX > 540 then (if ballX < 600 then (if r0[9] then 0 else r0[9]) else r0[9]) else r0[9]) else r0[9]) else r0[9]
-- ── Brick collision: row 1 (y=50..68) ──
-- Score + bounce FIRST
every 33 -> score = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if r1[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if r1[floor(ballX / 60)] then 3 else bvy) else bvy) else bvy) else bvy
-- ── Row 1: ORANGE (y=180..196) ──
every 33 -> score = if bvy < 0 then (if ballY < 198 then (if ballY > 178 then (if r1[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 198 then (if ballY > 178 then (if r1[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r1[0] = if ballY < 198 then (if ballY > 178 then (if ballX > 0 then (if ballX < 60 then (if r1[0] then 0 else r1[0]) else r1[0]) else r1[0]) else r1[0]) else r1[0]
every 33 -> r1[1] = if ballY < 198 then (if ballY > 178 then (if ballX > 60 then (if ballX < 120 then (if r1[1] then 0 else r1[1]) else r1[1]) else r1[1]) else r1[1]) else r1[1]
every 33 -> r1[2] = if ballY < 198 then (if ballY > 178 then (if ballX > 120 then (if ballX < 180 then (if r1[2] then 0 else r1[2]) else r1[2]) else r1[2]) else r1[2]) else r1[2]
every 33 -> r1[3] = if ballY < 198 then (if ballY > 178 then (if ballX > 180 then (if ballX < 240 then (if r1[3] then 0 else r1[3]) else r1[3]) else r1[3]) else r1[3]) else r1[3]
every 33 -> r1[4] = if ballY < 198 then (if ballY > 178 then (if ballX > 240 then (if ballX < 300 then (if r1[4] then 0 else r1[4]) else r1[4]) else r1[4]) else r1[4]) else r1[4]
every 33 -> r1[5] = if ballY < 198 then (if ballY > 178 then (if ballX > 300 then (if ballX < 360 then (if r1[5] then 0 else r1[5]) else r1[5]) else r1[5]) else r1[5]) else r1[5]
every 33 -> r1[6] = if ballY < 198 then (if ballY > 178 then (if ballX > 360 then (if ballX < 420 then (if r1[6] then 0 else r1[6]) else r1[6]) else r1[6]) else r1[6]) else r1[6]
every 33 -> r1[7] = if ballY < 198 then (if ballY > 178 then (if ballX > 420 then (if ballX < 480 then (if r1[7] then 0 else r1[7]) else r1[7]) else r1[7]) else r1[7]) else r1[7]
every 33 -> r1[8] = if ballY < 198 then (if ballY > 178 then (if ballX > 480 then (if ballX < 540 then (if r1[8] then 0 else r1[8]) else r1[8]) else r1[8]) else r1[8]) else r1[8]
every 33 -> r1[9] = if ballY < 198 then (if ballY > 178 then (if ballX > 540 then (if ballX < 600 then (if r1[9] then 0 else r1[9]) else r1[9]) else r1[9]) else r1[9]) else r1[9]
-- Then destroy the brick
every 33 -> r1[0] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 0 then (if ballX < 60 then (if r1[0] then 0 else r1[0]) else r1[0]) else r1[0]) else r1[0]) else r1[0]) else r1[0]
every 33 -> r1[1] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 60 then (if ballX < 120 then (if r1[1] then 0 else r1[1]) else r1[1]) else r1[1]) else r1[1]) else r1[1]) else r1[1]
every 33 -> r1[2] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 120 then (if ballX < 180 then (if r1[2] then 0 else r1[2]) else r1[2]) else r1[2]) else r1[2]) else r1[2]) else r1[2]
every 33 -> r1[3] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 180 then (if ballX < 240 then (if r1[3] then 0 else r1[3]) else r1[3]) else r1[3]) else r1[3]) else r1[3]) else r1[3]
every 33 -> r1[4] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 240 then (if ballX < 300 then (if r1[4] then 0 else r1[4]) else r1[4]) else r1[4]) else r1[4]) else r1[4]) else r1[4]
every 33 -> r1[5] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 300 then (if ballX < 360 then (if r1[5] then 0 else r1[5]) else r1[5]) else r1[5]) else r1[5]) else r1[5]) else r1[5]
every 33 -> r1[6] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 360 then (if ballX < 420 then (if r1[6] then 0 else r1[6]) else r1[6]) else r1[6]) else r1[6]) else r1[6]) else r1[6]
every 33 -> r1[7] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 420 then (if ballX < 480 then (if r1[7] then 0 else r1[7]) else r1[7]) else r1[7]) else r1[7]) else r1[7]) else r1[7]
every 33 -> r1[8] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 480 then (if ballX < 540 then (if r1[8] then 0 else r1[8]) else r1[8]) else r1[8]) else r1[8]) else r1[8]) else r1[8]
every 33 -> r1[9] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 then (if ballX > 540 then (if ballX < 600 then (if r1[9] then 0 else r1[9]) else r1[9]) else r1[9]) else r1[9]) else r1[9]) else r1[9]
-- ── Row 2: YELLOW (y=160..176) ──
every 33 -> score = if bvy < 0 then (if ballY < 178 then (if ballY > 158 then (if r2[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 178 then (if ballY > 158 then (if r2[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r2[0] = if ballY < 178 then (if ballY > 158 then (if ballX > 0 then (if ballX < 60 then (if r2[0] then 0 else r2[0]) else r2[0]) else r2[0]) else r2[0]) else r2[0]
every 33 -> r2[1] = if ballY < 178 then (if ballY > 158 then (if ballX > 60 then (if ballX < 120 then (if r2[1] then 0 else r2[1]) else r2[1]) else r2[1]) else r2[1]) else r2[1]
every 33 -> r2[2] = if ballY < 178 then (if ballY > 158 then (if ballX > 120 then (if ballX < 180 then (if r2[2] then 0 else r2[2]) else r2[2]) else r2[2]) else r2[2]) else r2[2]
every 33 -> r2[3] = if ballY < 178 then (if ballY > 158 then (if ballX > 180 then (if ballX < 240 then (if r2[3] then 0 else r2[3]) else r2[3]) else r2[3]) else r2[3]) else r2[3]
every 33 -> r2[4] = if ballY < 178 then (if ballY > 158 then (if ballX > 240 then (if ballX < 300 then (if r2[4] then 0 else r2[4]) else r2[4]) else r2[4]) else r2[4]) else r2[4]
every 33 -> r2[5] = if ballY < 178 then (if ballY > 158 then (if ballX > 300 then (if ballX < 360 then (if r2[5] then 0 else r2[5]) else r2[5]) else r2[5]) else r2[5]) else r2[5]
every 33 -> r2[6] = if ballY < 178 then (if ballY > 158 then (if ballX > 360 then (if ballX < 420 then (if r2[6] then 0 else r2[6]) else r2[6]) else r2[6]) else r2[6]) else r2[6]
every 33 -> r2[7] = if ballY < 178 then (if ballY > 158 then (if ballX > 420 then (if ballX < 480 then (if r2[7] then 0 else r2[7]) else r2[7]) else r2[7]) else r2[7]) else r2[7]
every 33 -> r2[8] = if ballY < 178 then (if ballY > 158 then (if ballX > 480 then (if ballX < 540 then (if r2[8] then 0 else r2[8]) else r2[8]) else r2[8]) else r2[8]) else r2[8]
every 33 -> r2[9] = if ballY < 178 then (if ballY > 158 then (if ballX > 540 then (if ballX < 600 then (if r2[9] then 0 else r2[9]) else r2[9]) else r2[9]) else r2[9]) else r2[9]
-- ── Row 3: GREEN (y=140..156) ──
every 33 -> score = if bvy < 0 then (if ballY < 158 then (if ballY > 138 then (if r3[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 158 then (if ballY > 138 then (if r3[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r3[0] = if ballY < 158 then (if ballY > 138 then (if ballX > 0 then (if ballX < 60 then (if r3[0] then 0 else r3[0]) else r3[0]) else r3[0]) else r3[0]) else r3[0]
every 33 -> r3[1] = if ballY < 158 then (if ballY > 138 then (if ballX > 60 then (if ballX < 120 then (if r3[1] then 0 else r3[1]) else r3[1]) else r3[1]) else r3[1]) else r3[1]
every 33 -> r3[2] = if ballY < 158 then (if ballY > 138 then (if ballX > 120 then (if ballX < 180 then (if r3[2] then 0 else r3[2]) else r3[2]) else r3[2]) else r3[2]) else r3[2]
every 33 -> r3[3] = if ballY < 158 then (if ballY > 138 then (if ballX > 180 then (if ballX < 240 then (if r3[3] then 0 else r3[3]) else r3[3]) else r3[3]) else r3[3]) else r3[3]
every 33 -> r3[4] = if ballY < 158 then (if ballY > 138 then (if ballX > 240 then (if ballX < 300 then (if r3[4] then 0 else r3[4]) else r3[4]) else r3[4]) else r3[4]) else r3[4]
every 33 -> r3[5] = if ballY < 158 then (if ballY > 138 then (if ballX > 300 then (if ballX < 360 then (if r3[5] then 0 else r3[5]) else r3[5]) else r3[5]) else r3[5]) else r3[5]
every 33 -> r3[6] = if ballY < 158 then (if ballY > 138 then (if ballX > 360 then (if ballX < 420 then (if r3[6] then 0 else r3[6]) else r3[6]) else r3[6]) else r3[6]) else r3[6]
every 33 -> r3[7] = if ballY < 158 then (if ballY > 138 then (if ballX > 420 then (if ballX < 480 then (if r3[7] then 0 else r3[7]) else r3[7]) else r3[7]) else r3[7]) else r3[7]
every 33 -> r3[8] = if ballY < 158 then (if ballY > 138 then (if ballX > 480 then (if ballX < 540 then (if r3[8] then 0 else r3[8]) else r3[8]) else r3[8]) else r3[8]) else r3[8]
every 33 -> r3[9] = if ballY < 158 then (if ballY > 138 then (if ballX > 540 then (if ballX < 600 then (if r3[9] then 0 else r3[9]) else r3[9]) else r3[9]) else r3[9]) else r3[9]
-- ── Row 4: BLUE (top, y=120..136) — hit last by ball ──
every 33 -> score = if bvy < 0 then (if ballY < 138 then (if ballY > 118 then (if r4[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 138 then (if ballY > 118 then (if r4[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r4[0] = if ballY < 138 then (if ballY > 118 then (if ballX > 0 then (if ballX < 60 then (if r4[0] then 0 else r4[0]) else r4[0]) else r4[0]) else r4[0]) else r4[0]
every 33 -> r4[1] = if ballY < 138 then (if ballY > 118 then (if ballX > 60 then (if ballX < 120 then (if r4[1] then 0 else r4[1]) else r4[1]) else r4[1]) else r4[1]) else r4[1]
every 33 -> r4[2] = if ballY < 138 then (if ballY > 118 then (if ballX > 120 then (if ballX < 180 then (if r4[2] then 0 else r4[2]) else r4[2]) else r4[2]) else r4[2]) else r4[2]
every 33 -> r4[3] = if ballY < 138 then (if ballY > 118 then (if ballX > 180 then (if ballX < 240 then (if r4[3] then 0 else r4[3]) else r4[3]) else r4[3]) else r4[3]) else r4[3]
every 33 -> r4[4] = if ballY < 138 then (if ballY > 118 then (if ballX > 240 then (if ballX < 300 then (if r4[4] then 0 else r4[4]) else r4[4]) else r4[4]) else r4[4]) else r4[4]
every 33 -> r4[5] = if ballY < 138 then (if ballY > 118 then (if ballX > 300 then (if ballX < 360 then (if r4[5] then 0 else r4[5]) else r4[5]) else r4[5]) else r4[5]) else r4[5]
every 33 -> r4[6] = if ballY < 138 then (if ballY > 118 then (if ballX > 360 then (if ballX < 420 then (if r4[6] then 0 else r4[6]) else r4[6]) else r4[6]) else r4[6]) else r4[6]
every 33 -> r4[7] = if ballY < 138 then (if ballY > 118 then (if ballX > 420 then (if ballX < 480 then (if r4[7] then 0 else r4[7]) else r4[7]) else r4[7]) else r4[7]) else r4[7]
every 33 -> r4[8] = if ballY < 138 then (if ballY > 118 then (if ballX > 480 then (if ballX < 540 then (if r4[8] then 0 else r4[8]) else r4[8]) else r4[8]) else r4[8]) else r4[8]
every 33 -> r4[9] = if ballY < 138 then (if ballY > 118 then (if ballX > 540 then (if ballX < 600 then (if r4[9] then 0 else r4[9]) else r4[9]) else r4[9]) else r4[9]) else r4[9]
-- ── Sound effects ──
every 33 -> play_tone(if ballY > 365 then (if ballX > paddleX then (if ballX < paddleX + 80 then 440 else 0) else 0) else 0, 60)
every 33 -> play_tone(if ballY < 68 then (if ballY > 28 then 880 else 0) else 0, 40)
every 33 -> play_tone(if ballY > 410 then 110 else 0, 300, "sawtooth")
every 33 -> play_tone(if ballY > 348 then (if ballX > paddleX then (if ballX < paddleX + 80 then 440 else 0) else 0) else 0, 60)
every 33 -> play_tone(if ballY < 218 then (if ballY > 118 then 880 else 0) else 0, 40)
every 33 -> play_tone(if ballY > 395 then 110 else 0, 300, "sawtooth")
-- Stream for viewers
stream breakout on "ws://localhost:9100/peer/breakout" {
@ -104,28 +153,28 @@ view breakout_game = column [
-- ── The Court ──
stack { style: "position:relative; width:600px; height:420px; background:linear-gradient(180deg,#0c0c1d,#1a1a3e); border:2px solid #334155; border-radius:16px; margin:0.5rem auto; overflow:hidden; box-shadow:0 0 40px rgba(168,85,247,0.15)" } [
-- Row 0 bricks (red) — positioned with inline style only
-- Row 4: Blue bricks (TOP — farthest from paddle, hit last)
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:32px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#f87171,#ef4444); opacity:{if r0[i] then 1 else 0}; transition:opacity 0.2s" } []
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:120px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#60a5fa,#3b82f6); opacity:{if r4[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 1 bricks (orange)
-- Row 3: Green bricks
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:52px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#fb923c,#f97316); opacity:{if r1[i] then 1 else 0}; transition:opacity 0.2s" } []
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:140px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#4ade80,#22c55e); opacity:{if r3[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 2 bricks (yellow)
-- Row 2: Yellow bricks
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:72px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#fde68a,#facc15); opacity:{if r2[i] then 1 else 0}; transition:opacity 0.2s" } []
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:160px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#fde68a,#facc15); opacity:{if r2[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 3 bricks (green)
-- Row 1: Orange bricks
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:92px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#4ade80,#22c55e); opacity:{if r3[i] then 1 else 0}; transition:opacity 0.2s" } []
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:180px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#fb923c,#f97316); opacity:{if r1[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 4 bricks (blue)
-- Row 0: Red bricks (BOTTOM — closest to paddle, hit first)
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:112px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#60a5fa,#3b82f6); opacity:{if r4[i] then 1 else 0}; transition:opacity 0.2s" } []
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:200px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#f87171,#ef4444); opacity:{if r0[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Paddle (purple glow)
column { style: "position:absolute; width:80px; height:12px; top:380px; background:linear-gradient(180deg,#c084fc,#a855f7); border-radius:6px; box-shadow:0 0 12px rgba(168,85,247,0.5); transition:left 0.05s", left: paddleX } []
column { style: "position:absolute; width:80px; height:12px; top:360px; background:linear-gradient(180deg,#c084fc,#a855f7); border-radius:6px; box-shadow:0 0 12px rgba(168,85,247,0.5); transition:left 0.05s", left: paddleX } []
-- Ball (white glow)
column { style: "position:absolute; width:12px; height:12px; background:radial-gradient(circle,#fff,#e2e8f0); border-radius:50%; box-shadow:0 0 12px rgba(255,255,255,0.6)", top: ballY, left: ballX } []
@ -138,15 +187,15 @@ view breakout_game = column [
variant: "primary"
}
button "⬅️ Left" {
click: paddleX = if paddleX > 0 then paddleX - 30 else paddleX,
click: paddleX = if paddleX > 0 then paddleX - 40 else paddleX,
variant: "secondary"
}
button "➡️ Right" {
click: paddleX = if paddleX < 520 then paddleX + 30 else paddleX,
click: paddleX = if paddleX < 520 then paddleX + 40 else paddleX,
variant: "secondary"
}
button "🔄 Reset" {
click: score = 0; lives = 3; paused = 0; bvx = 3; bvy = -3; ballX = 300; ballY = 350; paddleX = 250; r0[0] = 1; r0[1] = 1; r0[2] = 1; r0[3] = 1; r0[4] = 1; r0[5] = 1; r0[6] = 1; r0[7] = 1; r0[8] = 1; r0[9] = 1; r1[0] = 1; r1[1] = 1; r1[2] = 1; r1[3] = 1; r1[4] = 1; r1[5] = 1; r1[6] = 1; r1[7] = 1; r1[8] = 1; r1[9] = 1; r2[0] = 1; r2[1] = 1; r2[2] = 1; r2[3] = 1; r2[4] = 1; r2[5] = 1; r2[6] = 1; r2[7] = 1; r2[8] = 1; r2[9] = 1; r3[0] = 1; r3[1] = 1; r3[2] = 1; r3[3] = 1; r3[4] = 1; r3[5] = 1; r3[6] = 1; r3[7] = 1; r3[8] = 1; r3[9] = 1; r4[0] = 1; r4[1] = 1; r4[2] = 1; r4[3] = 1; r4[4] = 1; r4[5] = 1; r4[6] = 1; r4[7] = 1; r4[8] = 1; r4[9] = 1,
click: score = 0; lives = 3; paused = 0; bvx = 4; bvy = -4; ballX = 300; ballY = 320; paddleX = 250; r0[0] = 1; r0[1] = 1; r0[2] = 1; r0[3] = 1; r0[4] = 1; r0[5] = 1; r0[6] = 1; r0[7] = 1; r0[8] = 1; r0[9] = 1; r1[0] = 1; r1[1] = 1; r1[2] = 1; r1[3] = 1; r1[4] = 1; r1[5] = 1; r1[6] = 1; r1[7] = 1; r1[8] = 1; r1[9] = 1; r2[0] = 1; r2[1] = 1; r2[2] = 1; r2[3] = 1; r2[4] = 1; r2[5] = 1; r2[6] = 1; r2[7] = 1; r2[8] = 1; r2[9] = 1; r3[0] = 1; r3[1] = 1; r3[2] = 1; r3[3] = 1; r3[4] = 1; r3[5] = 1; r3[6] = 1; r3[7] = 1; r3[8] = 1; r3[9] = 1; r4[0] = 1; r4[1] = 1; r4[2] = 1; r4[3] = 1; r4[4] = 1; r4[5] = 1; r4[6] = 1; r4[7] = 1; r4[8] = 1; r4[9] = 1,
variant: "destructive"
}
]