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 +1 -1
- package/engine/luanette-node.js +0 -0
- package/lib/tables.js +84 -2
- package/libs/actor.lua +63 -0
- package/libs/anim.lua +36 -0
- package/libs/cam.lua +56 -0
- package/libs/dialog.lua +84 -0
- package/libs/particles.lua +53 -0
- package/libs/state.lua +31 -0
- package/libs/timer.lua +47 -0
- package/libs/tween.lua +90 -0
- package/libs/vec.lua +53 -0
- package/luanette.mjs +43 -2
- package/mcp.mjs +11 -2
- package/package.json +2 -2
- package/templates/platformer.lua +20 -6
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"}]}]
|
package/engine/luanette-node.js
CHANGED
|
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
|
|
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
|
package/libs/dialog.lua
ADDED
|
@@ -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 =
|
|
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,
|
|
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,
|
|
4
|
-
//
|
|
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.
|
|
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/",
|
package/templates/platformer.lua
CHANGED
|
@@ -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:
|
|
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
|
-
--
|
|
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
|
-
|
|
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
|
|
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
|
|
117
|
+
if has(cx, cy, COIN) then
|
|
104
118
|
mset(cx, cy)
|
|
105
119
|
score, left = score + 1, left - 1
|
|
106
120
|
sfx(9)
|