BLIT386 1.5.0 - the same world, every time
The random release - a seeded PRNG, stateless coordinate hashes, three kinds of noise - plus the full easing curve library and an exposure-curve palette fade.
Math.random() is perfectly fine right up until a player sends you a screenshot of something broken in a world you
cannot get back. Then you are staring at a cave that will never exist again, trying to reason about a bug from a JPEG. I
have done this more times than I would like to admit, and every time I told myself I would add a seed later.
1.5.0 is later. The engine has a real random system now: a PRNG you can hand a number to, coordinate hashes that remember nothing and still never change their mind, and three kinds of noise. A world either comes back exactly or does not come back at all, and you get to pick which.
One seed, one world
BT.random is a live shared generator, time-seeded when the engine starts, so out of the box it behaves like the
Math.random() you replaced. The difference is that you can stop it drifting:
import { } from 'blit386';
.(1234);
const = ..(1, 6);
const = ..(['common', 'rare', 'legendary'], [80, 18, 2]);
const = ..([1, 2, 3, 4, 5]);Beyond the obvious draws there are the ones you actually reach for at 1am - bool, sign, pick, angle, gaussian
- and spatial helpers like insideRect and direction8, each with an allocation-free *To(out) variant for the
render loop. getState / setState / clone / fork are there for when one generator is not enough and you want the
loot table to stop stealing numbers from the terrain.
The random-basics demo runs five scenes through it: shuffling, weighted drops, scatter, flips, and walkers. Seeded worlds is the one that makes the point better than any paragraph can - two worlds side by side, each labeled with its seed, and copying one seed over makes the halves identical.
Hashes, which store nothing
A seeded generator is a sequence, and a sequence has to be walked from the start. That is the wrong shape for a world
the player wanders through in no particular order. So there are coordinate hashes: hash1i, hash2i, hash3i for an
unsigned 32-bit value, and hash1, hash2, hash3 for the same thing scaled into [0, 1).
import { } from 'blit386';
const = (140, 32, 1234) % 100 < 12;Same coordinates, same seed, same answer, forever, with nothing stored anywhere. Walk east for ten minutes and walk back, and the tree is still the tree. Coordinate patterns is an endless world built entirely this way, and it keeps not a single tile in memory.
For the smooth version there is ValueNoise, PerlinNoise, and SimplexNoise, each seedable, each with the matching
fbm* for fractal Brownian motion:
import { } from 'blit386';
const = new (1234);
const = .(12.5, 4.25);fBm defaults to four octaves, persistence 0.5, lacunarity 2, with amplitudes normalized so the sum stays in range.
The noise demo puts all three side by side at matched settings with an octaves slider
and a terrain ramp, which is the fastest way to learn why anyone bothers having three.
Details in API: Random, and the reasoning about streams and procedural worlds in the Random Guide.
Curves, finally all of them
Easing had linear and quadratic, which covers roughly the first afternoon of caring about motion. EasingFunction now
carries the full set - sine, cubic, quartic, quintic, expo, circ, back, elastic, bounce, each in / out / in-out - and
interpolate() applies any of them across number, Vector2i, Color32, and Rect2i:
import { , } from 'blit386';
const = new (0, 0);
const = new (320, 240);
const = ('bounce-out', , , 0.5);One warning, because it caught me: interpolate(easing, start, end, t) and applyEasing(t, easing) take their
arguments in opposite orders. That is history, not design, and it is now written down in the kit skill so your assistant
stops guessing.
A fade that behaves like a camera
BT.paletteFade crossfades every palette entry on the same schedule, which is correct and also nothing like what
happens when a real iris closes. BT.paletteFadeExposure does the other thing: it interpolates each channel in linear
light, and offsets every entry's schedule by its luminance, so bright entries rise first and hold longest while dark
entries arrive late and crush early. Toe and shoulder out of one knob, highlightLead.
import { , type } from 'blit386';
declare const : ;
.(, 1500, { : 0.2 });Every entry still lands exactly on the target, so nothing drifts. The honest limitation is that a palette fade acts per color index and not per pixel, so a dark object in a bright scene fades on the dark schedule regardless of where it is standing. The exposure fade demo runs both fades on one shared palette so you can watch the difference rather than take my word for it.
Underneath it there are Color32#toLinear and #toSrgb, with in-place variants, using the real sRGB piecewise transfer
function instead of a bare 2.2 power. Channels stay 8-bit, so treat linear light as a working space and not somewhere to
keep things.
The splash, and knowing you are in dev
The engine has a splash now - the logo fading in on its own 16-step gray ramp, holding, fading out before your first
frame. It is not decoration: your init() runs concurrently and the hold extends until assets settle, so it is the
loading screen you were going to have to build anyway. It hands off into your palette with a single continuous
BT.paletteFadeExposure, which is why that fade exists at all. Any key, click, or tap skips it, and the press is
swallowed so it does not also fire in your game.
If you are upgrading, this is the one thing to know: the splash shows by default in release builds. Development is
unaffected. isSplashEnabled: false in configure(), or ?nosplash, turns it off.
That development-versus-release distinction needed a real answer rather than a guess, so BT.isDevMode is there now. It
resolves from the marker the blit386/vite plugin sets, falls back to a live Vite HMR context, and otherwise reports
release. Worth saying plainly: a build that skips the plugin silently gets release behavior. See
the splash guide and Core.
Other changes
Text stopped losing characters
BitmapFont learned fallback glyphs. A font can define a glyph keyed by U+FFFD, and any character without its own
glyph now draws that instead of vanishing. The vanishing was the actual bug - a skipped character did not advance the
pen either, so the next character drew on top of the one before it and a whole line turned to soup. The built-in system
font gained 79 glyphs while I was in there: dashes, arrows, uppercase Greek, media icons, a heart. hasGlyph() still
reports true glyph presence rather than fallback coverage, deliberately.
One repository, one version
blit386-demos, create-blit386, and the docs site merged into this repo, and the three published packages moved to a
single lockstep version. A release tag is now what deploys blit386.dev and demos.blit386.dev. The practical effect is
that a scaffolded game gets a kit matching the engine it pins, without anyone having to remember the pairing.
There is also a Fez-style hypercube rotating on a 256x256 canvas, which has nothing to do with any of the above. I just wanted to see a tesseract in 256 colors.
The full list, in the usual Keep a Changelog shape, is at the changelog.
npm create blit386@latest my-gamePick a seed you like. You can always get that world back.