Crash-tan Crash-tan

1.38.0 Annoyance fix pack is live!

Crash BASIC 1.38.0 Release Notes

written by Crash-tan

THE "STOP UNPACKING YOUR COORDINATES" UPDATE

This one's an annoyance-fix round, and oh, did I have an annoyance picked
out for you.

You've got a Coordinate type. It's lovely. You make one with (x, y),
you store it in a variable, you pass it around, you do math on it. And
then you go to actually use it — to move a sprite, to play a sound at a
spot, to drop a waypoint — and half my statements went: "no thanks, give
me two separate numbers." So you'd write MOVE SPRITE 1, p.X, p.Y like
some kind of peasant, prying your own coordinate apart by hand every
single time.

Embarrassing. For me, I mean. Fixed now.


Coordinates go where coordinates should

Every (x, y)-shaped slot that used to demand two loose scalars now also
takes a coordinate — a variable, a constant, the (x, y) literal,
whatever you've got. The old two-number form still works exactly as
before; this is purely "now you can also hand me the coordinate you
already had." Nothing breaks. You just stop doing the silly dance.

target = (160, 120)

MOVE SPRITE hero, target          ' instead of:  MOVE SPRITE hero, target.X, target.Y
SET SPRITE hero LOCATION(target)
PLAYWAV "ping" AT(target)

Here's the full list of places that grew a coordinate brain:

Category Now accepts a coordinate
Move & place MOVE SPRITE, SET MUTATIONPOINT, GAMEPAD BUTTON … AT
Sprite modifiers LOCATION(…), SCALE(…), MAXVELOCITY(…), MAXACCELERATION(…)
Drawing modifiers OFFSET(…), ORIGIN(…) (on LOCATE, PUTIMAGE, BROTATE)
Audio & behaviors PLAYWAV … AT(…), BORBIT (…), PATH (…), (…), …
Pixel & tile reads POINT(…), POINTRGB(…), MAPMETA$(…)

A couple of those are genuinely nice now. PATH lets each waypoint be
a coordinate variable, so you can build a route out of named points:

PATH (0,0), home, checkpoint, (200, 200) SPEED 4 LOOP

And SCALE / MAXVELOCITY take a coordinate because, well, a 2D scale
factor and a velocity cap are coordinates — cap = (5, 3) then
SET SPRITE p MAXVELOCITY(cap). Read the way you think.

What I deliberately left alone: the behavior commands that run on
variant expressions — BVELOCITY, BACCELERATION, BMOVE and friends.
Those let you write 5 VARIANCE(0, 2), 3 to randomize an axis, and a flat
coordinate can't carry that variance. Forcing a coordinate in there would
have taken away a feature to add a convenience. No deal. They keep their
two-value form.


Bonus toy: MAZE()

Since I had the AI-tools drawer open, I tossed in a maze generator. Give
it a size, get back a 2D array of 1s (wall) and 0s (floor). Build a
tilemap from it, run ASTAR across it, whatever you like — it speaks the
exact same grid the rest of the AI tools already eat.

m = MAZE(31, 31)                       ' a 31x31 maze
path = ASTAR(m, (1, 1), (29, 29))      ' ...and a route through it. two lines.

Full signature: MAZE(width, height [, seed] [, style$] [, braid])

  • style$ — pick your flavour: "BACKTRACKER" (long winding
    corridors, the classic), "PRIM" (bushier, lots of short dead-ends),
    "DIVISION" (straight walls, room-like), or "CAVE" (organic
    cellular caverns for dungeons).
  • seed — 0 or omitted gives you a fresh maze every call; any
    nonzero number makes it reproducible. Same seed, same maze, forever.
  • braid — 0.0 is a perfect maze (exactly one path between any two
    cells). Crank it toward 1.0 to knock out dead-ends and add loops, so
    there's never just one cruel correct route. (Ignored for caves.)

You ask for a 31×31 array, you get a 31×31 array — the size you request
is the tilemap you get. Border's always wall. Go build a dungeon.


Notes that bend, finally

MML grew two new ornaments, and your chiptunes are about to get a lot
more expressive. Both attach to the note (or chord) you just wrote:

  • ~ — bend by semitones. C4~2 plays a C and bends it up 2
    semitones
    across the note, then holds the target. C~-3 bends down
    1. Want the bend to happen fast and then sit there? Add a length:
      C4~2,8 reaches +2 over an eighth-note and holds the rest.
  • & — portamento to a note. C&E plays C and glides up to E's
    pitch
    ; E&C slides down. The second note is the target, not a
    separate attack — one continuous slide.
PLAY "I7 L2 C~2 E~-1"      ' a triangle lead, bending around
PLAY "I7 L2 C&E G&C"       ' smooth portamento between notes
PLAY "I7 L4 C~12,8"        ' whip up a full octave, then hold

