{"id":"hurreman/roblox-conversation","name":"roblox-conversation","scope":"hurreman","platform":"roblox","description":"Data-driven, server-authoritative NPC dialogue for hurreman's Roblox games: definitions as plain data, game-registered conditions and actions, tagged ProximityPrompt sources, and a client controller that drives any Presenter UI","version":"0.1.0","latest":"0.1.0","versions":["0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"5a852a9fbbcfe397fdbae2dcec35fad4fc2e84a34b69403fe740e0d6e08c07eb","likes":0,"downloads":0,"install":"forest install hurreman/roblox-conversation","url":"https://forest.dev/p/roblox/hurreman/roblox-conversation","files":"https://api.forest.dev/ai/package/roblox/hurreman/roblox-conversation/files","readme":"# roblox-conversation\n\nData-driven, server-authoritative NPC dialogue: Slime Nebula's conversation framework, with Magic Neighborhood's tag binder and global text placeholders, as objects a game builds instead of module singletons.\n\n```toml\n# wally.toml\n[dependencies]\nConversation = \"hurreman/roblox-conversation@0.1.0\"\n```\n\n## How it fits together\n\n- **Definitions are plain data.** Nodes hold Text, a list of random variants or `{Var}` placeholders. Choices go `Next` to another node, `Close`, or run an opaque `Action`, gated by an opaque `Condition` / `HideCondition`, with `Hide` or `Disable` visibility and highlights.\n- **The game registers what condition and action names mean.** Conditions fail closed and actions are no-ops when missing; handler errors are caught.\n- **The server is the trust boundary.** Clients only send `(conversationId, nodeId, choiceIndex)`, and the views they get never carry an Action or Condition. Every request is re-checked: an active session, the node they were shown, the choice exists, its gates still hold, and no request is already in progress.\n- **The client controller never builds UI.** It drives any Presenter (`Init`, `Open`, `ShowNode`, `SetChoices`, `Close`).\n\n```lua\n-- server\nlocal Conversation = require(ReplicatedStorage.Packages.Conversation)\nlocal service = Conversation.Service.new({\n\tsend = function(player, action, payload) net:SendTo(player, \"Conversation\", action, payload) end,\n\tglobals = { EventEgg = function() return Seasons.eggName(\"Event\") end }, -- {EventEgg} in any text\n\tonStarted = function(player, id, source) progress:Fire(\"TalkTo\", player, id) end,\n})\nservice.Conditions:Register(\"HasCompletedQuest\", function(player, args) return quests:Done(player, args.QuestId), \"Finish the quest first\" end)\nservice.Actions:Register(\"OpenVendor\", function(player, args, ctx) vendors:Open(player, args.VendorId); ctx.Close(\"Vendor\") end)\nservice:Listen({ choice = net:Function(\"ConversationChoice\"), close = net:Event(\"ConversationClose\") })\nConversation.Binder.new(service):Start(script.Definitions) -- id = module name; tag models \"ConversationSource\"\n\n-- client\nlocal client = Conversation.Client.new({\n\tpresenter = DialoguePresenter,\n\tevent = net:Event(\"Conversation\"), choice = net:Function(\"ConversationChoice\"), close = net:Event(\"ConversationClose\"),\n})\nclient:Start()\nclient:OnOpened(function(source) ... end) -- fires once per transition, like OnClosed\n```\n\nBinder attributes, set on the prompt or the model (the prompt's value wins):\n- `ConversationId` (required)\n- `ConversationDisplayName`\n- `ConversationSourceKind`\n- `ConversationPortraitId`\n- `ConversationMaxDistance`\n\n## Moving the games over\n\n- **Slime Nebula:**\n  - `ConversationService`, its Registry, Session and both resolvers become a `Service`. `ConditionResolver.Register(...)` calls keep working on `service.Conditions`, with a dot or a colon.\n  - `ConversationInteractionBinder` becomes `Binder`, or stays as the game's own binder calling `service:Start`.\n  - `ConversationClientController` becomes `Client`. `Opened` / `Closed` become `OnOpened` / `OnClosed`.\n  - The `Integrations/` handlers register on the service and are unchanged.\n- **Magic Neighborhood:**\n  - `server/Conversation/*` become `Service` plus `Binder`. Its `GlobalVars` become `globals`, and `Definitions/` passes to `Binder:Start`.\n  - The client controller becomes `Client`, with its own `DialoguePresenter`.\n\n## Two things found in Slime Nebula's code\n\nBoth are fixed in Slime Nebula since it moved onto this package (2026-10-26).\n\n- **TalkTo progress never fires.** `ConversationService:Start` fires TalkTo progress through `ServerScriptService.Progress.ProgressManager`, but the file never declares `ServerScriptService`. The call errors inside its `pcall` and is silently swallowed. (Glorpo's prompt fires its own TalkTo, so that one still works.) The package replaces this with the `onStarted` hook.\n- **Its Conversation spec fails on its own code.** Commit `bf750bac` (2026-08-28) added two highlight choices to the test definition without updating the visible-choice counts. Nothing runs the spec: `RunAllTests` has `ENABLE_TESTS_IN_STUDIO = false`, and the spec isn't in the Lune list.\n  - `tests/specs/SlimeNebula.spec.luau` runs the spec against the package with those counts corrected, and the choices looked up by Index. Every other assertion is as written.\n\n## Developing\n\n```bash\nlune run tests/run    # specs on hurreman/luau-spec (DevPackages, else ../luau-spec)\n```\n\nThe Lune specs cover:\n- Slime Nebula's spec, corrected;\n- view contents, placeholders and globals, onStarted, the Busy guard, ctx.Close, fail-closed conditions, registry validation, Listen and leaving;\n- lifecycle closes: source destroyed or removed, character died, a replaced session;\n- the client: presenter calls, choice round trips, one Closed per transition, closing past MaxDistance, rejected choices.\n\nThe binder hasn't been run: a ProximityPrompt only fires on real player input in Play mode.\n","readmeTruncated":false}