npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gbalua

v0.4.1

Published

Write Game Boy Advance games in a PICO-8-flavored Lua that compiles to native ARM.

Downloads

722

Readme

GBA Lua SDK

npm version

Make games for the Game Boy Advance by writing a PICO-8-flavored Lua instead of C or ARM assembly.

The SDK ahead-of-time compiles your Lua to C, builds it with a bundled ARM toolchain against a libtonc + maxmod runtime, and produces a .gba ROM that runs in mGBA and on real hardware. No interpreter, no VM: your Lua becomes native ARM machine code.

If you know PICO-8 you'll feel at home (spr/btn/_init/_update/_draw, Lua syntax) - but this SDK leans into what the GBA can do that a fantasy console can't: 128 hardware sprites, rotate/scale (affine) sprites, four scrolling tile layers, Mode 7, hardware windows, alpha blending and fades, mosaic, per-scanline raster effects, battery saves, and streamed module music. Familiarity, not compatibility.

This is a fork of the GameTank Lua SDK; it reuses that compiler front-end and PICO-8 number model, and retargets the back end to native ARM.

Your first game

The whole hello, one main.lua with no asset files: a greeting plus a hardware sprite you move with the d-pad. _update60 runs the movement 60 times a second; _draw redraws the sprite every frame (examples/hello/main.lua):

-- The screen is 240x160. spr uses the built-in default sheet, so no art file
-- is needed. Colors are PICO-8-style indices 0-15 (1 dark-blue, 14 pink).
local x, y = 112, 72

function _update60()               -- 60fps input + movement
  if (btn(1)) then x += 2 end      -- right
  if (btn(0)) then x -= 2 end      -- left
  if (btn(3)) then y += 2 end      -- down
  if (btn(2)) then y -= 2 end      -- up
end

function _draw()
  cls(1)                           -- dark-blue background layer
  print("hello gba", 88, 24, 14)   -- greeting near the top, pink
  spr(0, x, y, 2, 2)               -- the hardware sprite, redrawn every frame
end

Build it and play it in a window - no external emulator needed:

node bin/gbalua.js run examples/hello/main.lua

gbalua run opens a window over the bundled mGBA core (arrows = d-pad, Z/X = B/A, Enter = START), via the shared romdev-core-runner SDL host. Or build a .gba ROM to ship:

node bin/gbalua.js build examples/hello/main.lua -o hello.gba

Run hello.gba in mGBA (or any GBA emulator) or flash it to a cartridge. That's the whole loop: write main.lua, run it, ship the .gba.

Colors are PICO-8-style indices 0-15 (0 black, 1 dark-blue, 10 yellow, 14 pink); pal() / spr_col() reach the full 15-bit BGR555 palette (32768 colors) at runtime when you want more.

Featured example: a full shmup

examples/starfall is a complete little shmup - hardware sprites, a scrolling starfield, music + sfx, a HUD, win/lose states - in one main.lua. That's what this SDK is for:

node bin/gbalua.js build examples/starfall/main.lua \
  --sheet examples/starfall/shmup_sheet.png \
  --map examples/starfall/space_bg.png -o starfall.gba

Requirements

  • Node.js 24+
  • nothing elsenpm install brings the whole toolchain as dependencies: arm-gcc + libtonc + maxmod, all as WebAssembly (via romdev-platform-gba + romdev-maxmod). No devkitPro, no native tools to build or install.

The build runs the WASM toolchain in-process (cc1-armasldobjcopy) — no server, no daemons. It can also talk to a running romdev server instead (GBALUA_BACKEND=mcp, faster for rapid rebuilds since the server keeps warm toolchain workers). See compiler/build-gba.mjs.

gbalua run opens the window through romdev-core-runner (the one SDL host shared across the whole SDK family and the romdev playtest tool). The window needs @kmamal/sdl, an optional dependency of the runner (pulled by npm install, prebuilt for the common platforms); on a headless box or unsupported platform, run prints a clear message and you build a .gba for an external emulator instead. build never needs it.

The screen and the two rendering modes

The GBA screen is 240×160. Two ways to draw, and they are one either-or (a hardware mode bit); sprites, sound, and all the color effects compose over both.

  • Tile mode (Mode 0) - the real game path. Four hardware tile layers that scroll for free, plus 128 hardware sprites on top. This is how scrolling GBA games work; it's what the flagship example (starfall) uses.
  • Bitmap mode (Mode 4) - an immediate-mode 8bpp framebuffer for pset/rect/circ/line + text. Simplest to start with (the hello above), single-buffered, slower per-pixel. Sprites still compose on top.

You don't pick a mode explicitly - using tile-layer verbs (map_show, tileset, layer_*, mode7) puts you in tile mode; the immediate draw verbs use bitmap mode.

The PICO-8 contract

Define _update60() (60 fps) or _update() (30 fps), plus _draw(), and optionally _init(). The runtime latches input before each update and ends the frame after _draw() (OAM flush, vblank).

Numbers are PICO-8 numbers: 16.16 fixed point. sin/cos/atan2 use turns (0..1) with PICO-8's screen-space-inverted sin. The compiler infers which values stay integral and keeps them in fast 32-bit ints - an optimization, never a semantic change.

The dialect keeps PICO-8's syntax: +=-style compound assignment, one-line if (cond) stmt / while (cond) stmt, !=, \ floor division, // comments, hex/binary literals with fractions, and multiple assignment (x, y = 64, 32).

Arrays are 1-indexed (Lua/PICO-8 style): a = array(8), then a[1] is the first element (a[0] is out of bounds).

API at a glance

