salahzar

brain.roblox

May 28th, 2026
16,711
2
Never
4
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.59 KB | None | 2 0
  1. -- BRAIN.ROBLOX.LUA - AI-Driven NPC Brain for Roblox
  2. -- Analogous to brain.lsl (Second Life). Place as a Script inside an NPC Model.
  3. -- Model name format: "NPCName.AreaName"
  4. -- Set a StringAttribute "ServerURL" on the Model to your Nexus server URL.
  5. --
  6. -- Required model structure:
  7. -- Model (named "NPCName.AreaName", with StringAttribute ServerURL)
  8. -- HumanoidRootPart (or any BasePart as primary)
  9. -- Humanoid
  10. -- ClickDetector (auto-created if missing)
  11. -- Animation objects (optional, named to match anim= commands)
  12.  
  13. local Players = game:GetService("Players")
  14. local HttpService = game:GetService("HttpService")
  15. local Chat = game:GetService("Chat")
  16.  
  17. -- ============================================
  18. -- CONFIGURATION
  19. -- ============================================
  20.  
  21. local DEBUG = false
  22.  
  23. local model = script.Parent
  24. local rootPart = model:FindFirstChild("HumanoidRootPart")
  25. or (model:IsA("Model") and model.PrimaryPart or nil)
  26. or model:FindFirstChildWhichIsA("BasePart")
  27. local humanoid = model:FindFirstChildOfClass("Humanoid")
  28.  
  29. local SERVER_URL = model:GetAttribute("ServerURL") or ""
  30. local NPC_NAME = ""
  31. local CURRENT_AREA = ""
  32. local TIMEOUT = 300 -- seconds
  33.  
  34. -- ============================================
  35. -- COLORS
  36. -- ============================================
  37.  
  38. local RED = Color3.new(1, 0, 0)
  39. local WHITE = Color3.new(1, 1, 1)
  40. local GREEN = Color3.new(0, 1, 0)
  41. local YELLOW = Color3.new(1, 1, 0)
  42. local BLUE = Color3.new(0.3, 0.7, 1)
  43.  
  44. -- ============================================
  45. -- STATE (forward-declared so functions can cross-reference)
  46. -- ============================================
  47.  
  48. local isConversing = false
  49. local currentPlayer: Player? = nil
  50. local conversationTimer: thread? = nil
  51. local listenConnection: RBXScriptConnection? = nil
  52. local billboardGui: BillboardGui? = nil
  53.  
  54. -- Forward declarations
  55. local endConversation: (boolean) -> ()
  56. local resetConversationTimeout: () -> ()
  57.  
  58. -- ============================================
  59. -- BILLBOARD LABEL
  60. -- ============================================
  61.  
  62. local function setLabel(text: string, color: Color3)
  63. if not billboardGui then
  64. local gui = Instance.new("BillboardGui")
  65. gui.Name = "StatusBillboard"
  66. gui.Size = UDim2.new(0, 220, 0, 70)
  67. gui.StudsOffset = Vector3.new(0, 3.5, 0)
  68. gui.AlwaysOnTop = true
  69. gui.Parent = rootPart
  70. or model:FindFirstChildWhichIsA("BasePart")
  71. or (model:IsA("BasePart") and model or nil)
  72. or model
  73.  
  74. local label = Instance.new("TextLabel")
  75. label.Name = "Label"
  76. label.Size = UDim2.fromScale(1, 1)
  77. label.BackgroundTransparency = 1
  78. label.TextColor3 = WHITE
  79. label.TextScaled = true
  80. label.TextWrapped = true
  81. label.Font = Enum.Font.GothamBold
  82. label.Parent = gui
  83.  
  84. billboardGui = gui
  85. end
  86.  
  87. local label = billboardGui:FindFirstChild("Label") :: TextLabel?
  88. if label then
  89. label.Text = text
  90. label.TextColor3 = color
  91. end
  92. end
  93.  
  94. -- ============================================
  95. -- PART COLOR INDICATOR (like llSetColor in LSL)
  96. -- ============================================
  97.  
  98. local function setColor(color: Color3)
  99. for _, part in model:GetDescendants() do
  100. if part:IsA("BasePart") and part.Name ~= "HumanoidRootPart" then
  101. part.Color = color
  102. end
  103. end
  104. end
  105.  
  106. -- ============================================
  107. -- NPC SPEECH
  108. -- ============================================
  109.  
  110. local function npcSay(message: string)
  111. local part = rootPart
  112. or model:FindFirstChildWhichIsA("BasePart")
  113. or (model:IsA("BasePart") and model or nil)
  114. if part then
  115. Chat:Chat(part, message, Enum.ChatColor.Blue)
  116. end
  117. if DEBUG then print("[" .. NPC_NAME .. "] " .. message) end
  118. end
  119.  
  120. local function ownerSay(message: string)
  121. print("[NPC:" .. NPC_NAME .. "] " .. message)
  122. end
  123.  
  124. -- ============================================
  125. -- JSON HELPERS
  126. -- ============================================
  127.  
  128. local function escapeJSON(s: string): string
  129. s = s:gsub("\\", "\\\\")
  130. s = s:gsub('"', '\\"')
  131. s = s:gsub("\n", "\\n")
  132. s = s:gsub("\r", "\\r")
  133. s = s:gsub("\t", "\\t")
  134. return s
  135. end
  136.  
  137. -- Minimal flat-JSON key extractor (handles string values only)
  138. local function extractJSON(body: string, key: string): string
  139. -- Match "key": "value" allowing escaped quotes inside value
  140. local val = body:match('"' .. key .. '":%s*"(.-[^\\])"')
  141. or body:match('"' .. key .. '":%s*"()"') -- empty string
  142. if val then
  143. val = val:gsub('\\"', '"')
  144. val = val:gsub("\\n", "\n")
  145. val = val:gsub("\\\\", "\\")
  146. end
  147. -- Also handle boolean/number values (returned as-is without quotes)
  148. if not val then
  149. val = body:match('"' .. key .. '":%s*([%w%.%-]+)')
  150. end
  151. return val or ""
  152. end
  153.  
  154. -- ============================================
  155. -- ANIMATIONS
  156. -- ============================================
  157.  
  158. local animTracks: { [string]: AnimationTrack } = {}
  159.  
  160. local function playAnim(animName: string)
  161. if not humanoid then return end
  162. animName = animName:match("^%s*(.-)%s*$") -- trim whitespace
  163.  
  164. local animObj = model:FindFirstChild(animName)
  165. if not animObj or not animObj:IsA("Animation") then
  166. if DEBUG then ownerSay("Animation not found: " .. animName) end
  167. return
  168. end
  169.  
  170. -- Stop previous track for this name if still playing
  171. if animTracks[animName] then
  172. animTracks[animName]:Stop()
  173. end
  174.  
  175. local track = humanoid:LoadAnimation(animObj)
  176. track:Play()
  177. animTracks[animName] = track
  178. end
  179.  
  180. -- ============================================
  181. -- SL COMMAND PROCESSING
  182. -- ============================================
  183.  
  184. local function processSLCommands(slCommands: string)
  185. -- Parse [key=value;key=value] style blocks from sl_commands field
  186. for key, value in slCommands:gmatch("([%w]+)=([^;%]]+)") do
  187. local k = key:lower()
  188. if k == "anim" or k == "emote" then
  189. playAnim(value)
  190. elseif k == "llsettext" then
  191. setLabel(value, WHITE)
  192. elseif k == "face" then
  193. if DEBUG then ownerSay("face command (no-op in Roblox): " .. value) end
  194. elseif k == "teleport" then
  195. if DEBUG then ownerSay("teleport command: " .. value) end
  196. elseif k == "lookup" then
  197. if DEBUG then ownerSay("lookup command: " .. value) end
  198. end
  199. end
  200. end
  201.  
  202. -- ============================================
  203. -- RESPONSE PARSING
  204. -- ============================================
  205.  
  206. local function parseAnimations(text: string)
  207. local animName = text:match("%[anim=([^%]]+)%]")
  208. if animName then
  209. playAnim(animName)
  210. end
  211. end
  212.  
  213. local function cleanText(text: string): string
  214. text = text:gsub("%[%a+=[^%]]*%]", "") -- strip [tag=value] blocks
  215. text = text:match("^%s*(.-)%s*$") -- trim
  216. return text
  217. end
  218.  
  219. local function handleChatResponse(body: string)
  220. setColor(WHITE)
  221. resetConversationTimeout()
  222.  
  223. if DEBUG then
  224. ownerSay("=== RESPONSE RECEIVED === len=" .. #body)
  225. end
  226.  
  227. local npcResponse = extractJSON(body, "npc_response")
  228. if npcResponse == "" then
  229. if DEBUG then ownerSay("ERROR: npc_response empty") end
  230. return
  231. end
  232.  
  233. parseAnimations(npcResponse)
  234.  
  235. local slCommands = extractJSON(body, "sl_commands")
  236. if slCommands ~= "" then
  237. processSLCommands(slCommands)
  238. end
  239.  
  240. npcResponse = cleanText(npcResponse)
  241. if npcResponse ~= "" then
  242. npcSay(npcResponse)
  243. else
  244. if DEBUG then ownerSay("ERROR: cleaned response empty") end
  245. end
  246. end
  247.  
  248. -- ============================================
  249. -- HTTP HELPERS
  250. -- ============================================
  251.  
  252. local function httpPost(endpoint: string, data: string): (boolean, string, number)
  253. local ok, result = pcall(HttpService.RequestAsync, HttpService, {
  254. Url = SERVER_URL .. endpoint,
  255. Method = "POST",
  256. Headers = { ["Content-Type"] = "application/json" },
  257. Body = data,
  258. })
  259. if ok then
  260. return true, (result :: any).Body, (result :: any).StatusCode
  261. end
  262. return false, tostring(result), 0
  263. end
  264.  
  265. local function httpGet(endpoint: string): (boolean, string, number)
  266. local ok, result = pcall(HttpService.RequestAsync, HttpService, {
  267. Url = SERVER_URL .. endpoint,
  268. Method = "GET",
  269. })
  270. if ok then
  271. return true, (result :: any).Body, (result :: any).StatusCode
  272. end
  273. return false, tostring(result), 0
  274. end
  275.  
  276. -- ============================================
  277. -- CONVERSATION TIMEOUT
  278. -- ============================================
  279.  
  280. resetConversationTimeout = function()
  281. if conversationTimer then
  282. task.cancel(conversationTimer)
  283. conversationTimer = nil
  284. end
  285. if not isConversing then return end
  286. conversationTimer = task.delay(TIMEOUT, function()
  287. if isConversing then
  288. ownerSay("Timeout: ending conversation with " .. (currentPlayer and currentPlayer.Name or "?"))
  289. endConversation(true)
  290. end
  291. end)
  292. end
  293.  
  294. -- ============================================
  295. -- CONVERSATION MANAGEMENT
  296. -- ============================================
  297.  
  298. endConversation = function(sayGoodbye: boolean)
  299. if not isConversing then return end
  300.  
  301. local playerName = currentPlayer and currentPlayer.Name or ""
  302.  
  303. -- Notify server asynchronously
  304. local leaveData = string.format(
  305. '{"player_name":"%s","npc_name":"%s","area":"%s","action":"leaving","message":"Avatar leaving","status":"end"}',
  306. escapeJSON(playerName), escapeJSON(NPC_NAME), escapeJSON(CURRENT_AREA)
  307. )
  308. task.spawn(function()
  309. httpPost("/api/leave_npc", leaveData)
  310. end)
  311.  
  312. if conversationTimer then
  313. task.cancel(conversationTimer)
  314. conversationTimer = nil
  315. end
  316. if listenConnection then
  317. listenConnection:Disconnect()
  318. listenConnection = nil
  319. end
  320.  
  321. if sayGoodbye and playerName ~= "" then
  322. npcSay("È stato un piacere parlare con te, " .. playerName .. "!")
  323. end
  324.  
  325. setColor(WHITE)
  326. currentPlayer = nil
  327. isConversing = false
  328. setLabel("Touch to talk to\n" .. NPC_NAME, GREEN)
  329. end
  330.  
  331. local function sendMessage(msg: string, playerName: string)
  332. if not isConversing then
  333. if DEBUG then ownerSay("ERROR: Not conversing") end
  334. return
  335. end
  336.  
  337. setColor(RED)
  338. resetConversationTimeout()
  339.  
  340. local chatData = string.format(
  341. '{"message":"%s","player_name":"%s","npc_name":"%s","area":"%s"}',
  342. escapeJSON(msg), escapeJSON(playerName), escapeJSON(NPC_NAME), escapeJSON(CURRENT_AREA)
  343. )
  344.  
  345. if DEBUG then ownerSay("Sending /api/chat...") end
  346.  
  347. task.spawn(function()
  348. local ok, body, status = httpPost("/api/chat", chatData)
  349. if ok and status == 200 then
  350. handleChatResponse(body)
  351. else
  352. setColor(WHITE)
  353. if DEBUG then
  354. ownerSay("HTTP error " .. tostring(status))
  355. ownerSay("Body: " .. body:sub(1, 200))
  356. end
  357. npcSay("Scusa, non posso risponderti ora.")
  358. end
  359. end)
  360. end
  361.  
  362. local function startConversation(player: Player)
  363. if isConversing then return end
  364.  
  365. currentPlayer = player
  366. isConversing = true
  367. setLabel("Conversing with\n" .. player.Name, BLUE)
  368.  
  369. local senseData = string.format(
  370. '{"name":"%s","npcname":"%s","area":"%s"}',
  371. escapeJSON(player.Name), escapeJSON(NPC_NAME), escapeJSON(CURRENT_AREA)
  372. )
  373.  
  374. if DEBUG then ownerSay("Sending /sense for " .. player.Name) end
  375.  
  376. task.spawn(function()
  377. local ok, body, status = httpPost("/sense", senseData)
  378. if ok and status == 200 then
  379. handleChatResponse(body)
  380. resetConversationTimeout()
  381.  
  382. -- Listen for player chat messages (server-side via Player.Chatted)
  383. listenConnection = player.Chatted:Connect(function(message: string)
  384. sendMessage(message, player.Name)
  385. end)
  386. else
  387. isConversing = false
  388. currentPlayer = nil
  389. setLabel("Touch to talk to\n" .. NPC_NAME, GREEN)
  390. if DEBUG then ownerSay("Sense failed: " .. tostring(status)) end
  391. end
  392. end)
  393. end
  394.  
  395. -- ============================================
  396. -- INITIALIZATION
  397. -- ============================================
  398.  
  399. local function init()
  400. if SERVER_URL == "" or not SERVER_URL:match("^https?://") then
  401. setLabel("ERROR: ServerURL\nattribute not set", RED)
  402. ownerSay("ERROR: Set StringAttribute 'ServerURL' on Model (e.g., http://212.227.64.143:5000)")
  403. return
  404. end
  405.  
  406. -- Parse "NPCName.AreaName" from model name
  407. local dotPos = model.Name:find("%.")
  408. if dotPos and dotPos > 1 then
  409. NPC_NAME = model.Name:sub(1, dotPos - 1)
  410. CURRENT_AREA = model.Name:sub(dotPos + 1)
  411. ownerSay("NPC: " .. NPC_NAME .. " | Area: " .. CURRENT_AREA)
  412. ownerSay("Server: " .. SERVER_URL)
  413. else
  414. ownerSay("ERROR: Model name must be 'NPCName.AreaName'")
  415. setLabel("ERROR: Invalid\nmodel name", RED)
  416. return
  417. end
  418.  
  419. setLabel("Verifying\n" .. NPC_NAME .. "...", YELLOW)
  420.  
  421. -- Check server health, then verify NPC exists
  422. task.spawn(function()
  423. local ok, body, status = httpGet("/health")
  424. if not ok or status ~= 200 then
  425. setLabel("Server offline\n" .. NPC_NAME, RED)
  426. ownerSay("ERROR: Server not reachable (status " .. tostring(status) .. ")")
  427. return
  428. end
  429.  
  430. local version = extractJSON(body, "version")
  431. ownerSay("Server online" .. (version ~= "" and (" v" .. version) or ""))
  432.  
  433. local verifyData = string.format(
  434. '{"npc_name":"%s","area":"%s"}',
  435. escapeJSON(NPC_NAME), escapeJSON(CURRENT_AREA)
  436. )
  437. local vok, vbody, vstatus = httpPost("/api/npc/verify", verifyData)
  438. if vok and vstatus == 200 then
  439. local found = extractJSON(vbody, "found")
  440. if found == "true" then
  441. local caps = ""
  442. if extractJSON(vbody, "has_teleport") == "true" then caps = caps .. "TP|" end
  443. if extractJSON(vbody, "has_llsettext") == "true" then caps = caps .. "TXT|" end
  444. if extractJSON(vbody, "has_notecard") == "true" then caps = caps .. "NC" end
  445. setLabel("Touch to talk to\n" .. NPC_NAME .. "\n[" .. caps .. "]", GREEN)
  446. ownerSay("NPC verified! Click to talk.")
  447. else
  448. setLabel("NPC not found\n" .. NPC_NAME, RED)
  449. ownerSay("ERROR: NPC '" .. NPC_NAME .. "' not found in database!")
  450. end
  451. else
  452. setLabel("Verify failed\n" .. NPC_NAME, RED)
  453. ownerSay("ERROR: Verify request failed (status " .. tostring(vstatus) .. ")")
  454. end
  455. end)
  456. end
  457.  
  458. -- ============================================
  459. -- CLICK DETECTION (replaces touch_start)
  460. -- ============================================
  461.  
  462. local clickDetector = model:FindFirstChildOfClass("ClickDetector")
  463. if not clickDetector then
  464. clickDetector = Instance.new("ClickDetector")
  465. clickDetector.MaxActivationDistance = 10
  466. clickDetector.Parent = rootPart or model
  467. end
  468.  
  469. clickDetector.MouseClick:Connect(function(player: Player)
  470. -- Second click by same player ends conversation
  471. if isConversing and currentPlayer == player then
  472. endConversation(true)
  473. return
  474. end
  475. -- Ignore click if NPC is busy with someone else
  476. if isConversing then return end
  477.  
  478. startConversation(player)
  479. end)
  480.  
  481. -- End conversation when player leaves the game
  482. Players.PlayerRemoving:Connect(function(player: Player)
  483. if isConversing and currentPlayer == player then
  484. endConversation(false)
  485. end
  486. end)
  487.  
  488. -- ============================================
  489. -- RUN
  490. -- ============================================
  491.  
  492. init()
Advertisement