灵符lingfu

lingdev

↓ 0 downloads

LingDev: a Godot / Unity-style game editor written in Ling. 2-D and 3-D scenes, Flash-style timeline and cutscenes, HUDs over 3-D, Ling scripting, audio / music / MIDI, builds for Windows, Linux, macOS, web, Raspberry Pi and LingOS. Ships with Ling Agent (an N64-style spy FPS), Ling Breaker (a 3-D breakout) and Moko's Picture Book (a kawaii storybook platformer).

Windows · x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdev
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (136)

ling/rt/rt_brick.ling
# rt_brick.ling - "brick" mode: a 3-D breakout. A scene with `mode=brick` in its ex field becomes
# a stage: box nodes tagged `brick` are the bricks (ex: t=normal|tough|steel|bomb|crystal|gold,
# hp=N, drop=E|M|L|S|F|C|B|1|R, slide=amplitude), the node tagged `paddle` is the paddle, and the
# scene ex gives the field (field=xmin,xmax,zmax), the stage number, its music and `.beats` file.
#
# Solid substepped ball physics, a cel-shaded depth-buffered look with ink outlines, FXAA and
# bloom, firework bursts on every impact, brick debris, power-ups, lasers, multiball, a shield,
# extra lives, combos, a beat-synced world (the MIDI-derived .beats onsets pulse the bricks, the
# floor grid and the lights), brick chimes tuned to the song's key, and the HUD.

fn brick_on() { return st_get("bk_on", 0.0) > 0.5 }

# ── colours ──────────────────────────────────────────────────────────────────
fn bk_row_col(r) {
    bind cols = [c_rgb(255.0, 70.0, 110.0), c_rgb(255.0, 150.0, 60.0), c_rgb(255.0, 220.0, 70.0), c_rgb(90.0, 230.0, 120.0), c_rgb(60.0, 200.0, 255.0), c_rgb(120.0, 120.0, 255.0), c_rgb(200.0, 100.0, 255.0), c_rgb(255.0, 110.0, 210.0)]
    bind k = floor(r) - floor(floor(r) / 8.0) * 8.0
    return list_get(cols, k)
}
fn bk_type_code(t) {
    if t == "tough" { return 1.0 }
    if t == "steel" { return 2.0 }
    if t == "bomb" { return 3.0 }
    if t == "crystal" { return 4.0 }
    if t == "gold" { return 5.0 }
    return 0.0
}
fn bk_pu_letters() { return ["E", "M", "L", "S", "F", "C", "B", "1", "R"] }
fn bk_pu_names() { return ["EXPAND", "MULTIBALL", "LASER", "SLOW", "FIREBALL", "CATCH", "SHIELD", "EXTRA LIFE", "SHRINK"] }
fn bk_pu_col(k) {
    bind cols = [c_rgb(70.0, 150.0, 255.0), c_rgb(255.0, 80.0, 220.0), c_rgb(255.0, 60.0, 60.0), c_rgb(70.0, 230.0, 110.0), c_rgb(255.0, 140.0, 30.0), c_rgb(255.0, 225.0, 60.0), c_rgb(80.0, 240.0, 240.0), c_rgb(255.0, 255.0, 255.0), c_rgb(130.0, 130.0, 140.0)]
    return list_get(cols, clampf(k, 0.0, 8.0))
}
fn bk_bright(c, k) { return c_rgb(c_r(c) + (255.0 - c_r(c)) * k, c_g(c) + (255.0 - c_g(c)) * k, c_b(c) + (255.0 - c_b(c)) * k) }
fn bk_dark(c, k) { return c_rgb(c_r(c) * k, c_g(c) * k, c_b(c) * k) }

# ── sound ────────────────────────────────────────────────────────────────────
fn bk_sfx_names() { return ["bell", "bell_hi", "paddle", "wall", "shatter", "clank", "crack", "pop", "firework", "boom", "laser", "drop", "powerup", "powerdown", "oneup", "lose", "launch", "clear", "gameover", "blip", "select", "stick"] }
fn bk_sfx_load() {
    if st_get("bk_sfx_loaded", 0.0) > 0.5 || !plat_has_audio() { return 0.0 }
    bind names = bk_sfx_names()
    bind i = 0.0
    while i < len(names) {
        bind n = list_get(names, i)
        st_set("bk_sfx_" + n, plat_sample_load(game_path("assets/sfx/" + n + ".wav")))
        bind i = i + 1.0
    }
    st_set("bk_sfx_loaded", 1.0)
    return 0.0
}
fn bk_sfx(name, vol, pitch) {
    if !plat_has_audio() { return 0.0 }
    bind id = st_get("bk_sfx_" + name, 0.0 - 1.0)
    if id >= 0.0 { plat_sample_play_pitch(id, vol * st_get("bk_vol", 1.0), pitch) }
    return 0.0
}
# a brick chime: the row picks a degree of the song's pentatonic scale
fn bk_chime(row, vol) {
    bind minor = st_get("bk_minor", 1.0) > 0.5
    bind deg = [0.0, 2.0, 4.0, 7.0, 9.0]
    if minor { bind deg = [0.0, 3.0, 5.0, 7.0, 10.0] }
    bind r = floor(row) + st_get("bk_combo", 0.0)
    bind octave = floor(r / 5.0)
    bind semi = st_get("bk_key", 0.0) + list_get(deg, r - octave * 5.0) + min(octave, 2.0) * 12.0
    if semi > 14.0 { bind semi = semi - 12.0 }
    bk_sfx("bell", vol, pow(2.0, (semi - 5.0) / 12.0))
    return 0.0
}

# ── beats (from tools/make_breaker_audio.py) ─────────────────────────────────
fn bk_beats_load(path) {
    bind names = ["kick", "snare", "hat", "bass"]
    bind i = 0.0
    while i < 4.0 {
        st_lclear("bt_" + list_get(names, i))
        st_set("bt_i_" + list_get(names, i), 0.0)
        bind i = i + 1.0
    }
    st_set("bk_key", 0.0)
    st_set("bk_minor", 1.0)
    bind text = plat_read(game_path(path))
    if text == "" { return 0.0 }
    bind lines = str_split(text, "\n")
    bind li = 0.0
    while li < len(lines) {
        bind parts = str_split(trim(list_get(lines, li)), " ")
        if len(parts) > 1.5 {
            bind head = list_get(parts, 0)
            if head == "key" {
                st_set("bk_key", to_number(list_get(parts, 1)))
                if len(parts) > 2.5 { st_set("bk_minor", sel(list_get(parts, 2) == "minor", 1.0, 0.0)) }
            } else if head == "kick" || head == "snare" || head == "hat" || head == "bass" {
                bind k = 1.0
                while k < len(parts) {
                    st_lpush("bt_" + head, to_number(list_get(parts, k)))
                    bind k = k + 1.0
                }
            } else if head == "bpm" { st_set("bk_bpm", to_number(list_get(parts, 1))) }
        }
        bind li = li + 1.0
    }
    return 0.0
}
# how many onsets of `name` were crossed since the last call (music position `pos`)
fn bk_beat_hits(name, pos) {
    bind key = "bt_" + name
    bind n = st_llen(key)
    bind i = st_get("bt_i_" + name, 0.0)
    if pos < st_get("bk_last_pos", 0.0) - 1.0 { bind i = 0.0 }
    bind hits = 0.0
    while i < n && st_lget(key, i) <= pos {
        if st_lget(key, i) > pos - 0.25 { bind hits = hits + 1.0 }
        bind i = i + 1.0
    }
    st_set("bt_i_" + name, i)
    return hits
}

