249 lines
7.5 KiB
Plaintext
249 lines
7.5 KiB
Plaintext
# Bouncing-shapes sandbox — a tour of brolang on top of raylib.
|
|
#
|
|
# Click to spawn a shape under the cursor, WASD/arrows to blow them around,
|
|
# SPACE to clear. The shape nearest the cursor is highlighted with its stats.
|
|
#
|
|
# Feature tour: native structs, enums, tagged unions + match (value, statement,
|
|
# payload capture, void variants, contextual construction), optionals + unwrap,
|
|
# value-loops (`yield :blk`), fallible functions with try/catch, defer, ranged
|
|
# and pointer-capturing for-loops, while, break/continue, compound assignment,
|
|
# scalar C interop, and multi-line strings.
|
|
|
|
# --- screen and physics constants -------------------------------------------
|
|
W :: 900
|
|
H :: 540
|
|
|
|
CAP usize :: 64
|
|
|
|
GRAV :: 0.18 # downward pull per frame
|
|
DAMP :: 0.82 # energy kept on a wall bounce
|
|
FORCE :: 0.9 # wind impulse from a key press
|
|
SPINMAX :: 3.0 # spin magnitude cap
|
|
RING_PAD :: 6.0 # highlight ring spacing
|
|
|
|
# --- shapes -----------------------------------------------------------------
|
|
Kind :: enum {
|
|
circle
|
|
square
|
|
triangle
|
|
}
|
|
|
|
Ball :: struct {
|
|
x f32
|
|
y f32
|
|
dx f32
|
|
dy f32
|
|
radius f32
|
|
kind Kind
|
|
}
|
|
|
|
# A frame's worth of player intent, as a tagged union. Each arm carries exactly
|
|
# the data that action needs (or `void` when it needs null).
|
|
Command :: union(enum) {
|
|
spawn Vector2 # spawn a shape at this point
|
|
push struct { dx f32, dy f32 } # blow every shape this way
|
|
clear void
|
|
idle void
|
|
}
|
|
|
|
# Spawning can fail when the backing array is full; the error carries the cap so
|
|
# the caller can report it.
|
|
SpawnError :: union(enum) {
|
|
full struct { cap usize }
|
|
}
|
|
|
|
# value-match used as an expression source: each arm yields a Color.
|
|
color_for func(k Kind) Color {
|
|
col :: match k {
|
|
.circle: Color{ r = 235, g = 90, b = 90, a = 255 }
|
|
.square: Color{ r = 90, g = 205, b = 130, a = 255 }
|
|
.triangle: Color{ r = 105, g = 160, b = 245, a = 255 }
|
|
}
|
|
return col
|
|
}
|
|
|
|
# value-match dispatching to a contextual return, used to cycle spawn kind.
|
|
next_kind func(k Kind) Kind {
|
|
return match k {
|
|
.circle: .square
|
|
.square: .triangle
|
|
.triangle: .circle
|
|
}
|
|
}
|
|
|
|
# Read this frame's input into a single Command (contextual union construction:
|
|
# `.clear`, `.spawn{...}`, `.push{...}` are built against the return type).
|
|
read_command func() Command {
|
|
if IsMouseButtonPressed(MOUSE_BUTTON_LEFT) return .spawn{ GetMousePosition() }
|
|
if IsKeyPressed(KEY_SPACE) return .clear
|
|
|
|
fx f32 = 0.0
|
|
fy f32 = 0.0
|
|
if IsKeyDown(KEY_A) fx -= FORCE
|
|
if IsKeyDown(KEY_D) fx += FORCE
|
|
if IsKeyDown(KEY_W) fy -= FORCE
|
|
if IsKeyDown(KEY_S) fy += FORCE
|
|
if IsKeyDown(KEY_LEFT) fx -= FORCE
|
|
if IsKeyDown(KEY_RIGHT) fx += FORCE
|
|
if IsKeyDown(KEY_UP) fy -= FORCE
|
|
if IsKeyDown(KEY_DOWN) fy += FORCE
|
|
|
|
moved :: fx != 0.0 or fy != 0.0
|
|
if (moved) return .push{ dx = fx, dy = fy }
|
|
return .idle
|
|
}
|
|
|
|
# Fallible capacity check: returns the slot index to fill, or fails `.full`.
|
|
reserve func(used usize) usize ! SpawnError {
|
|
if (used >= CAP) return .full{ cap = CAP }
|
|
return used
|
|
}
|
|
|
|
# Advance one ball: gravity, integrate, bounce off the four walls with damping.
|
|
# `b` is a pointer into the array, so the writes land in place.
|
|
step func(b @mut Ball) void {
|
|
b.dy += GRAV
|
|
b.x += b.dx
|
|
b.y += b.dy
|
|
|
|
if (b.x < b.radius) {
|
|
b.x = b.radius
|
|
b.dx = -b.dx * DAMP
|
|
}
|
|
right :: f32(W) - b.radius
|
|
if (b.x > right) {
|
|
b.x = right
|
|
b.dx = -b.dx * DAMP
|
|
}
|
|
if (b.y < b.radius) {
|
|
b.y = b.radius
|
|
b.dy = -b.dy * DAMP
|
|
}
|
|
floor :: f32(H) - b.radius
|
|
if (b.y > floor) {
|
|
b.y = floor
|
|
b.dy = -b.dy * DAMP
|
|
}
|
|
}
|
|
|
|
draw_ball func(b @Ball, highlight bool) void {
|
|
col :: color_for(b.kind)
|
|
center Vector2 = Vector2{ x = b.x, y = b.y }
|
|
match b.kind {
|
|
.circle: DrawCircleV(center, b.radius, col)
|
|
.square: DrawPoly(center, 4, b.radius, 45.0, col)
|
|
.triangle: DrawPoly(center, 3, b.radius, 0.0, col)
|
|
}
|
|
if highlight {
|
|
ring Color = Color{ r = 250, g = 245, b = 200, a = 255 }
|
|
DrawPoly(center, 24, b.radius + RING_PAD, 0.0, ring)
|
|
}
|
|
}
|
|
|
|
main func() i32 {
|
|
SetConfigFlags(FLAG_MSAA_4X_HINT)
|
|
InitWindow(W, H, "brolang — bouncing shapes")
|
|
defer CloseWindow() # runs on every exit path out of main
|
|
SetTargetFPS(60)
|
|
|
|
help ::
|
|
`[click] spawn a shape [WASD/arrows] blow wind
|
|
`[space] clear
|
|
|
|
balls [CAP]mut Ball = undefined
|
|
count usize = 0 # number of live balls, in slots 0..count
|
|
kc Kind = .circle # next kind to spawn
|
|
spin f32 = 1.0 # rotates spawn velocity for variety
|
|
at_cap bool = false # show the "at capacity" banner
|
|
|
|
bg :: Color{ r = 24, g = 26, b = 34, a = 255 }
|
|
text :: Color{ r = 225, g = 225, b = 230, a = 255 }
|
|
warn :: Color{ r = 245, g = 180, b = 90, a = 255 }
|
|
|
|
while !WindowShouldClose() {
|
|
# --- input -> command -----------------------------------------------
|
|
cmd :: read_command()
|
|
match cmd {
|
|
.spawn |at|: {
|
|
slot :: reserve(count) catch |e| {
|
|
match e {
|
|
.full |info|: at_cap = true
|
|
}
|
|
yield CAP # sentinel: >= CAP means "didn't fit"
|
|
}
|
|
if (slot < CAP) {
|
|
balls[slot] = Ball{
|
|
x = f32(at.x), y = f32(at.y),
|
|
dx = FORCE * 6.0 * spin,
|
|
dy = -FORCE * 5.0,
|
|
radius = 18.0,
|
|
kind = kc,
|
|
}
|
|
count += 1
|
|
kc = next_kind(kc)
|
|
spin = -spin * 1.2
|
|
if (spin > SPINMAX or spin < -SPINMAX) spin = 1.0
|
|
at_cap = false
|
|
}
|
|
}
|
|
.push |f|: {
|
|
for (&balls) |@b, i| {
|
|
if (i >= count) break
|
|
b.dx += f.dx
|
|
b.dy += f.dy
|
|
}
|
|
}
|
|
.clear: {
|
|
count = 0
|
|
at_cap = false
|
|
}
|
|
.idle: {}
|
|
}
|
|
|
|
# --- physics --------------------------------------------------------
|
|
for (&balls) |@b, i| {
|
|
if (i >= count) break
|
|
step(b)
|
|
}
|
|
|
|
# --- which shape is under the cursor? (optional via a value-loop) ---
|
|
mouse :: GetMousePosition()
|
|
sel :: for 0..(count) |i| blk: {
|
|
c Vector2 = Vector2{ x = balls[i].x, y = balls[i].y }
|
|
if CheckCollisionPointCircle(mouse, c, balls[i].radius) yield :blk i
|
|
yield null
|
|
}
|
|
|
|
# --- draw -----------------------------------------------------------
|
|
BeginDrawing()
|
|
ClearBackground(bg)
|
|
|
|
for (&balls) |@b, i| {
|
|
if (i >= count) break
|
|
hot bool = false
|
|
if sel |s| {
|
|
if (s == i) hot = true # true only for the hovered ball
|
|
}
|
|
draw_ball(b, hot)
|
|
}
|
|
|
|
DrawText(help, 16, 16, 20, text)
|
|
|
|
if sel |s| {
|
|
label :: match balls[s].kind {
|
|
.circle: "circle"
|
|
.square: "square"
|
|
.triangle: "triangle"
|
|
}
|
|
DrawText(label, 16, H - 36, 20, text)
|
|
}
|
|
|
|
if (at_cap) DrawText("at capacity", W - 170, 16, 20, warn)
|
|
|
|
DrawFPS(W - 90, H - 28)
|
|
EndDrawing()
|
|
}
|
|
|
|
return 0
|
|
}
|