luanette 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/api.json CHANGED
@@ -1 +1 @@
1
- [{"name":"abs","section":"Time, numbers, saving","help":[{"sig":"abs(x)","does":"the usual"}]},{"name":"act","section":"The host","help":[{"sig":"act(name, [arg])","does":"a command to the console: \"open\" opens the cart named arg; \"volume\" and \"brightness\" set that setting to the step arg (\"0\" to \"3\"). The launcher uses all three"}]},{"name":"btn","section":"Input","help":[{"sig":"btn(b)","does":"true while button b is held"}]},{"name":"btnp","section":"Input","help":[{"sig":"btnp(b)","does":"true on the frame b went down"}]},{"name":"buzz","section":"Feedback","help":[{"sig":"buzz(ms)","does":"the vibration motor, 10-500 ms (this board has none: caps().buzz is false)"}]},{"name":"camera","section":"Drawing","help":[{"sig":"camera(x, y)","does":"offset everything drawn by (-x, -y) / reset"},{"sig":"camera()","does":"offset everything drawn by (-x, -y) / reset"}]},{"name":"caps","section":"The host","help":[{"sig":"caps()","does":"what the cart runs on, as a table: the canvas size, where the picture sits, the board's name, what it has (touch, tilt, sound...). The fields are under \"Versions and what a cart runs on\""}]},{"name":"ceil","section":"Time, numbers, saving","help":[{"sig":"ceil(x)","does":"the usual"}]},{"name":"circ","section":"Drawing","help":[{"sig":"circ(x, y, r, [c])","does":"a circle outline / filled circle"}]},{"name":"circfill","section":"Drawing","help":[{"sig":"circfill(x, y, r, [c])","does":"a circle outline / filled circle"}]},{"name":"clip","section":"Drawing","help":[{"sig":"clip(x, y, w, h)","does":"draw only inside a rectangle / reset"},{"sig":"clip()","does":"draw only inside a rectangle / reset"}]},{"name":"cls","section":"Drawing","help":[{"sig":"cls([c])","does":"clear the screen (default 0)"}]},{"name":"data","section":"The host","help":[{"sig":"data(name)","does":"the latest value of a channel as a table, or nil: \"battery\" (mv, percent, charging, usb), \"carts\" (the carts on the board: a list of name, title, about), \"settings\" (volume and brightness, each a step from 0 to 3)"}]},{"name":"date","section":"Time, numbers, saving","help":[{"sig":"date([t])","does":"local time (the host's timezone, kept for offline use) as a table: year, month, day, hour, min, sec, wday (1 = Sunday), yday; for now() when t is left out. nil while the date is unknown"}]},{"name":"flr","section":"Time, numbers, saving","help":[{"sig":"flr(x)","does":"the usual"}]},{"name":"fps","section":"Time, numbers, saving","help":[{"sig":"fps(rate)","does":"the frame rate from the next frame (any rate that divides 60: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) / lazy: active while there's input (a touch on the cart, a button) and half a second after, idle otherwise / the rate now. An idle cart wakes at once when a touch comes down or BOOT is pressed, so a slow idle rate costs no response time: give idle the slowest that still shows what moves on its own (a blink twice a second needs 4; a seconds counter 1 or 2). A cart doing a few frames a second instead of 30 or 60 spends that much less of the board's battery on its frames. Move things by time(), not by frames, when the rate changes"},{"sig":"fps(active, idle)","does":"the frame rate from the next frame (any rate that divides 60: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) / lazy: active while there's input (a touch on the cart, a button) and half a second after, idle otherwise / the rate now. An idle cart wakes at once when a touch comes down or BOOT is pressed, so a slow idle rate costs no response time: give idle the slowest that still shows what moves on its own (a blink twice a second needs 4; a seconds counter 1 or 2). A cart doing a few frames a second instead of 30 or 60 spends that much less of the board's battery on its frames. Move things by time(), not by frames, when the rate changes"},{"sig":"fps()","does":"the frame rate from the next frame (any rate that divides 60: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) / lazy: active while there's input (a touch on the cart, a button) and half a second after, idle otherwise / the rate now. An idle cart wakes at once when a touch comes down or BOOT is pressed, so a slow idle rate costs no response time: give idle the slowest that still shows what moves on its own (a blink twice a second needs 4; a seconds counter 1 or 2). A cart doing a few frames a second instead of 30 or 60 spends that much less of the board's battery on its frames. Move things by time(), not by frames, when the rate changes"}]},{"name":"ground","section":"3D","help":[{"sig":"ground(y, [color])","does":"shadows fall on the plane at height y, in color (the ground's color in shadow, 0xRRGGBB; default black)"}]},{"name":"hd.circle","section":"The full-color layer (hd)","help":[{"sig":"hd.circle(x, y, r, rgb, [a])","does":"a filled circle / a ring width px wide inside radius r"}]},{"name":"hd.clear","section":"The full-color layer (hd)","help":[{"sig":"hd.clear([rgb, a])","does":"wipe the canvas (inside the clip) to clear, or to a color at alpha a (255)"}]},{"name":"hd.clip","section":"The full-color layer (hd)","help":[{"sig":"hd.clip(x, y, w, h)","does":"draw only inside a box / anywhere (inside an hd.repaint: inside the box cut to the repaint's / the repaint's box)"},{"sig":"hd.clip()","does":"draw only inside a box / anywhere (inside an hd.repaint: inside the box cut to the repaint's / the repaint's box)"}]},{"name":"hd.frame","section":"The full-color layer (hd)","help":[{"sig":"hd.frame(x, y, w, h, rgb, [width, radius, a])","does":"a box's outline, width px (1)"}]},{"name":"hd.gradient","section":"The full-color layer (hd)","help":[{"sig":"hd.gradient(x, y, w, h, top, bottom, [a])","does":"a box shading from top to bottom, ordered-dithered (RGB565 has 32 steps of red and blue: a slow dark gradient would band)"}]},{"name":"hd.image","section":"The full-color layer (hd)","help":[{"sig":"hd.image(name, x, y, [w, h, a])","does":"draws an image (below) with its top left at x, y (to the pixel), at its own size or scaled to w x h (nearest pixel), at alpha a over its own; returns true, or false when there's no such image (yet)"}]},{"name":"hd.images","section":"The full-color layer (hd)","help":[{"sig":"hd.images()","does":"the names of the cart's own images"}]},{"name":"hd.inset","section":"The full-color layer (hd)","help":[{"sig":"hd.inset(y, [h])","does":"how far in from each side rows y to y + h show on this glass (its corners are rounded): keep a bar or a line of text that far in. 0 for the window, and for rows clear of the corners"}]},{"name":"hd.line","section":"The full-color layer (hd)","help":[{"sig":"hd.line(x0, y0, x1, y1, rgb, [width, a])","does":"a line width px wide (1), round ends"}]},{"name":"hd.measure","section":"The full-color layer (hd)","help":[{"sig":"hd.measure(text, [size, font])","does":"its width and line height"}]},{"name":"hd.order","section":"The full-color layer (hd)","help":[{"sig":"hd.order(\"over\" | \"under\")","does":"the layer over the picture (default), or under it: then the picture's color 0 shows the layer through, as over a 3D scene. Under a 3D scene's picture too: scene, layer, picture, in that order when \"under\""}]},{"name":"hd.rect","section":"The full-color layer (hd)","help":[{"sig":"hd.rect(x, y, w, h, rgb, [a, radius])","does":"a filled box, its corners rounded by radius"}]},{"name":"hd.repaint","section":"The full-color layer (hd)","help":[{"sig":"hd.repaint(x, y, w, h, fn, ...)","does":"calls fn(...) with all drawing kept inside the box, then puts the clip back: part of the screen drawn again by the code that draws it. fn paints the box's background too (the old pixels are there). Repaints nest (the inner box is cut to the outer); fn isn't called for an empty box"}]},{"name":"hd.ring","section":"The full-color layer (hd)","help":[{"sig":"hd.ring(x, y, r, rgb, [width, a])","does":"a filled circle / a ring width px wide inside radius r"}]},{"name":"hd.scanlines","section":"The full-color layer (hd)","help":[{"sig":"hd.scanlines(on)","does":"the scanlines on the layer (default on) / whether they're on"},{"sig":"hd.scanlines()","does":"the scanlines on the layer (default on) / whether they're on"}]},{"name":"hd.scroll","section":"The full-color layer (hd)","help":[{"sig":"hd.scroll(x, y, w, h, dy)","does":"moves what's in the box by dy rows (down when positive); the strip it uncovers keeps stale pixels, for the cart to draw again. The whole width (x 0, w the canvas's) moves nothing (the rows are re-mapped): scrolling a list costs only its new strip. A narrower box copies its pixels. ui.scroller does all this"}]},{"name":"hd.text","section":"The full-color layer (hd)","help":[{"sig":"hd.text(text, x, y, rgb, [size, font, align, a])","does":"text from (x, y) = the top of its line; returns its width. font: \"mono\" (JetBrains Mono, 12/14/16/20/24 px), \"bold\" (16/18/24/32) or \"vt\" (VT323, big terminal digits: 30/46/72, ASCII only); size snaps to the nearest the font has (16). align: \"left\", \"center\" or \"right\" of x. Latin-1, punctuation, arrows, box drawing, ★ ♥ ✓ and friends; anything else shows as a small box"}]},{"name":"hd.wrap","section":"The full-color layer (hd)","help":[{"sig":"hd.wrap(text, width, [size, font])","does":"a table of lines that fit width (at spaces; \\n breaks; a word too long is broken)"}]},{"name":"hide","section":"3D","help":[{"sig":"hide(o, [hidden])","does":"hide it (hide(o, false) shows it again)"}]},{"name":"light","section":"3D","help":[{"sig":"light(azimuth, elevation, [intensity, ambient])","does":"the sun's direction in degrees, its strength 0-255 (255), and the shadows' color (0x22283E)"}]},{"name":"line","section":"Drawing","help":[{"sig":"line(x0, y0, x1, y1, [c])","does":"a line"}]},{"name":"load","section":"Time, numbers, saving","help":[{"sig":"load()","does":"keep a small table (numbers, strings, booleans, nested tables; at most 2 KB) across runs / get it back (nil the first time). Whole numbers come back exactly (timestamps included)"}]},{"name":"log","section":"Feedback","help":[{"sig":"log(...)","does":"a line in the log: each value as tostring gives it, spaces between. The playground shows it in its Console, the board in its serial log, check.py in its result's log. At most 20 lines a frame and 200 characters a line"}]},{"name":"map","section":"Drawing","help":[{"sig":"map([cx, cy, sx, sy, cw, ch])","does":"the map (MAP, below): cw x ch cells from cell (cx, cy), drawn as sprites with that cell's top left at (sx, sy). With nothing, the whole map at (0, 0). Color 0 is transparent; the camera moves it, and only the cells on screen cost anything"}]},{"name":"max","section":"Time, numbers, saving","help":[{"sig":"max(a, b)","does":"the usual"}]},{"name":"mget","section":"Drawing","help":[{"sig":"mget(cx, cy)","does":"the sprite in a map cell, or nil for an empty cell or one off the map"}]},{"name":"mid","section":"Time, numbers, saving","help":[{"sig":"mid(a, b, c)","does":"the usual"}]},{"name":"min","section":"Time, numbers, saving","help":[{"sig":"min(a, b)","does":"the usual"}]},{"name":"mset","section":"Drawing","help":[{"sig":"mset(cx, cy, n)","does":"put sprite n (0-63) in a cell / empty it. It changes the map for this run, not MAP's text"},{"sig":"mset(cx, cy)","does":"put sprite n (0-63) in a cell / empty it. It changes the map for this run, not MAP's text"}]},{"name":"msize","section":"Drawing","help":[{"sig":"msize()","does":"the map's width and height in cells"}]},{"name":"music","section":"Feedback","help":[{"sig":"music(n)","does":"your song from pattern n on / stop it (below)"},{"sig":"music(-1)","does":"your song from pattern n on / stop it (below)"}]},{"name":"now","section":"Time, numbers, saving","help":[{"sig":"now()","does":"the wall clock: seconds since 1970 (UTC), a whole number, or nil while the board doesn't know the date. It knows it from its RTC (kept through reboots and power-off while the battery lasts), which push.py sets; use it for anything that lives while the cart is closed (save(now()), then now() - saved on the next run)"}]},{"name":"obj","section":"3D","help":[{"sig":"obj(shape, color, [shine, unlit])","does":"an object: the shape placed in the scene, in color (0xRRGGBB); shine 0-255 adds a highlight; unlit shows the color as it is. Returns its number"}]},{"name":"orient","section":"3D","help":[{"sig":"orient(o, m1, ..., m9)","does":"turn it by a rotation matrix (rows), for carts that keep their own (physics)"},{"sig":"orient(o, {m1, ..., m9})","does":"turn it by a rotation matrix (rows), for carts that keep their own (physics)"}]},{"name":"paint","section":"3D","help":[{"sig":"paint(o, color, [shine, unlit])","does":"recolor it"}]},{"name":"pal","section":"Drawing","help":[{"sig":"pal(c0, c1)","does":"draw color c0 as c1 from now on / reset"},{"sig":"pal()","does":"draw color c0 as c1 from now on / reset"}]},{"name":"palette","section":"Drawing","help":[{"sig":"palette(i, 0xRRGGBB)","does":"change what color index i (0-15) looks like, for the whole picture (this frame included) / its color now / the starting 16 again. A cart still has 16 colors at a time; this picks which 16 (an exact red, softer pastels). pal remaps indices, palette the colors behind them"},{"sig":"palette(i)","does":"change what color index i (0-15) looks like, for the whole picture (this frame included) / its color now / the starting 16 again. A cart still has 16 colors at a time; this picks which 16 (an exact red, softer pastels). pal remaps indices, palette the colors behind them"},{"sig":"palette()","does":"change what color index i (0-15) looks like, for the whole picture (this frame included) / its color now / the starting 16 again. A cart still has 16 colors at a time; this picks which 16 (an exact red, softer pastels). pal remaps indices, palette the colors behind them"}]},{"name":"pget","section":"Drawing","help":[{"sig":"pget(x, y)","does":"set / read one pixel (pget returns the color index)"}]},{"name":"place","section":"3D","help":[{"sig":"place(o, x, y, z, [rx, ry, rz])","does":"move object o there, turned by rx, ry, rz degrees (about x, then y, then z)"}]},{"name":"print","section":"Drawing","help":[{"sig":"print(text, x, y, [c])","does":"text in the 4x6 font (3x5 glyphs, 4 px a character, so 32 a line); returns the x after the text. Letters show as capitals; \\n starts a new line at x; whole numbers print without .0"}]},{"name":"project","section":"3D","help":[{"sig":"project(x, y, z)","does":"where a point in the scene lands on the picture: x, y, depth (picture pixels, for labels), or nil behind the camera"}]},{"name":"pset","section":"Drawing","help":[{"sig":"pset(x, y, [c])","does":"set / read one pixel (pget returns the color index)"}]},{"name":"rect","section":"Drawing","help":[{"sig":"rect(x0, y0, x1, y1, [c])","does":"a box outline / filled box (corners inclusive)"}]},{"name":"rectfill","section":"Drawing","help":[{"sig":"rectfill(x0, y0, x1, y1, [c])","does":"a box outline / filled box (corners inclusive)"}]},{"name":"res","section":"3D","help":[{"sig":"res(mode)","does":"the scene drawn at \"full\" size (384 x 384, the default), \"half\" (192 x 192, each pixel shown 2x2: a quarter of the drawing, chunkier), or \"auto\" (the board draws at half while frames run over and goes back to full when there's room) / the size it's drawn at now"},{"sig":"res()","does":"the scene drawn at \"full\" size (384 x 384, the default), \"half\" (192 x 192, each pixel shown 2x2: a quarter of the drawing, chunkier), or \"auto\" (the board draws at half while frames run over and goes back to full when there's room) / the size it's drawn at now"}]},{"name":"rnd","section":"Time, numbers, saving","help":[{"sig":"rnd([x])","does":"a random number in [0, x) (x defaults to 1); rnd(t) with a table picks an element"}]},{"name":"save","section":"Time, numbers, saving","help":[{"sig":"save(t)","does":"keep a small table (numbers, strings, booleans, nested tables; at most 2 KB) across runs / get it back (nil the first time). Whole numbers come back exactly (timestamps included)"}]},{"name":"sfx","section":"Feedback","help":[{"sig":"sfx(n)","does":"a built-in sound: 0 blip, 1 coin, 2 hit, 3 jump, 4 explosion, 5 select, 6 error, 7 success; 8-63 your own (below); sfx(-1) stops yours"}]},{"name":"sgn","section":"Time, numbers, saving","help":[{"sig":"sgn(x)","does":"the usual"}]},{"name":"shadow","section":"3D","help":[{"sig":"shadow(o, [on])","does":"object o casts a shadow along the sun onto the ground (shadow(o, false) stops it). Costs its faces again"}]},{"name":"shape","section":"3D","help":[{"sig":"shape(\"box\", w, [h, d])","does":"a box (h, d default to w)"},{"sig":"shape(\"sphere\", r, [segs])","does":"a sphere (segs around, default 16)"},{"sig":"shape(\"torus\", R, r, [segs])","does":"a ring around the z axis (facing the camera at the start)"},{"sig":"shape(\"cylinder\", r, h, [segs])","does":"along y (the cone's point up)"},{"sig":"shape(\"cone\", r, h, [segs])","does":"along y (the cone's point up)"},{"sig":"shape(verts, faces, [smooth, uvs])","does":"your own: verts a flat list {x, y, z, x, y, z, ...}, faces a flat list of vertex numbers (from 1) in threes, each a triangle. Faces are turned outward from the shape's center, so wind them any way (meant for convex shapes: dice, gems). Flat-shaded unless smooth. uvs: texture coordinates {u, v, u, v, ...}, a pair a vertex, 0 to 1 across a texture (it repeats past them)"}]},{"name":"skin","section":"3D","help":[{"sig":"skin(o, tex, [ru, rv])","does":"object o in texture tex, repeated ru x rv times across its shape (default 1; rv defaults to ru), lit as its shine and unlit say (its color is set aside; skin(o) brings it back). A box's faces each show the whole texture, a sphere's, cylinder's and cone's wrap around their side (caps across), a torus's around and through its tube"}]},{"name":"sky","section":"3D","help":[{"sig":"sky(top, [bottom])","does":"the background, a gradient from top to bottom (0xRRGGBB each)"}]},{"name":"spr","section":"Drawing","help":[{"sig":"spr(n, x, y, [flip_x], [flip_y])","does":"sprite n from SPRITES (below), color 0 transparent"}]},{"name":"srand","section":"Time, numbers, saving","help":[{"sig":"srand(seed)","does":"seed rnd"}]},{"name":"stat","section":"Time, numbers, saving","help":[{"sig":"stat(n)","does":"stat(0) memory in KB, stat(1) the last frame's Lua time as a fraction of a frame, stat(7) frames a second"}]},{"name":"texture","section":"3D","help":[{"sig":"texture(w, h, texels)","does":"a texture: w x h texels (4, 8, 16, 32 or 64 each way), texels a string of color numbers as SPRITES are (hex digits row by row; spaces and line breaks ignored), in the palette as it is when made (palette() first for other colors). Returns its number (up to 16)"}]},{"name":"tilt","section":"Input","help":[{"sig":"tilt()","does":"ax, ay, each about -1..1: which way things would roll. ax > 0 when the right edge is lower, ay > 0 when the bottom edge is lower (screen directions: roll a ball by ax, ay and it goes downhill). Measured from how the board was held when the cart started (lying on a desk at a lean counts as level); level() takes the pose it's in now as level"}]},{"name":"time","section":"Time, numbers, saving","help":[{"sig":"time()","does":"seconds since the cart started (it keeps counting while the screen is dark)"}]},{"name":"touch","section":"Input","help":[{"sig":"touch()","does":"x, y in screen pixels while the cart window is touched, else nil / the same in canvas units (3x finer: for the full-color layer)"},{"sig":"touch(true)","does":"x, y in screen pixels while the cart window is touched, else nil / the same in canvas units (3x finer: for the full-color layer)"}]},{"name":"trifill","section":"Drawing","help":[{"sig":"trifill(x0, y0, x1, y1, x2, y2, [c])","does":"a filled triangle. Fractions count (to 1/16 px): triangles sharing an edge meet with no gap and no overlap, so meshes tile cleanly. For solid 3D: drop faces turned away, draw far to near"}]},{"name":"ui.changed","section":"The full-color layer (hd)","help":[{"sig":"ui.changed(key, ...)","does":"true the first time, and whenever the values differ from those given for key last time (one by one, with ==): ask each frame what a part of the screen shows, and repaint it when the answer is true: if ui.changed(\"score\", score, lives) then hd.repaint(0, 40, W, 60, draw_score) end. Ask about a part every frame it's on screen, also on a frame the whole screen is drawn anyway, so what's remembered stays what's shown"}]},{"name":"ui.scroller","section":"The full-color layer (hd)","help":[{"sig":"ui.scroller(x, y, w, h, draw)","does":"a box whose content (a list, a page) scrolls: draw(top, y0, y1) draws the content with its first row at screen row top, and only screen rows y0 to y1 are being painted (skip what's outside them). Returns s:"}]},{"name":"view","section":"3D","help":[{"sig":"view(x, y, z, [yaw, pitch, fov])","does":"the camera: at x, y, z, looking along +z (yaw 90 looks along +x, pitch 90 up); fov in degrees (60). Default view(0, 0, -420)"}]},{"name":"wake","section":"Feedback","help":[{"sig":"wake(secs)","does":"light the screen in secs seconds even if it went dark (a timer that's up) / cancel. The cart's frames resume then"},{"sig":"wake()","does":"light the screen in secs seconds even if it went dark (a timer that's up) / cancel. The cart's frames resume then"}]}]
1
+ [{"name":"abs","section":"Time, numbers, saving","help":[{"sig":"abs(x)","does":"the usual"}]},{"name":"act","section":"The host","help":[{"sig":"act(name, [arg])","does":"a command to the console: \"open\" opens the cart named arg; \"volume\" and \"brightness\" set that setting to the step arg (\"0\" to \"3\"). The launcher uses all three"}]},{"name":"btn","section":"Input","help":[{"sig":"btn(b)","does":"true while button b is held"}]},{"name":"btnp","section":"Input","help":[{"sig":"btnp(b)","does":"true on the frame b went down"}]},{"name":"buzz","section":"Feedback","help":[{"sig":"buzz(ms)","does":"the vibration motor, 10-500 ms (this board has none: caps().buzz is false)"}]},{"name":"camera","section":"Drawing","help":[{"sig":"camera(x, y)","does":"offset everything drawn by (-x, -y) / reset"},{"sig":"camera()","does":"offset everything drawn by (-x, -y) / reset"}]},{"name":"caps","section":"The host","help":[{"sig":"caps()","does":"what the cart runs on, as a table: the canvas size, where the picture sits, the board's name, what it has (touch, tilt, sound...). The fields are under \"Versions and what a cart runs on\""}]},{"name":"ceil","section":"Time, numbers, saving","help":[{"sig":"ceil(x)","does":"the usual"}]},{"name":"circ","section":"Drawing","help":[{"sig":"circ(x, y, r, [c])","does":"a circle outline / filled circle"}]},{"name":"circfill","section":"Drawing","help":[{"sig":"circfill(x, y, r, [c])","does":"a circle outline / filled circle"}]},{"name":"clip","section":"Drawing","help":[{"sig":"clip(x, y, w, h)","does":"draw only inside a rectangle / reset"},{"sig":"clip()","does":"draw only inside a rectangle / reset"}]},{"name":"cls","section":"Drawing","help":[{"sig":"cls([c])","does":"clear the screen (default 0)"}]},{"name":"cos","section":"Time, numbers, saving","help":[{"sig":"cos(t)","does":"turns, not radians (below)"}]},{"name":"data","section":"The host","help":[{"sig":"data(name)","does":"the latest value of a channel as a table, or nil: \"battery\" (mv, percent, charging, usb), \"carts\" (the carts on the board: a list of name, title, about), \"settings\" (volume and brightness, each a step from 0 to 3)"}]},{"name":"date","section":"Time, numbers, saving","help":[{"sig":"date([t])","does":"local time (the host's timezone, kept for offline use) as a table: year, month, day, hour, min, sec, wday (1 = Sunday), yday; for now() when t is left out. nil while the date is unknown"}]},{"name":"fget","section":"Drawing","help":[{"sig":"fget(n)","does":"sprite n's flags as a number (API 6) / whether it has flag f (0-7). A sprite not in FLAGS has none"},{"sig":"fget(n, f)","does":"sprite n's flags as a number (API 6) / whether it has flag f (0-7). A sprite not in FLAGS has none"}]},{"name":"flr","section":"Time, numbers, saving","help":[{"sig":"flr(x)","does":"the usual"}]},{"name":"fps","section":"Time, numbers, saving","help":[{"sig":"fps(rate)","does":"the frame rate from the next frame (any rate that divides 60: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) / lazy: active while there's input (a touch on the cart, a button) and half a second after, idle otherwise / the rate now. An idle cart wakes at once when a touch comes down or BOOT is pressed, so a slow idle rate costs no response time: give idle the slowest that still shows what moves on its own (a blink twice a second needs 4; a seconds counter 1 or 2). A cart doing a few frames a second instead of 30 or 60 spends that much less of the board's battery on its frames. Move things by time(), not by frames, when the rate changes"},{"sig":"fps(active, idle)","does":"the frame rate from the next frame (any rate that divides 60: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) / lazy: active while there's input (a touch on the cart, a button) and half a second after, idle otherwise / the rate now. An idle cart wakes at once when a touch comes down or BOOT is pressed, so a slow idle rate costs no response time: give idle the slowest that still shows what moves on its own (a blink twice a second needs 4; a seconds counter 1 or 2). A cart doing a few frames a second instead of 30 or 60 spends that much less of the board's battery on its frames. Move things by time(), not by frames, when the rate changes"},{"sig":"fps()","does":"the frame rate from the next frame (any rate that divides 60: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) / lazy: active while there's input (a touch on the cart, a button) and half a second after, idle otherwise / the rate now. An idle cart wakes at once when a touch comes down or BOOT is pressed, so a slow idle rate costs no response time: give idle the slowest that still shows what moves on its own (a blink twice a second needs 4; a seconds counter 1 or 2). A cart doing a few frames a second instead of 30 or 60 spends that much less of the board's battery on its frames. Move things by time(), not by frames, when the rate changes"}]},{"name":"fset","section":"Drawing","help":[{"sig":"fset(n, f, on)","does":"turn flag f of sprite n on or off / set all eight from v's bits. It writes FLAGS (making it when the cart has none)"},{"sig":"fset(n, v)","does":"turn flag f of sprite n on or off / set all eight from v's bits. It writes FLAGS (making it when the cart has none)"}]},{"name":"ground","section":"3D","help":[{"sig":"ground(y, [color])","does":"shadows fall on the plane at height y, in color (the ground's color in shadow, 0xRRGGBB; default black)"}]},{"name":"hd.circle","section":"The full-color layer (hd)","help":[{"sig":"hd.circle(x, y, r, rgb, [a])","does":"a filled circle / a ring width px wide inside radius r"}]},{"name":"hd.clear","section":"The full-color layer (hd)","help":[{"sig":"hd.clear([rgb, a])","does":"wipe the canvas (inside the clip) to clear, or to a color at alpha a (255)"}]},{"name":"hd.clip","section":"The full-color layer (hd)","help":[{"sig":"hd.clip(x, y, w, h)","does":"draw only inside a box / anywhere (inside an hd.repaint: inside the box cut to the repaint's / the repaint's box)"},{"sig":"hd.clip()","does":"draw only inside a box / anywhere (inside an hd.repaint: inside the box cut to the repaint's / the repaint's box)"}]},{"name":"hd.frame","section":"The full-color layer (hd)","help":[{"sig":"hd.frame(x, y, w, h, rgb, [width, radius, a])","does":"a box's outline, width px (1)"}]},{"name":"hd.gradient","section":"The full-color layer (hd)","help":[{"sig":"hd.gradient(x, y, w, h, top, bottom, [a])","does":"a box shading from top to bottom, ordered-dithered (RGB565 has 32 steps of red and blue: a slow dark gradient would band)"}]},{"name":"hd.image","section":"The full-color layer (hd)","help":[{"sig":"hd.image(name, x, y, [w, h, a])","does":"draws an image (below) with its top left at x, y (to the pixel), at its own size or scaled to w x h (nearest pixel), at alpha a over its own; returns true, or false when there's no such image (yet)"}]},{"name":"hd.images","section":"The full-color layer (hd)","help":[{"sig":"hd.images()","does":"the names of the cart's own images"}]},{"name":"hd.inset","section":"The full-color layer (hd)","help":[{"sig":"hd.inset(y, [h])","does":"how far in from each side rows y to y + h show on this glass (its corners are rounded): keep a bar or a line of text that far in. 0 for the window, and for rows clear of the corners"}]},{"name":"hd.line","section":"The full-color layer (hd)","help":[{"sig":"hd.line(x0, y0, x1, y1, rgb, [width, a])","does":"a line width px wide (1), round ends"}]},{"name":"hd.measure","section":"The full-color layer (hd)","help":[{"sig":"hd.measure(text, [size, font])","does":"its width and line height"}]},{"name":"hd.order","section":"The full-color layer (hd)","help":[{"sig":"hd.order(\"over\" | \"under\")","does":"the layer over the picture (default), or under it: then the picture's color 0 shows the layer through, as over a 3D scene. Under a 3D scene's picture too: scene, layer, picture, in that order when \"under\""}]},{"name":"hd.rect","section":"The full-color layer (hd)","help":[{"sig":"hd.rect(x, y, w, h, rgb, [a, radius])","does":"a filled box, its corners rounded by radius"}]},{"name":"hd.repaint","section":"The full-color layer (hd)","help":[{"sig":"hd.repaint(x, y, w, h, fn, ...)","does":"calls fn(...) with all drawing kept inside the box, then puts the clip back: part of the screen drawn again by the code that draws it. fn paints the box's background too (the old pixels are there). Repaints nest (the inner box is cut to the outer); fn isn't called for an empty box"}]},{"name":"hd.ring","section":"The full-color layer (hd)","help":[{"sig":"hd.ring(x, y, r, rgb, [width, a])","does":"a filled circle / a ring width px wide inside radius r"}]},{"name":"hd.scanlines","section":"The full-color layer (hd)","help":[{"sig":"hd.scanlines(on)","does":"the scanlines on the layer (default on) / whether they're on"},{"sig":"hd.scanlines()","does":"the scanlines on the layer (default on) / whether they're on"}]},{"name":"hd.scroll","section":"The full-color layer (hd)","help":[{"sig":"hd.scroll(x, y, w, h, dy)","does":"moves what's in the box by dy rows (down when positive); the strip it uncovers keeps stale pixels, for the cart to draw again. The whole width (x 0, w the canvas's) moves nothing (the rows are re-mapped): scrolling a list costs only its new strip. A narrower box copies its pixels. ui.scroller does all this"}]},{"name":"hd.text","section":"The full-color layer (hd)","help":[{"sig":"hd.text(text, x, y, rgb, [size, font, align, a])","does":"text from (x, y) = the top of its line; returns its width. font: \"mono\" (JetBrains Mono, 12/14/16/20/24 px), \"bold\" (16/18/24/32) or \"vt\" (VT323, big terminal digits: 30/46/72, ASCII only); size snaps to the nearest the font has (16). align: \"left\", \"center\" or \"right\" of x. Latin-1, punctuation, arrows, box drawing, ★ ♥ ✓ and friends; anything else shows as a small box"}]},{"name":"hd.wrap","section":"The full-color layer (hd)","help":[{"sig":"hd.wrap(text, width, [size, font])","does":"a table of lines that fit width (at spaces; \\n breaks; a word too long is broken)"}]},{"name":"hide","section":"3D","help":[{"sig":"hide(o, [hidden])","does":"hide it (hide(o, false) shows it again)"}]},{"name":"level","section":"Input","help":[{"sig":"level()","does":"nothing: tilt() is measured from the pose the board is in now (for a \"hold it how you like, then tap\" start)"}]},{"name":"light","section":"3D","help":[{"sig":"light(azimuth, elevation, [intensity, ambient])","does":"the sun's direction in degrees, its strength 0-255 (255), and the shadows' color (0x22283E)"}]},{"name":"line","section":"Drawing","help":[{"sig":"line(x0, y0, x1, y1, [c])","does":"a line"}]},{"name":"load","section":"Time, numbers, saving","help":[{"sig":"load()","does":"keep a small table (numbers, strings, booleans, nested tables; at most 2 KB) across runs / get it back (nil the first time). Whole numbers come back exactly (timestamps included). Saving often is fine: the board writes to flash only when the table changed, and at most every 2 s (the last one is written when the cart closes)"}]},{"name":"log","section":"Feedback","help":[{"sig":"log(...)","does":"a line in the log: each value as tostring gives it, spaces between. The playground shows it in its Console, the board in its serial log, check.py in its result's log. At most 20 lines a frame and 200 characters a line"}]},{"name":"map","section":"Drawing","help":[{"sig":"map([cx, cy, sx, sy, cw, ch, layer])","does":"the map (MAP, below): cw x ch cells from cell (cx, cy), drawn as sprites with that cell's top left at (sx, sy). With nothing, the whole map at (0, 0). Color 0 is transparent; the camera moves it, and only the cells on screen cost anything. With layer (API 6), only the cells whose sprite has one of those flags (fget(n) & layer ~= 0): draw the ground, then the player, then map(0, 0, 0, 0, 128, 64, 2) for the treetops in front"}]},{"name":"max","section":"Time, numbers, saving","help":[{"sig":"max(a, b)","does":"the usual"}]},{"name":"mget","section":"Drawing","help":[{"sig":"mget(cx, cy)","does":"the sprite in a map cell, or nil for an empty cell or one off the map"}]},{"name":"mid","section":"Time, numbers, saving","help":[{"sig":"mid(a, b, c)","does":"the usual"}]},{"name":"min","section":"Time, numbers, saving","help":[{"sig":"min(a, b)","does":"the usual"}]},{"name":"mset","section":"Drawing","help":[{"sig":"mset(cx, cy, n)","does":"put sprite n (0-63) in a cell / empty it. It changes the map for this run, not MAP's text"},{"sig":"mset(cx, cy)","does":"put sprite n (0-63) in a cell / empty it. It changes the map for this run, not MAP's text"}]},{"name":"msize","section":"Drawing","help":[{"sig":"msize()","does":"the map's width and height in cells"}]},{"name":"music","section":"Feedback","help":[{"sig":"music(n)","does":"your song from pattern n on / stop it (below)"},{"sig":"music(-1)","does":"your song from pattern n on / stop it (below)"}]},{"name":"now","section":"Time, numbers, saving","help":[{"sig":"now()","does":"the wall clock: seconds since 1970 (UTC), a whole number, or nil while the board doesn't know the date. It knows it from its RTC (kept through reboots and power-off while the battery lasts), which push.py sets; use it for anything that lives while the cart is closed (save(now()), then now() - saved on the next run)"}]},{"name":"obj","section":"3D","help":[{"sig":"obj(shape, color, [shine, unlit])","does":"an object: the shape placed in the scene, in color (0xRRGGBB); shine 0-255 adds a highlight; unlit shows the color as it is. Returns its number"}]},{"name":"orient","section":"3D","help":[{"sig":"orient(o, m1, ..., m9)","does":"turn it by a rotation matrix (rows), for carts that keep their own (physics)"},{"sig":"orient(o, {m1, ..., m9})","does":"turn it by a rotation matrix (rows), for carts that keep their own (physics)"}]},{"name":"paint","section":"3D","help":[{"sig":"paint(o, color, [shine, unlit])","does":"recolor it"}]},{"name":"pal","section":"Drawing","help":[{"sig":"pal(c0, c1)","does":"draw color c0 as c1 from now on / reset"},{"sig":"pal()","does":"draw color c0 as c1 from now on / reset"}]},{"name":"palette","section":"Drawing","help":[{"sig":"palette(i, 0xRRGGBB)","does":"change what color index i (0-15) looks like, for the whole picture (this frame included) / its color now / the starting 16 again. A cart still has 16 colors at a time; this picks which 16 (an exact red, softer pastels). pal remaps indices, palette the colors behind them"},{"sig":"palette(i)","does":"change what color index i (0-15) looks like, for the whole picture (this frame included) / its color now / the starting 16 again. A cart still has 16 colors at a time; this picks which 16 (an exact red, softer pastels). pal remaps indices, palette the colors behind them"},{"sig":"palette()","does":"change what color index i (0-15) looks like, for the whole picture (this frame included) / its color now / the starting 16 again. A cart still has 16 colors at a time; this picks which 16 (an exact red, softer pastels). pal remaps indices, palette the colors behind them"}]},{"name":"palt","section":"Drawing","help":[{"sig":"palt(c, on)","does":"whether color c is transparent in spr, sspr and map (API 6) / only color 0 again. palt(0, false) draws black; palt(14, true) makes pink the see-through color"},{"sig":"palt()","does":"whether color c is transparent in spr, sspr and map (API 6) / only color 0 again. palt(0, false) draws black; palt(14, true) makes pink the see-through color"}]},{"name":"pget","section":"Drawing","help":[{"sig":"pget(x, y)","does":"set / read one pixel (pget returns the color index)"}]},{"name":"place","section":"3D","help":[{"sig":"place(o, x, y, z, [rx, ry, rz])","does":"move object o there, turned by rx, ry, rz degrees (about x, then y, then z)"}]},{"name":"print","section":"Drawing","help":[{"sig":"print(text, x, y, [c])","does":"text in the 4x6 font (3x5 glyphs, 4 px a character, so 32 a line); returns the x after the text. Letters show as capitals; \\n starts a new line at x; whole numbers print without .0"}]},{"name":"project","section":"3D","help":[{"sig":"project(x, y, z)","does":"where a point in the scene lands on the picture: x, y, depth (picture pixels, for labels), or nil behind the camera"}]},{"name":"pset","section":"Drawing","help":[{"sig":"pset(x, y, [c])","does":"set / read one pixel (pget returns the color index)"}]},{"name":"rect","section":"Drawing","help":[{"sig":"rect(x0, y0, x1, y1, [c])","does":"a box outline / filled box (corners inclusive)"}]},{"name":"rectfill","section":"Drawing","help":[{"sig":"rectfill(x0, y0, x1, y1, [c])","does":"a box outline / filled box (corners inclusive)"}]},{"name":"res","section":"3D","help":[{"sig":"res(mode)","does":"the scene drawn at \"full\" size (384 x 384, the default), \"half\" (192 x 192, each pixel shown 2x2: a quarter of the drawing, chunkier), or \"auto\" (the board draws at half while frames run over and goes back to full when there's room) / the size it's drawn at now"},{"sig":"res()","does":"the scene drawn at \"full\" size (384 x 384, the default), \"half\" (192 x 192, each pixel shown 2x2: a quarter of the drawing, chunkier), or \"auto\" (the board draws at half while frames run over and goes back to full when there's room) / the size it's drawn at now"}]},{"name":"rnd","section":"Time, numbers, saving","help":[{"sig":"rnd([x])","does":"a random number in [0, x) (x defaults to 1); rnd(t) with a table picks an element"}]},{"name":"save","section":"Time, numbers, saving","help":[{"sig":"save(t)","does":"keep a small table (numbers, strings, booleans, nested tables; at most 2 KB) across runs / get it back (nil the first time). Whole numbers come back exactly (timestamps included). Saving often is fine: the board writes to flash only when the table changed, and at most every 2 s (the last one is written when the cart closes)"}]},{"name":"sfx","section":"Feedback","help":[{"sig":"sfx(n)","does":"a built-in sound: 0 blip, 1 coin, 2 hit, 3 jump, 4 explosion, 5 select, 6 error, 7 success; 8-63 your own (below); sfx(-1) stops yours"}]},{"name":"sgn","section":"Time, numbers, saving","help":[{"sig":"sgn(x)","does":"the usual"}]},{"name":"shadow","section":"3D","help":[{"sig":"shadow(o, [on])","does":"object o casts a shadow along the sun onto the ground (shadow(o, false) stops it). Costs its faces again"}]},{"name":"shape","section":"3D","help":[{"sig":"shape(\"box\", w, [h, d])","does":"a box (h, d default to w)"},{"sig":"shape(\"sphere\", r, [segs])","does":"a sphere (segs around, default 16)"},{"sig":"shape(\"torus\", R, r, [segs])","does":"a ring around the z axis (facing the camera at the start)"},{"sig":"shape(\"cylinder\", r, h, [segs])","does":"along y (the cone's point up)"},{"sig":"shape(\"cone\", r, h, [segs])","does":"along y (the cone's point up)"},{"sig":"shape(verts, faces, [smooth, uvs])","does":"your own: verts a flat list {x, y, z, x, y, z, ...}, faces a flat list of vertex numbers (from 1) in threes, each a triangle. Faces are turned outward from the shape's center, so wind them any way (meant for convex shapes: dice, gems). Flat-shaded unless smooth. uvs: texture coordinates {u, v, u, v, ...}, a pair a vertex, 0 to 1 across a texture (it repeats past them)"}]},{"name":"sin","section":"Time, numbers, saving","help":[{"sig":"sin(t)","does":"turns, not radians (below)"}]},{"name":"skin","section":"3D","help":[{"sig":"skin(o, tex, [ru, rv])","does":"object o in texture tex, repeated ru x rv times across its shape (default 1; rv defaults to ru), lit as its shine and unlit say (its color is set aside; skin(o) brings it back). A box's faces each show the whole texture, a sphere's, cylinder's and cone's wrap around their side (caps across), a torus's around and through its tube"}]},{"name":"sky","section":"3D","help":[{"sig":"sky(top, [bottom])","does":"the background, a gradient from top to bottom (0xRRGGBB each)"}]},{"name":"spr","section":"Drawing","help":[{"sig":"spr(n, x, y, [flip_x], [flip_y])","does":"sprite n (0-255) from SPRITES (below), color 0 transparent (palt changes which)"},{"sig":"spr(n, x, y, w, h, [flip_x], [flip_y])","does":"a block of w x h sprites (API 6): n, n + 1, ... row by row, w across, so a 16 x 16 character is four sprites in a row of the table. Flipping flips the whole block. A number as the fourth argument is what makes it a block"}]},{"name":"srand","section":"Time, numbers, saving","help":[{"sig":"srand(seed)","does":"seed rnd"}]},{"name":"sspr","section":"Drawing","help":[{"sig":"sspr(n, x, y, dw, dh, [flip_x], [flip_y], [w], [h])","does":"sprite n (or the w x h block from it) stretched or shrunk to dw x dh pixels (API 6), nearest pixel: big titles, zooms, a boss"}]},{"name":"stat","section":"Time, numbers, saving","help":[{"sig":"stat(n)","does":"stat(0) memory in KB, stat(1) the last frame's Lua time as a fraction of a frame, stat(7) frames a second"}]},{"name":"texture","section":"3D","help":[{"sig":"texture(w, h, texels)","does":"a texture: w x h texels (4, 8, 16, 32 or 64 each way), texels a string of color numbers as SPRITES are (hex digits row by row; spaces and line breaks ignored), in the palette as it is when made (palette() first for other colors). Returns its number (up to 16)"}]},{"name":"tilt","section":"Input","help":[{"sig":"tilt()","does":"ax, ay, each about -1..1: which way things would roll. ax > 0 when the right edge is lower, ay > 0 when the bottom edge is lower (screen directions: roll a ball by ax, ay and it goes downhill). Measured from how the board was held when the cart started (lying on a desk at a lean counts as level)"}]},{"name":"time","section":"Time, numbers, saving","help":[{"sig":"time()","does":"seconds since the cart started (it keeps counting while the screen is dark)"}]},{"name":"touch","section":"Input","help":[{"sig":"touch()","does":"x, y in screen pixels while the cart window is touched, else nil / the same in canvas units (3x finer: for the full-color layer)"},{"sig":"touch(true)","does":"x, y in screen pixels while the cart window is touched, else nil / the same in canvas units (3x finer: for the full-color layer)"}]},{"name":"trifill","section":"Drawing","help":[{"sig":"trifill(x0, y0, x1, y1, x2, y2, [c])","does":"a filled triangle. Fractions count (to 1/16 px): triangles sharing an edge meet with no gap and no overlap, so meshes tile cleanly. For solid 3D: drop faces turned away, draw far to near"}]},{"name":"ui.changed","section":"The full-color layer (hd)","help":[{"sig":"ui.changed(key, ...)","does":"true the first time, and whenever the values differ from those given for key last time (one by one, with ==): ask each frame what a part of the screen shows, and repaint it when the answer is true: if ui.changed(\"score\", score, lives) then hd.repaint(0, 40, W, 60, draw_score) end. Ask about a part every frame it's on screen, also on a frame the whole screen is drawn anyway, so what's remembered stays what's shown"}]},{"name":"ui.scroller","section":"The full-color layer (hd)","help":[{"sig":"ui.scroller(x, y, w, h, draw)","does":"a box whose content (a list, a page) scrolls: draw(top, y0, y1) draws the content with its first row at screen row top, and only screen rows y0 to y1 are being painted (skip what's outside them). Returns s:"}]},{"name":"view","section":"3D","help":[{"sig":"view(x, y, z, [yaw, pitch, fov])","does":"the camera: at x, y, z, looking along +z (yaw 90 looks along +x, pitch 90 up); fov in degrees (60). Default view(0, 0, -420)"}]},{"name":"wake","section":"Feedback","help":[{"sig":"wake(secs)","does":"light the screen in secs seconds even if it went dark (a timer that's up) / cancel. The cart's frames resume then"},{"sig":"wake()","does":"light the screen in secs seconds even if it went dark (a timer that's up) / cancel. The cart's frames resume then"}]}]
Binary file
package/lib/tables.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // The cart's data tables as the playground's editors read and write them: SOUNDS, MUSIC, SPRITES,
2
- // each `NAME = { [n] = "text", -- name ... }` in the cart's code. Shared by the sound and sprite
2
+ // each `NAME = { [n] = "text", -- name ... }` in the cart's code, and FLAGS, `{ [n] = number }`. Shared by the sound and sprite
3
3
  // editors (and the command-line tool).