# ── enter / leave ────────────────────────────────────────────────────────────
fn bk_lists() { return ["bk_x", "bk_x0", "bk_z", "bk_hw", "bk_hd", "bk_hh", "bk_hp", "bk_hp0", "bk_t", "bk_c", "bk_alive", "bk_fl", "bk_die", "bk_drop", "bk_slide", "bk_row", "bk_node", "bl_x", "bl_z", "bl_vx", "bl_vz", "bl_stuck", "bl_off", "pu_x", "pu_z", "pu_k", "pu_t", "lz_x", "lz_z", "db_x", "db_y", "db_z", "db_vx", "db_vy", "db_vz", "db_l", "db_c", "db_s", "pp_x", "pp_z", "pp_txt", "pp_t", "pp_c", "ex_i", "ex_t", "st_x", "st_y", "st_z", "st_s", "st_c"] }
fn brick_enter() {
    bind keys = bk_lists()
    bind i = 0.0
    while i < len(keys) {
        st_lclear(list_get(keys, i))
        bind i = i + 1.0
    }
    st_set("bk_on", 1.0)
    bk_sfx_load()
    plat_pfx_clear()
    plat_pfx_max(4000.0)
    fx_reset()
    # a fresh run starts at stage 1 (or whichever stage the menu picked) with 3 lives
    if game_var("newrun") > 0.5 { st_set("bk_run", 0.0)  game_set("newrun", 0.0) }
    if st_get("bk_run", 0.0) < 0.5 {
        st_set("bk_run", 1.0)
        st_set("bk_score", 0.0)
        st_set("bk_lives", 3.0)
        st_set("bk_next_life", 25000.0)
    }
    st_set("bk_hi", to_number(plat_read(game_path("hiscore.txt"))))
    bind fld = nums_parse(sc_ex("field", "-7.2,7.2,19.2"))
    st_set("bk_xmin", nums_get(fld, 0, 0.0 - 7.2))
    st_set("bk_xmax", nums_get(fld, 1, 7.2))
    st_set("bk_zmax", nums_get(fld, 2, 19.2))
    st_set("bk_stage", to_number(sc_ex("stage", "1")))
    st_set("bk_title", sc_ex("title", sc_get("name", "")))
    st_set("bk_base_speed", to_number(sc_ex("speed", "10")))
    bk_beats_load(sc_ex("beats", ""))
    st_set("bk_last_pos", 0.0)
    # bricks from the scene's nodes
    bind n = nd_count()
    bind i = 0.0
    while i < n {
        if nd_get(i, "tag") == "brick" {
            bind t = bk_type_code(nd_ex(i, "t", "normal"))
            bind hp0 = to_number(nd_ex(i, "hp", sel(t == 1.0, "3", "1")))
            st_lpush("bk_x", nd_get(i, "px"))
            st_lpush("bk_x0", nd_get(i, "px"))
            st_lpush("bk_z", nd_get(i, "pz"))
            st_lpush("bk_hw", nd_get(i, "sx"))
            st_lpush("bk_hd", nd_get(i, "sz"))
            st_lpush("bk_hh", nd_get(i, "sy"))
            st_lpush("bk_hp", hp0)
            st_lpush("bk_hp0", hp0)
            st_lpush("bk_t", t)
            st_lpush("bk_c", nd_get(i, "col"))
            st_lpush("bk_alive", 1.0)
            st_lpush("bk_fl", 0.0)
            st_lpush("bk_die", 0.0)
            bind dr = nd_ex(i, "drop", "")
            st_lpush("bk_drop", sel(dr == "", 0.0 - 1.0, l_index(bk_pu_letters(), dr)))
            st_lpush("bk_slide", to_number(nd_ex(i, "slide", "0")))
            st_lpush("bk_row", to_number(nd_ex(i, "row", "0")))
            st_lpush("bk_node", i)
        }
        bind i = i + 1.0
    }
    bind p = nd_find_tag("paddle")
    st_set("bk_pz", sel(p >= 0.0, nd_get(p, "pz"), 0.6))
    st_set("bk_px", 0.0)
    st_set("bk_pvx", 0.0)
    st_set("bk_pw", 1.3)
    st_set("bk_pwt", 1.3)
    bind timers = ["bk_t_expand", "bk_t_laser", "bk_t_slow", "bk_t_fire", "bk_t_catch", "bk_t_shrink", "bk_barrier", "bk_combo", "bk_shake", "bk_flash", "bk_pk", "bk_ps", "bk_pb", "bk_cool", "bk_msg_t", "bk_paused", "bk_hits"]
    bind i = 0.0
    while i < len(timers) {
        st_set(list_get(timers, i), 0.0)
        bind i = i + 1.0
    }
    st_set("bk_speed", st_get("bk_base_speed", 10.0))
    bk_new_ball_on_paddle()
    st_set("bk_state", "intro")
    st_set("bk_state_t", 0.0)
    # a distant star field
    bind k = 0.0
    while k < 140.0 {
        bind a = rand() * 6.283
        bind r = 40.0 + rand() * 60.0
        st_lpush("st_x", cos(a) * r)
        st_lpush("st_y", 0.0 - 6.0 - rand() * 50.0)
        st_lpush("st_z", 30.0 + sin(a) * r * 0.5 + rand() * 40.0)
        st_lpush("st_s", 0.25 + rand() * 0.9)
        st_lpush("st_c", list_get([c_rgb(255.0, 255.0, 255.0), c_rgb(170.0, 200.0, 255.0), c_rgb(255.0, 200.0, 240.0), c_rgb(255.0, 240.0, 190.0)], floor(rand() * 3.99)))
        bind k = k + 1.0
    }
    plat_post(1.0, 1.0, 1.3, c_rgb(10.0, 8.0, 24.0), 0.45)
    plat_capture_mouse()
    return 0.0
}
fn brick_leave() {
    if brick_on() {
        plat_post(0.0, 0.0, 0.0, 0.0, 0.0)
        plat_release_mouse()
        plat_pfx_clear()
    }
    st_set("bk_on", 0.0)
    return 0.0
}

# ── balls ────────────────────────────────────────────────────────────────────
fn bk_new_ball_on_paddle() {
    st_lpush("bl_x", st_get("bk_px", 0.0))
    st_lpush("bl_z", st_get("bk_pz", 0.6) + 0.7)
    st_lpush("bl_vx", 0.0)
    st_lpush("bl_vz", 0.0)
    st_lpush("bl_stuck", 1.0)
    st_lpush("bl_off", (rand() - 0.5) * 0.6)
    return 0.0
}
fn bk_ball_del(b) {
    st_ldel("bl_x", b)  st_ldel("bl_z", b)  st_ldel("bl_vx", b)  st_ldel("bl_vz", b)  st_ldel("bl_stuck", b)  st_ldel("bl_off", b)
    return 0.0
}
fn bk_add_ball(x, z, vx, vz) {
    if st_llen("bl_x") >= 8.0 { return 0.0 }
    st_lpush("bl_x", x)
    st_lpush("bl_z", z)
    st_lpush("bl_vx", vx)
    st_lpush("bl_vz", vz)
    st_lpush("bl_stuck", 0.0)
    st_lpush("bl_off", 0.0)
    return 0.0
}
fn bk_ball_speed() {
    bind s = st_get("bk_speed", 10.0)
    if st_get("bk_t_slow", 0.0) > 0.0 { bind s = s * 0.62 }
    return s
}
fn bk_launch(b) {
    bind off = st_lget("bl_off", b) / max(0.3, st_get("bk_pw", 1.3))
    bind ang = clampf(off * 0.9 + (rand() - 0.5) * 0.25, 0.0 - 1.0, 1.0)
    bind s = bk_ball_speed()
    st_lset("bl_vx", b, sin(ang) * s)
    st_lset("bl_vz", b, cos(ang) * s)
    st_lset("bl_stuck", b, 0.0)
    bk_sfx("launch", 0.6, 1.0)
    return 0.0
}

# ── effects ──────────────────────────────────────────────────────────────────
# the ball bursts like a firework shell where it strikes
fn bk_firework(x, z, c, big) {
    bind y = 0.0 - 0.35
    bind hi = bk_bright(c, 0.55)
    if big > 0.5 {
        plat_pfx_burst(x, y - 0.4, z, 90.0, 11.0, 1.6, 0.22, 0.0, hi, c_rgb(255.0, 255.0, 255.0), 1.0, 0.55, 0.0, 0.0 - 3.0, 0.0)
        plat_pfx_burst(x, y - 0.4, z, 40.0, 6.0, 1.9, 0.5, 0.0, c, c_rgb(255.0, 230.0, 160.0), 0.0, 0.4, 0.0, 0.0 - 2.0, 0.0)
        plat_pfx_burst(x, y - 0.4, z, 60.0, 15.0, 1.3, 0.16, 0.0, c_rgb(255.0, 255.0, 255.0), bk_bright(bk_row_col(rand() * 8.0), 0.3), 1.0, 0.7, 0.0, 0.0 - 4.0, 0.0)
        fx_light(x, y - 1.5, z, hi, 7.0, 14.0, 0.5)
        return 0.0
    }
    plat_pfx_burst(x, y, z, 26.0, 7.5, 0.85, 0.15, 0.0, hi, c_rgb(255.0, 255.0, 255.0), 1.0, 0.5, 0.0, 0.0 - 1.5, 0.0)
    plat_pfx_burst(x, y, z, 8.0, 2.5, 0.5, 0.55, 0.05, c_rgb(255.0, 255.0, 255.0), c, 0.0, 0.0, 0.0, 0.0, 0.0)
    fx_light(x, y - 0.8, z, hi, 3.5, 7.0, 0.22)
    return 0.0
}
fn bk_debris(x, z, c, n) {
    bind i = 0.0
    while i < n {
        if st_llen("db_x") > 110.0 {
            st_ldel("db_x", 0.0)  st_ldel("db_y", 0.0)  st_ldel("db_z", 0.0)  st_ldel("db_vx", 0.0)  st_ldel("db_vy", 0.0)
            st_ldel("db_vz", 0.0)  st_ldel("db_l", 0.0)  st_ldel("db_c", 0.0)  st_ldel("db_s", 0.0)
        }
        st_lpush("db_x", x + (rand() - 0.5) * 1.2)
        st_lpush("db_y", 0.0 - 0.35 - rand() * 0.3)
        st_lpush("db_z", z + (rand() - 0.5) * 0.5)
        st_lpush("db_vx", (rand() - 0.5) * 7.0)
        st_lpush("db_vy", 0.0 - 3.0 - rand() * 5.0)
        st_lpush("db_vz", (rand() - 0.5) * 6.0)
        st_lpush("db_l", 1.0 + rand() * 0.8)
        st_lpush("db_c", sel(rand() < 0.3, bk_bright(c, 0.4), c))
        st_lpush("db_s", 0.06 + rand() * 0.11)
        bind i = i + 1.0
    }
    return 0.0
}
fn bk_popup(x, z, txt, c) {
    st_lpush("pp_x", x)
    st_lpush("pp_z", z)
    st_lpush("pp_txt", txt)
    st_lpush("pp_t", 0.0)
    st_lpush("pp_c", c)
    return 0.0
}
fn bk_message(m) {
    st_set("bk_msg", m)
    st_set("bk_msg_t", 2.2)
    return 0.0
}
fn bk_add_score(pts, x, z, c) {
    bind mult = 1.0 + min(st_get("bk_combo", 0.0), 16.0) * 0.25
    bind gain = floor(pts * mult / 10.0) * 10.0
    st_set("bk_score", st_get("bk_score", 0.0) + gain)
    bind txt = "+" + num_str(gain)
    if mult > 1.01 { bind txt = txt + "  x" + num_str(floor(mult * 100.0) / 100.0) }
    bk_popup(x, z, txt, c)
    if st_get("bk_score", 0.0) >= st_get("bk_next_life", 25000.0) {
        st_set("bk_next_life", st_get("bk_next_life", 25000.0) + 25000.0)
        st_set("bk_lives", st_get("bk_lives", 3.0) + 1.0)
        bk_sfx("oneup", 0.8, 1.0)
        bk_message("EXTRA LIFE!")
    }
    return 0.0
}

