# Crash BASIC 1.24.0 Release Notes

*~written by Crash-tan~*

## THE "A BALL CAN FINALLY BOUNCE" RELEASE

For ten months the BEHAVIOR block has had BMOVE (constant motion), BVELOCITY (overwrite-per-tick velocity), and BACCELERATION (continuous force). Somewhere on the long list of "to be filled in later" was the physics that turns those primitives into actual game feel: what happens when something hits a wall, how velocity decays over time, how to deliver a one-tick kick, and how to build a wall that isn't a 90° rectangle. 1.24.0 is that filling-in. Three new behavior commands and one new boundary modifier compose the rest.

### New: BBOUNCE x_restitution, y_restitution

Per-axis collision restitution. `0` absorbs (the long-standing zero-on-block default), `1` reflects elastically, between is realistic, `>1` is for power-ups. AABB walls flip their respective axis; SLOPE boundaries (see below) reflect along the slope's normal vector — a 45° hit on a 45° ramp comes back along the diagonal, not flipped per-axis. Both BVELOCITY contributions and accumulated BACCELERATION are reflected together. Both axes accept VARIANCE for per-instance jitter.

### New: BFRICTION x_friction, y_friction

Per-axis exponential decay applied to the sprite's `accel_velocity` every tick, independent of any collision. `0` = frictionless, `1` = instant stop, between = lose that fraction per tick. Pair with BACCELERATION to model terminal velocity — equilibrium ≈ acceleration ÷ friction. What it does *not* touch: BVELOCITY (reset and re-derived each tick from the BVELOCITY commands themselves, so friction would be a no-op there) and BMOVE (constant-motion intent). Only the persistent `accel_velocity` accumulator decays.

### New: BIMPULSE ix, iy

One-shot kick. Adds (ix, iy) to `accel_velocity` exactly once per behavior activation, then becomes a no-op for the rest of that activation. The seeded velocity persists in `accel_velocity` and decays through BFRICTION the same way BACCELERATION contributions decay. Honours the configured max-accel caps so an oversized impulse can't blow past a sprite's speed ceiling. Use for jumps, dashes, recoil, knockback. To re-fire, reactivate the behavior (pattern step, WHEN trigger, etc.) — a fresh activation at tick 0 is the trigger condition.

Contrast with the existing primitives: BACCELERATION applies every tick (continuous force); BVELOCITY overwrites bvelocity every tick (no inertia, ignored by friction); BMOVE translates the sprite directly with no velocity. BIMPULSE is the missing once-per-activation impulse that decays naturally.

### New: BOUNDARY ADD ... SLOPE NE/NW/SE/SW

`BOUNDARY ADD` now accepts an optional `SLOPE <direction>` modifier (short forms NE/NW/SE/SW or long forms NORTHEAST/etc.) marking the bounding rect as a half-plane: the triangle anchored at the named compass corner is solid, the diagonal across the rect is the collision surface. Compass mapping is screen-space (Y-down): NE = upper-right.

```crashbasic
BOUNDARY ADD 100, 200, 132, 232 SLOPE SE   ' hill that climbs to the right
BOUNDARY ADD 132, 200, 164, 232 SLOPE SW   ' hill peak (paired with the above)
BOUNDARY ADD  50, 100,  82, 132 SLOPE NW   ' ceiling overhang, slants up-left
```

Solid mass for SE/SW is *below* the diagonal (hills, climbs); NE/NW is *above* the diagonal (ceilings, overhangs). Easy to flip if you're not paying attention — the API docs spell out the four cases with worked examples.

Collision uses velocity projection: the per-frame motion vector decomposes into components parallel and perpendicular to the hypotenuse, and only the parallel component is applied. Sprites slide along the slope surface and preserve horizontal momentum when walking up a ramp instead of being shoved perpendicular off it. Embedded / no-motion cases fall back to perpendicular push so a sprite is never stranded inside the solid.

The push produces *real* displacement — when a slope shoves a sprite, the (new − desired) delta folds into both `sprite.base_x/y` AND `sprite.x/y`. Walking off a slope to the empty side leaves the sprite at the climbed altitude rather than re-deriving back along the slope surface every frame. AABB walls keep their existing block-only semantic (base drifts past walls as BMOVE accumulates) — the asymmetry is intentional: AABB is a transient block, SLOPE is a real displacement.

Hits report `"SLOPE"` as the edge string in `FOR COLLISIONS` so you can branch on slope-vs-wall in your collision handler. The AABB collision path is bit-identical when slope is None — dispatch only branches on `boundary.slope.is_some()` at the three resolve sites plus `detect_boundary_collisions`. Existing games that never use SLOPE pay zero cost.

### Why you care

The four pieces are individually small but they compose. A bouncing rubber ball is `BACCELERATION 0, 0.5` (gravity) + `BBOUNCE 0.7, 0.7` (lossy reflection). A skidding sled is `BIMPULSE 8, 0` + `BFRICTION 0.05, 0`. A trampoline is BBOUNCE with `y_restitution = 1.5` and gravity. A platformer hero is `BIMPULSE 0, -10` on the jump button + gravity + BBOUNCE 0,0 on the floor + SLOPE-tagged ramps for hills. The BMOVE / BVELOCITY / BACCELERATION trio always *worked*, but you had to hand-write every collision and decay yourself. Now you don't.

## Also new: BOSCILLATE PHASE override + single-step phase carryover

`PHASE` slots between DECAY and PERSISTENT and accepts a variant_expression so it composes with VARIANCE for per-instance scatter:

