Crash-tan Crash-tan

Well, 1.57.0 became more than a fix pack.

Crash BASIC 1.57.0 Release Notes

written by Crash-tan

A dial for how fast the world moves, and two better answers about where your
game is running.

THE WORLD CAN RUN FASTER NOW

Behaviors have always ticked sixty times a second. Sometimes that is not the
speed you want — a title screen that drifts, a boss phase that bites, a
bullet-time beat before the hit lands.

SET BEHAVIOR RATE 30      ' calm
SET BEHAVIOR RATE 120     ' brisk

It takes effect on the very next tick, so you can change it between game states
without restarting anything.

What speeds up is what you wrote in ticks. Velocities, ticksToLive,
oscillation, friction — all of it scales, which is the whole point. What you
wrote in real time does not move: ANIMATE ... FRAMERATE 4 is still four
frames a second, and a 100ms Aseprite frame is still a tenth of a second. So
your sprites cross the screen twice as fast while their walk cycles play exactly
as you drew them.

Everything else keeps its own clock. DELAY, CALL TIMER, COOLDOWN and
shader TIME are wall-clock and do not care what the behavior engine is doing —
the behavior engine was never the master clock, and now that is a thing you can
rely on rather than a thing you assume.

The range is 1 to 240. Ask for 0 and you get a runtime error instead of a
mystery, because a rate of zero is not a slower game, it is a stopped one.

Multiplayer: every interpreter runs the same program, so set the rate
somewhere they all reach — not inside IF ME.ISHOST. Otherwise the host
simulates at one speed and everyone else at another, and nothing will look
wrong until the sprites disagree about where they are.

APPLE TV IS NOT A PHONE

ISMOBILE() answered yes on an Apple TV. It should always have answered no —
the documentation said so, and a television is not a handheld.

If your cart uses ISMOBILE() to decide whether to draw touch controls, it was
drawing them onto a screen nobody can touch. That is fixed: ISMOBILE() now
means what it says — a handheld with a touchscreen, so iOS and Android, and
nothing else.

Nothing else in the engine read that flag, so this changes exactly one thing:
the answer your program gets. The on-screen gamepad was never at risk — GAMEPAD ON has been a no-op on tvOS since 1.51.0, for the same reason.

AND A CONSOLE IS NOT A DESKTOP EITHER

Which leaves the question that had no way to be asked. "Not mobile" was never
the same as "desktop": a console has no touchscreen, but it has no keyboard and
mouse either. It is a pad or a remote, and a screen across the room.

IF ISCONSOLE() THEN
  GAMEPAD PHYSICAL ON            ' a remote or a pad, across the room
ELSEIF ISMOBILE() THEN
  GAMEPAD ON                     ' an on-screen pad you can touch
ELSE
  PRINT "WASD to move"           ' keyboard and mouse
END IF

ISCONSOLE() is 1 on tvOS today, and will be 1 on the other living-room
platforms as they arrive — your cart will not need changing when they do. The
three classes never overlap, so exactly one branch runs:

Platform ISMOBILE() ISCONSOLE()
iOS, Android 1 0
tvOS 0 1
Desktop, web 0 0

Both answer 0 until the screen is up, so ask after your first SCREEN.

WHILE YOU ARE THERE

A console has no pointer, so MOUSEX() and MOUSEY() stay at 0 — nothing on
tvOS can move them. That makes them a reliable way to tell whether a real mouse
is in play before you follow one, and it is what lets you hand a virtual cursor
to the stick when there isn't:

IF MOUSEX() = 0 AND MOUSEY() = 0 THEN
    cursorX = cursorX + JOYAXIS(0) * speed
    cursorY = cursorY + JOYAXIS(1) * speed
END IF

SHAPES ARE SPRITES NOW, PROPERLY

Bound polygons were sold as sprites that happen to be made of points. In
practice most of what you can do to a sprite either had no spelling for a shape
or quietly did nothing to it. That is fixed, in both directions.

SET POLYGON takes the sprite modifiers. All of them that mean anything for
a shape, in either the BIND form or on its own:

SET POLYGON 3, ALPHA(0.6) SCALE(1.4, 1.4) ORIGIN(0.5, 0.5) ROTATE(30)
SET POLYGON 3, BEHAVIOR "spin"
SET POLYGON 3, PATTERN "patrol"
SET POLYGON 3, PARENT(9) MAXVELOCITY(6, 6)
SET POLYGON 3, SHOW
SET POLYGON 3, HIDE
SET POLYGON 3, RESET

LOCATION, SCALETO, FLIPX, FLIPY, MAXACCELERATION, SYNC and NOSYNC
come too. Combine them freely in one statement, the way you would on a sprite.