# ── bricks ───────────────────────────────────────────────────────────────────
fn bk_points(t) { return list_get([50.0, 120.0, 200.0, 150.0, 120.0, 500.0], clampf(t, 0.0, 5.0)) }
fn bk_breakable_left() {
    bind n = 0.0
    bind i = 0.0
    while i < st_llen("bk_x") {
        if st_lget("bk_alive", i) > 0.5 && st_lget("bk_t", i) != 2.0 { bind n = n + 1.0 }
        bind i = i + 1.0
    }
    return n
}
# damage brick i; force = destroys anything (fireball, explosions)
fn bk_hit_brick(i, x, z, force) {
    if st_lget("bk_alive", i) < 0.5 { return 0.0 }
    bind t = st_lget("bk_t", i)
    bind c = st_lget("bk_c", i)
    st_lset("bk_fl", i, 0.14)
    if t == 2.0 && force < 0.5 {
        bk_sfx("clank", 0.7, 0.9 + rand() * 0.2)
        plat_pfx_burst(x, 0.0 - 0.35, z, 14.0, 6.0, 0.4, 0.1, 0.0, c_rgb(255.0, 240.0, 200.0), c_rgb(255.0, 160.0, 60.0), 1.0, 0.8, 0.0, 0.0, 0.0)
        return 0.0
    }
    bind hp = st_lget("bk_hp", i) - sel(force > 0.5, 99.0, 1.0)
    st_lset("bk_hp", i, hp)
    if hp > 0.0 {
        bk_sfx("crack", 0.6, 0.9 + rand() * 0.25)
        bk_chime(st_lget("bk_row", i), 0.35)
        bk_debris(x, z, c, 3.0)
        return 0.0
    }
    # destroyed
    st_lset("bk_alive", i, 0.0)
    st_lset("bk_die", i, 0.35)
    st_set("bk_combo", st_get("bk_combo", 0.0) + 1.0)
    st_set("bk_hits", st_get("bk_hits", 0.0) + 1.0)
    bind bx = st_lget("bk_x", i)
    bind bz = st_lget("bk_z", i)
    bk_add_score(bk_points(t), bx, bz, bk_bright(c, 0.3))
    bk_debris(bx, bz, c, sel(t == 4.0, 14.0, 9.0))
    bk_chime(st_lget("bk_row", i), 0.5)
    if t == 4.0 { bk_sfx("shatter", 0.75, 1.15) } else { bk_sfx("shatter", 0.5, 0.85 + rand() * 0.35) }
    if t == 5.0 {
        plat_pfx_burst(bx, 0.0 - 0.5, bz, 40.0, 5.0, 1.2, 0.3, 0.0, c_rgb(255.0, 240.0, 150.0), c_rgb(255.0, 180.0, 40.0), 0.0, 0.3, 0.0, 0.0 - 2.0, 0.0)
        bk_sfx("bell_hi", 0.6, 1.0)
    }
    if t == 3.0 {
        # bombs take their neighbours with them, a beat later
        bk_sfx("boom", 0.8, 0.9 + rand() * 0.2)
        bk_firework(bx, bz, c_rgb(255.0, 120.0, 40.0), 1.0)
        st_set("bk_shake", 0.5)
        st_set("bk_flash", 0.5)
        bind j = 0.0
        while j < st_llen("bk_x") {
            if j != i && st_lget("bk_alive", j) > 0.5 {
                bind dx = st_lget("bk_x", j) - bx
                bind dz = st_lget("bk_z", j) - bz
                if dx * dx + dz * dz < 2.5 * 2.5 {
                    st_lpush("ex_i", j)
                    st_lpush("ex_t", 0.09 + sqrt(dx * dx + dz * dz) * 0.03)
                }
            }
            bind j = j + 1.0
        }
    }
    # power-up?
    bind drop = st_lget("bk_drop", i)
    if drop < 0.0 {
        bind chance = list_get([0.1, 0.2, 0.3, 0.25, 1.0, 0.3], clampf(t, 0.0, 5.0))
        if rand() < chance { bind drop = bk_random_pu() }
    }
    if drop >= 0.0 { bk_spawn_pu(bx, bz, drop) }
    return 0.0
}
fn bk_random_pu() {
    bind r = rand()
    if r < 0.04 { return 7.0 }
    if r < 0.12 { return 8.0 }
    return floor(rand() * 6.99)
}
fn bk_spawn_pu(x, z, k) {
    st_lpush("pu_x", x)
    st_lpush("pu_z", z)
    st_lpush("pu_k", k)
    st_lpush("pu_t", 0.0)
    bk_sfx("drop", 0.45, 1.0)
    return 0.0
}

# ── power-ups ────────────────────────────────────────────────────────────────
fn bk_apply_pu(k) {
    bind nm = list_get(bk_pu_names(), k)
    bk_message(nm)
    if k == 8.0 { bk_sfx("powerdown", 0.7, 1.0) } else if k == 7.0 { bk_sfx("oneup", 0.85, 1.0) } else { bk_sfx("powerup", 0.7, 1.0) }
    if k == 0.0 { st_set("bk_t_expand", 15.0)  st_set("bk_t_shrink", 0.0) }
    if k == 1.0 {
        bind nb = st_llen("bl_x")
        bind b = 0.0
        while b < nb {
            if st_lget("bl_stuck", b) < 0.5 {
                bind vx = st_lget("bl_vx", b)
                bind vz = st_lget("bl_vz", b)
                bind a = atan2(vx, vz)
                bind s = sqrt(vx * vx + vz * vz)
                bk_add_ball(st_lget("bl_x", b), st_lget("bl_z", b), sin(a + 0.45) * s, cos(a + 0.45) * s)
                bk_add_ball(st_lget("bl_x", b), st_lget("bl_z", b), sin(a - 0.45) * s, cos(a - 0.45) * s)
            } else {
                bk_add_ball(st_lget("bl_x", b), st_lget("bl_z", b) + 0.2, 0.0 - 3.0, bk_ball_speed())
                bk_add_ball(st_lget("bl_x", b), st_lget("bl_z", b) + 0.2, 3.0, bk_ball_speed())
            }
            bind b = b + 1.0
        }
    }
    if k == 2.0 { st_set("bk_t_laser", 12.0) }
    if k == 3.0 { st_set("bk_t_slow", 10.0) }
    if k == 4.0 { st_set("bk_t_fire", 8.0) }
    if k == 5.0 { st_set("bk_t_catch", 15.0) }
    if k == 6.0 { st_set("bk_barrier", 1.0) }
    if k == 7.0 { st_set("bk_lives", st_get("bk_lives", 3.0) + 1.0) }
    if k == 8.0 { st_set("bk_t_shrink", 10.0)  st_set("bk_t_expand", 0.0) }
    return 0.0
}

# ── input ────────────────────────────────────────────────────────────────────
fn bk_fire_pressed() {
    bind md = mouse_down() || key_down("space") || plat_gamepad_button(0.0) || plat_gamepad_rt() > 0.5
    bind was = st_get("bk_fire_was", 0.0) > 0.5
    st_set("bk_fire_was", sel(md, 1.0, 0.0))
    return md && !was
}
fn bk_fire_held() { return mouse_down() || key_down("space") || plat_gamepad_button(0.0) || plat_gamepad_rt() > 0.5 }

