testbed (raylib)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
heap :: import "@std/mem/heap"
|
||||
|
||||
printf c_func(fmt *c_char, ...) c_int
|
||||
#printf c_func(fmt *c_char, ...) c_int
|
||||
|
||||
main func() i32 {
|
||||
memory ?*mut u8 = heap.alloc(4)
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
# 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 none).
|
||||
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 none
|
||||
}
|
||||
|
||||
# --- 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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user