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

gtlua

v0.2.10

Published

Make games for the [**GameTank**](https://gametank.zone/) - Clyde Shaffer's open 8-bit console (65C02 CPU, hardware blitter, a 128×128 screen) - by writing Lua instead of 6502 assembly or C.

Readme

GameTank Lua SDK

Make games for the GameTank - Clyde Shaffer's open 8-bit console (65C02 CPU, hardware blitter, a 128×128 screen) - by writing Lua instead of 6502 assembly or C.

Write games in a PICO-8-flavored Lua dialect. The SDK compiles it to C, builds it with cc65 against a bundled GameTank runtime, and produces a .gtr cartridge (a flat 32 KB EEPROM, or a 2 MB FLASH2M banked cart for bigger games - chosen automatically) that runs in the emulator, on gametank.zone, and on real hardware via GTFO.

No interpreter, no VM: your Lua becomes native 65C02 machine code. The screen is the same 128×128 as PICO-8's, and the API is PICO-8-shaped (spr/btn/_init/_update/_draw, Lua syntax), so if you know PICO-8 you'll feel at home - but gt-lua is its own thing targeting real GameTank hardware, not a PICO-8 clone. If you don't know PICO-8, you don't need to.

Your first game

This is a complete GameTank game. No assets, no boilerplate - one main.lua:

function _draw()
  cls(1)                          -- dark blue background

  print("hello gametank", 38, 14, 14)   -- title text, pink, near the top

  -- a smiley face, drawn entirely with shapes (no sprite sheet needed)
  circfill(64, 72, 26, 10)        -- head: a big yellow circle
  rectfill(53, 62, 58, 68, 0)     -- left eye: a black square
  rectfill(70, 62, 75, 68, 0)     -- right eye
  circfill(64, 82, 9, 0)          -- mouth: a black circle
end

Build it and play it in a window:

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

Or compile it to a .gtr cartridge (to run in an emulator or flash to hardware):

node bin/gtlua.js build examples/hello/main.lua -o hello.gtr

That's the whole loop: write main.lua, run it, ship the .gtr. Colors are PICO-8-style indices 0-15 (0 black, 1 dark-blue, 10 yellow, 14 pink); gt.rgb() reaches the full 256-color GameTank palette when you want more.

Requirements

  • Node.js 24+ (the bundled cc65 + emulator WASM need it for WASM threading / SIMD)

  • nothing else - the C toolchain (cc65) and the emulator both come bundled as WebAssembly via npm install. No native tools to build or install.

    (Optional: if you'd rather use a native cc65 - because you already have one, or want the source-clone path - the SDK uses it automatically when present. See "Build backends" below.)

gtlua run opens a window via the optional @kmamal/sdl dependency (pulled by npm install, prebuilt for the common platforms). On a headless box or an unsupported platform it falls back to an external GameTank emulator; gtlua build never needs it.

Quickstart (zero install)

Clone it, install once, and you can build and run GameTank games with no native toolchain and no separate emulator:

git clone https://github.com/monteslu/gametank_lua_sdk && cd gametank_lua_sdk
npm install                              # pulls the bundled cc65 WASM + emulator

# build one of the examples to a .gtr cartridge:
node bin/gtlua.js build examples/orbit/main.lua
# -> examples/orbit/main.gtr, in ~2 s (first) / instant after

# ...or build AND play it in a window, no emulator install needed:
node bin/gtlua.js run examples/orbit/main.lua

gtlua run opens a window (arrows move, Z/X/C are the buttons, Enter is start). The .gtr it builds also runs in the GameTankEmulator, on gametank.zone, or flashed to a real cartridge via GTFO.

Build backends

By default the build uses native cc65 if it's on your PATH or built into tools/ (via scripts/install_tools.sh), otherwise the bundled WASM cc65 from npm install. Both produce byte-for-byte identical carts. Force one with GTLUA_TOOLCHAIN=wasm or GTLUA_TOOLCHAIN=native. So this works as a pure clone-and-build SDK (like the official GameTank C SDK) too - install a native cc65 and skip npm install if you prefer.

Start your own game

Copy an example as a starting point and build your main.lua:

cp -r examples/orbit mygame
node bin/gtlua.js build mygame/main.lua -o mygame/game.gtr
# --sheet mygame/gfx.gtg     add a sprite sheet (see docs/GRAPHICS.md)
# --frames mygame/gfx.gsi    add a frame table for sprf (docs/SPRITES.md)
# --num8                     8.8 number mode, faster math (docs/performance.md)

Test it as you go with run (it builds, then opens the game in a window so you can play it immediately - the tight edit/build/play loop):

node bin/gtlua.js run mygame/main.lua           # same flags as build

node bin/gtlua.js c <file.lua> prints the generated C (for debugging).

Visual Studio Code

The repo ships VS Code tasks for one-key build & run. Open the folder in VS Code, open any .lua, and:

  • Ctrl+Shift+B (Cmd+Shift+B on macOS) runs gtlua: Build on the open file, producing <name>.gtr next to it.
  • gtlua: Build & Run (from Terminal → Run Task…) builds, then launches the ROM in a detected emulator.

The Run task finds an emulator the same way the run_emulator scripts do: the GAMETANK_EMULATOR env var first, then gte or GameTankEmulator on your PATH. Point GAMETANK_EMULATOR at your GameTankEmulator build if it isn't on PATH.

The PICO-8 contract

Define _update60() (60 fps) or _update() (30 fps), plus _draw(), and optionally _init(). The runtime latches inputs before each update and ends the frame after _draw() (blitter drain, vblank, page flip).

Numbers are PICO-8 numbers: 16.16 fixed point, −32768 to 32767.99998, wrap on overflow, division by zero saturates. sin/cos/atan2 use turns (0..1) with PICO-8's screen-space-inverted sin. Under the hood the compiler infers which values stay integral and keeps them in fast 16-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 (0x11.4, 0b101.1), button glyphs (⬅️➡️⬆️⬇️🅾️❎), and multiple assignment (x, y = 64, 32).

Have a PICO-8 cart to bring over? docs/PORTING.md is the step-by-step walkthrough (import the art with gtlua gfx import, the sound with p8sfx, and what changes because the GameTank is different hardware); the per-function compatibility map is docs/CHEATSHEET_FOR_PICO8_USERS.md.

API

| | | |---|---| | lifecycle | _init _update _update60 _draw | | graphics | cls camera color pset rect rectfill circ circfill line sset spr(n,x,y,[w,h],[flip_x,flip_y]) - flips are free (hardware blitter mirror) | | sprites | 8×8-grid spr(n) off a .gtg sheet (--sheet, docs/GRAPHICS.md); sprf(frame,x,y,[fx],[fy]) for arbitrary-size / animated frames off a .gsi table (docs/SPRITES.md) | | input | btn(i,[pl]) btnp(i,[pl]) - indices 0-3 d-pad, 4=🅾️(GT A), 5=❎(GT B), 6=GT C, 7=START; btnp has PICO-8 auto-repeat | | math | flr ceil abs sgn sqrt min max mid sin cos atan2 rnd srand t/time | | data | array(n,[v]) - 16-bit elements · array8(n,[v]) - byte elements 0-255, half the RAM and ~2× faster in counting loops | | sound | sfx(n,[ch]) music(n,[loop]) (built-in FM effects/tunes - see below); song(data,[loop]) plays a native .gtm2 FM song (docs/MUSIC.md); low-level gt.note/gt.noteoff | | gametank extras | gt.rgb(b) - raw palette byte (the GameTank has 256 colors), gt.border(c), gt.ticks(), gt.starfield_*, gt.bg_compose/gt.bg_draw (see below) |

Colors are raw GameTank bytes 0255. For PICO-8 familiarity, a static 0–15 color literal in a draw call (cls(1), rectfill(...,8)) is baked to its GameTank byte at compile time (zero runtime cost). gt.rgb() reaches the full 256-color palette: gt.rgb(255,128,0) picks the nearest hardware color to that RGB, or gt.rgb(byte) takes a raw byte. Use it anywhere a color is expected: rectfill(x,y,w,h, gt.rgb(255,128,0)). A color computed at runtime is used as a raw byte, not re-mapped from 0–15 (see docs/PALETTE.md); there is no runtime pal().

Fast backgrounds: gt.bg_compose / gt.bg_draw

Drawing a tilemap with a per-tile spr() loop costs one blit per visible tile, and on the GameTank a blit is ~1200 cycles of setup regardless of size - a screenful of tiles blows the ~50-blit/frame budget for 30fps. The GameTank has 512 KB of sprite RAM (32 pages of 128×128) and the SDK normally uses only one (the sheet), so you can pre-render a static background into a spare page once and blit the whole thing as a single cheap blit each frame:

local map = array(16*16)          -- your tile indices (0 = empty)
function _init()
  -- ... fill map from your level data ...
  gt.bg_compose(map, 16, 0, 0, 16, 16)  -- (map, cols, cx, cy, cw, ch) -> bg page
end
function _draw()
  gt.bg_draw()                    -- one big blit of the composed page
  -- then draw your moving sprites on top with spr()
end

gt.bg_compose reads tiles from the loaded --sheet (cell N is at sheet cell (N%16, N//16)), clears the page to color 0, and paints the cw×ch window starting at map cell (cx,cy); tile 0 is left empty. It's a one-time cost of up to a second or so of CPU time (the canvas clear alone is 64 KB of writes) - call it at level load, not every frame, and expect the screen to sit black until _init returns.

The bg page is a 256×256 canvas (cw/ch up to 32 cells), so a level bigger than one screen composes once and scrolls for free: gt.bg_draw(sx, sy) blits a 128×128 window at source offset (sx,sy) (0–128 in each axis), seamlessly across the internal page boundaries - pass your camera position to scroll. Moving/animated tiles still want spr() on top.

Sound: sfx / music

The GameTank has a second 65C02 audio coprocessor (a 4-channel, 4-operator FM synth). PICO-8 style, you trigger sound by index - no tracker files to author:

function _init()
  music(0)              -- start built-in tune 0, looping
end
function _update60()
  if btnp(4) then sfx(0) end     -- 🅾️ -> jump sound
  if btnp(5) then sfx(3) end     -- ❎ -> explosion
end

Built-in effects (sfx(n,[ch]), n = 0–7): 0 jump · 1 pickup · 2 shoot · 3 explode · 4 blip · 5 powerup · 6 hurt · 7 select. Omit ch to auto-assign one of the 4 channels, or pass 0–3 to pin it. Built-in tunes (music(n,[loop]), n = 0–1): loops by default; music(-1) stops, music(n,false) plays once. A per-frame sequencer (ported from the upstream GameTank tracker) advances envelopes + steps the song automatically - it costs almost nothing when nothing is playing. For a single raw tone, the low-level gt.note(ch,note,vol) / gt.noteoff(ch) primitives are still there.

Shipped since: native .gtg sprite sheets + .gsi/sprf frame tables, native .gtm2 FM songs, a gtlua gfx converter, and a PICO-8 art/sound importer. Coming next (see docs/PICO8.md for the full roadmap): map/ mget/fget tile APIs, print-time string building, and cartdata saves.

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, gtlua refuses to guess). No nil, closures, metatables, coroutines, or goto. Every unsupported feature is a compile-time error that says what to write instead.

Repo layout

compiler/ Lua→C compiler (plain JS ESM) · sdk/ the C/asm runtime (register protocols, fixed-point core, math tables, interrupt handlers, cc65 startup/linker files) · bin/gtlua.js CLI · tools/ cc65 (built by scripts/install_tools.sh) · examples/ · test/ (node --test).

Docs

| doc | what | |---|---| | docs/CHEATSHEET.md | the full gt-lua API reference | | docs/CHEATSHEET_FOR_PICO8_USERS.md | per-function PICO-8 compatibility map | | docs/PORTING.md | bringing a PICO-8 cart over, step by step | | docs/GRAPHICS.md | the .gtg sprite-sheet format + gtlua gfx converter | | docs/SPRITES.md | frame tables (.gsi) + sprf | | docs/MUSIC.md · docs/sfx.md | native .gtm2 FM songs · PICO-8 sfx import | | docs/PALETTE.md | the GameTank color palette | | docs/performance.md | making it fast (--num8, blit budget, the gt.* engines) | | docs/SPEC.md · docs/PICO8.md | language spec · design doc + roadmap | | PROVENANCE.md | attribution (Clyde Shaffer's GameTank SDK) |

The examples/ in this repo are small, self-contained gt-lua programs (orbit, mathcheck, pad-square, audio) - the quickest way to see the API in use. Full games built with gt-lua live in their own repos (each a derivative work under its own license): Combo Pool, newleste, Cherry Bomb, and more.

Making it fast

The GameTank is ~1000× slower than the machine PICO-8 runs on, so a naive port can drop to single-digit fps. docs/performance.md is the field guide: the two things that dominate a frame (blit count and fixed-point math), the ~19,000-cycle fixed-point % / / footgun and how to dodge it, per-primitive blit budgets, and how to profile a slow cart by bisection. Read it before optimizing - measure, don't guess.

License

MIT. sdk/ hardware files adapted from clydeshaffer/gametank_sdk (MIT).