# ── the update ───────────────────────────────────────────────────────────────
fn brick_update(dt) {
    if !brick_on() { return 0.0 }
    bind dt = min(dt, 0.033)
    # pause
    if key_pressed("p") || (key_pressed("escape") && st_get("game_embedded", 0.0) < 0.5) {
        st_set("bk_paused", sel(st_get("bk_paused", 0.0) > 0.5, 0.0, 1.0))
        if st_get("bk_paused", 0.0) > 0.5 { plat_release_mouse() } else { plat_capture_mouse() }
        bk_sfx("blip", 0.5, 1.0)
    }
    if st_get("bk_paused", 0.0) > 0.5 {
        if key_pressed("q") { bk_game_over() }
        return 0.0
    }
    bind st = st_get("bk_state", "play")
    st_set("bk_state_t", st_get("bk_state_t", 0.0) + dt)
    bind stt = st_get("bk_state_t", 0.0)
    # music sync
    bind pos = 0.0
    if plat_has_audio() { bind pos = plat_music_pos() }
    if bk_beat_hits("kick", pos) > 0.0 { st_set("bk_pk", 1.0) }
    if bk_beat_hits("snare", pos) > 0.0 { st_set("bk_ps", 1.0) }
    if bk_beat_hits("bass", pos) > 0.0 { st_set("bk_pb", 1.0) }
    if bk_beat_hits("hat", pos) > 0.0 { st_set("bk_ph", 1.0) }
    st_set("bk_last_pos", pos)
    st_set("bk_pk", max(0.0, st_get("bk_pk", 0.0) - dt * 6.0))
    st_set("bk_ps", max(0.0, st_get("bk_ps", 0.0) - dt * 5.0))
    st_set("bk_pb", max(0.0, st_get("bk_pb", 0.0) - dt * 7.0))
    st_set("bk_ph", max(0.0, st_get("bk_ph", 0.0) - dt * 12.0))
    # timers
    bind tl = ["bk_t_expand", "bk_t_laser", "bk_t_slow", "bk_t_fire", "bk_t_catch", "bk_t_shrink", "bk_shake", "bk_flash", "bk_cool", "bk_msg_t"]
    bind i = 0.0
    while i < len(tl) {
        st_set(list_get(tl, i), max(0.0, st_get(list_get(tl, i), 0.0) - dt))
        bind i = i + 1.0
    }
    # paddle
    bind pwt = 1.3
    if st_get("bk_t_expand", 0.0) > 0.0 { bind pwt = 2.2 }
    if st_get("bk_t_shrink", 0.0) > 0.0 { bind pwt = 0.8 }
    st_set("bk_pw", st_get("bk_pw", 1.3) + (pwt - st_get("bk_pw", 1.3)) * min(1.0, dt * 8.0))
    bind pw = st_get("bk_pw", 1.3)
    bind px0 = st_get("bk_px", 0.0)
    bind px = px0 + mouse_dx() * 0.022
    bind ax = 0.0
    if key_down("a") || key_down("left") { bind ax = ax - 1.0 }
    if key_down("d") || key_down("right") { bind ax = ax + 1.0 }
    if abs(plat_gamepad_lx()) > 0.15 { bind ax = ax + plat_gamepad_lx() }
    bind px = px + ax * 16.0 * dt
    # attract / test mode: the paddle chases the lowest incoming ball
    if st_get("game_autoplay", 0.0) > 0.5 {
        bind best = 99.0
        bind tx = px
        bind b = 0.0
        while b < st_llen("bl_x") {
            if st_lget("bl_z", b) < best && (st_lget("bl_vz", b) < 0.0 || st_lget("bl_stuck", b) > 0.5) {
                bind best = st_lget("bl_z", b)
                bind tx = st_lget("bl_x", b) + sin(st_get("game_t", 0.0) * 1.3) * pw * 0.6
            }
            bind b = b + 1.0
        }
        bind px = px0 + clampf(tx - px0, 0.0 - 22.0 * dt, 22.0 * dt)
    }
    bind px = clampf(px, st_get("bk_xmin", 0.0 - 7.2) + pw + 0.3, st_get("bk_xmax", 7.2) - pw - 0.3)
    st_set("bk_pvx", (px - px0) / max(dt, 0.001))
    st_set("bk_px", px)
    bind pz = st_get("bk_pz", 0.6)
    # state machine
    bind fire = bk_fire_pressed()
    if st_get("game_autoplay", 0.0) > 0.5 && stt > 1.0 { bind fire = true }
    if st == "intro" {
        if stt > 0.6 && (fire || stt > 3.5) {
            st_set("bk_state", "play")
            st_set("bk_state_t", 0.0)
            bind st = "play"
        }
    }
    if st == "clear" {
        if stt < 2.6 && floor(stt * 3.0) != floor((stt - dt) * 3.0) {
            bk_firework(st_get("bk_xmin", 0.0 - 7.0) + rand() * 14.0, 8.0 + rand() * 9.0, bk_row_col(rand() * 8.0), 1.0)
            bk_sfx("firework", 0.6, 0.85 + rand() * 0.3)
        }
        if stt > 3.6 { bk_next_stage() }
    }
    if st == "lost" {
        if stt > 1.6 {
            if st_get("bk_lives", 0.0) < 0.5 { bk_game_over() } else {
                bk_new_ball_on_paddle()
                st_set("bk_state", "play")
                st_set("bk_state_t", 0.0)
            }
        }
    }
    # balls
    if st == "play" { bk_balls_step(dt, px, pz, pw, fire) }
    # lasers
    if st == "play" && st_get("bk_t_laser", 0.0) > 0.0 && bk_fire_held() && st_get("bk_cool", 0.0) <= 0.0 && bk_any_flying() {
        st_set("bk_cool", 0.28)
        st_lpush("lz_x", px - pw + 0.25)
        st_lpush("lz_z", pz + 0.4)
        st_lpush("lz_x", px + pw - 0.25)
        st_lpush("lz_z", pz + 0.4)
        bk_sfx("laser", 0.4, 1.0 + rand() * 0.1)
    }
    bk_lasers_step(dt)
    bk_powerups_step(dt, px, pz, pw)
    bk_bricks_step(dt)
    bk_debris_step(dt)
    # popups rise
    bind i = st_llen("pp_t") - 1.0
    while i >= 0.0 {
        bind t = st_lget("pp_t", i) + dt
        if t > 1.1 { st_ldel("pp_x", i)  st_ldel("pp_z", i)  st_ldel("pp_txt", i)  st_ldel("pp_t", i)  st_ldel("pp_c", i) } else { st_lset("pp_t", i, t) }
        bind i = i - 1.0
    }
    # beat twinkles in the sky
    if st_get("bk_ph", 0.0) > 0.95 && st_llen("st_x") > 0.0 {
        bind k = floor(rand() * (st_llen("st_x") - 0.01))
        plat_pfx_burst(st_lget("st_x", k), st_lget("st_y", k), st_lget("st_z", k), 6.0, 2.0, 0.5, 0.5, 0.0, c_rgb(255.0, 255.0, 255.0), st_lget("st_c", k), 0.0, 0.0, 0.0, 0.0, 0.0)
    }
    plat_pfx_step(dt, 9.8, 0.9, 0.0 - 0.02, 0.35)
    fx_update(dt)
    bk_camera(dt)
    # stage clear?
    if st == "play" && bk_breakable_left() < 0.5 {
        st_set("bk_state", "clear")
        st_set("bk_state_t", 0.0)
        bk_sfx("clear", 0.9, 1.0)
        bk_add_score(1000.0 * st_get("bk_stage", 1.0), 0.0, 10.0, c_rgb(255.0, 255.0, 200.0))
        bk_message("STAGE CLEAR")
    }
    return 0.0
}
fn bk_any_flying() {
    bind b = 0.0
    while b < st_llen("bl_x") {
        if st_lget("bl_stuck", b) < 0.5 { return true }
        bind b = b + 1.0
    }
    return false
}

