{"id":"kyrorblx/lori","name":"lori","scope":"kyrorblx","platform":"roblox","description":"Small Roblox debug UI library","version":"0.1.7","latest":"0.1.7","versions":["0.1.7"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"977da85877cd0f759e3d380288825cea8e464b681d19065f9b3ab1c401183a2e","likes":0,"downloads":1,"install":"forest install kyrorblx/lori","url":"https://forest.dev/p/roblox/kyrorblx/lori","files":"https://api.forest.dev/ai/package/roblox/kyrorblx/lori/files","readme":"<p align=\"center\">\r\n  <img src=\"assets/logo.png\" alt=\"Lori\" width=\"300\">\r\n</p>\r\n\r\n<p align=\"center\">\r\n  Tiny Roblox debug UI for traces, live rows, timings, casts, and graphs.\r\n</p>\r\n\r\n<p align=\"center\">\r\n  <a href=\"#install\">Install</a>\r\n  ·\r\n  <a href=\"#how-it-works\">How It Works</a>\r\n  ·\r\n  <a href=\"#using-it\">Using It</a>\r\n  ·\r\n  <a href=\"#graphs\">Graphs</a>\r\n  ·\r\n  <a href=\"#api\">API</a>\r\n</p>\r\n\r\n## What Is This?\r\n\r\nLori is a little client overlay for debugging while you play.\r\n\r\nIt is for the stuff you normally print, but want to actually see in-game:\r\nFPS, current state, what part you are standing on, what a raycast hit, how long something took, memory, script activity, or any number you want graphed.\r\n\r\n<table align=\"center\">\r\n  <tr>\r\n    <td width=\"50%\" align=\"center\">\r\n      <img src=\"assets/showcase-graphs.gif\" alt=\"Lori graph showcase\">\r\n    </td>\r\n    <td width=\"50%\" align=\"center\">\r\n      <img src=\"assets/showcase-world.gif\" alt=\"Lori world showcase\">\r\n    </td>\r\n  </tr>\r\n  <tr>\r\n    <td colspan=\"2\" align=\"center\">\r\n      <img src=\"assets/showcase-debug.gif\" alt=\"Lori debug showcase\">\r\n    </td>\r\n  </tr>\r\n</table>\r\n\r\n## Install\r\n\r\nPut `Lori.rbxm` in `ReplicatedStorage/Packages/Lori`, then require it from a `LocalScript`.\r\n\r\n```luau\r\nlocal Lori = require(game.ReplicatedStorage.Packages.Lori)\r\n```\r\n\r\nFor roblox-ts/npm:\r\n\r\n```sh\r\nnpm install @kyrorblx/lori\r\n```\r\n\r\n```ts\r\nimport Lori = require(\"@kyrorblx/lori\");\r\n```\r\n\r\nFor Wally:\r\n\r\n```toml\r\n[dependencies]\r\nLori = \"kyrorblx/lori@0.1.3\"\r\n```\r\n*We're still waiting for uplift games to approve us :(, give us a bit for wally!*\r\n\r\nLori is client-side. Put your demo or setup code in `StarterPlayerScripts`, `StarterGui`, or another client place. Server scripts should not mount the overlay because the UI lives in `PlayerGui`.\r\n\r\n## How It Works\r\n\r\nLori has two layers:\r\n\r\n- `Lori.create()` makes an isolated client with its own rows, graphs, theme, config, and template providers.\r\n- `Lori.trace(...)`, `Lori.mount(...)`, and the other direct `Lori.*` calls use one shared default client.\r\n\r\nUse `Lori.create()` for real projects so different systems do not fight over one global overlay.\r\n\r\nMounting creates a `ScreenGui` under `PlayerGui`. After that, traces and graphs create compact UI rows inside the configured stack position. If you call a trace before mounting, Lori mounts itself automatically.\r\n\r\nBy default, `studioOnly = true`, so Lori does nothing in live published servers unless you pass `studioOnly = false`.\r\n\r\n## Using It\r\n\r\nMost of the time you want one Lori client:\r\n\r\n```luau\r\nlocal lori = Lori.create()\r\n\r\nlori:mount(nil, {\r\n\tstudioOnly = false,\r\n\tmaxItems = 8,\r\n})\r\n```\r\n\r\nThen send rows:\r\n\r\n```luau\r\nlori:success(\"demo\", \"loaded\")\r\nlori:warn(\"net\", \"retry\", { count = 2 })\r\nlori:trace(\"weapon\", \"fire\", { ammo = 12 })\r\n```\r\n\r\nRows are built from:\r\n\r\n- `scope`: where it came from, like `weapon`\r\n- `action`: what happened, like `fire`\r\n- `fields`: extra values\r\n- `tone`: color style, like `trace`, `success`, `warn`\r\n\r\nThe displayed row is basically `scope action key=value key=value`. Field order is alphabetical so rows do not jump around when tables are built in a different order.\r\n\r\nUse these for events. If something changes constantly, use a keyed row or a watch.\r\n\r\n## Keyed Rows\r\n\r\nA normal trace makes a new row. A keyed row updates the same row.\r\n\r\n```luau\r\nlori:push({\r\n\tkey = \"player.ground\",\r\n\tscope = \"player\",\r\n\taction = \"standing\",\r\n\ttone = \"phase\",\r\n\tsticky = true,\r\n\tfields = {\r\n\t\tpart = \"Baseplate\",\r\n\t},\r\n})\r\n```\r\n\r\nPush the same key again and it changes that row instead of adding another one.\r\n\r\nThis is useful for state, target, ammo, current room, current floor part, and anything else where there should only be one visible line.\r\n\r\nUse `sticky = true` for rows that should stay until changed or dismissed. Without `sticky`, the row fades out after `duration` seconds.\r\n\r\n## Watches\r\n\r\nWatches are keyed rows where fields can be functions.\r\n\r\n```luau\r\nlori:watch(\"player.stats\", {\r\n\thealth = function()\r\n\t\treturn math.floor(humanoid.Health)\r\n\tend,\r\n\tspeed = function()\r\n\t\treturn math.floor(root.AssemblyLinearVelocity.Magnitude)\r\n\tend,\r\n})\r\n```\r\n\r\nLori keeps refreshing the row. You can also push the same key later with different functions and it will use the new ones.\r\n\r\nWatch fields are called with `pcall`, so one bad field shows `err` instead of killing the overlay. Use `options.interval` to control refresh rate:\r\n\r\n```luau\r\nlori:watch(\"player.stats\", fields, {\r\n\tinterval = 0.25,\r\n\ttone = \"success\",\r\n})\r\n```\r\n\r\n## Timing\r\n\r\nUse `time` when you want to know how long something took.\r\n\r\n```luau\r\nlocal done = lori:time(\"weapon\", \"raycast\")\r\nlocal result = workspace:Raycast(origin, direction, params)\r\ndone({ hit = result and result.Instance.Name or \"none\" })\r\n```\r\n\r\nThere are also cast helpers:\r\n\r\n```luau\r\nlori:raycast(\"gun\", origin, direction, params)\r\nlori:spherecast(\"sensor\", origin, radius, direction, params)\r\nlori:blockcast(\"hitbox\", cframe, size, direction, params)\r\n```\r\n\r\nThey return the normal Roblox result, but Lori records timing for the perf rows.\r\n\r\n`time` is best when you already own the code block. `measure` wraps a callback and rethrows errors after recording the time. Cast helpers are just convenience wrappers around `Workspace` casts.\r\n\r\n## Graphs\r\n\r\nGraphs are for any number over time.\r\n\r\nNot just memory or FPS. Anything numeric works:\r\nraycast time, casts per second, script activity, AI count, ping, queue size, damage, pathfinding time, network messages, whatever.\r\n\r\nCreate one:\r\n\r\n```luau\r\nlocal graph = lori:graph(\"ray ms\", {\r\n\tunit = \"ms\",\r\n\twarnAt = 1,\r\n\tfailAt = 3,\r\n})\r\n```\r\n\r\nPush values:\r\n\r\n```luau\r\ngraph:push(0.42)\r\n```\r\n\r\nThat is it. Lori keeps the last samples and draws the columns.\r\n\r\nExample idea for script activity:\r\n\r\n```luau\r\nlocal activity = lori:graph(\"activity\", { unit = \"/s\", tone = \"phase\" })\r\nlocal count = 0\r\nlocal elapsed = 0\r\n\r\nlocal function recordActivity()\r\n\tcount += 1\r\nend\r\n\r\ngame:GetService(\"RunService\").Heartbeat:Connect(function(dt)\r\n\telapsed += dt\r\n\tif elapsed >= 1 then\r\n\t\tactivity:push(count / elapsed)\r\n\t\tcount = 0\r\n\t\telapsed = 0\r\n\tend\r\nend)\r\n```\r\n\r\nCall `recordActivity()` wherever your system does work.\r\n\r\nGraph options:\r\n\r\n- `tone`: graph color\r\n- `style`: currently only `bars`\r\n- `unit`: text after the number, like `ms`, `fps`, `/s`\r\n- `range`: fixed `{ min, max }`; without it Lori auto-ranges\r\n- `warnAt`: column turns warn color at this value\r\n- `failAt`: column turns error color at this value\r\n- `format`: custom display function\r\n\r\nThe bar gradient uses the graph `tone` color. Change the tone or override that tone with `lori:setTheme(...)`.\r\n\r\nGraphs keep a rolling sample window. If no fixed `range` is provided, Lori auto-ranges from visible samples and adds padding so small changes are readable.\r\n\r\nGraph labels show the current value plus `min`, `max`, and `avg` for the visible window.\r\n\r\nHover a graph column to show a tooltip for that sample. While the tooltip is open, that graph visually pauses so the value does not move under your mouse. Moving away resumes it.\r\n\r\nThe performance template includes ping, FPS, and memory graphs by default.\r\n\r\n## Speaking Of, The Performance Template!\r\n\r\nLori starts empty unless you ask for the template:\r\n\r\n```luau\r\nlori:mount(nil, {\r\n\tstudioOnly = false,\r\n\ttemplate = Lori.Templates.Performance,\r\n})\r\n```\r\n\r\nThat adds:\r\n\r\n- FPS/frame row\r\n- ping graph\r\n- FPS graph\r\n- memory graph\r\n\r\nYou can remove built-in template providers with `templateBlacklist`.\r\n\r\n```luau\r\nlori:mount(nil, {\r\n\tstudioOnly = false,\r\n\ttemplate = Lori.Templates.Performance,\r\n\ttemplateBlacklist = Lori.Templates.ItemBlacklister.New().Memory().FPS(),\r\n})\r\n```\r\n\r\nThat example keeps the frame row and ping graph, but removes the memory and FPS graphs.\r\n\r\nPlain string tables also work:\r\n\r\n```luau\r\nlori:mount(nil, {\r\n\ttemplate = Lori.Templates.Performance,\r\n\ttemplateBlacklist = { \"MemGraph\", \"FpsGraph\" },\r\n})\r\n```\r\n\r\nProvider names:\r\n\r\n- `Frame`\r\n- `PingGraph`\r\n- `FpsGraph`\r\n- `MemGraph`\r\n\r\nUse `Lori.Templates.Empty` if you want only your own rows.\r\n\r\n## Theme\r\n\r\nThemes are just colors.\r\n\r\n```luau\r\nlori:setTheme({\r\n\twarn = Color3.fromRGB(255, 180, 60),\r\n\tbackground = Color3.fromRGB(12, 12, 16),\r\n})\r\n```\r\n\r\nMain color keys:\r\n\r\n- `trace`, `phase`, `success`, `warn`, `error`\r\n- `key`, `value`, `muted`\r\n- `text`, `background`\r\n\r\n## Config\r\n\r\nCommon options:\r\n\r\n- `studioOnly`: only run in Studio\r\n- `enabled`: disables Lori entirely when false\r\n- `uiVisible` / `visible` / `showUI`: mount and keep updating Lori, but hide or show the `ScreenGui`\r\n- `maxItems`: max trace rows\r\n- `interval`: update delay in seconds, default `0.1`\r\n- `minInterval`: lowest allowed update delay, default `0.04`\r\n- `anchor`: stack position, default `tr`\r\n- `rightOffset`, `leftOffset`, `topOffset`, `bottomOffset`\r\n- `position`: legacy top-right offset alias\r\n- `width`: max row width, `0` means automatic\r\n- `fontSize`, `rowHeight`, `gap`, `padX`, `cornerRadius`\r\n- `graphWidth`, `graphHeight`, `graphColumns`, `statsSamples`\r\n- `motionTime`, `exitTime`, `fadeTime`\r\n- `uiScale`, `scaleBaseWidth`, `minScale`, `maxScale`\r\n\r\nAnchors:\r\n\r\n- `tr` / `topRight` / `top-right`\r\n- `tl` / `topLeft` / `top-left`\r\n- `br` / `bottomRight` / `bottom-right`\r\n- `bl` / `bottomLeft` / `bottom-left`\r\n\r\nBottom anchors stay pinned to the bottom and grow upward as rows appear.\r\n\r\n`cornerRadius` can be a scale value like `0.3` or a pixel value like `6`. Rows, graph cards, graph labels, and tooltips all share the same radius.\r\n\r\nYou can pass config directly or under `config`; these are equivalent:\r\n\r\n```luau\r\nlori:mount(nil, { maxItems = 8 })\r\nlori:mount(nil, { config = { maxItems = 8 } })\r\n```\r\n\r\nViewport scaling changes the actual text size, row size, padding, and graph size together, so the text measurement stays correct.\r\n\r\n## UI Visibility\r\n\r\nUse `enabled = false` when you want Lori to do nothing. Use `uiVisible = false` when you want Lori mounted and collecting/updating rows, but hidden from the player.\r\n\r\n```luau\r\nlocal lori = Lori.create({\r\n\tuiVisible = false,\r\n})\r\n\r\nlori:mount()\r\nlori:show()\r\nlori:hide()\r\nlori:toggleVisible()\r\nlori:setVisible(true)\r\n```\r\n\r\nThe aliases `visible` and `showUI` are accepted in config too, so teams can use whichever reads best in their settings module.\r\n\r\n## API\r\n\r\n```luau\r\nLori.create(options?)\r\n\r\nlori:mount(parent?, options?)\r\nlori:setVisible(visible)\r\nlori:toggleVisible(force?)\r\nlori:show()\r\nlori:hide()\r\nlori:trace(scope, action, fields?, duration?)\r\nlori:phase(scope, action, fields?, duration?)\r\nlori:success(scope, action, fields?, duration?)\r\nlori:warn(scope, action, fields?, duration?)\r\nlori:error(scope, action, fields?, duration?)\r\n\r\nlori:push(event)\r\nlori:watch(key, fields, options?)\r\nlori:graph(name, options?)\r\n\r\nlori:time(scope, action)\r\nlori:measure(scope, action, callback)\r\nlori:record(scope, action, elapsedMs, fields?)\r\n\r\nlori:raycast(scope, origin, direction, params?)\r\nlori:spherecast(scope, origin, radius, direction, params?)\r\nlori:blockcast(scope, cframe, size, direction, params?)\r\n\r\nlori:dismiss(key)\r\nlori:clear()\r\nlori:unmount()\r\nlori:destroy()\r\n```\r\n\r\nGraph handles:\r\n\r\n```luau\r\ngraph:push(value)\r\ngraph:rescale(factor)\r\ngraph:remove()\r\n```\r\n\r\nYou can use the global `Lori.trace(...)` style too, but for real projects `Lori.create()` is cleaner.\r\n","readmeTruncated":false}