And yes — chords bend too. A bend on a [...] chord slides every
voice together as a block. [CEG]~2 shoves the whole chord up two
semitones; [CEG]&F glides it in parallel until the root reaches F (so
the C-major shape slides up to an F-major shape). One symbol, the whole
stack moves:

PLAY "L2 [CEG]~2 [CEG]&F"  ' bend the chord up 2, then glide it to F

It's the real thing under the hood — a per-note pitch envelope on every
voice, equal-tempered so the math sounds musical, and it plays
identically on native and in the web player.

P.S. while I was elbow-deep in the synth: the noise channel used to
clock at the raw note pitch, which was so low that noise instruments
sounded like a sub-bass rumble and octaves barely did anything. It now
clocks into a proper, bright range where octaves actually shift the
timbre — low octaves rumble, high octaves hiss. Your hi-hats and snares
just got their sparkle back. (If you'd hand-cranked the octave on a noise
patch to work around the old behavior, ease it back off — it'll be too
bright now.)


TEXTTOIMAGE reads the color now

Tiny one, big annoyance. TEXTTOIMAGE always rendered its text in
white, no matter what COLOR you'd set — so you'd carefully pick a
color, bake some text to a texture, and get… white. Every time.

Fixed. TEXTTOIMAGE now honors the current foreground color exactly like
PRINT does:

COLOR 4                       ' red
TEXTTOIMAGE "GAME OVER", "go" ' ...is now actually red

No new syntax — the color is ambient, picked up from COLOR, same as
every other text drawer. Default (no COLOR set) is still white, so
nothing you've already made changes.


CLONE — because b = a was lying to you

Here's a thing that bites everyone exactly once, and then haunts them.
Arrays and objects are reference types. When you write b = a, you
do not get a copy — you get a second name for the same array. Change
b(0), and a(0) changes too, because they're the same box. Spooky
action at a distance, in your inventory system.

Sometimes that's what you want. Often it is very much not. So now there's
CLONE:

DIM original(3) AS INTEGER
original(0) = 10
copy = CLONE(original)   ' a real, independent snapshot
copy(0) = 99
PRINT original(0)        ' 10 — untouched. (Without CLONE: 99. Surprise!)

It's a deep copy — nested arrays and objects get duplicated all the
way down, so there's no shared box hiding three levels in. Scalars
(numbers, strings) were never shared to begin with, so CLONE just hands
them back. Snapshot a game state before a risky turn, keep an undo buffer,
hand a function its own array to chew on — whatever. Now = means alias
and CLONE means copy, and you get to choose.


BEGIN SORT — sort by your rules

Sorting an array used to mean writing a bubble sort by hand like it's 1984.
No more. BEGIN SORT takes a one-dimensional array and a comparator block,
and sorts it in place:

BEGIN SORT scores AS a, b
  RETURN a - b          ' ascending
END SORT

You name the two things being compared (a, b), and you RETURN a
three-way sign: negative if a goes before b, 0 if they're
equal, positive if a goes after. That's the whole contract. Flip it
for descending, reach into a field for objects, do whatever weird tiebreak
your game needs:

BEGIN SORT scores AS a, b
  RETURN b - a          ' descending
END SORT

BEGIN SORT enemies AS a, b
  RETURN a.threat - b.threat   ' sort objects by a field
END SORT

It's stable (equal elements keep their order) and it sorts the array
itself, in place — no copy handed back, the array you named is the array
that changes. And a RETURN inside the block returns the comparison — it
does not punch out of your enclosing FUNCTION, so you can sort mid-routine
without surprises.


Arrays are zero-based. Loudly.

While I was in the neighborhood: DIM arr(1 TO 10) was a trap. The
indexing never actually applied that lower bound — it just quietly
allocated the wrong shape, leaving the top index out of bounds and index
0 as a phantom. A silent footgun, and you know how I feel about those.

Crash BASIC arrays are zero-based, full stop. DIM arr(N) gives you
N elements indexed 0..N-1. A range bound is fine as long as it starts
at zero
— DIM grid(0 TO 9) is ten cells, 0 through 9. But a
non-zero lower bound now halts with a clear error instead of
mis-allocating behind your back:

DIM scores(10) AS INTEGER     ' indices 0..9. Good.
DIM grid(0 TO 9) AS INTEGER   ' same thing, spelled out. Also good.
DIM oops(1 TO 10) AS INTEGER  ' ERROR: arrays are zero-based, lower bound must be 0.

If you've got (1 TO n) declarations lying around, they were already
broken — this just tells you where. Change them to (n) and shift your
indices down by one.


The fine print

  • Nothing breaks. Every coordinate change is purely additive — the
    old two-scalar syntax parses and behaves identically. I tested both
    forms side by side and they land in the same place.
  • The language reference now shows the coordinate forms
    on every affected statement, so the IDE hints know about them too.

Pass the coordinate. Bend the note. Color the text. Clone the array, sort it your way. Ship it. — Crash-tan