fn bk_balls_step(dt, px, pz, pw, fire) {
    bind br = 0.3
    bind pr = 0.32
    bind xmin = st_get("bk_xmin", 0.0 - 7.2)
    bind xmax = st_get("bk_xmax", 7.2)
    bind zmax = st_get("bk_zmax", 19.2)
    bind fireball = st_get("bk_t_fire", 0.0) > 0.0
    bind b = st_llen("bl_x") - 1.0
    while b >= 0.0 {
        bind x = st_lget("bl_x", b)
        bind z = st_lget("bl_z", b)
        bind vx = st_lget("bl_vx", b)
        bind vz = st_lget("bl_vz", b)
        bind dead = false
        if st_lget("bl_stuck", b) > 0.5 {
            bind x = px + st_lget("bl_off", b)
            bind z = pz + pr + br + 0.02
            if fire { bk_launch(b)  bind vx = st_lget("bl_vx", b)  bind vz = st_lget("bl_vz", b) }
        } else {
            bind s = bk_ball_speed()
            bind steps = max(1.0, ceil(s * dt / 0.12))
            bind h = dt / steps
            bind k = 0.0
            while k < steps && !dead {
                bind x = x + vx * h
                bind z = z + vz * h
                # walls
                if x < xmin + br { bind x = xmin + br  bind vx = abs(vx)  bk_wall_hit(x, z) }
                if x > xmax - br { bind x = xmax - br  bind vx = 0.0 - abs(vx)  bk_wall_hit(x, z) }
                if z > zmax - br { bind z = zmax - br  bind vz = 0.0 - abs(vz)  bk_wall_hit(x, z) }
                # paddle (a capsule along x)
                if vz < 0.0 && z < pz + pr + br + 0.05 && z > pz - pr - br {
                    bind cx = clampf(x, px - pw, px + pw)
                    bind dx = x - cx
                    bind dz = z - pz
                    if dx * dx + dz * dz < (br + pr) * (br + pr) {
                        bind off = clampf((x - px) / (pw + pr), 0.0 - 1.0, 1.0)
                        bind ang = off * 1.05 + clampf(st_get("bk_pvx", 0.0) * 0.012, 0.0 - 0.2, 0.2)
                        bind ang = clampf(ang, 0.0 - 1.15, 1.15)
                        st_set("bk_speed", min(st_get("bk_base_speed", 10.0) + 8.0, st_get("bk_speed", 10.0) + 0.18))
                        bind s = bk_ball_speed()
                        bind vx = sin(ang) * s
                        bind vz = cos(ang) * s
                        bind z = pz + pr + br + 0.01
                        st_set("bk_combo", 0.0)
                        st_set("bk_pb", 1.0)
                        bk_sfx("paddle", 0.6, 0.9 + abs(off) * 0.25)
                        plat_pfx_burst(x, 0.0 - 0.3, z, 10.0, 4.0, 0.35, 0.12, 0.0, c_rgb(160.0, 230.0, 255.0), c_rgb(80.0, 120.0, 255.0), 1.0, 0.3, 0.0, 0.0, 2.0)
                        if st_get("bk_t_catch", 0.0) > 0.0 {
                            st_lset("bl_stuck", b, 1.0)
                            st_lset("bl_off", b, clampf(x - px, 0.0 - pw, pw))
                            bk_sfx("stick", 0.6, 1.0)
                            bind k = steps
                        }
                    }
                }
                # bricks
                if z > 2.0 {
                    bind hit = bk_ball_vs_bricks(x, z, vx, vz, br, fireball)
                    if len(hit) > 4.5 {
                        bind x = list_get(hit, 0)
                        bind z = list_get(hit, 1)
                        bind vx = list_get(hit, 2)
                        bind vz = list_get(hit, 3)
                    }
                }
                # shield / lost
                if z < pz - 0.9 && st_get("bk_barrier", 0.0) > 0.5 {
                    st_set("bk_barrier", 0.0)
                    bind vz = abs(vz)
                    bind z = pz - 0.85
                    bk_sfx("clank", 0.6, 1.4)
                    bk_firework(x, z, c_rgb(80.0, 240.0, 240.0), 0.0)
                }
                if z < pz - 2.5 { bind dead = true }
                bind k = k + 1.0
            }
            # keep the speed and stop near-horizontal paths
            bind sp = sqrt(vx * vx + vz * vz)
            if sp > 0.001 {
                bind s = bk_ball_speed()
                bind vx = vx / sp * s
                bind vz = vz / sp * s
                if abs(vz) < s * 0.28 {
                    bind vz = sel(vz < 0.0, 0.0 - 1.0, 1.0) * s * 0.28
                    bind vx = sel(vx < 0.0, 0.0 - 1.0, 1.0) * sqrt(max(0.0, s * s - vz * vz))
                }
            }
            # trail
            if fireball {
                plat_pfx_emit(x + (rand() - 0.5) * 0.3, 0.0 - 0.3, z + (rand() - 0.5) * 0.3, (rand() - 0.5) * 1.5, 0.0 - 2.0 - rand() * 2.0, (rand() - 0.5) * 1.5, 0.45, 0.55, 0.05, c_rgb(255.0, 200.0, 80.0), c_rgb(255.0, 50.0, 10.0), 0.0, 0.0, 0.0 - 0.3, 1.0)
            } else {
                plat_pfx_emit(x, 0.0 - 0.3, z, 0.0, 0.0, 0.0, 0.28, 0.42, 0.05, c_rgb(190.0, 230.0, 255.0), c_rgb(120.0, 60.0, 255.0), 0.0, 0.0, 0.0, 0.7)
            }
        }
        if dead {
            bk_ball_del(b)
            if st_llen("bl_x") < 0.5 { bk_lose_life(x, z) }
        } else {
            st_lset("bl_x", b, x)
            st_lset("bl_z", b, z)
            st_lset("bl_vx", b, vx)
            st_lset("bl_vz", b, vz)
        }
        bind b = b - 1.0
    }
    return 0.0
}
fn bk_wall_hit(x, z) {
    bk_sfx("wall", 0.35, 0.95 + rand() * 0.1)
    st_set("bk_ps", max(st_get("bk_ps", 0.0), 0.6))
    plat_pfx_burst(x, 0.0 - 0.3, z, 6.0, 3.0, 0.3, 0.1, 0.0, c_rgb(200.0, 220.0, 255.0), c_rgb(120.0, 140.0, 255.0), 1.0, 0.3, 0.0, 0.0, 0.0)
    return 0.0
}
# returns [x, z, vx, vz, hit] after the first brick contact, or [] for none
fn bk_ball_vs_bricks(x, z, vx, vz, br, fireball) {
    bind n = st_llen("bk_x")
    bind i = 0.0
    while i < n {
        if st_lget("bk_alive", i) > 0.5 {
            bind bx = st_lget("bk_x", i)
            bind bz = st_lget("bk_z", i)
            bind hw = st_lget("bk_hw", i)
            bind hd = st_lget("bk_hd", i)
            if abs(x - bx) < hw + br && abs(z - bz) < hd + br {
                bind cx = clampf(x, bx - hw, bx + hw)
                bind cz = clampf(z, bz - hd, bz + hd)
                bind dx = x - cx
                bind dz = z - cz
                bind d2 = dx * dx + dz * dz
                if d2 < br * br {
                    bind nx = 0.0
                    bind nz = 0.0
                    if d2 > 0.000001 {
                        bind d = sqrt(d2)
                        bind nx = dx / d
                        bind nz = dz / d
                    } else {
                        # centre inside: leave by the shallow side
                        bind px = hw - abs(x - bx)
                        bind pz = hd - abs(z - bz)
                        if px < pz { bind nx = sel(x < bx, 0.0 - 1.0, 1.0) } else { bind nz = sel(z < bz, 0.0 - 1.0, 1.0) }
                    }
                    bind steel = st_lget("bk_t", i) == 2.0
                    bk_hit_brick(i, cx, cz, sel(fireball, 1.0, 0.0))
                    bk_firework(cx, cz, sel(fireball, c_rgb(255.0, 150.0, 40.0), st_lget("bk_c", i)), 0.0)
                    bk_sfx("pop", 0.35, 0.9 + rand() * 0.3)
                    st_set("bk_ball_flash", 0.12)
                    if fireball && !steel { return [x, z, vx, vz, 0.0] }
                    if fireball && steel && st_lget("bk_alive", i) < 0.5 { return [x, z, vx, vz, 0.0] }
                    bind vn = vx * nx + vz * nz
                    if vn < 0.0 {
                        bind vx = vx - 2.0 * vn * nx
                        bind vz = vz - 2.0 * vn * nz
                    }
                    if d2 > 0.000001 {
                        bind x = cx + nx * (br + 0.002)
                        bind z = cz + nz * (br + 0.002)
                    } else {
                        if nx != 0.0 { bind x = bx + nx * (hw + br + 0.002) } else { bind z = bz + nz * (hd + br + 0.002) }
                    }
                    return [x, z, vx, vz, 1.0]
                }
            }
        }
        bind i = i + 1.0
    }
    return []
}
fn bk_lose_life(x, z) {
    st_set("bk_lives", st_get("bk_lives", 1.0) - 1.0)
    st_set("bk_state", "lost")
    st_set("bk_state_t", 0.0)
    st_set("bk_shake", 0.7)
    st_set("bk_flash", 0.6)
    st_set("bk_t_expand", 0.0)  st_set("bk_t_laser", 0.0)  st_set("bk_t_slow", 0.0)  st_set("bk_t_fire", 0.0)  st_set("bk_t_catch", 0.0)  st_set("bk_t_shrink", 0.0)
    st_set("bk_speed", st_get("bk_base_speed", 10.0))
    st_lclear("pu_x")  st_lclear("pu_z")  st_lclear("pu_k")  st_lclear("pu_t")
    bk_firework(clampf(x, st_get("bk_xmin", 0.0 - 7.0) + 1.0, st_get("bk_xmax", 7.0) - 1.0), max(z, st_get("bk_pz", 0.6)), c_rgb(255.0, 90.0, 140.0), 1.0)
    bk_sfx("lose", 0.8, 1.0)
    bk_sfx("firework", 0.7, 1.0)
    if st_get("bk_lives", 0.0) < 0.5 { bk_message("GAME OVER") } else { bk_message("BALL LOST") }
    return 0.0
}

fn bk_lasers_step(dt) {
    bind i = st_llen("lz_x") - 1.0
    while i >= 0.0 {
        bind x = st_lget("lz_x", i)
        bind z = st_lget("lz_z", i) + 30.0 * dt
        bind gone = z > st_get("bk_zmax", 19.0)
        if !gone {
            bind n = st_llen("bk_x")
            bind j = 0.0
            while j < n && !gone {
                if st_lget("bk_alive", j) > 0.5 {
                    if abs(x - st_lget("bk_x", j)) < st_lget("bk_hw", j) + 0.05 && abs(z - st_lget("bk_z", j)) < st_lget("bk_hd", j) + 0.2 {
                        bk_hit_brick(j, x, st_lget("bk_z", j) - st_lget("bk_hd", j), 0.0)
                        plat_pfx_burst(x, 0.0 - 0.35, z, 10.0, 5.0, 0.35, 0.12, 0.0, c_rgb(255.0, 200.0, 200.0), c_rgb(255.0, 40.0, 40.0), 1.0, 0.5, 0.0, 0.0, 0.0 - 2.0)
                        bind gone = true
                    }
                }
                bind j = j + 1.0
            }
        }
        if gone { st_ldel("lz_x", i)  st_ldel("lz_z", i) } else { st_lset("lz_z", i, z) }
        bind i = i - 1.0
    }
    return 0.0
}
fn bk_powerups_step(dt, px, pz, pw) {
    bind i = st_llen("pu_x") - 1.0
    while i >= 0.0 {
        bind z = st_lget("pu_z", i) - 4.6 * dt
        st_lset("pu_t", i, st_lget("pu_t", i) + dt)
        bind x = st_lget("pu_x", i)
        bind got = abs(x - px) < pw + 0.55 && abs(z - pz) < 0.55
        if got {
            bind k = st_lget("pu_k", i)
            plat_pfx_burst(x, 0.0 - 0.4, pz, 30.0, 6.0, 0.7, 0.3, 0.0, bk_pu_col(k), c_rgb(255.0, 255.0, 255.0), 0.0, 0.2, 0.0, 0.0 - 3.0, 0.0)
            bk_apply_pu(k)
            st_ldel("pu_x", i)  st_ldel("pu_z", i)  st_ldel("pu_k", i)  st_ldel("pu_t", i)
        } else if z < pz - 3.0 {
            st_ldel("pu_x", i)  st_ldel("pu_z", i)  st_ldel("pu_k", i)  st_ldel("pu_t", i)
        } else { st_lset("pu_z", i, z) }
        bind i = i - 1.0
    }
    return 0.0
}
fn bk_bricks_step(dt) {
    bind t = st_get("game_t", 0.0)
    bind n = st_llen("bk_x")
    bind i = 0.0
    while i < n {
        st_lset("bk_fl", i, max(0.0, st_lget("bk_fl", i) - dt))
        st_lset("bk_die", i, max(0.0, st_lget("bk_die", i) - dt))
        bind sl = st_lget("bk_slide", i)
        if sl > 0.0 && st_lget("bk_alive", i) > 0.5 { st_lset("bk_x", i, st_lget("bk_x0", i) + sin(t * 0.9 + st_lget("bk_z", i)) * sl) }
        bind i = i + 1.0
    }
    # chained explosions
    bind i = st_llen("ex_t") - 1.0
    while i >= 0.0 {
        bind et = st_lget("ex_t", i) - dt
        if et <= 0.0 {
            bind j = st_lget("ex_i", i)
            st_ldel("ex_i", i)
            st_ldel("ex_t", i)
            bk_hit_brick(j, st_lget("bk_x", j), st_lget("bk_z", j), 1.0)
            bk_firework(st_lget("bk_x", j), st_lget("bk_z", j), st_lget("bk_c", j), 0.0)
        } else { st_lset("ex_t", i, et) }
        bind i = i - 1.0
    }
    return 0.0
}
fn bk_debris_step(dt) {
    bind i = st_llen("db_x") - 1.0
    while i >= 0.0 {
        bind l = st_lget("db_l", i) - dt
        if l <= 0.0 {
            st_ldel("db_x", i)  st_ldel("db_y", i)  st_ldel("db_z", i)  st_ldel("db_vx", i)  st_ldel("db_vy", i)
            st_ldel("db_vz", i)  st_ldel("db_l", i)  st_ldel("db_c", i)  st_ldel("db_s", i)
        } else {
            bind vy = st_lget("db_vy", i) + 22.0 * dt
            bind y = st_lget("db_y", i) + vy * dt
            bind vx = st_lget("db_vx", i)
            bind vz = st_lget("db_vz", i)
            bind s = st_lget("db_s", i)
            if y > 0.0 - s {
                bind y = 0.0 - s
                bind vy = 0.0 - abs(vy) * 0.38
                bind vx = vx * 0.7
                bind vz = vz * 0.7
            }
            st_lset("db_x", i, st_lget("db_x", i) + vx * dt)
            st_lset("db_z", i, st_lget("db_z", i) + vz * dt)
            st_lset("db_y", i, y)
            st_lset("db_vx", i, vx)
            st_lset("db_vy", i, vy)
            st_lset("db_vz", i, vz)
            st_lset("db_l", i, l)
        }
        bind i = i - 1.0
    }
    return 0.0
}