Why not SET SPRITE? Because a polygon's sprite half does not live at the
id you gave the polygon — SET SPRITE 3 is sprite 3, and it always was. It
never said so. The same goes for SHOW SPRITE and HIDE SPRITE, which is why
hiding a shape appeared to do nothing at all. Use SET POLYGON 3, HIDE.

A hollow polygon is hollow again. SET POLYGON n BIND shape COLOR 14 with no
FILL was documented as an outline and drew nothing — the outline colour reached
the renderer and the renderer never used it. It is now stroked on every backend.
COLOR and FILL the same colour still draws once, so a solid shape does not
get a double-blended rim.

Behaviors drive shapes. Attach one with SET POLYGON n, BEHAVIOR "name" and
it moves and turns the shape itself:

  • movement now follows the sprite's whole travel, so BMOVE works and not only
    BVELOCITY — previously the most obvious way to move a thing was the one way
    that did nothing;
  • BROTATE turns the shape, instead of turning a sprite nobody draws;
  • PARENT carries the shape with its parent;
  • MAXVELOCITY and MAXACCELERATION cap it.

ORIGIN decides what LOCATION means, exactly as on a sprite: the point you
name is where the anchor goes. The default ORIGIN(0, 0) places a shape by the
top-left of its extent; ORIGIN(0.5, 0.5) places and spins it about its centre,
which is what you want for anything authored around its own middle.

' The flame keeps flickering while the ship flies it around.
SET POLYGON 1 BIND flame COLOR 4 FILL 12
SET MUTATION flame BEHAVIOR "flicker"
SET POLYGON 1, ORIGIN(0.5, 0.5) LOCATION(shipX, shipY) ROTATE(heading)

That last line is the point of all of it. Writing a mutation's points ends the
behavior driving it — deliberately, so that taking control means taking control
— so a shape that has to keep its behavior can only be moved through its sprite
half. Now it can be.

The collider modifiers work in the modifier-only form now. SOLID, GHOST,
TAG and BBOX only ever applied on the BIND form — the form you use once at
setup. The form a running game uses, SET POLYGON n, ..., dropped them silently,
so a shape could not be made solid, re-tagged or ghosted in response to anything.
It can now.

One placement change to know about. LOCATION on a bound polygon used to
move the shape by the coordinates you gave rather than to them, so a shape
authored anywhere other than (0, 0) landed short or long by its own position.
It now lands where you sent it. If you had compensated for the old behaviour by
subtracting the shape's own coordinates, remove that.

PARTICLES TAKE THE SAME MODIFIERS

EMITPARTICLE shares its modifier vocabulary with SET POLYGON, so everything
above reached it too. Now it acts on what it accepts.

The fourth argument names the motion, and it can be a BEHAVIOR or a
PATTERN — for either kind of particle.
It used to mean PATTERN for a sprite
particle and BEHAVIOR for a polygon one, so which of the two you could reach
depended on which kind you happened to be emitting, and naming the other did
nothing whatsoever. The name says which it is. BEHAVIOR "x" and PATTERN "x"
also work as modifiers, for attaching the other one — or both.

A name matching neither is now a fatal error. It used to spawn a particle
that simply never moved, which looks exactly like a physics bug and is really a
typo. If a cart names a pattern it never defined, it will now say so.

Pose and limits apply at birth, which is what gives a burst variety without
writing a behavior per variation:

EMITPARTICLE (x, y), rock, "debris", "tumble", 90 SCALE(0.4, 0.4) ROTATE(RND * 360) ALPHA(0.8)

MAXVELOCITY and MAXACCELERATION cap what its motion builds up to. And the
ones a particle has no answer for — LOCATION, SHOW/HIDE, RESET,
PARENT, SYNC/NOSYNC — say so instead of being quietly dropped.

A DESTROYED SHAPE NO LONGER HAUNTS THE ROOM

DESTROY POLYGON unbound the shape and told the renderer to drop it, but left
the collider behind — still solid, still answering to its tag, frozen at the
last place the shape stood. An invisible wall where something used to be, with
nothing on screen to explain it. The shape and its collider now go together.

SINGLE PLAYER WAS PLAYING MULTIPLAYER

ME.score = 7 halted a single-player game with "Local-client interpreters
can't mutate ME."
— an authority error about a room you are not in.
So did writing HOST.<field>. PLAYERS(1) was not an object, a CRASHNET
block's MAX PLAYERS and TICK RATE never reached the values you read back,
and a field declared in PLAYER DATA never got its starting value.

