nose-pluto/bin/breakout.ts
2025-09-29 21:18:39 -07:00

149 lines
3.1 KiB
TypeScript

/// <reference lib="dom" />
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(game: GameContext) {
game.clear("#6C6FF6")
const boardW = COLS * CELL
const boardH = ROWS * CELL + 100
const offsetX = (game.width - boardW) / 2
const offsetY = (game.height - boardH) / 2
const c = game.ctx
c.save()
c.translate(offsetX, offsetY)
// background
game.rectfill(0, 0, boardW, boardH, "black")
// paddle
game.rectfill(
paddleX,
ROWS * CELL + 60,
paddleX + PADDLE_W * CELL,
ROWS * CELL + 60 + CELL,
"white"
)
// ball
game.circfill(ball.x, ball.y, 6, "red")
// bricks
for (const b of bricks) {
if (b.alive) {
const color = pickColor(b.x, b.y)
game.rectfill(b.x, b.y, (b.x + (CELL * 2)) - .5, (b.y + CELL) - .5, color)
}
}
// score!
game.text(`Score: ${score}`, 5, boardH - 18, "cyan", 12)
c.restore()
// ya dead
if (dead) {
game.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"
}