# ── camera ───────────────────────────────────────────────────────────────────
fn bk_camera(dt) {
    bind px = st_get("bk_px", 0.0)
    bind sway = st_get("bk_cam_sway", 0.0)
    bind sway = sway + (px * 0.06 - sway) * min(1.0, dt * 3.0)
    st_set("bk_cam_sway", sway)
    bind sh = st_get("bk_shake", 0.0)
    bind jx = (rand() - 0.5) * sh * 0.6
    bind jy = (rand() - 0.5) * sh * 0.6
    bind intro = 1.0 - clampf(st_get("bk_state_t", 0.0) / 1.4, 0.0, 1.0)
    if st_get("bk_state", "") != "intro" { bind intro = 0.0 }
    bind e = intro * intro
    bind dist = 16.6 + e * 9.0
    bind pitch = 0.9 + e * 0.25 - st_get("bk_pk", 0.0) * 0.004
    cam_orbit(jx + sway * 2.0, 0.0 - 0.5 + jy, 7.9, sway + e * 0.6, pitch, dist, 1.0)
    return 0.0
}
fn bk_next_stage() {
    bind next = sc_ex("next", "")
    st_set("gs_score", num_str(st_get("bk_score", 0.0)))
    st_set("gs_stage", num_str(st_get("bk_stage", 1.0)))
    bk_save_hi()
    brick_leave()
    if next == "" { st_set("bk_run", 0.0)  game_goto("scenes/victory.lscene") } else { game_goto(next) }
    return 0.0
}
fn bk_game_over() {
    st_set("gs_score", num_str(st_get("bk_score", 0.0)))
    st_set("gs_stage", num_str(st_get("bk_stage", 1.0)))
    bk_save_hi()
    st_set("bk_run", 0.0)
    brick_leave()
    bk_sfx("gameover", 0.9, 1.0)
    game_goto("scenes/gameover.lscene")
    return 0.0
}
fn bk_save_hi() {
    bind hi = max(st_get("bk_hi", 0.0), st_get("bk_score", 0.0))
    st_set("bk_hi", hi)
    st_set("gs_hi", num_str(hi))
    st_set("gs_newhi", sel(st_get("bk_score", 0.0) >= hi && hi > 0.0, "NEW HIGH SCORE!", ""))
    plat_write(game_path("hiscore.txt"), num_str(hi))
    return 0.0
}

# ── drawing ──────────────────────────────────────────────────────────────────
# lights before the scene's shapes are lit: the balls glow, the beat pumps the key light
fn brick_lights() {
    if !brick_on() { return 0.0 }
    bind pk = st_get("bk_pk", 0.0)
    plat_light(0.0, 0.0 - 9.0, 6.0, 255.0, 240.0, 255.0, 1.6 + pk * 0.9, 40.0)
    plat_light(0.0 - 9.0, 0.0 - 4.0, 18.0, 255.0, 80.0, 200.0, 1.4 + st_get("bk_ps", 0.0) * 1.4, 22.0)
    plat_light(9.0, 0.0 - 4.0, 18.0, 80.0, 160.0, 255.0, 1.4 + st_get("bk_ps", 0.0) * 1.4, 22.0)
    bind b = 0.0
    while b < min(3.0, st_llen("bl_x")) {
        bind c = sel(st_get("bk_t_fire", 0.0) > 0.0, c_rgb(255.0, 140.0, 40.0), c_rgb(150.0, 200.0, 255.0))
        plat_light(st_lget("bl_x", b), 0.0 - 1.0, st_lget("bl_z", b), c_r(c), c_g(c), c_b(c), 2.2, 6.0)
        bind b = b + 1.0
    }
    return 0.0
}
fn bk_box(c, x, y, z, sx, sy, sz, rx, ry, rz) {
    plat_color(c)
    plat_shape("box", x, y, z, sx, sy, sz, rx, ry, rz, 0.0)
    return 0.0
}
fn brick_world_native() {
    if !brick_on() { return 0.0 }
    bind t = st_get("game_t", 0.0)
    bind pk = st_get("bk_pk", 0.0)
    bind ps = st_get("bk_ps", 0.0)
    bind xmin = st_get("bk_xmin", 0.0 - 7.2)
    bind xmax = st_get("bk_xmax", 7.2)
    bind zmax = st_get("bk_zmax", 19.2)
    bk_deco(t, pk, ps, xmin, xmax, zmax)
    # bricks
    bind n = st_llen("bk_x")
    bind i = 0.0
    while i < n {
        bind alive = st_lget("bk_alive", i) > 0.5
        bind die = st_lget("bk_die", i)
        if alive || die > 0.0 {
            bind x = st_lget("bk_x", i)
            bind z = st_lget("bk_z", i)
            bind hw = st_lget("bk_hw", i)
            bind hd = st_lget("bk_hd", i)
            bind hh = st_lget("bk_hh", i) * (1.0 + pk * 0.22)
            bind ty = st_lget("bk_t", i)
            bind c = st_lget("bk_c", i)
            bind fl = st_lget("bk_fl", i)
            if fl > 0.0 { bind c = c_mix(c, c_rgb(255.0, 255.0, 255.0), fl / 0.14) }
            bind sc = 1.0
            bind lift = 0.0
            bind spin = 0.0
            if !alive {
                bind k = die / 0.35
                bind sc = k
                bind lift = (1.0 - k) * 1.2
                bind spin = (1.0 - k) * 2.5
                bind c = c_mix(c_rgb(255.0, 255.0, 255.0), c, k)
            }
            bind y = 0.0 - hh - lift
            bk_box(c, x, y, z, hw * sc, hh * sc, hd * sc, spin * 0.5, spin, 0.0)
            # bevelled cap
            bk_box(bk_bright(c, 0.35), x, y - hh * sc, z, hw * sc * 0.82, 0.04 * sc, hd * sc * 0.7, spin * 0.5, spin, 0.0)
            if alive {
                if ty == 1.0 {
                    # tough: cracks as hits land
                    bind dmg = st_lget("bk_hp0", i) - st_lget("bk_hp", i)
                    if dmg > 0.5 { bk_box(bk_dark(c, 0.3), x - hw * 0.2, y - hh - 0.05, z, hw * 0.5, 0.012, 0.03, 0.0, 0.5, 0.0) }
                    if dmg > 1.5 { bk_box(bk_dark(c, 0.3), x + hw * 0.3, y - hh - 0.05, z + hd * 0.2, 0.03, 0.012, hd * 0.6, 0.0, 0.3, 0.0) }
                    bk_box(bk_dark(c, 0.55), x, y, z - hd - 0.01, hw * 0.9, hh * 0.25, 0.01, 0.0, 0.0, 0.0)
                } else if ty == 2.0 {
                    bk_box(c_rgb(235.0, 240.0, 255.0), x, y - hh - 0.05, z, hw * 0.9, 0.012, 0.035, 0.0, 0.0, 0.0)
                    bk_box(c_rgb(90.0, 95.0, 110.0), x - hw * 0.75, y - hh - 0.06, z, 0.06, 0.02, 0.06, 0.0, 0.0, 0.0)
                    bk_box(c_rgb(90.0, 95.0, 110.0), x + hw * 0.75, y - hh - 0.06, z, 0.06, 0.02, 0.06, 0.0, 0.0, 0.0)
                } else if ty == 3.0 {
                    bind glow = 0.5 + 0.5 * sin(t * 9.0 + i)
                    plat_color(c_mix(c_rgb(255.0, 80.0, 20.0), c_rgb(255.0, 240.0, 120.0), glow))
                    plat_shape("sphere", x, y - hh - 0.12, z, 0.2, 0.2, 0.2, 0.0, 0.0, 0.0, 0.0)
                } else if ty == 4.0 {
                    bind tw = 0.5 + 0.5 * sin(t * 4.0 + i * 1.7)
                    plat_tm_billboard(0.0 - 1.0, x + hw * 0.5, y - hh - 0.1, z, 0.5 + tw * 0.4, 0.0, c_rgb(200.0, 255.0, 255.0), 0.5 + tw * 0.5, 1.0)
                } else if ty == 5.0 {
                    bind tw = 0.5 + 0.5 * sin(t * 5.0 + i * 2.3)
                    plat_tm_billboard(0.0 - 1.0, x - hw * 0.4, y - hh - 0.12, z, 0.35 + tw * 0.5, 0.0, c_rgb(255.0, 240.0, 160.0), tw, 1.0)
                }
            }
        }
        bind i = i + 1.0
    }
    # paddle: a capsule with glowing caps
    bind px = st_get("bk_px", 0.0)
    bind pz = st_get("bk_pz", 0.6)
    bind pw = st_get("bk_pw", 1.3)
    bind pb = st_get("bk_pb", 0.0)
    bind body = c_rgb(230.0, 235.0, 255.0)
    if st_get("bk_t_catch", 0.0) > 0.0 { bind body = c_rgb(255.0, 240.0, 150.0) }
    plat_color(body)
    plat_shape("capsule", px, 0.0 - 0.34, pz, 0.32, pw + 0.32, 0.32, 0.0, 0.0, 1.5708, 0.0)
    bind capc = c_mix(c_rgb(80.0, 150.0, 255.0), c_rgb(200.0, 240.0, 255.0), pb)
    plat_color(capc)
    plat_shape("sphere", px - pw, 0.0 - 0.34, pz, 0.36, 0.36, 0.36, 0.0, 0.0, 0.0, 0.0)
    plat_shape("sphere", px + pw, 0.0 - 0.34, pz, 0.36, 0.36, 0.36, 0.0, 0.0, 0.0, 0.0)
    bk_box(c_mix(c_rgb(255.0, 80.0, 200.0), c_rgb(255.0, 255.0, 255.0), pb), px, 0.0 - 0.66, pz, pw * 0.7, 0.025, 0.05, 0.0, 0.0, 0.0)
    if st_get("bk_t_laser", 0.0) > 0.0 {
        bk_box(c_rgb(255.0, 60.0, 60.0), px - pw + 0.25, 0.0 - 0.75, pz + 0.1, 0.08, 0.18, 0.2, 0.0, 0.0, 0.0)
        bk_box(c_rgb(255.0, 60.0, 60.0), px + pw - 0.25, 0.0 - 0.75, pz + 0.1, 0.08, 0.18, 0.2, 0.0, 0.0, 0.0)
    }
    # shield
    if st_get("bk_barrier", 0.0) > 0.5 {
        bind sc = c_mix(c_rgb(60.0, 220.0, 240.0), c_rgb(255.0, 255.0, 255.0), 0.5 + 0.5 * sin(t * 6.0))
        bk_box(sc, 0.0, 0.0 - 0.15, pz - 0.95, (xmax - xmin) * 0.5, 0.05, 0.05, 0.0, 0.0, 0.0)
    }
    # balls
    bind fireball = st_get("bk_t_fire", 0.0) > 0.0
    bind flash = st_get("bk_ball_flash", 0.0)
    st_set("bk_ball_flash", max(0.0, flash - 0.016))
    bind b = 0.0
    while b < st_llen("bl_x") {
        bind bx = st_lget("bl_x", b)
        bind bz = st_lget("bl_z", b)
        bind r = 0.3 * (1.0 + flash * 2.5)
        plat_color(sel(fireball, c_rgb(255.0, 210.0, 120.0), c_rgb(255.0, 255.0, 255.0)))
        plat_shape("sphere", bx, 0.0 - 0.3, bz, r, r, r, 0.0, t * 6.0, 0.0, 0.0)
        plat_tm_billboard(0.0 - 1.0, bx, 0.0 - 0.3, bz, 2.0 + flash * 5.0, 0.0, sel(fireball, c_rgb(255.0, 120.0, 30.0), c_rgb(130.0, 190.0, 255.0)), 0.85, 1.0)
        bind b = b + 1.0
    }
    # lasers
    bind i = 0.0
    while i < st_llen("lz_x") {
        bk_box(c_rgb(255.0, 90.0, 90.0), st_lget("lz_x", i), 0.0 - 0.5, st_lget("lz_z", i), 0.04, 0.04, 0.45, 0.0, 0.0, 0.0)
        plat_tm_billboard(0.0 - 1.0, st_lget("lz_x", i), 0.0 - 0.5, st_lget("lz_z", i) + 0.4, 0.8, 0.0, c_rgb(255.0, 80.0, 80.0), 0.9, 1.0)
        bind i = i + 1.0
    }
    # power-up capsules
    bind i = 0.0
    while i < st_llen("pu_x") {
        bind k = st_lget("pu_k", i)
        bind c = bk_pu_col(k)
        bind x = st_lget("pu_x", i)
        bind z = st_lget("pu_z", i)
        bind spin = st_lget("pu_t", i) * 4.0
        plat_color(c)
        plat_shape("capsule", x, 0.0 - 0.45, z, 0.26, 0.55, 0.26, spin, 0.0, 1.5708, 0.0)
        bk_box(c_rgb(255.0, 255.0, 255.0), x, 0.0 - 0.45, z, 0.18, 0.28, 0.28, spin, 0.0, 0.0)
        plat_tm_billboard(0.0 - 1.0, x, 0.0 - 0.5, z, 1.6, 0.0, c, 0.55, 1.0)
        bind i = i + 1.0
    }
    # debris
    bind i = 0.0
    while i < st_llen("db_x") {
        bind s = st_lget("db_s", i) * clampf(st_lget("db_l", i) * 2.0, 0.0, 1.0)
        bind l = st_lget("db_l", i)
        bk_box(st_lget("db_c", i), st_lget("db_x", i), st_lget("db_y", i), st_lget("db_z", i), s, s, s, l * 7.0, l * 5.0, l * 3.0)
        bind i = i + 1.0
    }
    plat_pfx_draw()
    return 0.0
}

