diff --git a/app/nose/bin/breakout.ts b/app/nose/bin/breakout.ts new file mode 100644 index 0000000..d3d5e3e --- /dev/null +++ b/app/nose/bin/breakout.ts @@ -0,0 +1,148 @@ +/// +export const game = true + +import type { GameContext, InputState } from "@/shared/game" + +const CELL = 20 +const COLS = 30 // 600 px wide +const ROWS = 12 // 240 px tall of bricks + +const PADDLE_W = 6 +const PADDLE_H = 1 +const BALL_SPEED = 4 + +const ROW_COLORS = ["red", "orange", "yellow", "green", "blue", "magenta"] + +let paddleX = 0 +let ball = { x: 0, y: 0, dx: BALL_SPEED, dy: -BALL_SPEED } +let bricks: { x: number, y: number, alive: boolean }[] = [] +let score = 0 +let dead = false + +export function init() { + paddleX = (COLS * CELL - PADDLE_W * CELL) / 2 + ball = { + x: COLS * CELL / 2, + y: ROWS * CELL + 60, + dx: BALL_SPEED, + dy: -BALL_SPEED + } + + bricks = [] + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c += 2) { + bricks.push({ + x: c * CELL, + y: r * CELL, + alive: true + }) + } + } + + dead = false +} + +export function update(_delta: number, input: InputState) { + if (dead) return + + if (input.pressed.has("ArrowLeft") || input.pressed.has("a")) { + paddleX -= 6 + } + if (input.pressed.has("ArrowRight") || input.pressed.has("d")) { + paddleX += 6 + } + + // keep paddle on screen + paddleX = Math.max(0, Math.min(paddleX, COLS * CELL - PADDLE_W * CELL)) + + // move ball + ball.x += ball.dx + ball.y += ball.dy + + // wall bounce + if (ball.x < 0 || ball.x > COLS * CELL) ball.dx *= -1 + if (ball.y < 0) ball.dy *= -1 + + // paddle bounce + const paddleY = ROWS * CELL + 60 + const paddleH = CELL + + if ( + ball.x > paddleX && + ball.x < paddleX + PADDLE_W * CELL && + ball.y + 6 >= paddleY && + ball.y - 6 <= paddleY + paddleH + ) { + ball.dy *= -1 + ball.y = paddleY - 6 + } + + // brick collision + for (const b of bricks) { + if (!b.alive) continue + if ( + ball.x > b.x && + ball.x < b.x + (CELL * 2) && + ball.y > b.y && + ball.y < b.y + CELL + ) { + score += 100 + b.alive = false + ball.dy *= -1 + } + } + + // death + if (ball.y > ROWS * CELL + 100) dead = true +} + +export function draw(ctx: GameContext) { + ctx.clear("#6C6FF6") + + const boardW = COLS * CELL + const boardH = ROWS * CELL + 100 + const offsetX = (ctx.width - boardW) / 2 + const offsetY = (ctx.height - boardH) / 2 + + const c = ctx.ctx + c.save() + c.translate(offsetX, offsetY) + + // background + ctx.rectfill(0, 0, boardW, boardH, "black") + + // paddle + ctx.rectfill( + paddleX, + ROWS * CELL + 60, + paddleX + PADDLE_W * CELL, + ROWS * CELL + 60 + CELL, + "white" + ) + + // ball + ctx.circfill(ball.x, ball.y, 6, "red") + + // bricks + for (const b of bricks) { + if (b.alive) { + const color = pickColor(b.x, b.y) + ctx.rectfill(b.x, b.y, (b.x + (CELL * 2)) - .5, (b.y + CELL) - .5, color) + } + } + // score! + ctx.text(`Score: ${score}`, 5, boardH - 18, "cyan", 12) + + c.restore() + + // ya dead + if (dead) { + ctx.centerTextX("GAME OVER", boardH + 30, "red", 24) + } +} + +function pickColor(x: number, y: number): string { + const row = Math.floor(y / CELL) + const colorIndex = Math.floor(row / 2) % ROW_COLORS.length + return ROW_COLORS[colorIndex] || "white" +}