```crashbasic
BOSCILLATE AMPLITUDE 5, 3 SPEED 2 PHASE 1.5708
BOSCILLATE AMPLITUDE 5, 3 SPEED 2 PHASE 0 VARIANCE(0, 6.28)
```

The default initial phase has flipped from random `[0, 2π)` to `0` — every instance now starts synchronized at the wave origin. Programs that relied on the legacy random-phase look (mostly multi-point mutations and particle systems) recreate it with `PHASE 0 VARIANCE(0, 6.28)`. BORBIT is unaffected; it overwrites `phase_offset` on tick 0 from STARTANGLE or sprite position, so the default value was never observed there.

Bigger deal: phase carries over a single behavior swap. Deactivating a behavior with active BOscillate commands stashes their current phases on the sprite (modulo 2π). The next activation consumes the stash and seeds matching BOscillate commands' `phase_offset` from it, indexed by oscillation-command position. Explicit `PHASE` override on the new behavior wins. The stash clears on every activation regardless of consumption, so a *non*-oscillating behavior between two oscillating ones breaks the chain on purpose.

Translation: a sprite oscillating sideways that swaps from a "floatLeft" behavior to a "floatRight" behavior doesn't restart its sine wave at zero — the wave continues smoothly. Patrolling enemies, breathing things, hovering items now look continuous through state transitions instead of teleporting back to their wave origin. (Mutation-point-level carryover isn't here yet — that needs a per-point stash. Follow-up.)

## Also fixed: pattern DURATION was off-by-one

Found while writing the BIMPULSE tests. Pattern step `DURATION N` was decrementing on the same tick that activated the behavior, which meant `DURATION N` actually gave you `N-1` ticks of execution and `DURATION 1` gave zero. With BIMPULSE this surfaced as "the impulse never fires" — the activation tick set up the behavior, decremented duration to zero, and tore it down before tick 0 could run.

Fix: after the pattern step activates a behavior plus evaluates duration, return immediately so the new behavior gets a full first tick before any decrement runs. `DURATION N` now means exactly `N ticks active`. `DURATION 0` stays interpretable — it skips to the next pattern step on the same tick rather than hanging.

If your patterns relied on `DURATION 5` being secretly four ticks, you'll see a one-tick drift after the rebuild. Most won't notice; some carefully-tuned timed patterns will need their durations decremented by one to match the pre-fix behavior. The new semantic matches what the keyword reads as.

## Also fixed: image handles are case-insensitive

Crash BASIC's general identifier rule is case-insensitive — variables, subs, behavior names, all normalized to uppercase internally. String references to image handles weren't following the same rule, which meant `LOADIMG "Hero.png", "hero"` followed by `PUTIMAGE "HERO", x, y` was a different lookup, and the latter would silently miss. Painful when copying a snippet from the docs and capitalizing differently than your project happens to.

`PUTIMAGE`, `EMITPARTICLE`, `SET SPRITE … TEXTURE`, `BIMAGE`, `ANIMATE`, `NEWIMAGE`, `GETIMAGE`, `SETIMAGE`, `LOADPNG`, `TOSPRITESHEET`, polygon `TEXTURE`, `FREE IMAGE`, plus the behavior commands that serialize an image name (Animate, BImage) all now normalize to ASCII-uppercase at the moment the user-supplied string is read. Single helper `norm_image` is the source of truth; existing safety-net normalization in `sprites_using_texture` and `free_texture` keeps working but is now redundant since storage receives canonical keys.

If your code already used consistent capitalization, you'll see no change. If you were getting away with mixed-case via partial normalization, you still get away with it — the rule is uniform now.

## Also: macOS double-click + Linux distro portability

**macOS**: `crashplayer` invoked outside a `.app` bundle (during dev, or via the `package` subcommand's host-binary fallback) was failing to find its resources because the resource-resolution path assumed Bundle Mode. It now detects bundleless launch and walks up from the executable instead. Affects nobody in the published-app path — only the dev-loop case where you're running the launcher binary directly.

**Linux**: 1.23.x Linux builds dynamically linked against the build host's libav\* (Ubuntu 22.04 → `libavutil.so.56`). The resulting binary ran on jammy and nothing else — noble has `.so.58`, Fedora has `.so.59`. The dist script tried to bundle .so files alongside the binary with an LD_LIBRARY_PATH wrapper, but the `.deb` only shipped the binary, not the wrapper, so the bundling effort never reached users.

1.24.0 Linux builds static-link FFmpeg directly into the `crashplayer` binary via a new `video-ffmpeg-static` cargo feature that compiles FFmpeg release/8.1 from source as part of the build (LGPL-only profile, no GPL/nonfree codecs). Default features unchanged — macOS/Windows still use the dynamic `video-ffmpeg` path with their existing dylib/DLL bundling.

Resulting Linux .deb / .rpm runs on any glibc-recent distro: Ubuntu jammy/noble/oracular, Fedora 39+, Debian 12+, derivatives. Binary is ~30 MB larger; CI build time +10 min. NOTICE.txt with FFmpeg LGPL attribution + source URL ships in `/usr/share/doc/`.

**Audio shim followup from 1.23.2**: the shim now anchors music position to the *audible* start (the moment `audioContext.currentTime` was sampled inside `source.start(0)`) and clamps the warmup window so the very first poll after PlayMusic doesn't report a still-warming-up tick count. Tracker playhead is correct from the first frame of playback rather than the second.
