[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-3525":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":23,"hasPages":23,"topics":25,"createdAt":10,"pushedAt":10,"updatedAt":34,"readmeContent":35,"aiSummary":36,"trendingCount":16,"starSnapshotCount":16,"syncStatus":37,"lastSyncTime":38,"discoverSource":39},3525,"usagi","brettchalupa\u002Fusagi","brettchalupa","A simple 2D game engine for rapid prototyping with Lua, featuring live reload and cross-platform export; this repo is a mirror anddevelopment happens at: https:\u002F\u002Fcodeberg.org\u002Fbrettchalupa\u002Fusagi","https:\u002F\u002Fusagiengine.com",null,"Rust",768,33,5,6,0,25,115,657,75,8.59,"The Unlicense",false,"main",[26,27,28,29,30,31,32,33],"game-development","game-engine","game-engine-2d","love2d","lua","pico-8","rust-game-dev","usagi-engine","2026-06-12 02:00:51","\u003Cimg alt=\"Usagi Logo: pixel art bunny, Usagi Engine - Rapid 2D Prototyping\" src=\"\u002Fwebsite\u002Fcard-logo.png\" \u002F>\n\n# Usagi - Simple 2D Game Engine for Rapid Prototyping\n\nUsagi is a 2D game engine for making pixel art games in **Lua** 5.5. It features\nlive reload, single-command cross-platform export, and a pause menu with input\nremapping built in.\n\nUsagi is free software made by [Brett Chalupa](https:\u002F\u002Fbrettmakesgames.com) and\ndedicated to the public domain.\n[Support development of the engine by buying me a coffee.](https:\u002F\u002Fwww.buymeacoffee.com\u002Fbrettchalupa)\n\n\u003Cvideo controls crossorigin=\"anonymous\" type=\"video\u002Fmp4\" src=\"https:\u002F\u002Fassets.brettchalupa.com\u002Fusagi.mp4\">\u003C\u002Fvideo>\n\n**Links:** [usagiengine.com](https:\u002F\u002Fusagiengine.com),\n[Discord](https:\u002F\u002Fusagiengine.com\u002Fdiscord),\n[r\u002FUsagiEngine](https:\u002F\u002Freddit.com\u002Fr\u002FUsagiEngine),\n[Quickstart video](https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=0i1wIm6c6Rw),\n[YouTube Playlist](https:\u002F\u002Fwww.youtube.com\u002Fplaylist?list=PL0qDutCc8IQhkbS53etm9xV06XgEb4BEN)\n\n## Install\n\n**Linux, macOS:**\n\n```sh\ncurl -fsSL https:\u002F\u002Fusagiengine.com\u002Finstall.sh | sh\n```\n\n**Windows (PowerShell):**\n\n```powershell\nirm https:\u002F\u002Fusagiengine.com\u002Finstall.ps1 | iex\n```\n\nThe installer fetches the latest release from GitHub, verifies its SHA-256\nchecksum, installs `usagi` to `~\u002F.usagi\u002Fbin\u002F` (or `%USERPROFILE%\\.usagi\\bin\\` on\nWindows), and adds it to `PATH`.\n\nManual download:\n[GitHub Releases](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Freleases\u002Flatest) or\n[itch.io](https:\u002F\u002Fbrettchalupa.itch.io\u002Fusagi)\n\nLatest release: **v1.0.0**.\n\n[View the changelog.](https:\u002F\u002Fusagiengine.com\u002Fchangelog)\n\n![Rotating cube demo](\u002Fwebsite\u002Fdemo.gif)\n\n## Features\n\n- **Live reload.** `usagi dev` watches your code and assets; saves apply without\n  losing game state. Tweak a sprite in your editor and see it update instantly.\n- **One-command export.** `usagi export` packages your game for Linux, macOS,\n  Windows, and the web.\n- **Pause menu, free.** Built-in pause menu with sfx and music volume,\n  fullscreen toggle, and per-game keyboard + gamepad remapping.\n- **Easy save data.** One function to save and load your game state as a Lua\n  table.\n- **Small, fixed API.** You can't do everything, but you've got what you need to\n  make a great 2D game.\n- **Constraints to inspire creativity.** 320x180 default resolution, 16x16\n  default sprite grid, a single `sprites.png` for textures.\n\nBring your own sound effects, sprite editor, and music tools.\n\n![Menu preview showing Continue, Settings, Clear Save Data, Reset Game, and Quit options](\u002Fwebsite\u002Fmenu.png)\n\n## Hello, Usagi\n\nBootstrap a project and start it in dev mode:\n\n```sh\nusagi init my_game\ncd my_game\nusagi dev\n```\n\n`init` writes `main.lua` (with `_init` \u002F `_update` \u002F `_draw` stubs),\n`.luarc.json` for Lua LSP support, `.gitignore`, `meta\u002Fusagi.lua` (API type\nstubs), and `USAGI.md` (a copy of these docs).\n\nEdit `main.lua`, save, and the running game picks up the change without\nrestarting or losing state. Drawing \"Hello, Usagi!\" looks like:\n\n```lua\nfunction _draw(_dt)\n  gfx.clear(gfx.COLOR_BLACK)\n  gfx.text(\"Hello, Usagi!\", 10, 10, gfx.COLOR_WHITE)\nend\n```\n\n## Updating Usagi\n\nReplace the `usagi` binary with a newer release, or run `usagi update` to fetch\nthe latest. Then run `usagi refresh` inside a project to refresh the LSP type\nstubs and embedded docs (`meta\u002Fusagi.lua`, `.luarc.json`, `USAGI.md`). It won't\ntouch `main.lua`.\n\n## Feedback and Issues\n\nOpen a [GitHub issue](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Fissues\u002Fnew\u002Fchoose)\nfor feedback, requests, and bugs. Search first to avoid duplicates.\n\n## Goals and non-goals\n\nUsagi is for rapid 2D pixel-art prototyping in Lua. It's a great fit if you want\nto quickly try out an idea, if you're new to game programming, if you've hit\nPico-8's token limit, or if you want something simpler than Love2D.\n\nIt is **not** a fantasy console or a Love2D replacement. It doesn't target\nmobile or VR, and it isn't built for medium-to-large polished games.\n\n**Why Lua:** small, widely used in game tooling, and powerful enough to stay out\nof your way.\n\n## Project Layout\n\nAn Usagi game is either a single `.lua` file or a directory with a `main.lua` in\nit. Additional `.lua` files anywhere under the project root can be loaded with\nstock Lua's `require`. Optional assets live alongside the source code. Here's\nwhat a folder structure could look like for a multi-file project:\n\n```\nmy_game\u002F\n  main.lua           -- required: your game's entry point\n  sprites.png        -- optional: 16×16 sprite sheet (PNG with alpha)\n  palette.png        -- optional: custom palette (1px tall, one color per pixel)\n  font.png           -- optional: custom font (bake with `usagi font bake`)\n  enemies.lua        -- optional: require \"enemies\"\n  data\u002F\n    level.json       -- optional: JSON data, loadable with `usagi.read_json(\"level.json\")\n    dialog.txt       -- optional: text data, loadable with `usagi.read_text(\"dialog.txt\")\n  scenes\u002F\n    main_menu.lua    -- optional: require \"scenes.main_menu\" - source code can be in folders\n  sfx\u002F               -- optional: .wav files, file stems become sfx names\n    jump.wav\n    coin.wav\n  music\u002F             -- optional: .ogg\u002F.mp3\u002F.wav\u002F.flac, file stems become track names\n    overworld.ogg\n    boss.ogg\n  shaders\u002F           -- optional: post-process GLSL shaders (advanced; see Shaders)\n    crt.fs           -- desktop GLSL 330\n    crt_es.fs        -- web GLSL ES 100\n```\n\n`require \"name\"` resolves to `name.lua` in the project root, falling back to\n`name\u002Finit.lua` if that misses. Dotted names (`require \"world.tiles\"`) become\nslash-separated paths. The same lookup works inside a fused \u002F exported build, so\nmulti-file projects ship as a single binary or `.usagi` with no extra config.\n\nRun with:\n\n- `usagi init path\u002Fto\u002Fnew_game` bootstraps a project (main.lua stub,\n  `.luarc.json`, `.gitignore`, LSP stubs, `USAGI.md` docs).\n- `usagi dev path\u002Fto\u002Fmy_game` for live-reload development (script, sprites, and\n  sfx reload on save; [Reset](#reset) re-runs `_init`).\n- `usagi run path\u002Fto\u002Fmy_game` to run without live-reload.\n- `usagi tools [path]` opens the Usagi tools window (jukebox, tile picker). See\n  the **Tools** section below.\n- `usagi export path\u002Fto\u002Fmy_game` packages a game for distribution: zips for\n  Linux, macOS, Windows, and the web, plus a portable `.usagi` bundle. See the\n  **Export** section below.\n\nYou can also run Usagi commands without a path to have them run in the current\ndirectory, like `usagi dev` or `usagi export`.\n\n## Lua API\n\n**Philosophy:** keep it simple, name things clearly, and prefer fixed function\nsignatures.\n\n**Style**: for Lua, 2 spaces indent with `snake_case` for locals, function\nnames, and table fields. `SCREAMING_SNAKE_CASE` for file-scope constants\n(`local TICK = 0.12`, `gfx.COLOR_*`). Cross-frame globals are **`Capitalized`**.\nThe canonical game-state container is `State`, set inside `_init`. Module\nimports kept as globals are `Player = require(\"player\")`. The shipped\n`.luarc.json` enables `lowercase-global`, so any unguarded lowercase assignment\nat file scope is flagged as an accidental missing `local`. Engine API (`gfx`,\n`input`, `sfx`, `music`, `usagi`) stays lowercase and is exempt from the lint\nvia `meta\u002Fusagi.lua`.\n\n[View the Lua 5.5 docs for full language reference.](https:\u002F\u002Fwww.lua.org\u002Fmanual\u002F5.5\u002F)\n\n### Cheatsheet\n\n```lua\n-- Engine info \u002F config\n\nusagi.GAME_W\nusagi.GAME_H\nusagi.SPRITE_SIZE\nusagi.PLATFORM -- \"web\" | \"macos\" | \"linux\" | \"windows\" | \"unknown\"\nusagi.IS_DEV\nusagi.elapsed\nusagi.measure_text(text)\nusagi.save(t)\nusagi.load()\nusagi.read_json(path) -- read data\u002F\u003Cpath> as a Lua table\nusagi.read_text(path) -- read data\u002F\u003Cpath> as a string\nusagi.to_json(t) -- serialize a Lua table to a JSON string (same shape rules as usagi.save)\nusagi.menu_item(label, callback) -- up to 3; callback `return true` keeps menu open\nusagi.clear_menu_items()\nusagi.toggle_fullscreen() -- flips fullscreen, returns the new state as bool\nusagi.is_fullscreen()\nusagi.quit() -- terminate the main loop (no-op visually on web)\n\n-- Lifecycle callbacks\n\n_config()\n_init()\n_update(dt)\n_draw(dt)\n\n-- Graphics\n\ngfx.clear(color)\ngfx.text(text, x, y, color)\ngfx.text_ex(text, x, y, scale, rotation, color, alpha)\ngfx.rect(x, y, w, h, color)\ngfx.rect_fill(x, y, w, h, color)\ngfx.rect_ex(x, y, w, h, thickness, color)\ngfx.circ(x, y, r, color)\ngfx.circ_fill(x, y, r, color)\ngfx.circ_ex(x, y, r, thickness, color)\ngfx.line(x1, y1, x2, y2, color)\ngfx.line_ex(x1, y1, x2, y2, thickness, color)\ngfx.tri(x1, y1, x2, y2, x3, y3, color)\ngfx.tri_fill(x1, y1, x2, y2, x3, y3, color)\ngfx.px(x, y, color)\ngfx.get_px(x, y) -- read screen pixel: r, g, b, palette_index\ngfx.spr(index, x, y)\ngfx.spr_ex(index, x, y, flip_x, flip_y, rotation, tint, alpha)\ngfx.get_spr_px(index, x, y) -- read sprite-sheet pixel: r, g, b, palette_index\ngfx.sspr(sx, sy, sw, sh, dx, dy)\ngfx.sspr_ex(sx, sy, sw, sh, dx, dy, dw, dh, flip_x, flip_y, rotation, tint, alpha)\ngfx.shader_set(name)\ngfx.shader_uniform(name, value)\n\n-- Palette (PICO-8, 16 colors)\n\ngfx.COLOR_BLACK, gfx.COLOR_DARK_BLUE, gfx.COLOR_DARK_PURPLE, gfx.COLOR_DARK_GREEN\ngfx.COLOR_BROWN, gfx.COLOR_DARK_GRAY, gfx.COLOR_LIGHT_GRAY, gfx.COLOR_WHITE\ngfx.COLOR_RED,   gfx.COLOR_ORANGE,    gfx.COLOR_YELLOW,     gfx.COLOR_GREEN\ngfx.COLOR_BLUE,  gfx.COLOR_INDIGO,    gfx.COLOR_PINK,       gfx.COLOR_PEACH\n\n-- Off-palette pure (255,255,255). Identity tint for spr_ex \u002F sspr_ex.\ngfx.COLOR_TRUE_WHITE\n\n-- Sound\n\nsfx.play(name)\nsfx.play_ex(name, volume, pitch, pan)\nmusic.play(name)\nmusic.loop(name)\nmusic.stop()\nmusic.play_ex(name, volume, pitch, pan, loop)\nmusic.mutate(volume, pitch, pan)\n\n-- Input -- actions\n\ninput.pressed(action)\ninput.held(action)\ninput.released(action)\ninput.mapping_for(action)\ninput.last_source()\n\ninput.LEFT, input.RIGHT, input.UP, input.DOWN\ninput.BTN1, input.BTN2, input.BTN3\ninput.SOURCE_KEYBOARD, input.SOURCE_GAMEPAD\n\n-- Input -- mouse\n\ninput.mouse()\ninput.mouse_held(button)\ninput.mouse_pressed(button)\ninput.mouse_released(button)\ninput.mouse_scroll()\ninput.set_mouse_visible(visible)\ninput.mouse_visible()\n\ninput.MOUSE_LEFT, input.MOUSE_RIGHT, input.MOUSE_MIDDLE\n\n-- Input -- keyboard (bypasses the action keymap; prefer actions for game input)\n\ninput.key_held(key)\ninput.key_pressed(key)\ninput.key_released(key)\n\ninput.KEY_A   .. input.KEY_Z\ninput.KEY_0   .. input.KEY_9\ninput.KEY_F1  .. input.KEY_F12\ninput.KEY_SPACE, KEY_ENTER, KEY_ESCAPE, KEY_TAB, KEY_BACKSPACE, KEY_DELETE\ninput.KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN\ninput.KEY_LSHIFT, KEY_RSHIFT, KEY_LCTRL, KEY_RCTRL, KEY_LALT, KEY_RALT\ninput.KEY_BACKTICK, KEY_MINUS, KEY_EQUAL\ninput.KEY_LBRACKET, KEY_RBRACKET, KEY_BACKSLASH\ninput.KEY_SEMICOLON, KEY_APOSTROPHE, KEY_COMMA, KEY_PERIOD, KEY_SLASH\n\n-- Effects (juice)\n\neffect.hitstop(time)\neffect.screen_shake(time, intensity)\neffect.flash(time, color)\neffect.slow_mo(time, scale)\neffect.stop() -- stop all running effects\n\n-- Util -- math\n\nutil.clamp(v, lo, hi)\nutil.sign(v)\nutil.round(v)\nutil.approach(current, target, max_delta)\nutil.lerp(a, b, t)\nutil.wrap(v, lo, hi)\nutil.flash(t, hz)\nutil.remap(v, start_a, end_a, start_b, end_b)\n\n-- Util -- vectors\n\nutil.vec_normalize(v)\nutil.vec_dist(a, b)\nutil.vec_dist_sq(a, b)\nutil.vec_from_angle(angle, len)\n\n-- Util -- geometry\n\nutil.point_in_rect(p, r)\nutil.point_in_circ(p, c)\nutil.rect_overlap(a, b)\nutil.circ_overlap(a, b)\nutil.circ_rect_overlap(c, r)\n```\n\n### Compound assignment operators\n\nUsagi runs each `.lua` source through a tiny preprocessor before handing it to\nthe Lua VM, adding compound assignment sugar:\n\n| operator | rewrite     |\n| -------- | ----------- |\n| `+=`     | `x = x + y` |\n| `-=`     | `x = x - y` |\n| `*=`     | `x = x * y` |\n| `\u002F=`     | `x = x \u002F y` |\n| `%=`     | `x = x % y` |\n\n```lua\nState.score += 1\nState.timer += dt\n```\n\nLimitations: the rewrite is line-anchored, so `if cond then x += 1 end` is left\nas-is (use longhand). The LHS is duplicated verbatim, so `t[f()] += 1` calls\n`f()` twice.\n\nThe included `.luarc.json` from `usagi init` declares these as nonstandard\nsymbols so the lua-language-server does not underline them as syntax errors.\n\n### Callbacks\n\nDefine any of these as globals for Usagi to call them:\n\n- `_init()` — once at start, and on [Reset](#reset). Initialize `State` (and any\n  other cross-frame globals) here.\n- `_update(dt)` — each frame, before draw. `dt` is seconds since last frame.\n- `_draw(dt)` — each frame, after update. `dt` same as above.\n- `_config()` — optional. Called **once at startup, before the window opens**;\n  must return a config table.\n\n#### `_config`\n\nSupported fields:\n\n- `name`: display name. Drives the window title bar, the macOS `.app` bundle\n  directory (`Sprite Example.app`), the Info.plist `CFBundleName` \u002F\n  `CFBundleDisplayName`, and (after slugging to ASCII kebab-case) the archive\n  filenames + Linux\u002FWindows binary names produced by `usagi export`. Defaults to\n  the project directory name (`examples\u002Fspr\u002Fmain.lua` → \"spr\"); falls back to\n  \"Usagi\" if no path is available.\n- `pixel_perfect` (default `false`): when `true`, the game renders at integer\n  scale multiples only (1×, 2×, 3×, ...) with black letterbox bars filling any\n  leftover window space. When `false`, the game scales at any factor that fits\n  the window while preserving the game's aspect ratio, so bars only appear on\n  the axis with extra room, never distorting the image. The default is `false`\n  because at common fullscreen resolutions (720p, 1080p, 4K) the game's 320×180\n  native size lands on an integer multiple anyway, and it still looks good in\n  windowed mode.\n- `game_id`: reverse-DNS string like `com.brettmakesgames.snake`, namespaces\n  save data and the macOS bundle identifier. Optional.\n- `icon`: 1-based tile index into `sprites.png`, used as the window icon and (on\n  `usagi export --target macos`) the `.app` icon. Optional, defaults to Usagi\n  bunny.\n- `sprite_size` (default `16`): side length, in pixels, of one cell in\n  `sprites.png`. Drives `gfx.spr` indexing, the tilepicker tool's grid, and the\n  window-icon slicer. Your `sprites.png` must use a multiple of this value on\n  both axes; the window icon falls back to the default when the layout doesn't\n  fit. The value also flows into `usagi.SPRITE_SIZE` so Lua code can read the\n  active cell size. Optional.\n- `game_width` (default `320`) and `game_height` (default `180`): override the\n  game's render resolution. The internal render target is sized to these\n  dimensions; the window upscales to fit, preserving aspect ratio. Tested range\n  is roughly 320x180 to 640x360. Outside that, the pause-menu and tools UI are\n  pixel-fixed and may overflow at very small sizes or look sparse at very large\n  ones. Sprite size (`usagi.SPRITE_SIZE`, 16) and the bundled font (5x7) don't\n  scale with the resolution, so a 1280x720 game has tiny sprites and tiny text\n  relative to the screen. The web export templates the canvas backing-store and\n  aspect ratio from the configured resolution, so non-16:9 \u002F non-default games\n  ship correctly with the default shell (no `--web-shell` needed) and embed\n  cleanly in itch at any iframe size. Optional.\n- `pause_menu` (default `true`): when `true`, the engine intercepts Esc \u002F P \u002F\n  Enter \u002F gamepad Start to open the built-in pause overlay. Set to `false` and\n  those keys flow through to user code so the game can roll its own menu with\n  `usagi.menu_item`, `usagi.toggle_fullscreen`, `usagi.quit`, and the\n  `input.key_*` APIs. Disabling also turns off the keyboard remap UI, the Input\n  Tester, and gamepad-driven menu nav (sub-views of the same overlay), and\n  `usagi.menu_item` registrations no longer render. Suitable for keyboard-driven\n  prototypes. Optional.\n\n```lua\nfunction _config()\n  return {\n    name = \"Snake\",\n    pixel_perfect = true,\n    game_id = \"com.example.snake\",\n    icon = 1,\n    -- game_width = 480,   -- optional; default 320\n    -- game_height = 270,  -- optional; default 180\n    -- sprite_size = 32,   -- optional; default 16\n    -- pause_menu = false, -- optional; default true\n  }\nend\n```\n\n`icon` (optional) is a 1-based tile index into your `sprites.png`, same indexing\nas `gfx.spr`. Omitted, the embedded Usagi bunny is used. The chosen tile is\napplied to the game window on Linux\u002FWindows. At `usagi export --target macos`\ntime the same tile is scaled up and packed into `Resources\u002FAppIcon.icns` inside\nthe `.app`, which is what the macOS Dock\u002FFinder pick up.\n\n`_config()` runs before the runtime is fully alive (the window doesn't exist\nyet), so its return value is **read once at startup and cached**. Editing\n`_config()` while the game is running won't update the title or any future\nconfig field on save; restart the session to pick up changes.\n\n### `gfx`\n\nDraws to the screen. Positions are in game-space pixels (320×180). Colors are\npalette slot indices `1..16`; use the named constants.\n\n- `gfx.clear(color)` — fill the screen.\n- `gfx.rect(x, y, w, h, color)` — 1-pixel rectangle outline.\n- `gfx.rect_fill(x, y, w, h, color)` — filled rectangle.\n- `gfx.rect_ex(x, y, w, h, thickness, color)` — rectangle outline with a custom\n  stroke thickness in pixels.\n- `gfx.circ(x, y, r, color)` — 1-pixel circle outline centered at `(x, y)`.\n- `gfx.circ_fill(x, y, r, color)` — filled circle centered at `(x, y)`.\n- `gfx.circ_ex(x, y, r, thickness, color)` — circle outline with a custom stroke\n  thickness. Stroke is centered on the nominal radius, so stacking three\n  `circ_ex(x, y, r, 1, c)` \u002F `circ_ex(x, y, r-1, 1, c)` \u002F\n  `circ_ex(x, y, r-2, 1, c)` calls produces flush concentric rings with no gaps\n  — fixes the rounding-gap issue you get layering plain `gfx.circ` calls at\n  adjacent radii.\n- `gfx.line(x1, y1, x2, y2, color)` — 1-pixel line from `(x1, y1)` to\n  `(x2, y2)`.\n- `gfx.line_ex(x1, y1, x2, y2, thickness, color)` — line with a custom thickness\n  in pixels.\n- `gfx.tri(x1, y1, x2, y2, x3, y3, color)` — 1-pixel triangle outline from three\n  points. For a thicker outline, draw three `gfx.line_ex` calls.\n- `gfx.tri_fill(x1, y1, x2, y2, x3, y3, color)` — filled triangle from three\n  points. Vertex order doesn't matter; the winding is corrected for you so\n  arrows, spaceship nosecones, and the like just draw regardless of how you laid\n  out the points.\n- `gfx.px(x, y, color)` — set a single pixel.\n- `gfx.get_px(x, y)` returns `(r, g, b, palette_index)` for the pixel at\n  `(x, y)` on the most recently rendered frame. `palette_index` is the 1-based\n  slot for an exact RGB match or `nil` for off-palette colors. All four returns\n  are `nil` for off-screen coordinates and on the very first frame (before\n  anything has been drawn). Reads reflect the previous frame's finished image,\n  so they don't see in-progress draws inside the current `_draw`. The classic\n  use is collision-by-color: paint walls into the framebuffer with a known\n  color, then consult `gfx.get_px` on the proposed destination in `_update`.\n- `gfx.text(text, x, y, color)` — bundled monogram font (5×7 pixel font, 12 px\n  line height; see Credits below). Renders the engine's default Latin\u002FCyrillic\u002F\n  Greek glyph set, or your custom font if a `font.png` is present at the project\n  root (see \"Custom fonts\" below). To measure text dimensions, use\n  `usagi.measure_text` — it lives on `usagi` rather than `gfx` because\n  measurement is a pure utility (no render side-effect) and is callable from any\n  callback, including `_init`.\n- `gfx.text_ex(text, x, y, scale, rotation, color, alpha)` — extended `text`:\n  - `scale` (number) — font-size multiplier. **Use integers** (`1`, `2`, `3`)\n    for crisp text since atlas-baked fonts use POINT filtering and integer\n    scales preserve the pixel-art look. Fractional values blur.\n  - `rotation` (number) — radians. `0` is no rotation. Use `math.rad(45)` for\n    literal-degree values. Rotation pivots around the **center** of the\n    unrotated bounding box; `(x, y)` stays the top-left when `rotation = 0`.\n    Useful for juice: wiggling subtitles, tilted labels, score popups.\n  - `alpha` (number) — opacity in `0..1`. `1.0` is opaque, `0.0` is invisible.\n    Use for fade-in\u002Fout, dimmed UI, ghosted previews.\n- `gfx.spr(index, x, y)` — draw the 16×16 sprite at `index` (1 = top-left) from\n  `sprites.png`. Native size, no flips, no rotation, no tint, full opacity.\n- `gfx.spr_ex(index, x, y, flip_x, flip_y, rotation, tint, alpha)` — extended\n  `spr`. All eight args required:\n  - `flip_x` \u002F `flip_y` (boolean) — mirror left\u002Fright or top\u002Fbottom.\n  - `rotation` (number) — radians. `0` is no rotation. Use `math.rad(45)` for\n    literal-degree values. Rotation pivots around the **center** of the sprite;\n    `(x, y)` stays the top-left of the unrotated bounding box.\n  - `tint` (palette color) — multiplied over the sprite. `gfx.COLOR_TRUE_WHITE`\n    is the identity (no recolor). Other colors recolor the sprite (e.g.\n    `gfx.COLOR_RED` for a hit flash). Note that `gfx.COLOR_WHITE` is the Pico-8\n    palette white (`255,241,232`), which is _slightly_ warm and will shift\n    colors a touch; use it intentionally for a paper-aged look, or use\n    `gfx.COLOR_TRUE_WHITE` (off-palette pure white) when you want pixels to pass\n    through unchanged. Multiplicative semantics, so this can't produce a\n    full-white silhouette: for that, use a shader or draw a colored rect on top.\n  - `alpha` (number) — opacity in `0..1`. `1.0` is opaque, `0.0` is invisible.\n- `gfx.get_spr_px(index, x, y)` returns `(r, g, b, palette_index)` for a pixel\n  inside the `index` sprite cell on `sprites.png`. `index` is 1-based (same\n  shape as `gfx.spr`); `(x, y)` is the offset inside the cell, with `(0, 0)` as\n  that cell's top-left. All four returns are `nil` for an out-of-range index,\n  out-of-cell coordinates, a project with no `sprites.png`, or a fully\n  transparent source pixel (`gfx.spr` draws alpha-keyed, so a transparent pixel\n  reads as \"nothing here\" rather than as its backing RGB). Unlike `gfx.get_px`,\n  sprite reads are deterministic and unaffected by draw order: useful for\n  pixel-perfect sprite collision and for levels where you paint the layout into\n  the sheet and scan it at startup to spawn entities.\n- `gfx.sspr(sx, sy, sw, sh, dx, dy)` — draw an arbitrary `(sx, sy, sw, sh)`\n  rectangle from `sprites.png` at `(dx, dy)` at original size.\n- `gfx.sspr_ex(sx, sy, sw, sh, dx, dy, dw, dh, flip_x, flip_y, rotation, tint, alpha)`\n  — extended `sspr`: stretches to `(dw, dh)`, flips per the booleans, then\n  rotates \u002F tints \u002F sets alpha. Same semantics as `spr_ex`. All thirteen args\n  required.\n- `gfx.COLOR_BLACK`, `COLOR_DARK_BLUE`, `COLOR_DARK_PURPLE`, `COLOR_DARK_GREEN`,\n  `COLOR_BROWN`, `COLOR_DARK_GRAY`, `COLOR_LIGHT_GRAY`, `COLOR_WHITE`,\n  `COLOR_RED`, `COLOR_ORANGE`, `COLOR_YELLOW`, `COLOR_GREEN`, `COLOR_BLUE`,\n  `COLOR_INDIGO`, `COLOR_PINK`, `COLOR_PEACH` — palette slot indices `1..16`,\n  matching `gfx.spr` and Lua's array convention. The RGB at each slot is the\n  default Pico-8 palette unless a `palette.png` overrides it (see below). The\n  constants are slot indices, not RGB promises: if you swap palettes,\n  `gfx.COLOR_RED` still resolves through slot 9, but its actual color depends on\n  the active palette.\n- `gfx.COLOR_TRUE_WHITE` — slot `0`, pure `(255, 255, 255)`. Off-palette: stays\n  pure white even when a `palette.png` is loaded. Use as the identity tint for\n  `gfx.spr_ex` \u002F `gfx.sspr_ex` when you want sprites to draw with their source\n  colors untouched. The Pico-8 `gfx.COLOR_WHITE` is slightly warm\n  (`255, 241, 232`) and will tint sprites a touch peachy if you pass it as the\n  tint, fine if you want that look, but `gfx.COLOR_TRUE_WHITE` is the no-op.\n  (Indices below `0` or above the active palette's length render as magenta as\n  an obvious \"unknown color\" sentinel.)\n\nThe `_ex` variants pack every power-arg into one fixed signature instead of\ntrailing optionals. With a single `_ex` per primitive there's exactly one\ndecision per draw (\"simple or extended?\"). If you want shorter call sites, write\na thin wrapper.\n\n#### Custom palettes (`palette.png`)\n\nPut a `palette.png` at your project root to override the engine's default Pico-8\npalette. Pixels are read in **row-major** order (left-to-right, top-to-bottom):\n\n- **Any rectangular shape.** A 16x1 strip, 16x2 grid (32 colors), or 4x4 (16\n  colors) all work. Color count = `width × height`. Multi-row is fine for\n  organizing larger palettes.\n- **Each pixel = one slot.** Use lospec.com's \"1px cells\" export rather than the\n  larger cell-block versions (where each color is a 16x16 block of duplicates).\n- **Slot indices are 1-based.** The top-left pixel is slot 1. The `gfx.COLOR_*`\n  constants are `1..16` slot indices into the active palette.\n\nBehavior:\n\n- Missing `palette.png` → engine uses the Pico-8 default (16 colors).\n- Hot-reloads like `sprites.png`. Save a new `palette.png` over the old one and\n  the running game flips colors immediately.\n- Slot indices outside the palette range render as magenta (`255,0,255,255`) —\n  the existing \"unknown color\" sentinel. If your palette has 8 colors,\n  `gfx.COLOR_RED` (slot 9) and higher will be magenta. Define your own constants\n  in Lua for non-default palettes.\n- Bundled into `usagi export` automatically when present.\n- Custom color palettes do not apply to the Pause menu colors.\n\n**Recommended pattern: name your own slots when using a custom color palette.**\nThe built-in `gfx.COLOR_*` constants are named after Pico-8's slot ordering\n(slot 9 = `COLOR_RED`). With a custom palette, slot 9 might be a navy blue or a\nteal. The names don't match the colors anymore. Define your own constants once\nat the top of your project and use them everywhere:\n\n```lua\n-- e.g. for sweetie16\nlocal COLOR = {\n  NIGHT = 1, PURPLE = 2, RED = 3,    ORANGE = 4,\n  YELLOW = 5, LIME = 6,  GREEN = 7,  TEAL = 8,\n  NAVY = 9,  BLUE = 10,  SKY = 11,   CYAN = 12,\n  WHITE = 13, SILVER = 14, GRAY = 15, SHADOW = 16,\n}\n\ngfx.clear(COLOR.NIGHT)\ngfx.rect_fill(x, y, w, h, COLOR.RED)\n```\n\nWorkflow tip: `palette.png` loads directly into Aseprite's palette panel with\none click (\"Edit → Preferences → Palette → Load\"), so the same file drives both\nyour engine colors and the swatches you paint with.\n\nSee\n[`examples\u002Fpalette_swap`](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Ftree\u002Fmain\u002Fexamples\u002Fpalette_swap)\nfor a runnable demo (ships sweetie16, uses a `COLOR` table for its named slots).\n\n#### Custom fonts (`font.png`)\n\nPut a `font.png` at your project root to override the bundled monogram font used\nby `gfx.text` \u002F `gfx.text_ex` \u002F `usagi.measure_text`. The PNG is a baked glyph\natlas with metadata embedded as a zTXt chunk (see \"Baking\" below).\n\nScope of the override is intentionally narrow:\n\n- **Lua-drawn text uses the custom font.** Anything you draw with `gfx.text` or\n  `gfx.text_ex`.\n- **Engine UI uses the bundled font.** Pause menu, FPS overlay, error overlay,\n  tools window. So a wildly-sized custom font can't break engine layout.\n\nThe font's natural line height drives `usagi.measure_text` and the per-glyph\npositioning, so a smaller custom font (e.g., Misaki Gothic 8×8) renders at 8 px\nand a larger one (Silver 5×9) renders at 21 px, both crisp at integer scales.\n\n**Baking a font:**\n\n```bash\nusagi font bake \u003Cfont.ttf> \u003Csize>\n```\n\nExamples:\n\n```bash\n# Drop into the current project (writes font.png in current working directory by default)\nusagi font bake my_font.ttf 12\n\n# Skip the kanji block for a font that covers it\nusagi font bake misaki_gothic.ttf 8 --no-cjk\n\n# Write to a specific path\nusagi font bake silver.ttf 18 --out my_proj\u002Ffont.png\n```\n\nBehavior:\n\n- Pass the font's **natural design size** as the size arg. Pixel fonts only\n  rasterize crisply at the size their designer drew them at; rendering at other\n  sizes goes through FreeType's outline scaler and looks slightly fuzzy. Common\n  sizes: monogram at `15`, Silver at `18`, Misaki Gothic at `8`, Geist Pixel at\n  `16`.\n- The CJK Unified Ideographs block (~21k codepoints) is included by default.\n  Codepoints the font doesn't cover are skipped via the font's cmap, so this\n  costs nothing for non-CJK fonts. Pass `--no-cjk` if you want to skip the block\n  even when present.\n- Output is a single `font.png` with metadata in a zTXt chunk. Drop it next to\n  your `main.lua` and the engine picks it up automatically.\n- Bakes are reproducible: the same TTF + size yields byte-identical output.\n\nBehavior of the project drop-in:\n\n- Missing `font.png` → engine uses the bundled monogram font (current default).\n- Bundled into `usagi export` automatically when present.\n\n**Asian-language support:** the bundled monogram font covers Latin \u002F Cyrillic \u002F\npartial Greek but no CJK. For Japanese, Chinese, or Korean text, grab a pixel\nfont that covers the scripts you need and bake it:\n\n```bash\n# Silver: 5x9-ish with broad European + ~8k CJK ideographs + ~2k Hangul.\n# Download from https:\u002F\u002Fpoppyworks.itch.io\u002Fsilver (CC-BY-4.0).\nusagi font bake Silver.ttf 18\n# Drop the resulting font.png next to your project's main.lua.\n```\n\nSee\n[`examples\u002Fcustom_font`](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Ftree\u002Fmain\u002Fexamples\u002Fcustom_font)\nfor a working Silver-based demo that renders English, Cyrillic, Greek, and\nJapanese on the same screen.\n\n#### Scaling sprites\n\nThere's no scale param on `spr` \u002F `spr_ex` as those are fixed at the native\nsprite size. To draw a sprite scaled, use `sspr_ex` with a destination size that\ndiffers from the source size:\n\n```lua\n-- Draw sprite index 1 (16×16) at 2x scale at (x, y).\nlocal sz = usagi.SPRITE_SIZE\ngfx.sspr_ex(0, 0, sz, sz, x, y, sz * 2, sz * 2, false, false, 0, gfx.COLOR_TRUE_WHITE, 1.0)\n```\n\nIf you find yourself reaching for variants often, wrap them. These three helpers\ncover most games:\n\n```lua\n-- Scaled draw of a source rect on the sheet. Doesn't go through `spr`\n-- indexing — pick the source rect yourself with the TilePicker.\nfunction sspr_scaled(sx, sy, sw, sh, dx, dy, scale)\n  gfx.sspr_ex(\n    sx, sy, sw, sh,\n    dx, dy, sw * scale, sh * scale,\n    false, false, 0, gfx.COLOR_TRUE_WHITE, 1.0\n  )\nend\n\n-- Sprite by 1-based index with rotation around its center, native size.\nfunction spr_rot(index, x, y, rotation)\n  gfx.spr_ex(index, x, y, false, false, rotation, gfx.COLOR_TRUE_WHITE, 1.0)\nend\n\n-- Sprite by 1-based index with a tint applied, native size.\nfunction spr_tinted(index, x, y, tint)\n  gfx.spr_ex(index, x, y, false, false, 0, tint, 1.0)\nend\n```\n\nThe engine intentionally doesn't ship these as every game has slightly different\nconventions (whether scale should be integer-only, whether rotation centers\nsomewhere other than the middle, whether tinted draws also need alpha), and\nforcing one shape on everyone hurts more than it helps. Copy and adapt.\n\n### `input`\n\nAbstract input actions. Each action is a union over keyboard, gamepad buttons,\nand the left analog stick; any connected gamepad fires every action, so the\nSteam Deck's built-in pad and an external pad both work, and hot-swapping is\ntransparent.\n\n- `input.pressed(action)` — true only the frame the action first went down. Use\n  for one-shot actions (fire, jump, menu select).\n- `input.held(action)` — true while the action is held. Use for movement,\n  charging meters, \"hold to skip\" prompts.\n- `input.released(action)` — true only the frame the action first went up. Use\n  for charge-and-release mechanics (jump-on-release, slingshot pull-back).\n\n| Action  | Keyboard        | Gamepad                                          |\n| ------- | --------------- | ------------------------------------------------ |\n| `LEFT`  | arrow left \u002F A  | dpad left \u002F left stick left                      |\n| `RIGHT` | arrow right \u002F D | dpad right \u002F left stick right                    |\n| `UP`    | arrow up \u002F W    | dpad up \u002F left stick up                          |\n| `DOWN`  | arrow down \u002F S  | dpad down \u002F left stick down                      |\n| `BTN1`  | Z \u002F J           | south face (Xbox A, PS Cross), LB                |\n| `BTN2`  | X \u002F K           | east face (Xbox B, PS Circle), RB                |\n| `BTN3`  | C \u002F L           | north + west face (Xbox Y\u002FX, PS Triangle\u002FSquare) |\n\n`BTN1`\u002F`BTN2`\u002F`BTN3` are abstract action buttons. BTN3 binds both the north and\nwest face buttons because either is easier to reach than crossing the diamond\nfrom BTN1's south position.\n\n**Nintendo Switch face-button swap.** When a Switch pad is connected, BTN1 fires\nfrom the A button (east face) and BTN2 from the B button (south face), matching\nNintendo's \"A confirms, B cancels\" convention. Triggers (L\u002FR) and BTN3 are\nunchanged. The swap is automatic via `GetGamepadName`; from your game's\nperspective `input.pressed(input.BTN1)` still means \"primary action.\"\n\n`input.pressed` and `input.released` are edge-detected across keyboard, gamepad\nbuttons, and analog sticks. Tilting the stick past the deadzone fires a single\npress the frame it crosses; releasing fires the frame it falls back inside.\n\n#### Control glyphs (source-aware)\n\nFor UI prompts that adapt to the device the player is using:\n\n- `input.mapping_for(action)`: string label of the active source's primary\n  binding for `action` (e.g. `\"Z\"` on keyboard, `\"A\"` on Xbox, `\"Cross\"` on\n  PlayStation, `\"A\"` on Switch since Nintendo swaps BTN1 to its A button).\n  Gamepad family is auto-detected via `GetGamepadName`. Honors any keymap remap\n  the player has set via the pause menu's Configure Keys flow. Returns `nil` if\n  `action` is unknown or the active source has no binding for it (rare; only\n  after exotic remaps).\n- `input.last_source()`: string `\"keyboard\"` or `\"gamepad\"`, the source that\n  most recently fired any bound action. Switches only when a _bound_ input\n  fires, so menu keys (Esc\u002FEnter) and idle activity don't flip it.\n- `input.SOURCE_KEYBOARD`, `input.SOURCE_GAMEPAD`: the corresponding string\n  constants for comparing against `last_source()`.\n\n```lua\nlocal btn = input.mapping_for(input.BTN1) or \"?\"\ngfx.text(\"Press \" .. btn .. \" to jump\", 10, 10, gfx.COLOR_WHITE)\n```\n\n#### Mouse\n\n- `input.mouse()` — returns `x, y` for the cursor in game-space pixels (so the\n  values line up with `gfx.*` coords regardless of window size or pixel-perfect\n  scaling). When the cursor sits over the letterbox bars the values fall outside\n  `0..usagi.GAME_W` \u002F `0..usagi.GAME_H`, so a bounds check is the idiomatic way\n  to detect \"cursor is off the play area.\" See\n  [`examples\u002Fmouse`](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Fblob\u002Fmain\u002Fexamples\u002Fmouse\u002Fmain.lua).\n- `input.mouse_held(button)` — true while `button` is held.\n- `input.mouse_pressed(button)` — true the frame `button` first went down.\n- `input.mouse_released(button)` — true the frame `button` first went up.\n- `input.mouse_scroll()` — per-frame vertical scroll delta. Returns a number:\n  positive when scrolled up this frame, negative when down, `0` when no scroll.\n  Works the same on a mouse wheel and on a trackpad two-finger swipe. Match on\n  `> 0` \u002F `\u003C 0` rather than `== 1` since trackpads emit fractional values:\n\n  ```lua\n  local s = input.mouse_scroll()\n  if s > 0 then slot = math.max(1, slot - 1) end\n  if s \u003C 0 then slot = math.min(N, slot + 1) end\n  ```\n\n- `input.MOUSE_LEFT`, `input.MOUSE_RIGHT`, `input.MOUSE_MIDDLE` — the supported\n  buttons.\n- `input.set_mouse_visible(visible)` — show or hide the OS cursor over the game\n  window. Callable from `_init` to hide the cursor before the first frame draws\n  (handy for games that render their own cursor sprite).\n- `input.mouse_visible()` — true when the OS cursor is currently shown. Reflects\n  the latest `set_mouse_visible` call synchronously, so toggling reads\n  consistently: `input.set_mouse_visible(not input.mouse_visible())`.\n\n#### Direct keyboard (if you really need it)\n\nFor dev hotkeys (toggling debug overlays, screenshotting, F-key shortcuts) and\nfor keyboard-and-mouse-only games, you can read raw keyboard state by key:\n\n- `input.key_pressed(key)` — true the frame `key` first went down.\n- `input.key_held(key)` — true while `key` is held.\n- `input.key_released(key)` — true the frame `key` first went up.\n\n```lua\nif usagi.IS_DEV and input.key_pressed(input.KEY_F1) then\n  State.show_debug = not State.show_debug\nend\n```\n\n**Use sparingly for gameplay.** These bypass the action\u002Fkeymap system on\npurpose, meaning they don't honor the player's pause-menu key remaps and they\ndon't fire from a gamepad. Anything a player should be able to remap, or that a\ncontroller player needs to reach, belongs on `input.held` \u002F `input.pressed` \u002F\n`input.released` with an abstract action.\n\nAvailable constants (all `input.KEY_*`): letters `A`–`Z`, digits `0`–`9`,\nfunction keys `F1`–`F12`, `SPACE`, `ENTER`, `ESCAPE`, `TAB`, `BACKSPACE`,\n`DELETE`, arrows (`LEFT`, `RIGHT`, `UP`, `DOWN`), modifiers (`LSHIFT`, `RSHIFT`,\n`LCTRL`, `RCTRL`, `LALT`, `RALT`), and punctuation (`BACKTICK`, `MINUS`,\n`EQUAL`, `LBRACKET`, `RBRACKET`, `BACKSLASH`, `SEMICOLON`, `APOSTROPHE`,\n`COMMA`, `PERIOD`, `SLASH`). Numpad and the navigation cluster\n(Insert\u002FHome\u002FEnd\u002FPgUp\u002FPgDn) aren't exposed.\n[Open an issue](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Fissues\u002Fnew) or submit a PR\nif you need them.\n\nRaw gamepad reads (analog sticks, triggers, individual face buttons by index)\nare intentionally not exposed. The abstract `input.held(input.BTN1)` family\ncovers gamepad input; if you need finer-grained control than that, you've likely\noutgrown Usagi. Fork the engine or use Love2D!\n\n### `sfx`\n\n- `sfx.play(name)` — play `sfx\u002F\u003Cname>.wav`. Unknown names silently no-op.\n  Playing a sound while it's already playing restarts it.\n- `sfx.play_ex(name, volume, pitch, pan)` — fire-and-forget with per-call\n  params. Useful for varied one-shot effects without needing to commit extra\n  `.wav` files. All three params required:\n  - `volume` (number) — `0..1` multiplier on the pause-menu sfx volume. `1.0` is\n    identity. Clamped.\n  - `pitch` (number) — pitch multiplier. `1.0` is identity, `0.5` is an octave\n    down, `2.0` is an octave up. Useful with `math.random` for varied footsteps\n    \u002F coin pickups from a single .wav.\n  - `pan` (number) — stereo pan, `-1..1`. `-1` left, `0` center, `1` right.\n    Clamped.\n\n### `music`\n\nBackground music streamed from disk (or the fused bundle). Only one track plays\nat a time; calling `play`, `loop`, or `play_ex` while another is playing stops\nthe old one first.\n\n- `music.play(name)` — play `music\u002F\u003Cname>.\u003Cext>` once and stop at the end.\n- `music.loop(name)` — play and loop forever.\n- `music.stop()` — stop whatever's playing. No-op if nothing is.\n- `music.play_ex(name, volume, pitch, pan, loop)` — play with explicit initial\n  params. `loop` is a boolean (`true` to loop forever, `false` to play once).\n  The other params follow `sfx.play_ex`. The chosen volume \u002F pitch \u002F pan become\n  the initial values that subsequent `music.mutate` calls modulate from.\n- `music.mutate(volume, pitch, pan)` — modulate the **currently playing**\n  track's params in place. Replace semantics: each call sets the absolute\n  values, no stacking. No-op when nothing is playing. Use this for ducking music\n  under dialogue, pitch-warping during hitstun, and fade-outs on death. Volume \u002F\n  pitch \u002F pan ranges match `sfx.play_ex`. The engine doesn't expose getters by\n  design. Track values in your own game state if you want to tween (see\n  `examples\u002Fmusic`).\n\nAll four play \u002F loop \u002F stop \u002F play_ex calls are callable from `_init`, so a\ntitle track can start the moment the window opens (no one-frame gap waiting for\n`_update`).\n\nRecognized extensions: `.ogg`, `.mp3`, `.wav`, `.flac`. **OGG is recommended for\nmusic as they're small and cross-platform.**\n\nThe file stem is the name; `music\u002Fintro.ogg` is `music.play(\"intro\")`. Music\nlives in a separate directory from sfx because the formats and lifetimes differ\n— sfx is loaded fully into memory and one-shotted, music is decoded\nincrementally on the audio thread.\n\n### `util`\n\nDrop-in math and geometry helpers. Pure Lua, no engine state, available as a\nglobal `util` table.\n\nFunctions taking shaped tables (vectors `{x, y}`, rects `{x, y, w, h}`, circles\n`{x, y, r}`) check their args and raise an error pointing at _your_ call site\nwhen a field is missing, so a typo like `util.rect_overlap({x=0, y=0, w=10})`\nfails with `util.rect_overlap: arg 1 table missing or non-numeric field 'h'`\ninstead of a confusing nil-arithmetic explosion deep inside the helper.\n\n**Scalar math:**\n\n- `util.clamp(v, lo, hi)` — clamps `v` into `[lo, hi]`.\n- `util.sign(v)` — returns `-1`, `0`, or `1`. Lua doesn't have this built-in.\n- `util.round(v)` — half-up rounding to nearest integer. Pixel-snap world\n  positions on draw to keep sprites crisp.\n- `util.approach(current, target, max_delta)` — moves `current` toward `target`\n  by at most `max_delta`. Pass a delta scaled by `dt` for frame-rate\n  independence (`util.approach(p.vx, target, accel * dt)`).\n- `util.lerp(a, b, t)` — linear interpolation; `t = 0` → `a`, `t = 1` → `b`,\n  values outside `[0, 1]` extrapolate.\n- `util.wrap(v, lo, hi)` — wraps `v` into `[lo, hi)`. Cycle-safe for negatives.\n- `util.flash(t, hz)` — boolean from time, toggles `hz` times per second.\n- `util.remap(v, start_a, end_a, start_b, end_b)` — maps `v` from\n  `[start_a, end_a]` to `[start_b, end_b]`.\n\n**Vectors:**\n\n- `util.vec_normalize({x, y})` — returns a new unit-length vector. Zero in →\n  zero out (no divide-by-zero).\n- `util.vec_dist(a, b)` — distance between two `{x, y}` points.\n- `util.vec_dist_sq(a, b)` — squared distance, for \"is X closer than Y?\" hot\n  loops where you don't want the sqrt. Compare against `r * r`.\n- `util.vec_from_angle(angle, len?)` — vector at `angle` (radians) with\n  magnitude `len` (default 1). Pair with `math.atan(dy, dx)` to convert any\n  direction into a velocity.\n\n**Geometry overlap:**\n\n- `util.point_in_rect(p, r)` — point-in-rect hit test. Half-open `[x, x+w)` on\n  each axis: top\u002Fleft edges are inside, bottom\u002Fright edges are outside.\n- `util.point_in_circ(p, c)` — point-in-circle hit test. Boundary is outside\n  (matches `circ_overlap` convention).\n- `util.rect_overlap(a, b)` — AABB overlap. Edge-adjacent rects don't overlap.\n- `util.circ_overlap(a, b)` — circle-vs-circle. Tangent circles don't overlap.\n- `util.circ_rect_overlap(c, r)` — circle-vs-rect via closest-point method.\n\n### `usagi`\n\nEngine-level info.\n\n- `usagi.GAME_W`, `usagi.GAME_H` — game render dimensions (320, 180).\n- `usagi.SPRITE_SIZE` — side length, in pixels, of one cell in `sprites.png`\n  (default 16, set via `_config().sprite_size`). Use it for tile-grid math\n  instead of hardcoding 16:\n  `gfx.spr(idx, col * usagi.SPRITE_SIZE, row * usagi.SPRITE_SIZE)`.\n- `usagi.IS_DEV` — `true` when running under `usagi dev`; `false` under\n  `usagi run` and inside exported binaries. Useful for gating debug overlays,\n  dev menus, verbose logging:\n\n  ```lua\n  if usagi.IS_DEV then\n    gfx.text(\"debug\", 0, 0, gfx.COLOR_GREEN)\n  end\n  ```\n\n- `usagi.elapsed` — wall-clock seconds since the session started, updated once\n  per frame before `_update`. Frame-stable (every read in one frame returns the\n  same value). Survives [Reset](#reset); track your own counter from `_init` if\n  you need a per-run timer.\n- `usagi.measure_text(text)` — returns two values, `width, height` in pixels,\n  for `text` rendered in the bundled font. Pure utility (no rendering); call it\n  from `_init` to pre-compute layouts, or from `_update` \u002F `_draw` for dynamic\n  strings.\n\n  ```lua\n  local w, h = usagi.measure_text(\"Game Over\")\n  gfx.text(\"Game Over\", (usagi.GAME_W - w) \u002F 2, (usagi.GAME_H - h) \u002F 2,\n           gfx.COLOR_WHITE)\n  ```\n\n- `usagi.save(t)` — serialize a Lua table as JSON and persist it. Saves are\n  per-game (namespaced by `game_id` in `_config()`) so games made with usagi\n  don't clobber each other.\n- `usagi.load()` — return the previously saved table, or `nil` on first run.\n\n  ```lua\n  function _config()\n    return { name = \"My Game\", game_id = \"com.you.mygame\" }\n  end\n\n  function _init()\n    State = usagi.load() or { score = 0, best = 0 }\n  end\n\n  function _update(dt)\n    -- ... gameplay updates State.score, State.best ...\n    usagi.save(State)  -- call whenever you want to persist\n  end\n  ```\n\n  Save data is one JSON file. Nest your own structure inside it (settings,\n  unlocks, run state). There are no slots at the engine level.\n\n  Where saves live:\n  - Linux: `~\u002F.local\u002Fshare\u002F\u003Cgame_id>\u002Fsave.json`\n  - macOS: `~\u002FLibrary\u002FApplication Support\u002F\u003Cgame_id>\u002Fsave.json`\n  - Windows: `%APPDATA%\\\u003Cgame_id>\\save.json`\n  - Web: `localStorage`, key `usagi.save.\u003Cgame_id>`\n\n  `game_id` is a reverse-DNS string like `com.brettmakesgames.snake`. It's\n  required for save \u002F load but optional for games that never persist anything.\n\n  Native writes are atomic (`save.json.tmp` + rename), so a crash mid-write\n  leaves the previous save intact. JSON values must be representable: tables,\n  strings, numbers, booleans, nil. Functions, userdata, NaN, and circular tables\n  raise an error.\n\n  **Table keys must be either all strings (a map) or a dense `1..n` integer\n  array.** JSON has no integer-keyed map type, so sparse integer keys like\n  `{[6]=1, [7]=2}` and gaps like `{[1]=\"x\", [3]=\"z\"}` raise a clear error\n  instead of silently truncating. If you want a map indexed by integers,\n  stringify the keys (`{[tostring(level)] = time}`); if you want a list, fill\n  `1..n`.\n\n### Loading game data: JSON and text\n\nDrop arbitrary game data (levels, dialog, tunable configs) under a `data\u002F`\ndirectory at your project root. `usagi export` bundles the whole tree, so the\nsame paths resolve identically in dev and in exported builds.\n\n- `usagi.read_json(path)` — reads `data\u002F\u003Cpath>` as JSON and returns a Lua table.\n  JSON arrays come back as 1-indexed Lua arrays; JSON objects come back as\n  tables with string keys. Errors loudly on malformed JSON, missing file, or\n  non-UTF-8 bytes.\n- `usagi.read_text(path)` — reads `data\u002F\u003Cpath>` as a UTF-8 string. Use for\n  hand-rolled formats: CSV grids, dialog scripts, anything you want to parse\n  yourself in Lua.\n\nPaths are forward-slash relative to `data\u002F`. Nested subdirs are fine\n(`data\u002Flevels\u002F01.json` → `usagi.read_json(\"levels\u002F01.json\")`). Backslashes,\nabsolute paths, and `..` segments are rejected at the vfs boundary so a\nmalicious or buggy path can't escape `data\u002F`.\n\n```lua\nfunction _config()\n  return { name = \"Tile Demo\" }\nend\n\n-- Read at the top of the chunk so live reload picks up edits to the JSON\n-- without needing F5. The script re-runs whenever any data file mtime\n-- changes, the same way it does for any .lua file.\nlocal levels = usagi.read_json(\"levels.json\")\nlocal intro  = usagi.read_text(\"dialog\u002Fintro.txt\")\n\nfunction _draw(_dt)\n  gfx.clear(gfx.COLOR_BLACK)\n  gfx.text(intro, 4, 4, gfx.COLOR_WHITE)\n  -- ... iterate `levels` ...\nend\n```\n\nHot reload: any save to a file under `data\u002F` triggers the same script re-run\nthat a `.lua` save does. State globals (capitalized vars set in `_init`) are\npreserved across reloads; if you want a true reset, press F5. Bundled builds\nhave no mtimes, so hot reload is a dev-only convenience; exported games read\nonce from the bundle.\n\nFor CSV, use `read_text` + Lua splitting. A 3-line `string.gmatch` covers the\nsimple-grid case (see `examples\u002Flevel_from_csv\u002F`).\n\n### Encoding a Lua table as JSON\n\n`usagi.to_json(t)` returns a pretty-printed JSON string for any Lua table. It\nshares the validator with `usagi.save`, so the same shape rules apply: keys are\nall strings or a dense `1..n` integer array; functions, userdata, NaN, and\ncycles raise an error with a clear message.\n\nUseful when you want JSON without going through the save file: in-game devtools\noverlays, structured stdout logs, ad-hoc state inspection, or feeding data into\nanother tool. Pair with `usagi.read_json` if you ever want to round-trip; reach\nfor `usagi.dump` instead when you want a Lua table you could `require` or\nforgiving pretty-print that tolerates cycles and mixed-key tables.\n\n```lua\nlocal payload = { score = 200, run = { seed = 42, deaths = 1 } }\nprint(usagi.to_json(payload))\n-- {\n--   \"score\": 200,\n--   \"run\": { \"seed\": 42, \"deaths\": 1 }\n-- }\n```\n\nSee `examples\u002Fto_json.lua` for a runnable demo.\n\n### Effects: hitstop, screen shake, flash, slow-mo\n\nThe `effect.*` module gives you four engine-level juice primitives. Each is a\nsingle call from anywhere in `_init` \u002F `_update` \u002F `_draw`; the engine decays\nthem once per frame and threads them into the right point in the update \u002F render\nloop, so you don't have to plumb shake offsets through your draws or gate\n`_update` on a freeze flag.\n\n```lua\neffect.hitstop(0.06)                     -- freeze _update for 60 ms\neffect.screen_shake(0.3, 4)              -- shake 0.3 s, up to 4 game pixels\neffect.flash(0.1, gfx.COLOR_WHITE)       -- white flash, fades over 100 ms\neffect.slow_mo(1.5, 0.3)                 -- 1.5 s at 30% speed\n```\n\n- **`effect.hitstop(time)`** skips the call to `_update` for `time` seconds.\n  `_draw` still runs so the world stays on screen.\n- **`effect.screen_shake(time, intensity)`** offsets the RT-to-window blit.\n  `intensity` is a max offset in _game pixels_ (try 2-6); the magnitude decays\n  linearly to zero. Overlays drawn outside the world (the engine error overlay,\n  the REC indicator) stay anchored.\n- **`effect.flash(time, color)`** draws a full-screen overlay of palette `color`\n  on top of `_draw`'s output. Alpha decays from opaque to transparent. White on\n  hits, red on damage.\n- **`effect.slow_mo(time, scale)`** multiplies the `dt` passed to `_update` by\n  `scale`. `scale=0.5` is half-speed, `scale=2.0` is double-speed, `scale=0`\n  freezes (use `effect.hitstop` for that intent). The slow_mo timer itself\n  counts down at real wall-clock, so the cinematic always ends on schedule.\n- **`effect.stop()`** ends all currently running effects; useful when\n  transitioning between scenes or states in your game.\n\n**Stacking.** Across all four, longer duration wins; for the magnitude\nparameter, the latest call wins. `effect.screen_shake(0.1, 2)` followed by\n`effect.screen_shake(0.5, 4)` gives 0.5 s at intensity 4. Spam-calling is safe.\n\n**Pause.** When the engine pause overlay is open, effect timers don't tick and\nshake is suppressed under the \"PAUSED\" view, so nothing decays or rattles while\nthe game is held.\n\nSee\n[`examples\u002Feffect.lua`](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Fblob\u002Fmain\u002Fexamples\u002Feffect.lua)\nfor a runnable demo (one key per primitive plus a combo button).\n\n### Shaders (advanced)\n\nPost-process GLSL fragment shaders run as the final pass when the game's render\ntarget is blitted to the window. Use them for CRT effects, palette swaps,\nvignettes, color grading, and so on.\n\nCaptures have a known limitation (see below).\n\nAPI:\n\n- `gfx.shader_set(\"name\")`: activate `shaders\u002F\u003Cname>.fs` (and an optional\n  `shaders\u002F\u003Cname>.vs`).\n- `gfx.shader_set(nil)`: clear the active shader.\n- `gfx.shader_uniform(\"u_name\", v)`: queue a uniform write. `v` may be a number\n  (float) or a 2\u002F3\u002F4-length numeric table (vec2\u002Fvec3\u002Fvec4). Call this every\n  frame inside `_update` or `_draw` for animated values.\n\n```lua\nfunction _init() gfx.shader_set(\"crt\") end\n\nfunction _draw(_dt)\n  gfx.shader_uniform(\"u_time\", usagi.elapsed)\n  gfx.shader_uniform(\"u_resolution\", { usagi.GAME_W, usagi.GAME_H })\n  -- ... your normal gfx.* calls ...\nend\n```\n\n**Cross-platform shader files.** Desktop targets compile GLSL `#version 330`;\nthe web target uses GLSL ES `#version 100` (WebGL 1 \u002F GLES 2). Include two files\nalongside each other to support both:\n\n- `shaders\u002F\u003Cname>.fs`: desktop, `#version 330`, `in`\u002F`out`, `texture(...)`,\n  custom `out vec4 finalColor`.\n- `shaders\u002F\u003Cname>_es.fs`: web, `#version 100`, `precision mediump float;`,\n  `varying`, `texture2D(...)`, `gl_FragColor` output.\n\nWeb prefers `_es.fs` and falls back to `.fs`; desktop is the reverse. If only\none is included, every platform that loads it runs that one. The `fragTexCoord`,\n`fragColor`, and `texture0` inputs are provided by raylib on both targets. See\n`examples\u002Fshader\u002F` for a runnable CRT effect plus a Game Boy palette swap with\nboth variants of each.\n\n**Live reload.** Saving the active shader's `.fs` or `.vs` file rebuilds it\nin-place. Cached uniforms are replayed onto the new shader. Compile errors print\nto the terminal and keep the previous shader live.\n\n**Bundling.** `usagi export` walks `shaders\u002F` and includes every `.fs` \u002F `.vs`\nin the bundle, so shaders work the same in `usagi dev`, `usagi run`, `.usagi`\nfiles, and fused exes on every platform.\n\n**Captures don't include the shader.** F8 \u002F Cmd+F screenshots and F9 \u002F Cmd+G GIF\nrecording read the unshaded game render target, so post-process effects show up\non screen but not in the saved file. Tradeoff: the shader runs at window\nresolution (CRT scanlines look smooth, not blocky) and captures stay at the\ngame's 320x180 grid for clean shareable artifacts. If you need the shader baked\ninto a capture, use your OS's screen recorder or screenshot tool against the\ngame window.\n\nShaders resources:\n\n- [Raylib shaders demo](https:\u002F\u002Fwww.raylib.com\u002Fexamples\u002Fshaders\u002Floader.html?name=shaders_postprocessing)\n- [Raylib shaders source](https:\u002F\u002Fgithub.com\u002Fraysan5\u002Fraylib\u002Fblob\u002Fmaster\u002Fexamples\u002Fshaders\u002Fshaders_postprocessing.c)\n\n### Indexing\n\nSequence-style APIs (`gfx.spr`) are _1-based_ to match Lua conventions\n(`ipairs`, `t[1]`, `string.sub`). `gfx.spr(1, ...)` draws the top-left sprite.\n\nPalette constants are 1-based too: `gfx.COLOR_BLACK` is `1`, `gfx.COLOR_RED` is\n`9`. Pico-8's familiar `0..15` numbering is shifted up by one across the board\nso slot indices double as Lua array indices. Slot `0` and any index above the\nactive palette's length render as a magenta sentinel.\n\n### Randomness\n\nLua's `math.random` is available as-is. Lua auto-seeds its PRNG at startup, so\neach run of `usagi dev` \u002F `usagi run` (and each launch of an exported binary)\nproduces a fresh sequence. No engine call is needed before calling\n`math.random()`.\n\n```lua\nlocal n = math.random(1, 100)   -- integer in [1, 100]\nlocal f = math.random()         -- float in [0, 1)\n```\n\nIf you want a deterministic sequence (replays, tests, repeatable level\ngeneration) call stock Lua's `math.randomseed(n)` from `_init`. See\n[`examples\u002Frng.lua`](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Fblob\u002Fmain\u002Fexamples\u002Frng.lua)\nfor a small demo.\n\n### Coming from Pico-8?\n\nCheck out\n[`.\u002Fexamples\u002Fpico8`](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Ftree\u002Fmain\u002Fexamples\u002Fpico8)\nto see how you can drop in a `pico8.lua`, `require \"pico8\"`, and have a lot of\nthe same functions as Pico-8.\n\nThe Pico-8 shim allows you to write code like in Pico-8:\n\n```lua\n-- check for input\nif btn(0) then\n  State.p.x = State.p.x - State.p.spd * dt\nend\n\n-- draw a sprite from sprites.png\nspr(0, 20, 30)\n```\n\n## Shortcuts\n\n- Press **~** (grave\u002Ftilde) to toggle the FPS overlay. Hidden by default in\n  `dev`.\n- Press **Alt+Enter** to toggle borderless fullscreen. Persists in\n  `settings.json` and applies before the first frame on the next launch. No Lua\n  or `_config` surface by design; the player owns this setting.\n- Press **Esc**, **P**, or gamepad **Start** to pause. The same keys (plus\n  **BTN2**) close the menu. While paused, `_update` is skipped but `_draw` still\n  runs each frame, with the pause overlay rendered on top. Music pauses on menu\n  open and resumes on close.\n- Press **Shift+Esc** in dev mode to quit the game.\n- The engine keeps the last ~5 seconds of gameplay in memory at all times. Press\n  **F9** or **Cmd\u002FCtrl + G** to write that buffer out as a GIF in your user\n  Downloads dir, named `\u003Cgame>-YYYYMMDD-HHMMSS.gif` (where `\u003Cgame>` is the short\n  form of your `_config().game_id`, e.g.\n  `~\u002FDownloads\u002Fsnake-20260101-120000.gif`). Upscaled 2x (640×360) so they read\n  well when embedded online. Rolling buffer: trigger the save after the cool\n  moment, not before. Per-frame timing reflects real frame dt clamped to a 30fps\n  floor, so a game that stutters produces a GIF that plays at the same pace as\n  the game ran.\n- Press **F8** or **Cmd\u002FCtrl + F** to save a PNG screenshot to the same\n  Downloads bucket. Same 2x upscale as the gif recorder, lossless,\n  palette-exact.\n- Press **Shift+M** to toggle audio mute. Volumes flip between `0.0` and the\n  values stored in `settings.json` (both music and sfx default to `1.0` on first\n  boot, then track whatever the player set via the pause menu). Settings live in\n  the same per-game OS data dir as `save.json`; on web they're routed through\n  `localStorage` under `usagi.settings.\u003Cgame_id>`.\n\n### Writing Reload-Friendly Scripts\n\nAll Lua code chunks re-execute on save, so any top-level `local` bindings get\nre-bound on auto reload. A `local State` at module scope would get reset to a\nfresh table on every save and obliterate the running game; it has to be a\nglobal. The pattern:\n\n- **Mutable game state** → a single capitalized global, conventionally `State`,\n  assigned only inside `_init`. `_init` runs once at startup and on\n  [Reset](#reset), so the table outlives reloads. Saved edits keep your\n  in-progress game intact.\n- **Constants** → file-scope `local`. Re-binding to the same value each reload\n  is harmless.\n- **Required modules** → either file-scope `local Foo = require(\"foo\")`, or a\n  capitalized global `Foo = require(\"foo\")` if you want `Foo` reachable from\n  every file without re-requiring. Both work; the global form is convenient for\n  engine-wide tables like `Player`, `Enemy`.\n\nGeneral advice: if you want something to persist across the live reload, put it\ninto `State`. If you're tuning something and want to see it change\nautomatically, leave it inline.\n\nThe `.luarc.json` from `usagi init` enables the `lowercase-global` diagnostic to\ncatch the most common footgun: forgetting `local` and accidentally creating a\nglobal named `score`, `timer`, etc. Capitalize anything you actually mean to\nmake global; lowercase top-level assignments will warn.\n\nSee\n[`examples\u002Fhello_usagi.lua`](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Fblob\u002Fmain\u002Fexamples\u002Fhello_usagi.lua)\nand\n[`examples\u002Finput.lua`](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Fblob\u002Fmain\u002Fexamples\u002Finput.lua)\nfor some examples.\n\n### Reset\n\nUsagi watches the running script file and re-executes it when you save. The new\n`_update` and `_draw` take effect on the next frame. Your current game state can\nbe **preserved** across the reload so you can tweak logic mid-play without\nlosing progress.\n\n- `_init()` is **not** called on a save-triggered live reload.\n\nPress **F5** (or **Ctrl+R** \u002F **Cmd+R**) for a hard reset. The pause menu's\n**Reset Game** item does the same thing. Reset re-runs `_init()` so anything you\nbuild there starts from scratch, while leaving the rest of the session alone.\n\nWhat a reset clears:\n\n- `State` and any other globals you assign in `_init`, since `_init` re-runs.\n- In-flight engine effects: `effect.flash`, `effect.screen_shake`,\n  `effect.hitstop`, `effect.slow_mo`. Cleared before `_init` runs so a fresh\n  game can register new ones.\n- `usagi.menu_item` registrations from Lua. Re-register them inside `_init` if\n  you use them.\n\nWhat a reset leaves alone:\n\n- `usagi.elapsed` keeps counting from session start. Track your own counter from\n  `_init` for a per-run timer.\n- Music and sfx currently playing. Stop them in `_init` if you want silence on\n  reset.\n- On-disk state: save data, pause-menu volumes, fullscreen setting, and keyboard\n  \u002F gamepad remaps.\n- Loaded assets (`sprites.png`, sfx, music).\n- Any Lua state outside `_init`. The VM itself is not torn down, so file-scope\n  locals and globals you assign elsewhere persist across reset unless `_init`\n  overwrites them.\n\nYou can also make use of `usagi.IS_DEV` to set up your `State` on a reset to\nkeep you within a given scene or setup you want to refine quickly.\n\n## Examples\n\n[View the examples on GitHub.](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fusagi\u002Ftree\u002Fmain\u002Fexamples)\n\nThere are a variety of examples exercising the full Usagi API that you can\nbrowse and adapt. Their source is all public domain, so do with them what you\nwant.\n\n[Bomberfrog: Alpha](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fbomberfrog\u002Ftree\u002Falpha.1) is\na finished shoot-em-up made with Usagi that you can reference or use as a\nstarting point for your own game. It includes scene switching, dev-only\nfunctionality, score tracking, and more.\n\n[SokoWorld](https:\u002F\u002Fgithub.com\u002Fbrettchalupa\u002Fsokoworld) is a Sokoban puzzle game\nmade with Usagi with custom level parsing code, scene switching, and save data\ntracking.\n\n## Tools\n\n![Usagi tools window showing the TilePicker selected with monster sprites by Hexany Ives](.\u002Fwebsite\u002Ftools.png)\n\n`usagi tools [path]` opens a 1280×720 window with a tab bar for the available\ntools. The path is optional; pass a project directory (or a `.lua` file) to load\nits `sprites.png` and `sfx\u002F` assets. Without a path the tools open with empty\nstate.\n\nSwitch tools via the tab buttons or with **1** (Jukebox), **2** (TilePicker),\n**3** (SaveInspector), **4** (ColorPalette).\n\nJukebox and TilePicker live-reload their assets: drop a new WAV in `sfx\u002F` or\nsave a new `sprites.png` and the tools pick it up on the next frame.\n\n### Jukebox\n\nLists every `.wav` in `\u003Cproject>\u002Fsfx\u002F` and lets you audition them. Selected\nsounds play automatically on selection change (Pico-8 SFX editor style), so you\ncan just arrow through the list to hear each one.\n\n- **up** \u002F **down** or **W** \u002F **S** to select.\n- **space** or **enter** to replay the current selection.\n- Click a name to select + play.\n- Click the **Play** button in the right pane to replay.\n\n### TilePicker\n\nShows `\u003Cproject>\u002Fsprites.png` with a 1-based grid overlay matching `gfx.spr`.\nClick a tile to copy its index, or right-drag to grab a rectangle for `sspr`.\nThe current selection is shown in the header and highlighted on the sheet.\n\n- **WASD**, hold **middle mouse** and drag, or hold **space** and drag with the\n  left mouse to pan. **Q** \u002F **E** or the **scroll wheel** to zoom out \u002F in\n  (0.5×–20×). Wheel zoom is anchored on the cursor, so the pixel under the mouse\n  stays put. **0** resets the view.\n- **R** toggles the grid and index overlay.\n- **B** cycles the viewport background color (gray \u002F black \u002F white) so tiles\n  stay visible regardless of palette.\n- **Left click** a tile to copy its 1-based `spr` index.\n- **Right click + drag** to select a tile-aligned rectangle and copy\n  `sx,sy,sw,sh` ready to paste into `gfx.sspr(...)`. Drag direction doesn't\n  matter; the rect is normalized and clamped to the sheet.\n- The header shows the current selection and the sheet pixel coords under the\n  cursor as you move it over the image.\n\n### SaveInspector\n\nReads the project's `_config().game_id` and shows the current `save.json`\ncontents alongside the resolved file path. Useful for debugging save formats and\ninspecting state between runs without leaving the editor.\n\n- Save JSON is shown raw; the engine already pretty-prints it on write.\n- **R** or the **Refresh** button rereads the file from disk; the inspector\n  doesn't auto-poll, so hit refresh after the running game has saved.\n- **Clear** deletes the save file. The next `usagi.load()` returns `nil`.\n- **Open in File Manager** reveals the containing directory in the OS default\n  file manager (`xdg-open` on Linux, `open` on macOS, `explorer` on Windows).\n\n### ColorPalette\n\nShows swatches for each of the 16 default colors or your custom `palette.png`\nwith the ability to click to copy the Lua value to your clipboard.\n\n### Bring Your Own Tools\n\nUsagi doesn't include a sprite editor, sound effect generator, or music tracker.\nYou can find assets to use on [opengameart.org](https:\u002F\u002Fopengameart.org\u002F) and\n[itch.io](https:\u002F\u002Fitch.io) or make your own. Here are some tools worth checking\nout that work well with Usagi:\n\n- **Sprite Editors**:\n  - [Aseprite](https:\u002F\u002Fwww.aseprite.org\u002F): an excellent pixel art editor\n  - [Piskel](https:\u002F\u002Fwww.piskelapp.com\u002F): free, online sprite editor\n- **Sound**:\n  - [jsfxr](https:\u002F\u002Fsfxr.me\u002F): 8-bit sound effect generator; download WAVs\n  - [1BITDRAGON](https:\u002F\u002F1bitdragon.com\u002F): an easy-to-use music creation tool\n- **Map Editors**:\n  - [Tiled](https:\u002F\u002Fwww.mapeditor.org\u002F): free and open source map editor with\n    Lua export\n\n## Export\n\n`usagi export \u003Cpath>` packages a game for distribution. Default output is every\nplatform plus a portable bundle:\n\n```\n$ usagi export examples\u002Fsnake\n$ tree export\nexport\n├── snake-linux.zip      # Linux x86_64 fused exe\n├── snake-macos.zip      # macOS arm64 fused exe\n├── snake-windows.zip    # Windows x86_64 fused exe\n├── snake-web.zip        # web export: index.html + usagi.{js,wasm} + game.usagi\n└── snake.usagi          # portable bundle (usagi run snake.usagi)\n```\n\nOr pick one with `--target`:\n\n```\n$ usagi export examples\u002Fsnake --target web\n$ usagi export examples\u002Fsnake --target windows\n$ usagi export examples\u002Fsnake --target bundle\n```\n\n### Cross-Platform Templates\n\nNon-host platforms come from \"runtime templates\" published alongside each Usagi\nrelease. The CLI fetches them on first use, caches them per-OS, and verifies\neach archive against its `sha256` sidecar before extracting.\n\n- **Cache**: Linux `~\u002F.cache\u002Fusagi\u002Ftemplates\u002F`, macOS\n  `~\u002FLibrary\u002FCaches\u002Fcom.usagiengine.usagi\u002Ftemplates\u002F`, Windows\n  `%LOCALAPPDATA%\\usagiengine\\usagi\\cache\\templates\\`.\n- **Inspect \u002F wipe**: `usagi templates list`, `usagi templates clear`.\n- **Force re-download**: `--no-cache`.\n- **Mirror or fork**: set `USAGI_TEMPLATE_BASE` to override the default GitHub\n  Releases base URL.\n\nThe host platform always works offline. Linux x86_64 running\n`usagi export --target linux` (or the linux slice of `--target all`) fuses\nagainst the running binary directly: no cache lookup, no network. First-time\ncross-platform export needs network; subsequent runs are offline.\n\nOverride the template source explicitly:\n\n- `--template-path PATH\u002FTO\u002Fusagi-\u003Cver>-\u003Cos>.{tar.gz|zip}` to point at a loc","Usagi 是一个用于快速原型设计的2D游戏引擎，支持Lua 5.5编程。它具有实时重载功能，允许开发者在不丢失游戏状态的情况下即时查看代码和资源更改的效果；还提供了一键式跨平台导出命令，可以将游戏打包为适用于Linux、macOS、Windows以及Web的版本。此外，Usagi内置了一个带有音效与音乐控制、全屏切换及输入重映射功能的暂停菜单，并简化了游戏数据保存与加载过程。该引擎适合于希望快速开发像素艺术风格2D游戏的小团队或个人开发者使用。",2,"2026-06-11 02:54:42","CREATED_QUERY"]