4
4
  "use strict";
5
5
 
@@ -44,6 +44,88 @@ const Tables = (() => {
44
44
  return lines.join("\n");
45
45
  }
46
46
 
47
+ // NAME = { [n] = number, ... } (FLAGS): on one line or many, a value a number or numbers joined
48
+ // with | (`1 | 4`), comments anywhere. The entries, or why the editor can't read it.
49
+ function readNumbers(src, name) {
50
+ const m = new RegExp(`^${name}\\s*=\\s*\\{([^}]*)\\}`, "m").exec(src);
51
+ if (!m) {
52
+ return new RegExp(`(^|[^\\w.])${name}\\s*(=|\\[)`, "m").test(src)
53
+ ? { problem: `${name} is set up in the code in a way the editor can't read. Change it there, or delete it to start over here.` }
54
+ : { map: new Map() };
55
+ }
56
+ const map = new Map();
57
+ const body = m[1].split("\n").map((l) => l.replace(/--.*$/, "")).join(" ");
58
+ for (const raw of body.split(",")) {
59
+ const item = raw.trim();
60
+ if (!item) continue;
61
+ const e = /^\[(\d+)\]\s*=\s*((?:0x[0-9a-fA-F]+|\d+)(?:\s*\|\s*(?:0x[0-9a-fA-F]+|\d+))*)$/.exec(item);
62
+ if (!e) return { problem: `The editor can't read this part of ${name}: ${item}` };
63
+ map.set(Number(e[1]), e[2].split("|").reduce((v, t) => v | Number(t.trim()), 0));
64
+ }
65
+ return { map };
66
+ }
67
+
68
+ // Writes NAME = { [n] = v, -- note(n, v) ... } (its zeros left out), in place of the old one, or
69
+ // after the table named `after` (else after the header).
70
+ function writeNumbers(src, name, map, intro, note, after) {
71
+ const keys = [...map.keys()].filter((k) => map.get(k)).sort((a, b) => a - b);
72
+ const body = keys.map((k) => ` [${k}] = ${map.get(k)},${note ? " -- " + note(k, map.get(k)) : ""}`).join("\n");
73
+ const block = keys.length ? `${name} = {\n${body}\n}` : "";
74
+ const said = intro.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
+ const re = new RegExp(`(^-- ${said}\\n)?^${name}\\s*=\\s*\\{[^}]*\\}\\n?`, "m");
76
+ if (re.test(src)) return src.replace(re, block ? `-- ${intro}\n${block}\n` : "");
77
+ if (!block) return src;
78
+ const prev = after && new RegExp(`^${after}\\s*=\\s*\\{[^]*?^\\}\\n?`, "m").exec(src);
79
+ if (prev) {
80
+ const end = prev.index + prev[0].length;
81
+ return src.slice(0, end) + `-- ${intro}\n${block}\n` + src.slice(end);
82
+ }
83
+ const lines = src.split("\n");
84
+ let at = 0;
85
+ while (at < lines.length && lines[at].startsWith("--")) at++;
86
+ lines.splice(at, 0, "", `-- ${intro}`, block);
87
+ return lines.join("\n");
88
+ }
89
+
90
+ // A library (libs/*.lua): its first line says what it is, `-- lib: NAME VERSION, api N: about`,
91
+ // and `-- end lib: NAME` ends it. null when `text` doesn't start that way.
92
+ function libInfo(text) {
93
+ const m = /^-- lib: (\w+) (\d+), api (\d+): (.*)$/m.exec(text.split("\n")[0]);
94
+ return m ? { name: m[1], version: Number(m[2]), api: Number(m[3]), about: m[4] } : null;
95
+ }
96
+
97
+ // The cart with library `text` in it: in place of the version it has (between its markers), or
98
+ // after the libraries it has, or after the header. The header asks for the API it needs.
99
+ // Returns {src, name, was} (was: the version replaced, 0 for none).
100
+ function addLib(src, text) {
101
+ const info = libInfo(text);
102
+ if (!info) throw new Error("that isn't a library: its first line should be '-- lib: NAME VERSION, api N: ...'");
103
+ const block = text.replace(/\n*$/, "\n");
104
+ const old = new RegExp(`^-- lib: ${info.name} (\\d+)[^\\n]*\\n[^]*?^-- end lib: ${info.name}[ \\t]*(\\n|$)`, "m").exec(src);
105
+ let out, was = 0;
106
+ if (old) {
107
+ was = Number(old[1]);
108
+ out = src.slice(0, old.index) + block + src.slice(old.index + old[0].length);
109
+ } else {
110
+ const ends = [...src.matchAll(/^-- end lib: \w+[ \t]*\n?/gm)];
111
+ let at;
112
+ if (ends.length) {
113
+ const e = ends[ends.length - 1];
114
+ at = e.index + e[0].length;
115
+ if (!e[0].endsWith("\n")) { src = src + "\n"; at = at + 1; }
116
+ } else {
117
+ const lines = src.split("\n");
118
+ let n = 0;
119
+ // the header: its `-- key: value` lines (not a table's comment that follows it)
120
+ while (n < lines.length && /^--\s*[a-z][\w ]*:/i.test(lines[n]) && !/^-- (end )?lib:/.test(lines[n])) n++;
121
+ at = lines.slice(0, n).join("\n").length + (n ? 1 : 0);
122
+ }
123
+ const before = src.slice(0, at), after = src.slice(at);
124
+ out = before + "\n" + block + (after.startsWith("\n") || !after ? "" : "\n") + after;
125
+ }
126
+ return { src: withApi(out, info.api), name: info.name, was };
127
+ }
128
+
47
129
  // The header asks for API `min` at least (sounds and music need 3, spaced sprites 4), or a board
48
130
  // with older firmware would run the cart as one that doesn't need it.
49
131
  function withApi(src, min) {
@@ -61,7 +143,7 @@ const Tables = (() => {
61
143
  return lines.join("\n");
62
144
  }
63
145
 
64
- return { readTable, writeTable, withApi };
146
+ return { readTable, writeTable, readNumbers, writeNumbers, withApi, libInfo, addLib };
65
147
  })();
66
148
 
67
149
  if (typeof module !== "undefined") module.exports = Tables;
package/libs/actor.lua ADDED
@@ -0,0 +1,63 @@
1
+ -- lib: actor 1, api 6: boxes that move through the map and stop at solid cells: actor.move(me, dx, dy)
2
+ -- An actor is any table with x, y (its top left, in pixels) and w, h (its size). Solid cells are
3
+ -- map cells whose sprite has flag actor.SOLID (0 unless you change it): set it in the Sprites tab,
4
+ -- or FLAGS in the code. Cells off the map are open, unless actor.walls is true.
5
+ local actor = {SOLID = 0, walls = false}
6
+
7
+ -- Is the pixel at (x, y) in a solid cell?
8
+ function actor.solid_at(x, y)
9
+ local cx, cy = flr(x / 8), flr(y / 8)
10
+ local t = mget(cx, cy)
11
+ if t == nil then
12
+ if not actor.walls then return false end
13
+ local w, h = msize()
14
+ return cx < 0 or cy < 0 or cx >= w or cy >= h
15
+ end
16
+ return fget(t, actor.SOLID)
17
+ end
18
+
19
+ -- Would a's box, at (x, y), overlap a solid cell? (Points every 8 pixels and at the corners: any size.)
20
+ function actor.hits(a, x, y)
21
+ local x1, y1 = x + a.w - 1, y + a.h - 1
22
+ local px = x
23
+ while true do
24
+ local py = y
25
+ while true do
26
+ if actor.solid_at(px, py) then return true end
27
+ if py >= y1 then break end
28
+ py = math.min(py + 8, y1)
29
+ end
30
+ if px >= x1 then break end
31
+ px = math.min(px + 8, x1)
32
+ end
33
+ return false
34
+ end
35
+
36
+ -- Moves a by (dx, dy) pixels, a pixel at a time, stopping against solid cells; fractions add up
37
+ -- between calls (a speed of 0.5 moves a pixel every other frame). Returns hit_x, hit_y: whether it
38
+ -- stopped against something that way. Afterwards a.ground is true when a stands on something.
39
+ function actor.move(a, dx, dy)
40
+ a.rx, a.ry = (a.rx or 0) + dx, (a.ry or 0) + dy
41
+ local mx = a.rx >= 0 and flr(a.rx) or -flr(-a.rx)
42
+ local my = a.ry >= 0 and flr(a.ry) or -flr(-a.ry)
43
+ a.rx, a.ry = a.rx - mx, a.ry - my
44
+ local hit_x, hit_y = false, false
45
+ local s = mx > 0 and 1 or -1
46
+ while mx ~= 0 do
47
+ if actor.hits(a, a.x + s, a.y) then hit_x, a.rx = true, 0 break end
48
+ a.x, mx = a.x + s, mx - s
49
+ end
50
+ s = my > 0 and 1 or -1
51
+ while my ~= 0 do
52
+ if actor.hits(a, a.x, a.y + s) then hit_y, a.ry = true, 0 break end
53
+ a.y, my = a.y + s, my - s
54
+ end
55
+ a.ground = actor.hits(a, a.x, a.y + 1)
56
+ return hit_x, hit_y
57
+ end
58
+
59
+ -- Do the boxes of a and b overlap? (Two actors: the player and a coin, an enemy.)
60
+ function actor.overlap(a, b)
61
+ return a.x < b.x + b.w and b.x < a.x + a.w and a.y < b.y + b.h and b.y < a.y + a.h
62
+ end
63
+ -- end lib: actor
package/libs/anim.lua ADDED
@@ -0,0 +1,36 @@
1
+ -- lib: anim 1, api 1: sprite animations: local walk = anim.new({16, 17}, 8); spr(walk:frame(), x, y)
2
+ -- An animation is a list of sprite numbers shown at so many a second. It goes by time(), so it
3
+ -- holds at any frame rate.
4
+ local anim = {}
5
+ anim.__index = anim
6
+
7
+ -- frames: sprite numbers; fps: how many a second (8 if left out); loop: false to stop on the last.
8
+ function anim.new(frames, fps, loop)
9
+ return setmetatable({frames = frames, fps = fps or 8, loop = loop ~= false, t0 = time()}, anim)
10
+ end
11
+
12
+ -- From the first frame again.
13
+ function anim:restart()
14
+ self.t0 = time()
15
+ return self
16
+ end
17
+
18
+ -- The sprite to draw now.
19
+ function anim:frame()
20
+ local n = flr((time() - self.t0) * self.fps)
21
+ if self.loop then n = n % #self.frames else n = math.min(n, #self.frames - 1) end
22
+ return self.frames[n + 1]
23
+ end
24
+
25
+ -- Whether one that doesn't loop has reached its end.
26
+ function anim:done()
27
+ return not self.loop and (time() - self.t0) * self.fps >= #self.frames
28
+ end
29
+
30
+ -- Use this to change animations: `me.anim = anim.switch(me.anim, run)`. It restarts the new one
31
+ -- only when it's a change, so calling it every frame is fine.
32
+ function anim.switch(now, to)
33
+ if now ~= to then to:restart() end
34
+ return to
35
+ end
36
+ -- end lib: anim
package/libs/cam.lua ADDED
@@ -0,0 +1,56 @@
1
+ -- lib: cam 1, api 1: a camera that follows the player with a dead zone, stays inside the map, and shakes
2
+ -- In _draw: cam.follow(me.x, me.y), then cam.apply() before drawing the world, and camera() before
3
+ -- the score. The view is 128 x 128 pixels; cam.x, cam.y is its top left in the world.
4
+ local cam = {x = 0, y = 0, zone_w = 24, zone_h = 16, smooth = 0.25}
5
+
6
+ -- Keeps the view inside the box from (x0, y0) to (x1, y1) in pixels; with no box set, inside the
7
+ -- map (msize()) when the cart has one.
8
+ function cam.bounds(x0, y0, x1, y1)
9
+ cam.box = x0 and {x0, y0, x1, y1} or nil
10
+ end
11
+
12
+ local function clamp()
13
+ local b = cam.box
14
+ if not b and MAP then
15
+ local w, h = msize()
16
+ b = {0, 0, w * 8, h * 8}
17
+ end
18
+ if not b then return end
19
+ cam.x = b[3] - b[1] <= 128 and b[1] or mid(b[1], cam.x, b[3] - 128)
20
+ cam.y = b[4] - b[2] <= 128 and b[2] or mid(b[2], cam.y, b[4] - 128)
21
+ end
22
+
23
+ -- Moves the view so (x, y) stays in a zone_w x zone_h box in the middle, easing there by `smooth`
24
+ -- of the way each frame (1: at once).
25
+ function cam.follow(x, y)
26
+ local cx, cy = x - 64, y - 64
27
+ local hw, hh = cam.zone_w / 2, cam.zone_h / 2
28
+ local tx, ty = cam.x, cam.y
29
+ if cx > tx + hw then tx = cx - hw elseif cx < tx - hw then tx = cx + hw end
30
+ if cy > ty + hh then ty = cy - hh elseif cy < ty - hh then ty = cy + hh end
31
+ cam.x = cam.x + (tx - cam.x) * cam.smooth
32
+ cam.y = cam.y + (ty - cam.y) * cam.smooth
33
+ clamp()
34
+ end
35
+
36
+ -- Centers on (x, y) at once (a level's start).
37
+ function cam.snap(x, y)
38
+ cam.x, cam.y = x - 64, y - 64
39
+ clamp()
40
+ end
41
+
42
+ -- Shakes the view for secs seconds, by up to px pixels (2 if left out).
43
+ function cam.shake(secs, px)
44
+ cam.shake_until, cam.shake_px = time() + secs, px or 2
45
+ end
46
+
47
+ -- Sets camera() to the view (with any shake).
48
+ function cam.apply()
49
+ local ox, oy = 0, 0
50
+ if cam.shake_until and time() < cam.shake_until then
51
+ local p = cam.shake_px
52
+ ox, oy = flr(rnd(2 * p + 1)) - p, flr(rnd(2 * p + 1)) - p
53
+ end
54
+ camera(flr(cam.x + 0.5) + ox, flr(cam.y + 0.5) + oy)
55
+ end
56
+ -- end lib: cam
@@ -0,0 +1,84 @@
1
+ -- lib: dialog 1, api 1: a text box that types itself out a page at a time: dialog.say("Hi!\nNice day.")
2
+ -- In _update: `if dialog.update() then return end` (the game waits while it's open; A shows the
3
+ -- whole page, then the next). In _draw, last: dialog.draw(). Change dialog.x, y, w, h and the
4
+ -- colors to place it; lines wrap at words to fit.
5
+ local dialog = {x = 4, y = 86, w = 120, h = 38, bg = 1, border = 7, color = 7, speed = 40}
6
+ local pages, page, t0, fresh
7
+
8
+ -- Lines of text no more than cols characters, broken at spaces (and at "\n").
9
+ local function wrap(text, cols)
10
+ local lines = {}
11
+ for para in (text .. "\n"):gmatch("(.-)\n") do
12
+ local line = ""
13
+ for word in para:gmatch("%S+") do
14
+ if line == "" then
15
+ line = word
16
+ elseif #line + 1 + #word <= cols then
17
+ line = line .. " " .. word
18
+ else
19
+ lines[#lines + 1] = line
20
+ line = word
21
+ end
22
+ while #line > cols do
23
+ lines[#lines + 1] = line:sub(1, cols)
24
+ line = line:sub(cols + 1)
25
+ end
26
+ end
27
+ lines[#lines + 1] = line
28
+ end
29
+ return lines
30
+ end
31
+
32
+ -- Opens the box. text: a string, or a list of strings (each starts a page). done(): called when
33
+ -- the last page is closed.
34
+ function dialog.say(text, done)
35
+ local list = type(text) == "table" and text or {text}
36
+ local cols, rows = flr((dialog.w - 8) / 4), math.max(1, flr((dialog.h - 6) / 6))
37
+ pages = {}
38
+ for _, t in ipairs(list) do
39
+ local lines = wrap(tostring(t), cols)
40
+ for i = 1, #lines, rows do
41
+ pages[#pages + 1] = table.concat(lines, "\n", i, math.min(i + rows - 1, #lines))
42
+ end
43
+ end
44
+ page, t0, fresh, dialog.done = 1, time(), true, done
45
+ end
46
+
47
+ function dialog.active()
48
+ return pages ~= nil
49
+ end
50
+
51
+ local function shown()
52
+ return math.min(#pages[page], flr((time() - t0) * dialog.speed))
53
+ end
54
+
55
+ -- Call once a frame in _update; true while the box is open (and on the frame it closes).
56
+ function dialog.update()
57
+ if not pages then return false end
58
+ if fresh then fresh = false return true end -- the press that opened it isn't one for it
59
+ if btnp(4) then
60
+ if shown() < #pages[page] then
61
+ t0 = time() - #pages[page] / dialog.speed -- all of this page at once
62
+ elseif page < #pages then
63
+ page, t0 = page + 1, time()
64
+ else
65
+ pages = nil
66
+ if dialog.done then dialog.done() end
67
+ return true -- the press that closed it was the box's too (it mustn't open it again)
68
+ end
69
+ end
70
+ return true
71
+ end
72
+
73
+ function dialog.draw()
74
+ if not pages then return end
75
+ local x, y, w, h = dialog.x, dialog.y, dialog.w, dialog.h
76
+ rectfill(x, y, x + w - 1, y + h - 1, dialog.bg)
77
+ rect(x, y, x + w - 1, y + h - 1, dialog.border)
78
+ local n = shown()
79
+ print(pages[page]:sub(1, n), x + 4, y + 4, dialog.color)
80
+ if n >= #pages[page] and time() % 0.8 < 0.5 then
81
+ print(page < #pages and ">" or "*", x + w - 8, y + h - 8, dialog.border)
82
+ end
83
+ end
84
+ -- end lib: dialog
@@ -0,0 +1,53 @@
1
+ -- lib: particles 1, api 1: bursts of sparks, dust and smoke: particles.burst(x, y, 12, {color = 10})
2
+ -- Call particles.update() once a frame in _update and particles.draw() in _draw. At most
3
+ -- particles.max at once (200), so a big burst can't use up a frame's budget.
4
+ local particles = {max = 200}
5
+ local list, last = {}, nil
6
+
7
+ -- n particles from (x, y). o (all optional): color, or colors (a list to pick from); speed (pixels
8
+ -- a second, 40); angle (turns, 0.25 is up) and spread (turns, 1 is every way); life (seconds, 0.6);
9
+ -- gravity (pixels a second, each second: 200 falls); size (a radius; 0 is a pixel).
10
+ function particles.burst(x, y, n, o)
11
+ o = o or {}
12
+ for _ = 1, n do
13
+ if #list >= particles.max then return end
14
+ local a = (o.angle or 0) + (rnd(1) - 0.5) * (o.spread or 1)
15
+ local s = (o.speed or 40) * (0.5 + rnd(0.5))
16
+ list[#list + 1] = {x = x, y = y, vx = cos(a) * s, vy = sin(a) * s, t0 = time(),
17
+ life = (o.life or 0.6) * (0.7 + rnd(0.6)), g = o.gravity or 0,
18
+ c = o.colors and rnd(o.colors) or o.color or 7, r = o.size or 0}
19
+ end
20
+ end
21
+
22
+ function particles.update()
23
+ local now = time()
24
+ local dt = last and math.min(now - last, 0.1) or 0
25
+ last = now
26
+ local i = 1
27
+ while i <= #list do
28
+ local p = list[i]
29
+ if now - p.t0 >= p.life then
30
+ list[i] = list[#list] -- the last one takes its place: order doesn't matter
31
+ list[#list] = nil
32
+ else
33
+ p.vy = p.vy + p.g * dt
34
+ p.x, p.y = p.x + p.vx * dt, p.y + p.vy * dt
35
+ i = i + 1
36
+ end
37
+ end
38
+ end
39
+
40
+ function particles.draw()
41
+ for _, p in ipairs(list) do
42
+ if p.r > 0 then circfill(p.x, p.y, p.r, p.c) else pset(p.x, p.y, p.c) end
43
+ end
44
+ end
45
+
46
+ function particles.count()
47
+ return #list
48
+ end
49
+
50
+ function particles.clear()
51
+ list = {}
52
+ end
53
+ -- end lib: particles
package/libs/state.lua ADDED
@@ -0,0 +1,31 @@
1
+ -- lib: state 1, api 1: scenes (a title, the game, game over), each with its own update and draw
2
+ -- A scene is a table with any of enter(...), update(), draw() and leave(). state.go(scene, ...)
3
+ -- switches; call state.update() in _update and state.draw() in _draw.
4
+ local state = {}
5
+ local current, since = nil, 0
6
+
7
+ -- Leaves the scene there is and enters this one (enter gets the other arguments).
8
+ function state.go(scene, ...)
9
+ if current and current.leave then current.leave() end
10
+ current, since = scene, time()
11
+ if scene.enter then scene.enter(...) end
12
+ end
13
+
14
+ function state.update()
15
+ if current and current.update then current.update() end
16
+ end
17
+
18
+ function state.draw()
19
+ if current and current.draw then current.draw() end
20
+ end
21
+
22
+ -- Whether this is the scene now.
23
+ function state.is(scene)
24
+ return current == scene
25
+ end
26
+
27
+ -- Seconds since this scene began (for a title that fades in, a "press A" that waits a moment).
28
+ function state.time()
29
+ return time() - since
30
+ end
31
+ -- end lib: state
package/libs/timer.lua ADDED
@@ -0,0 +1,47 @@
1
+ -- lib: timer 1, api 1: run code later, or every so often: timer.after(2, open_door), timer.every(0.5, blink)
2
+ -- Call timer.update() once a frame, in _update. It goes by time(), so it holds at any frame rate.
3
+ local timer = {}
4
+ local list = {}
5
+
6
+ -- Calls fn() once, secs seconds from now. Returns a handle for timer.cancel.
7
+ function timer.after(secs, fn)
8
+ local h = {at = time() + secs, fn = fn}
9
+ list[#list + 1] = h
10
+ return h
11
+ end
12
+
13
+ -- Calls fn() every secs seconds, `times` times (forever if left out).
14
+ function timer.every(secs, fn, times)
15
+ local h = {at = time() + secs, every = secs, fn = fn, left = times}
16
+ list[#list + 1] = h
17
+ return h
18
+ end
19
+
20
+ function timer.cancel(h)
21
+ h.cancelled = true
22
+ end
23
+
24
+ -- Cancels them all (starting a level over, say).
25
+ function timer.clear()
26
+ for _, h in ipairs(list) do h.cancelled = true end
27
+ end
28
+
29
+ function timer.update()
30
+ local now = time()
31
+ local i = 1
32
+ while i <= #list do
33
+ local h = list[i]
34
+ if not h.cancelled and now >= h.at then
35
+ h.fn()
36
+ if h.every and (h.left == nil or h.left > 1) then
37
+ h.left = h.left and h.left - 1
38
+ h.at = h.at + h.every
39
+ if h.at < now then h.at = now + h.every end -- after a long pause, no burst of catching up
40
+ else
41
+ h.cancelled = true
42
+ end
43
+ end
44
+ if h.cancelled then table.remove(list, i) else i = i + 1 end
45
+ end
46
+ end
47
+ -- end lib: timer
package/libs/tween.lua ADDED
@@ -0,0 +1,90 @@
1
+ -- lib: tween 1, api 1: move numbers smoothly over time: tween.to(obj, 0.5, {x = 100}, ease.out_back)
2
+ -- tween.to changes fields of any table over a number of seconds, along an easing curve; call
3
+ -- tween.update() once a frame, in _update. It goes by time(), so it holds at any frame rate.
4
+ local ease = {}
5
+ function ease.linear(t) return t end
6
+ function ease.in_quad(t) return t * t end
7
+ function ease.out_quad(t) return t * (2 - t) end
8
+ function ease.in_out_quad(t)
9
+ if t < 0.5 then return 2 * t * t end
10
+ return -1 + (4 - 2 * t) * t
11
+ end
12
+ function ease.out_cubic(t)
13
+ t = t - 1
14
+ return t * t * t + 1
15
+ end
16
+ -- Goes a little past the end, then settles back.
17
+ function ease.out_back(t)
18
+ t = t - 1
19
+ return t * t * (2.70158 * t + 1.70158) + 1
20
+ end
21
+ -- Bounces at the end, like a dropped ball.
22
+ function ease.out_bounce(t)
23
+ if t < 1 / 2.75 then return 7.5625 * t * t end
24
+ if t < 2 / 2.75 then t = t - 1.5 / 2.75 return 7.5625 * t * t + 0.75 end
25
+ if t < 2.5 / 2.75 then t = t - 2.25 / 2.75 return 7.5625 * t * t + 0.9375 end
26
+ t = t - 2.625 / 2.75
27
+ return 7.5625 * t * t + 0.984375
28
+ end
29
+ -- Springs past the end and wobbles back.
30
+ function ease.out_elastic(t)
31
+ if t <= 0 or t >= 1 then return t end
32
+ return 2 ^ (-10 * t) * math.sin((t * 10 - 0.75) * (2 * math.pi / 3)) + 1
33
+ end
34
+
35
+ local tween = {}
36
+ local running = {}
37
+
38
+ -- Moves obj's fields to the values in `to` over `secs` seconds, along `easing` (ease.out_quad if
39
+ -- left out), then calls done(obj) if given. A field of obj that's already moving stops moving there
40
+ -- and goes from where it is now. Returns a handle for tween.cancel.
41
+ function tween.to(obj, secs, to, easing, done)
42
+ local t = {obj = obj, t0 = time(), secs = secs, from = {}, to = {}, easing = easing or ease.out_quad, done = done}
43
+ for k, v in pairs(to) do
44
+ for _, o in ipairs(running) do
45
+ if o.obj == obj then o.to[k] = nil end
46
+ end
47
+ t.from[k], t.to[k] = obj[k], v
48
+ end
49
+ running[#running + 1] = t
50
+ return t
51
+ end
52
+
53
+ -- Moves everything along; call once a frame.
54
+ function tween.update()
55
+ local now = time()
56
+ local i = 1
57
+ while i <= #running do
58
+ local o = running[i]
59
+ local p = o.secs > 0 and (now - o.t0) / o.secs or 1
60
+ if p >= 1 then
61
+ for k, b in pairs(o.to) do o.obj[k] = b end
62
+ table.remove(running, i)
63
+ if o.done then o.done(o.obj) end
64
+ else
65
+ local e = o.easing(p)
66
+ for k, b in pairs(o.to) do
67
+ local a = o.from[k]
68
+ o.obj[k] = a + (b - a) * e
69
+ end
70
+ i = i + 1
71
+ end
72
+ end
73
+ end
74
+
75
+ -- Stops a tween where it is (its done isn't called).
76
+ function tween.cancel(h)
77
+ for i, o in ipairs(running) do
78
+ if o == h then table.remove(running, i) return end
79
+ end
80
+ end
81
+
82
+ -- Whether any of obj's fields are moving (or anything at all, with no obj).
83
+ function tween.busy(obj)
84
+ if obj == nil then return #running > 0 end
85
+ for _, o in ipairs(running) do
86
+ if o.obj == obj and next(o.to) then return true end
87
+ end
88
+ return false
89
+ end
90
+ -- end lib: tween
package/libs/vec.lua ADDED
@@ -0,0 +1,53 @@
1
+ -- lib: vec 1, api 1: 2D vector math on plain numbers: length, normalize, distance, angle, rotate, lerp
2
+ -- Vectors here are two numbers, x and y, never tables, so nothing is allocated each frame.
3
+ -- Angles are in turns (1 is a full circle), as sin() and cos() take them, and y grows downwards:
4
+ -- vec.from(0.25) is 0, -1 (up the screen).
5
+ local vec = {}
6
+
7
+ -- The length of (x, y).
8
+ function vec.len(x, y)
9
+ return math.sqrt(x * x + y * y)
10
+ end
11
+
12
+ -- (x, y) scaled to length 1; (0, 0) stays (0, 0).
13
+ function vec.norm(x, y)
14
+ local l = math.sqrt(x * x + y * y)
15
+ if l == 0 then return 0, 0 end
16
+ return x / l, y / l
17
+ end
18
+
19
+ -- The distance from (ax, ay) to (bx, by).
20
+ function vec.dist(ax, ay, bx, by)
21
+ local dx, dy = bx - ax, by - ay
22
+ return math.sqrt(dx * dx + dy * dy)
23
+ end
24
+
25
+ -- The angle of (x, y), 0 to 1: 0 right, 0.25 up, 0.5 left, 0.75 down.
26
+ function vec.angle(x, y)
27
+ return (-math.atan(y, x) / (2 * math.pi)) % 1
28
+ end
29
+
30
+ -- A vector at angle a, len long (1 if left out).
31
+ function vec.from(a, len)
32
+ len = len or 1
33
+ return cos(a) * len, sin(a) * len
34
+ end
35
+
36
+ -- (x, y) turned by a (turns; 0.25 is a quarter turn the way vec.angle counts).
37
+ function vec.rot(x, y, a)
38
+ local c, s = cos(a), sin(a)
39
+ return x * c - y * s, x * s + y * c
40
+ end
41
+
42
+ -- The point t of the way from (ax, ay) to (bx, by) (t 0 to 1).
43
+ function vec.lerp(ax, ay, bx, by, t)
44
+ return ax + (bx - ax) * t, ay + (by - ay) * t
45
+ end
46
+
47
+ -- (x, y), shortened to at most max long.
48
+ function vec.limit(x, y, max)
49
+ local l = math.sqrt(x * x + y * y)
50
+ if l > max then return x / l * max, y / l * max end
51
+ return x, y
52
+ end
53
+ -- end lib: vec
package/luanette.mjs CHANGED
@@ -9,6 +9,8 @@
9
9
  // luanette song CART.lua [--from N] --wav OUT.wav its song
10
10
  // luanette link CART.lua [--name NAME] [--play] a playground share link (--play: opens in play mode)
11
11
  // luanette new NAME [--template blank|platformer|shooter|puzzle]
12
+ // luanette libs the libraries a cart can include
13
+ // luanette add LIB... CART.lua put libraries in a cart (or update them)
12
14
  // luanette mcp an MCP server over stdio (for agents)
13
15
  //
14
16
  // docs/cli.md has the details and the input script's format.
@@ -26,11 +28,12 @@ const find = (...tries) => tries.map((t) => path.join(HERE, t)).find((p) => fs.e
26
28
  const ENGINE = find("engine/luanette-node.js", "../web/build/luanette-node.js");
27
29
  const LIB = find("lib", "../web");
28
30
  const TEMPLATES = find("templates", "../templates");
31
+ const LIBS = find("libs", "../libs");
29
32
  export const PLAYGROUND = "https://geofresh.github.io/luanette/";
30
33
  const Gif = require(path.join(LIB, "gif.js"));
31
34
  const Tables = require(path.join(LIB, "tables.js"));
32
35
 
33
- export const BUDGET = 30000, MAX_SOURCE = 32 * 1024, API_VERSION = 5;
36
+ export const BUDGET = 30000, MAX_SOURCE = 32 * 1024, API_VERSION = 6;
34
37
  const W = 410, H = 502; // the glass
35
38
  const BUTTONS = { left: 1, right: 2, up: 4, down: 8, a: 16, b: 32 };
36
39
 
@@ -220,6 +223,9 @@ export async function check(src, name, opts = {}) {
220
223
  else if (api < 2 && /\bhd\s*\./.test(code)) warn("it uses the full-color layer (API 2): add '-- api: 2' to its header, so a board with older firmware says it needs an update instead of failing at its first hd call");
221
224
  if (api < 4 && /^\s*\[?\d*\]?\s*=?\s*"[0-9a-fA-F]+\s+[0-9a-fA-F]/m.test(code.split(/^SPRITES\s*=/m)[1] || "")) warn("its sprites have spaces between rows (API 4): add '-- api: 4' to its header, so a board with older firmware says it needs an update instead of failing to draw them");
222
225
  if (api < 5 && /^\s*MAP\s*=/m.test(code)) warn("it has a map (API 5): add '-- api: 5' to its header, so a board with older firmware says it needs an update instead of failing at its first map call");
226
+ // API 6, by what the source shows (check.py also sees sprites past 63 and spr's blocks as they run)
227
+ const api6 = [[/\bsspr\s*\(/, "sspr"], [/\bpalt\s*\(/, "palt"], [/\bf(get|set)\s*\(|^\s*FLAGS\s*=/m, "flags"]].filter(([re]) => re.test(code)).map(([, w]) => w);
228
+ if (api < 6 && api6.length) warn(`it uses API 6 (${api6.join(", ")}): add '-- api: 6' to its header, so a board with older firmware says it needs an update instead of failing at its first call`);
223
229
  if (api < 4 && run.log.length) warn("it calls log() (API 4): add '-- api: 4' to its header, so a board with older firmware says it needs an update instead of failing at its first log call");
224
230
  if (api < 3 && /\bmusic\s*\(|^\s*(SOUNDS|MUSIC)\s*=/m.test(code)) warn("it has its own sounds or music (API 3): add '-- api: 3' to its header, so a board with older firmware says it needs an update instead of failing at its first sfx or music call");
225
231
  if (!full) warn("it shows in the 384 x 384 window, about 30% of the glass left dark: add '-- canvas: full' (and '-- api: 2') and use the whole glass, laid out from caps(): the picture keeps its window, and the layer has the bands above and below it for a title, the score, what to do (docs/DESIGN.md, 1)");
@@ -387,6 +393,32 @@ export function newCart(name, template = "blank") {
387
393
  if (!fs.existsSync(file)) throw new Error(`no template ${template}: ${fs.readdirSync(TEMPLATES).map((f) => f.replace(".lua", "")).join(", ")}`);
388
394
  return fs.readFileSync(file, "utf8").replace(/^-- title: .*$/m, "-- title: " + name);
389
395
  }
396
+ // ---- libraries (libs/*.lua): code a cart includes, between `-- lib:` and `-- end lib:` lines ----------
397
+ const libText = (name) => {
398
+ const file = path.join(LIBS, name + ".lua");
399
+ if (!/^\w+$/.test(name) || !fs.existsSync(file)) throw new Error(`no library ${name}: ${libNames().join(", ")}`);
400
+ return fs.readFileSync(file, "utf8");
401
+ };
402
+ const libNames = () => fs.readdirSync(LIBS).filter((f) => f.endsWith(".lua")).map((f) => f.replace(".lua", "")).sort();
403
+ // Each one: name, version, the API it needs, what it does, and the bytes it adds to a cart (comments
404
+ // don't count, as for the cart's own code).
405
+ export async function libraries() {
406
+ const e = await loadEngine();
407
+ return libNames().map((n) => {
408
+ const text = libText(n);
409
+ return { ...Tables.libInfo(text), bytes: Buffer.byteLength(e.shipped(text).trim()) };
410
+ });
411
+ }
412
+ // The cart's source with these libraries in it, each in place of the version it has, if any.
413
+ export function addLibraries(src, names) {
414
+ const added = [];
415
+ for (const n of names) {
416
+ const r = Tables.addLib(src, libText(n));
417
+ src = r.src;
418
+ added.push({ name: n, version: Tables.libInfo(libText(n)).version, was: r.was || null });
419
+ }
420
+ return { src, added };
421
+ }
390
422
  export const templates = () => fs.readdirSync(TEMPLATES).filter((f) => f.endsWith(".lua")).map((f) => f.replace(".lua", ""));
391
423
 
392
424
  // ---- the command line ------------------------------------------------------------------------------
@@ -401,7 +433,7 @@ function args(list) {
401
433
  }
402
434
  return { pos, opt };
403
435
  }
404
- const USAGE = fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 15).map((l) => l.replace(/^\/\/ ?/, "")).join("\n");
436
+ const USAGE = fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 17).map((l) => l.replace(/^\/\/ ?/, "")).join("\n");
405
437
  const readCart = (file) => {
406
438
  if (!file || !fs.existsSync(file)) throw new Error(`no cart ${file || "given"}`);
407
439
  return { src: fs.readFileSync(file, "utf8"), name: path.basename(file, ".lua") };
@@ -444,6 +476,15 @@ async function main() {
444
476
  if (fs.existsSync(file)) throw new Error(`${file} is there already`);
445
477
  fs.writeFileSync(file, newCart(name, opt.template || "blank"));
446
478
  process.stdout.write(`wrote ${file} (from the ${opt.template || "blank"} template)\n`);
479
+ } else if (cmd === "libs") {
480
+ print(await libraries());
481
+ } else if (cmd === "add") {
482
+ const file = pos[pos.length - 1];
483
+ if (pos.length < 2 || !file.endsWith(".lua")) throw new Error("add LIB... CART.lua: the libraries, then the cart");
484
+ const { src } = readCart(file);
485
+ const r = addLibraries(src, pos.slice(0, -1));
486
+ fs.writeFileSync(file, r.src);
487
+ print({ ok: true, cart: file, added: r.added });
447
488
  } else if (cmd === "mcp") {
448
489
  await (await import("./mcp.mjs")).serve();
449
490
  } else {
package/mcp.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // `luanette mcp`: the command-line tool as an MCP server over stdio, for coding agents. JSON-RPC 2.0, a
2
2
  // message a line (the MCP stdio transport), no dependencies. Its tools check a cart, run it with input
3
- // and hand back what it drew as images, describe and render its sounds, make a share link, and look
4
- // up the API. Requests are handled one at a time (the engine is one).
3
+ // and hand back what it drew as images, describe and render its sounds, make a share link, look up the
4
+ // API, and add libraries. Requests are handled one at a time (the engine is one).
5
5
  //
6
6
  // claude mcp add luanette -- npx -y luanette mcp
7
7
  import fs from "node:fs";
@@ -52,6 +52,8 @@ const TOOLS = [
52
52
  { name: "new_cart", description: "A new cart's source from a template (blank, platformer, shooter, puzzle): full-screen, with sprites, sounds and log() to build on.",
53
53
  inputSchema: { type: "object", required: ["name"], properties: { name: { type: "string", description: "a-z, 0-9, _ and -" },
54
54
  template: { type: "string", enum: ["blank", "platformer", "shooter", "puzzle"] } } } },
55
+ { name: "add_library", description: "Puts tested library code into a cart (or updates it): actor (moving through the map, stopping at solid cells), anim (sprite animations), cam (a following camera), dialog (a text box), particles, state (scenes), timer, tween (easing), vec (2D math). With no library, lists them with what each adds in bytes. Use these instead of writing your own. The library's comments say how to call it. With path, the file is written.",
56
+ inputSchema: { type: "object", properties: { ...CART, library: { type: "array", items: { type: "string" }, description: "The libraries to add, by name" } } } },
55
57
  ];
56
58
 
57
59
  function cartOf(a) {
@@ -125,6 +127,13 @@ const handlers = {
125
127
  }
126
128
  return { content: [text(API.map((x) => x.name).join(" "))] };
127
129
  },
130
+ async add_library(a) {
131
+ if (!a.library || !a.library.length) return { content: [text(await L.libraries())] };
132
+ const { src } = cartOf(a);
133
+ const r = L.addLibraries(src, a.library);
134
+ if (a.path) fs.writeFileSync(path.resolve(a.path), r.src);
135
+ return { content: [text({ added: r.added, wrote: a.path || undefined }), ...(a.path ? [] : [text(r.src)])] };
136
+ },
128
137
  async new_cart(a) {
129
138
  return { content: [text(L.newCart(a.name, a.template || "blank"))] };
130
139
  },
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "luanette",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Check, run, hear and share carts for Luanette, a Lua game console for ESP32-S3 boards: the console's own engine in Node, nothing to build",
5
5
  "bin": { "luanette": "luanette.mjs" },
6
- "files": ["luanette.mjs", "pyrandom.mjs", "mcp.mjs", "engine/", "lib/", "templates/", "api.json", "README.md", "LICENSE"],
6
+ "files": ["luanette.mjs", "pyrandom.mjs", "mcp.mjs", "engine/", "lib/", "libs/", "templates/", "api.json", "README.md", "LICENSE"],
7
7
  "engines": { "node": ">=18" },
8
8
  "license": "MIT",
9
9
  "homepage": "https://geofresh.github.io/luanette/",
@@ -1,7 +1,7 @@
1
1
  -- title: Platformer
2
2
  -- by: you
3
3
  -- about: run and jump across a level three screens wide, and pick up every coin
4
- -- api: 5
4
+ -- api: 6
5
5
  -- canvas: full
6
6
 
7
7
  -- sprites (the playground's sprite editor writes this table)
@@ -11,6 +11,12 @@ SPRITES = {
11
11
  [3] = "bbbbbbbb b3bbb3bb 33b3b333 43334334 44444444 44544444 44444454 44444444", -- grass
12
12
  [4] = "44444444 44544444 44444454 44444444 45444444 44444544 44444444 44454444", -- dirt
13
13
  }
14
+ -- sprite flags: fget(n, f) (the playground's sprite editor writes this table)
15
+ FLAGS = {
16
+ [2] = 2, -- flags 1
17
+ [3] = 1, -- flags 0
18
+ [4] = 1, -- flags 0
19
+ }
14
20
  -- sounds (the playground's sound editor writes this table)
15
21
  SOUNDS = {
16
22
  [8] = "03 0000 18050 1f051 24045", -- jump
@@ -54,10 +60,18 @@ end
54
60
  local GRAVITY, JUMP, SPEED = 0.25, -3.6, 1.4
55
61
  local me, score, left, round, cam = {}, 0, 0, 0, 0
56
62
 
57
- -- A wall or floor at pixel (x, y): grass or dirt in that map cell.
63
+ -- Flag 0 is solid (grass, dirt) and flag 1 a coin: set in the Sprites tab, read with fget().
64
+ local SOLID, COIN = 0, 1
65
+
66
+ -- Whether map cell (cx, cy) holds a sprite with flag f (an empty cell has none).
67
+ local function has(cx, cy, f)
68
+ local t = mget(cx, cy)
69
+ return t ~= nil and fget(t, f)
70
+ end
71
+
72
+ -- A wall or floor at pixel (x, y).
58
73
  local function solid(x, y)
59
- local t = mget(flr(x / 8), flr(y / 8))
60
- return t == 3 or t == 4
74
+ return has(flr(x / 8), flr(y / 8), SOLID)
61
75
  end
62
76
 
63
77
  local function start()
@@ -66,7 +80,7 @@ local function start()
66
80
  left = 0
67
81
  for y = 0, h - 1 do
68
82
  for x = 0, w - 1 do
69
- if mget(x, y) == 2 then left = left + 1 end
83
+ if has(x, y, COIN) then left = left + 1 end
70
84
  end
71
85
  end
72
86
  me = {x = 8, y = 96, vx = 0, vy = 0, ground = false}
@@ -100,7 +114,7 @@ function _update()
100
114
  me.y = ny
101
115
  -- a coin in the cell at the player's middle
102
116
  local cx, cy = flr((me.x + 4) / 8), flr((me.y + 4) / 8)
103
- if mget(cx, cy) == 2 then
117
+ if has(cx, cy, COIN) then
104
118
  mset(cx, cy)
105
119
  score, left = score + 1, left - 1
106
120
  sfx(9)