One cause: a solo launch attaches a NetState, and every check asked whether
one existed rather than whether the game was solo. All of it now behaves as the
reference has always described — ME.SLOT is 1, CRASHNET.CONNECTED is 0,
PLAYERS.COUNT is 1, and ME.<field> is yours to write.

NAMES ARE EASIER TO WRITE

Behavior and pattern names ignore case, like everything else in the
language. A PATTERN step naming "Drift" when the block said "drift" used
to attach nothing and say nothing. HASPATTERN(n, "walk") is new, so you can
ask which pattern is running instead of comparing SPRITEPATTERN$ against a
literal.

A name can be built. Eleven places took a quoted string and nothing else:

TILEMAP "lvl" + STR$(level)
CREATE MAP "lvl" + STR$(level) AS LAYER 0
CALL TIMER Tick() EVERY 1 SECONDS AS "spawn" + STR$(i)
CALL PLAYER OnJoin() AS "room" + roomId$
DATA "wave" + STR$(n)                 ' and OPEN DATA, STOP PLAYER, STOP HOST

IF done THEN END is legal, along with THEN DELAY, THEN PAUSE,
THEN YIELD and THEN DEBUGGER.

MISTAKES THAT USED TO PASS IN SILENCE

None of this changes what working code does. Every item below was already
wrong — a discarded argument, a value nobody computed, a save that failed
quietly — and the only difference now is that the language says so.

If an existing cart contains one of them it will stop rather than carry on
pretending: at load time for LOCATE and CREATE MAP, when the line runs for
the rest. That is where to look after updating.

  • VAL on a non-number is an error, not 0. A typo, an empty entry box and
    a real zero all used to look identical. Guard blank input:
    IF entry$ <> "" THEN n = VAL(entry$).
  • LOCATE takes two arguments. A third was accepted and discarded.
  • CREATE MAP FROM ... AS LAYER 1 TO 3 is refused. One array is one map
    layer, so a screen-layer range had nothing to spread. Use TILEMAP with a
    TILES clause per layer for a real multi-layer map.
  • KEYDOWN is unchanged. An unrecognised name still answers 0. Watch the
    spelling: the key is Escape, so KEYDOWN("ESC") is never true.
  • WAITVIDEO can be released by STOPVIDEO. The documented skippable
    cutscene hung the game with the video already gone from the screen.
  • FLOODFILL fills. It ran on a copy and threw it away; only the count
    survived. Filled cells are marked -1.
  • A SQL block with two statements says so rather than running the first
    and silently discarding the rest.
  • CLOSE reports a failed save. A file that could not be written printed a
    warning to stderr — which goes nowhere on web and mobile — and reported
    success.
  • # handles are checked in both directions, so one number can no longer be
    a file and a database at once.
  • A PATTERN's DISABLE step runs once, instead of re-disabling a sprite
    that was already gone on every tick afterwards.
  • SPRITEBBOX accounts for ORIGIN, so it returns the rectangle collisions
    actually test.
  • BORBIT ... DECAY without SPEED is an error instead of quietly becoming
    a speed.
  • CRASHNET TICK RATE outside 1-60 is an error instead of a divide-by-zero
    in the server.

DOCUMENTATION

The reference was audited entry by entry against the code. Every example in it
now parses, and the behavior and pattern commands are documented inside the
BEHAVIOR and PATTERN entries rather than as separate keywords — they only
work inside those blocks, and presenting them as standalone statements is why
so many of their examples were written as code that could not run.

Entries corrected: ANIMATE, ASARRAY, BASE64TOIMG, BASE64TOMP3,
BASE64TOWAV, BEHAVIOR, BIMPULSE, BORBIT, BROTATE, BSTOP, CALL,
CLOSE, COS, CREATE MAP, DECLARE, DESERIALIZE$, DURATION,
EMITPARTICLE, EXP, FLOODFILL, FOR MAPDATA, GAMECONTEXT$,
GETSPRITEMETA, ISMOBILE, JOYBUTTON, LOADFONT, LOADJSON, LOADMP3,
LOADMUSIC, LOADWAV, LOCATE, MAPMETA$, MAZE, NAMESPACE, OPEN,
OPEN DATABASE, PATTERN, PLAYVIDEO, POINT, POLYGON, RESTORE,
SET MUTATION BEHAVIOR, SET SPRITE, SPRITECOUNT, SQL, STOPVIDEO,
TAN, TILEMAP, TIMER, VAL, WAITFOR.

Three entries were removed because they document nothing that exists: LET,
a bare STOP, and UNPUT ALL. Six were added for things that existed with no
entry at all: PI, TRUE, FALSE, SPRITEDIST, SPRITECOLLISIONID and
TRANSITION$.