# the stage's surroundings: floor grid, neon rails, the sky. Also drawn for a brick stage used
# as a menu backdrop (not in brick mode), with a slow idle pulse instead of the beat.
fn bk_deco(t, pk, ps, xmin, xmax, zmax) {
    # floor grid lines (glow on the kick)
    bind gc = c_mix(c_rgb(40.0, 50.0, 120.0), c_rgb(120.0, 200.0, 255.0), pk * 0.8)
    bind gx = xmin
    while gx <= xmax + 0.01 {
        bk_box(gc, gx, 0.0 - 0.005, (zmax - 1.5) * 0.5, 0.025, 0.006, (zmax + 1.5) * 0.5, 0.0, 0.0, 0.0)
        bind gx = gx + 1.8
    }
    bind gz = 0.0 - 1.5
    while gz <= zmax {
        bk_box(gc, 0.0, 0.0 - 0.005, gz, (xmax - xmin) * 0.5, 0.006, 0.025, 0.0, 0.0, 0.0)
        bind gz = gz + 1.8
    }
    # neon rails on the walls (flash on the snare)
    bind rc = c_mix(c_rgb(255.0, 70.0, 200.0), c_rgb(255.0, 255.0, 255.0), ps * 0.6)
    bind rc2 = c_mix(c_rgb(70.0, 170.0, 255.0), c_rgb(255.0, 255.0, 255.0), ps * 0.6)
    bk_box(rc, xmin - 0.12, 0.0 - 0.95, (zmax - 1.5) * 0.5, 0.06, 0.06, (zmax + 1.5) * 0.5, 0.0, 0.0, 0.0)
    bk_box(rc2, xmax + 0.12, 0.0 - 0.95, (zmax - 1.5) * 0.5, 0.06, 0.06, (zmax + 1.5) * 0.5, 0.0, 0.0, 0.0)
    bk_box(c_mix(rc, rc2, 0.5), 0.0, 0.0 - 0.95, zmax + 0.12, (xmax - xmin) * 0.5 + 0.2, 0.06, 0.06, 0.0, 0.0, 0.0)
    if st_llen("st_x") < 1.0 {
        bind k = 0.0
        while k < 140.0 {
            bind a = rand() * 6.283
            bind r = 40.0 + rand() * 60.0
            st_lpush("st_x", cos(a) * r)
            st_lpush("st_y", 0.0 - 6.0 - rand() * 50.0)
            st_lpush("st_z", 30.0 + sin(a) * r * 0.5 + rand() * 40.0)
            st_lpush("st_s", 0.25 + rand() * 0.9)
            st_lpush("st_c", list_get([c_rgb(255.0, 255.0, 255.0), c_rgb(170.0, 200.0, 255.0), c_rgb(255.0, 200.0, 240.0), c_rgb(255.0, 240.0, 190.0)], floor(rand() * 3.99)))
            bind k = k + 1.0
        }
    }
    # the sky: stars, a ringed planet, nebula glows
    bind i = 0.0
    while i < st_llen("st_x") {
        bind tw = 0.6 + 0.4 * sin(t * 2.0 + i * 1.3)
        plat_tm_billboard(0.0 - 1.0, st_lget("st_x", i), st_lget("st_y", i), st_lget("st_z", i), st_lget("st_s", i) * (1.0 + st_get("bk_ph", 0.0) * 0.3), 0.0, st_lget("st_c", i), tw, 1.0)
        bind i = i + 1.0
    }
    plat_tm_billboard(0.0 - 1.0, 0.0 - 30.0, 0.0 - 22.0, 70.0, 60.0, 0.0, c_rgb(120.0, 40.0, 160.0), 0.35, 1.0)
    plat_tm_billboard(0.0 - 1.0, 35.0, 0.0 - 30.0, 80.0, 70.0, 0.0, c_rgb(30.0, 80.0, 170.0), 0.3, 1.0)
    plat_color(c_rgb(255.0, 170.0, 120.0))
    plat_shape("sphere", 24.0, 0.0 - 16.0, 62.0, 7.0, 7.0, 7.0, 0.3, t * 0.05, 0.0, 0.0)
    plat_color(c_rgb(255.0, 220.0, 170.0))
    plat_shape("torus", 24.0, 0.0 - 16.0, 62.0, 11.0, 0.5, 11.0, 0.35, 0.0, 0.2, 0.0)
    return 0.0
}
fn brick_deco_native() {
    if brick_on() || sc_ex("mode", "") != "brick" { return 0.0 }
    bind t = st_get("game_t", 0.0)
    bind fld = nums_parse(sc_ex("field", "-7.2,7.2,19.2"))
    bind pk = 0.5 + 0.5 * sin(t * 3.0)
    bk_deco(t, pk * 0.5, 0.0, nums_get(fld, 0, 0.0 - 7.2), nums_get(fld, 1, 7.2), nums_get(fld, 2, 19.2))
    return 0.0
}

