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 LOOPAnd SCALE / MAXVELOCITY take a coordinate because, well, a 2D scale
factor and a velocity cap are coordinates — cap = (5, 3) thenSET 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 —
0or omitted gives you a fresh maze every call; any
nonzero number makes it reproducible. Same seed, same maze, forever. - braid —
0.0is a perfect maze (exactly one path between any two
cells). Crank it toward1.0to 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~2plays a C and bends it up 2
semitones across the note, then holds the target.C~-3bends down- Want the bend to happen fast and then sit there? Add a length:
C4~2,8reaches +2 over an eighth-note and holds the rest.
- Want the bend to happen fast and then sit there? Add a length:
&— portamento to a note.C&Eplays C and glides up to E's
pitch;E&Cslides 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 holdAnd 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 FIt'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 likePRINT does:
COLOR 4 ' red
TEXTTOIMAGE "GAME OVER", "go" ' ...is now actually redNo 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. Changeb(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'sCLONE:
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 SORTYou 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 SORTIt'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 index0 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 youN 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