| | | |---|---| | lifecycle | _init _update _update60 _draw | | bitmap draw | cls camera color pset pget sset clip rect rectfill circ circfill line | | 16-bit bitmap | mode15() true color · rgb15(r,g,b) · cls15 pset15 flip15 - a 160×128 BGR555 framebuffer | | sprites | spr(n,x,y,[w,h],[fx,fy]) hardware OBJ · spr8(t,x,y,[flip]) · spr_pal spr_prio | | affine sprites | sprr(n,x,y,angle,scale) rotate+scale · sprr2(n,x,y,angle,sx,sy) non-uniform | | tile layers | map_show tileset tilemap layer_show layer_pri layer_scroll parallax camera mget/tget/tset | | mode 7 | mode7() · mode7_cam(x,y,angle,[zoom]) · mode7_off() - an affine plane on BG2 | | affine BG | abg_setup(tiles,ntiles,map,msize,[pal]) · abg_cam(x,y,angle,[zoom]) · abg_off() - your own rotate/scale layer | | windows | window(x0,y0,x1,y1) spotlight · window_inside/window_outside/window_obj · window_off | | color effects | fade(amount,[white]) · blend(layer,alpha) · blend_off · mosaic/mosaic2 · backdrop · screen_off/screen_on | | palette | pal(i,r,g,b) BG · spr_col(i,r,g,b) OBJ · hgradient(table) per-scanline backdrop | | animation | anim(slot,first,last,fps) loop · anim_once · anim_pingpong · anim_reset · anim_done | | input | btn(i,[pl]) btnp(i,[pl]) - 0-3 d-pad, 4=A, 5=B, 6=L, 7=R, 8=START, 9=SELECT | | math | flr ceil abs sgn sqrt min max mid sin cos atan2 rnd srand t/time + bit ops | | data | array(n,[v]) 16.16 · array8(n,[v]) bytes 0-255 · pool(n) · dma(dst,src,n)/dma_fill fast moves | | save | save(slot,array8,n) · load(slot,array8,n) - battery SRAM, 16 slots × 1 KB | | timer | timer_start() · timer_read() - free-running Timer 3, sub-frame profiling | | sound | music(n,[loop]) module music · sfx(n,[ch]) · sfx_ex(n,vol,pan,pitch) · sfx_volume |

See docs/CHEATSHEET.md for every verb with signatures.

Assets

--sheet sprites.png imports a sprite sheet, --map level.png a tilemap, and --mode7 plane.png an affine plane, via a self-contained PNG → tile converter (compiler/png-tiles.mjs). All three also accept the formats artists actually work in — Aseprite (.ase/.aseprite, frame 0 flattened) and Tiled (.tmx, visible layers composited; embed the tileset in the map) — imported, never re-invented. They're CLI flags on build:

node bin/gbalua.js build --target gba mygame/main.lua \
  --sheet mygame/sprites.ase --map mygame/level.tmx -o mygame/game.gba

The examples/ directory shows each subsystem in use:

| example | shows | |---|---| | showcase | a scene-cycling tour of the whole feature set (L/R to switch scenes) | | starfall | a complete shmup - tile mode, sprite HUD, module music + SFX | | effects | blend / fade in bitmap mode | | mode7 | an affine plane you rotate, zoom, and drive over | | windows | a hardware spotlight over the Mode 7 plane | | anim | frame-range animation helpers | | hwtest | hgradient raster gradient, SRAM save/load, the timer |

Sound

Music is a streamed module (maxmod), and SFX are sampled one-shots - trigger both by index, PICO-8 style:

function _init()
  music(0)                       -- start module 0, looping
end
function _update60()
  if btnp(4) then sfx(0) end      -- A -> a sound effect
  if btnp(5) then sfx_ex(1, 512, 200, 1.5) end  -- B -> louder, panned, +pitch
end

music(-1) stops; music(n, false) plays once. sfx_ex(n, vol, pan, pitch) gives per-shot volume (0-1024), pan (0-255, 128 center), and pitch (a 16.16 multiplier); sfx_volume(v) sets the master SFX level.

Bring your own music with --music song.xm on build (repeatable — music(0) plays the first module, music(1) the second, …; .xm/.mod/ .it/.s3m all work). The soundbank is compiled at build time by romdev-maxmod, a faithful pure-JS port of devkitPro's mmutil. Or link a prebuilt bank with --soundbank bank.bin. With neither flag, the default soundbank (assets/soundbank.bin) ships a chiptune as module 0.

Not-Lua walls (loud, never silent)

Conditions must be boolean (if x ~= 0 then, not if x then - Lua calls 0 truthy, C doesn't, and the compiler refuses to guess). No nil, closures, metatables, coroutines, string concatenation, or goto. Every unsupported feature is a compile-time error that says what to write instead.

Repo layout

compiler/ Lua→C compiler + the GBA build driver (build-gba.mjs) and PNG importer (png-tiles.mjs) · gba-sdk/ the C runtime (thin libtonc/maxmod wrappers: gba_api.c frame+sprites+input, gba_bg.c tile layers, gba_mode7.c, gba_win.c, gba_fx.c effects, gba_sound.c, gba_hw.c save+timer, gba_text.c, gba_anim.c, gba_math.c) · assets/ the default soundbank + its generators · bin/gbalua.js CLI · examples/.

Docs

| doc | what | |---|---| | docs/CHEATSHEET.md | the full GBA Lua API reference | | docs/CHEATSHEET_FOR_PICO8_USERS.md | per-function PICO-8 → GBA map |

License

MIT. gba-sdk/ wraps libtonc and maxmod (both zlib/BSD-style); the compiler front-end derives from the GameTank Lua SDK (MIT).