# ── HUD ──────────────────────────────────────────────────────────────────────
fn bk_fmt(n) {
    bind s = num_str(floor(n))
    bind out = ""
    bind k = 0.0
    bind l = str_len(s)
    while k < l {
        if k > 0.0 && (l - k) - floor((l - k) / 3.0) * 3.0 < 0.5 { bind out = out + "," }
        bind out = out + str_slice(s, k, 1.0)
        bind k = k + 1.0
    }
    return out
}
fn brick_hud(x, y, w, h) {
    if !brick_on() { return 0.0 }
    bind s = h / 720.0
    bind t = st_get("game_t", 0.0)
    bind ink = c_rgb(235.0, 240.0, 255.0)
    bind dim = c_rgb(150.0, 160.0, 210.0)
    # screen flash
    if st_get("bk_flash", 0.0) > 0.0 { g_rect_a(x, y, w, h, c_rgb(255.0, 255.0, 255.0), st_get("bk_flash", 0.0) * 0.35) }
    # popups
    bind i = 0.0
    while i < st_llen("pp_x") {
        bind pt = st_lget("pp_t", i)
        bind p = cam_project(st_lget("pp_x", i), 0.0 - 1.0 - pt * 1.6, st_lget("pp_z", i))
        if cam_visible(list_get(p, 2)) {
            bind a = clampf((1.1 - pt) * 2.0, 0.0, 1.0)
            bind txt = st_lget("pp_txt", i)
            g_text_a_c(list_get(p, 0), list_get(p, 1), 16.0 * s, st_lget("pp_c", i), txt, a)
        }
        bind i = i + 1.0
    }
    # top bar
    g_rect_a(x, y, w, 64.0 * s, c_rgb(8.0, 6.0, 24.0), 0.55)
    g_rect_a(x, y + 64.0 * s, w, 2.0 * s, c_mix(c_rgb(255.0, 80.0, 200.0), c_rgb(80.0, 180.0, 255.0), 0.5 + 0.5 * sin(t)), 0.8)
    g_text(x + 24.0 * s, y + 8.0 * s, 12.0 * s, dim, "SCORE")
    g_text(x + 24.0 * s, y + 24.0 * s, 28.0 * s, ink, bk_fmt(st_get("bk_score", 0.0)))
    g_text(x + 260.0 * s, y + 8.0 * s, 12.0 * s, dim, "HI-SCORE")
    g_text(x + 260.0 * s, y + 28.0 * s, 20.0 * s, c_rgb(255.0, 220.0, 120.0), bk_fmt(max(st_get("bk_hi", 0.0), st_get("bk_score", 0.0))))
    g_text_c(x + w * 0.5, y + 8.0 * s, 12.0 * s, dim, "STAGE " + num_str(st_get("bk_stage", 1.0)))
    g_text_c(x + w * 0.5, y + 26.0 * s, 22.0 * s, ink, st_get("bk_title", ""))
    # lives
    bind lives = st_get("bk_lives", 0.0)
    g_text_r(x + w - 24.0 * s, y + 8.0 * s, 12.0 * s, dim, "LIVES")
    bind k = 0.0
    while k < min(lives, 8.0) {
        bind cx = x + w - 34.0 * s - k * 26.0 * s
        g_disc_a(cx, y + 40.0 * s, 11.0 * s, c_rgb(120.0, 180.0, 255.0), 0.35)
        g_disc(cx, y + 40.0 * s, 7.0 * s, c_rgb(240.0, 245.0, 255.0))
        bind k = k + 1.0
    }
    if lives > 8.0 { g_text_r(x + w - 34.0 * s - 8.0 * 26.0 * s, y + 30.0 * s, 16.0 * s, ink, "x" + num_str(lives)) }
    # active power-ups
    bind names = ["bk_t_expand", "bk_t_laser", "bk_t_slow", "bk_t_fire", "bk_t_catch", "bk_t_shrink"]
    bind ks = [0.0, 2.0, 3.0, 4.0, 5.0, 8.0]
    bind fulls = [15.0, 12.0, 10.0, 8.0, 15.0, 10.0]
    bind cx = x + 24.0 * s
    bind k = 0.0
    while k < 6.0 {
        bind v = st_get(list_get(names, k), 0.0)
        if v > 0.0 {
            bind pk = list_get(ks, k)
            bind c = bk_pu_col(pk)
            g_rrect_a(cx, y + h - 64.0 * s, 132.0 * s, 40.0 * s, 8.0 * s, c_rgb(10.0, 8.0, 30.0), 0.7)
            g_disc(cx + 20.0 * s, y + h - 44.0 * s, 13.0 * s, c)
            g_text_c(cx + 20.0 * s, y + h - 53.0 * s, 15.0 * s, c_rgb(20.0, 20.0, 40.0), list_get(bk_pu_letters(), pk))
            g_text(cx + 40.0 * s, y + h - 60.0 * s, 12.0 * s, ink, list_get(bk_pu_names(), pk))
            g_rect_a(cx + 40.0 * s, y + h - 38.0 * s, 82.0 * s, 5.0 * s, c_rgb(60.0, 60.0, 90.0), 0.8)
            g_rect(cx + 40.0 * s, y + h - 38.0 * s, 82.0 * s * clampf(v / list_get(fulls, k), 0.0, 1.0), 5.0 * s, c)
            bind cx = cx + 142.0 * s
        }
        bind k = k + 1.0
    }
    if st_get("bk_barrier", 0.0) > 0.5 {
        g_rrect_a(cx, y + h - 64.0 * s, 110.0 * s, 40.0 * s, 8.0 * s, c_rgb(10.0, 8.0, 30.0), 0.7)
        g_disc(cx + 20.0 * s, y + h - 44.0 * s, 13.0 * s, bk_pu_col(6.0))
        g_text_c(cx + 20.0 * s, y + h - 53.0 * s, 15.0 * s, c_rgb(20.0, 20.0, 40.0), "B")
        g_text(cx + 40.0 * s, y + h - 52.0 * s, 12.0 * s, ink, "SHIELD")
    }
    # combo
    bind combo = st_get("bk_combo", 0.0)
    if combo >= 3.0 {
        bind pulse = 1.0 + st_get("bk_pk", 0.0) * 0.12
        g_text_r(x + w - 24.0 * s, y + h - 60.0 * s, 26.0 * s * pulse, c_mix(c_rgb(255.0, 200.0, 80.0), c_rgb(255.0, 90.0, 200.0), 0.5 + 0.5 * sin(t * 6.0)), "COMBO x" + num_str(floor((1.0 + min(combo, 16.0) * 0.25) * 100.0) / 100.0))
    }
    # messages
    if st_get("bk_msg_t", 0.0) > 0.0 {
        bind a = clampf(st_get("bk_msg_t", 0.0) * 2.0, 0.0, 1.0)
        bind sz = (34.0 + (1.0 - clampf((2.2 - st_get("bk_msg_t", 0.0)) * 6.0, 0.0, 1.0)) * 20.0) * s
        g_text_a_c(x + w * 0.5, y + h * 0.42, sz, c_rgb(255.0, 255.0, 255.0), st_get("bk_msg", ""), a)
    }
    # intro card
    if st_get("bk_state", "") == "intro" {
        bind a = clampf(st_get("bk_state_t", 0.0) * 2.0, 0.0, 1.0)
        g_rect_a(x, y + h * 0.36, w, 120.0 * s, c_rgb(8.0, 6.0, 24.0), 0.6 * a)
        g_text_a_c(x + w * 0.5, y + h * 0.36 + 14.0 * s, 16.0 * s, c_rgb(255.0, 140.0, 220.0), "STAGE " + num_str(st_get("bk_stage", 1.0)), a)
        g_text_a_c(x + w * 0.5, y + h * 0.36 + 36.0 * s, 40.0 * s, c_rgb(255.0, 255.0, 255.0), st_get("bk_title", ""), a)
        if floor(t * 2.0) - floor(t * 2.0 / 2.0) * 2.0 < 0.5 { g_text_a_c(x + w * 0.5, y + h * 0.36 + 90.0 * s, 14.0 * s, dim, "click or press SPACE to launch", a) }
    }
    # pause
    if st_get("bk_paused", 0.0) > 0.5 {
        g_rect_a(x, y, w, h, c_rgb(6.0, 4.0, 18.0), 0.6)
        g_text_c(x + w * 0.5, y + h * 0.4, 48.0 * s, ink, "PAUSED")
        g_text_c(x + w * 0.5, y + h * 0.4 + 70.0 * s, 16.0 * s, dim, "P resume      Q quit to the game over screen")
    }
    return 0.0
}
# centred text with alpha
fn g_text_a_c(cx, y, size, c, s, a) {
    if a <= 0.01 { return 0.0 }
    bind w = g_text_w(size, s)
    if a > 0.98 {
        g_text(cx - w * 0.5 + 2.0, y + 2.0, size, c_rgb(10.0, 6.0, 30.0), s)
        g_text(cx - w * 0.5, y, size, c, s)
        return 0.0
    }
    g_text(cx - w * 0.5, y, size, c_mix(c_rgb(10.0, 8.0, 30.0), c, a), s)
    return 0.0
}

← Back to index