Dialogue System Roblox GUI Template with NPC Chat & Choices
An NPC dialogue UI with typewriter text, speaker portrait placeholder, and branching choice buttons. Designed for story-driven Roblox experiences with a charming cartoon aesthetic. Features smooth typewriter text animation, animated NPC portraits, player choice branching, and full dialogue tree support for complex narrative flows.

Live preview
Dialogue System
1920×1080Includes real, production-ready Luau logic — not a static mockup.
What's inside
- • Typewriter text effect
- • Speaker name + portrait
- • Branching choice buttons
- • Dialogue tree stub
- • Continue / skip button
How to use this template
The Dialogue System template shows NPC dialogue with typewriter text, speaker portrait, and branching choices. Paste the client Luau into StarterGui and a dialogue box appears at the bottom of the screen. The text appears one character at a time using a RunService loop, and players can click to skip the typewriter and reveal the full line. When the line finishes, branching choice buttons appear below the dialogue box. The layout uses Scale-based sizing, so the dialogue box fits any screen. To customize, replace the placeholder dialogue tree with your own lines and choices. The typewriter speed is configurable. Wire the choice clicks to your quest or story logic. Test in Studio by advancing through the dialogue and confirming the choices appear at the right time.
Add it to your game in 3 steps
- 1
Copy the Luau
Open the template in the web editor and click Export. Choose LocalScript to copy the full client Luau to your clipboard.
- 2
Paste into StarterGui
In Roblox Studio, right-click StarterGui in the Explorer, insert a LocalScript, and paste the copied Luau. Rename it to match the template.
- 3
Press Play to verify
Click Play in Studio. The GUI should appear exactly as designed. Tweak any Size or Position values in the script if you want to adjust the layout.
1--[[2 Dialogue System - Client LocalScript3 An NPC dialogue UI with typewriter text, speaker portrait, and branching choices.4 Place in: StarterPlayer > StarterPlayerScripts5--]]67local Players = game:GetService("Players")8local TweenService = game:GetService("TweenService")9local UserInputService = game:GetService("UserInputService")10local ReplicatedStorage = game:GetService("ReplicatedStorage")1112local player = Players.LocalPlayer13local playerGui = player:WaitForChild("PlayerGui")1415-- ============================================================16-- REMOTE EVENTS17-- ============================================================18local remotes = ReplicatedStorage:WaitForChild("Dialogue_System_Remotes")19local startDialogueEvent = remotes:WaitForChild("StartDialogue")20local advanceDialogueEvent = remotes:WaitForChild("AdvanceDialogue")21local selectChoiceEvent = remotes:WaitForChild("SelectChoice")22local endDialogueEvent = remotes:WaitForChild("EndDialogue")23local requestDialogueEvent = remotes:WaitForChild("RequestDialogue")24local portraitUpdateEvent = remotes:WaitForChild("PortraitUpdate")25local emotionUpdateEvent = remotes:WaitForChild("EmotionUpdate")2627-- ============================================================28-- CONFIGURATION29-- ============================================================30local CONFIG = {31 -- Colors - warm cartoon RPG palette32 BG_PRIMARY = Color3.fromRGB(45, 40, 55),33 BG_SECONDARY = Color3.fromRGB(60, 55, 75),34 BG_ACCENT = Color3.fromRGB(80, 75, 100),35 TEXT_WHITE = Color3.fromRGB(255, 250, 240),36 TEXT_GRAY = Color3.fromRGB(180, 175, 170),37 TEXT_YELLOW = Color3.fromRGB(255, 220, 100),38 NAME_TAG_BG = Color3.fromRGB(120, 80, 180),39 CHOICE_BG = Color3.fromRGB(70, 65, 90),40 CHOICE_HOVER = Color3.fromRGB(100, 95, 130),41 CHOICE_SELECTED = Color3.fromRGB(140, 130, 180),42 PORTRAIT_BORDER = Color3.fromRGB(160, 140, 100),43 PORTRAIT_BG = Color3.fromRGB(35, 30, 45),44 CONTINUE_GLOW = Color3.fromRGB(200, 180, 100),45 OVERLAY_COLOR = Color3.fromRGB(0, 0, 0),4647 -- Animation48 TWEEN_FAST = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),49 TWEEN_NORMAL = TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),50 TWEEN_SLOW = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),51 TWEEN_BOUNCE = TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out),52 TYPEWRITER_SPEED = 0.035, -- seconds per character53 CHOICE_STAGGER = 0.08,54 CONTINUE_BLINK_SPEED = 0.6,5556 -- Layout57 DIALOGUE_WIDTH = 700,58 DIALOGUE_HEIGHT = 200,59 PORTRAIT_SIZE = 140,60 CHOICE_HEIGHT = 40,61 NAME_TAG_HEIGHT = 32,62}6364-- ============================================================65-- STATE66-- ============================================================67local state = {68 isActive = false,69 currentText = "",70 targetText = "",71 typewriterTimer = nil,72 canContinue = false,73 currentSpeaker = "",74 currentEmotion = "neutral",75 choices = {},76 dialogueHistory = {},77 historyOpen = false,78 currentPortraitId = "",79}8081-- ============================================================82-- HELPER FUNCTIONS83-- ============================================================8485local function createInstance(className, props)86 local instance = Instance.new(className)87 for key, value in pairs(props) do88 instance[key] = value89 end90 return instance91end9293local function createCorner(parent, radius)94 local corner = Instance.new("UICorner")95 corner.CornerRadius = UDim.new(0, radius or 8)96 corner.Parent = parent97 return corner98end99100local function createStroke(parent, color, thickness)101 local stroke = Instance.new("UIStroke")102 stroke.Color = color or CONFIG.PORTRAIT_BORDER103 stroke.Thickness = thickness or 2104 stroke.Parent = parent105 return stroke106end107108local function createGradient(parent, color1, color2, rotation)109 local gradient = Instance.new("UIGradient")110 gradient.Color = ColorSequence.new({111 ColorSequenceKeypoint.new(0, color1 or CONFIG.BG_PRIMARY),112 ColorSequenceKeypoint.new(1, color2 or CONFIG.BG_SECONDARY),113 })114 gradient.Rotation = rotation or 0115 gradient.Parent = parent116 return gradient117end118119-- ============================================================120-- UI CONSTRUCTION121-- ============================================================122123local screenGui = createInstance("ScreenGui", {124 Name = "Dialogue_System",125 ResetOnSpawn = false,126 ZIndexBehavior = Enum.ZIndexBehavior.Sibling,127 Parent = playerGui,128})129130-- --------------------------------------------------------131-- DARK OVERLAY132-- --------------------------------------------------------133local darkOverlay = createInstance("Frame", {134 Name = "DarkOverlay",135 Size = UDim2.fromScale(1, 1),136 BackgroundColor3 = CONFIG.OVERLAY_COLOR,137 BackgroundTransparency = 1,138 BorderSizePixel = 0,139 ZIndex = 9,140 Parent = screenGui,141 Visible = false,142})143144-- --------------------------------------------------------145-- DIALOGUE CONTAINER146-- --------------------------------------------------------147local dialogueContainer = createInstance("Frame", {148 Name = "DialogueContainer",149 Size = UDim2.fromOffset(CONFIG.DIALOGUE_WIDTH, CONFIG.DIALOGUE_HEIGHT),150 Position = UDim2.new(0.5, 0, 1, -60),151 AnchorPoint = Vector2.new(0.5, 1),152 BackgroundTransparency = 1,153 ZIndex = 10,154 Parent = screenGui,155 Visible = false,156})157158-- --------------------------------------------------------159-- PORTRAIT CONTAINER160-- --------------------------------------------------------161local portraitContainer = createInstance("Frame", {162 Name = "PortraitContainer",163 Size = UDim2.fromOffset(CONFIG.PORTRAIT_SIZE, CONFIG.PORTRAIT_SIZE),164 Position = UDim2.fromOffset(-20, -30),165 BackgroundTransparency = 1,166 ZIndex = 11,167 Parent = dialogueContainer,168})169170local portraitBg = createInstance("Frame", {171 Name = "PortraitBg",172 Size = UDim2.fromScale(1, 1),173 BackgroundColor3 = CONFIG.PORTRAIT_BG,174 BorderSizePixel = 0,175 ZIndex = 11,176 Parent = portraitContainer,177})178createCorner(portraitBg, 16)179createStroke(portraitBg, CONFIG.PORTRAIT_BORDER, 3)180181-- Inner glow182local portraitGlow = createInstance("Frame", {183 Name = "PortraitGlow",184 Size = UDim2.new(1, -6, 1, -6),185 Position = UDim2.fromScale(0.5, 0.5),186 AnchorPoint = Vector2.new(0.5, 0.5),187 BackgroundColor3 = CONFIG.BG_SECONDARY,188 BorderSizePixel = 0,189 ZIndex = 12,190 Parent = portraitBg,191})192createCorner(portraitGlow, 13)193194local portraitImage = createInstance("ImageLabel", {195 Name = "PortraitImage",196 Size = UDim2.new(1, -12, 1, -12),197 Position = UDim2.fromScale(0.5, 0.5),198 AnchorPoint = Vector2.new(0.5, 0.5),199 BackgroundTransparency = 1,200 Image = "", -- Set dynamically via portrait update201 ZIndex = 13,202 Parent = portraitBg,203})204createCorner(portraitImage, 10)205206-- Placeholder when no image207local portraitPlaceholder = createInstance("TextLabel", {208 Name = "PortraitPlaceholder",209 Size = UDim2.fromScale(1, 1),210 BackgroundTransparency = 1,211 Text = "NPC",212 TextColor3 = CONFIG.TEXT_GRAY,213 TextSize = 36,214 Font = Enum.Font.GothamBlack,215 ZIndex = 13,216 Parent = portraitImage,217})218219-- Speaker name below portrait220local speakerName = createInstance("TextLabel", {221 Name = "SpeakerName",222 Size = UDim2.new(1, 0, 0, 20),223 Position = UDim2.fromOffset(0, CONFIG.PORTRAIT_SIZE + 4),224 BackgroundTransparency = 1,225 Text = "",226 TextColor3 = CONFIG.TEXT_WHITE,227 TextSize = 12,228 Font = Enum.Font.GothamBold,229 TextXAlignment = Enum.TextXAlignment.Center,230 ZIndex = 11,231 Parent = portraitContainer,232})233234-- Emotion indicator235local emotionIndicator = createInstance("Frame", {236 Name = "EmotionIndicator",237 Size = UDim2.fromOffset(24, 24),238 Position = UDim2.new(1, -10, 0, -5),239 AnchorPoint = Vector2.new(1, 0),240 BackgroundColor3 = CONFIG.CONTINUE_GLOW,241 BorderSizePixel = 0,242 ZIndex = 14,243 Parent = portraitContainer,244 Visible = false,245})246createCorner(emotionIndicator, 12)247248local emotionText = createInstance("TextLabel", {249 Name = "EmotionText",250 Size = UDim2.fromScale(1, 1),251 BackgroundTransparency = 1,252 Text = "N",253 TextColor3 = CONFIG.BG_PRIMARY,254 TextSize = 14,255 Font = Enum.Font.GothamBlack,256 ZIndex = 15,257 Parent = emotionIndicator,258})259260-- Portrait entry animation offset261portraitContainer.Position = UDim2.fromOffset(-200, -30)262263-- --------------------------------------------------------264-- DIALOGUE BOX (Text area)265-- --------------------------------------------------------266local dialogueBox = createInstance("Frame", {267 Name = "DialogueBox",268 Size = UDim2.new(1, -CONFIG.PORTRAIT_SIZE + 10, 1, 0),269 Position = UDim2.fromOffset(CONFIG.PORTRAIT_SIZE - 10, 0),270 BackgroundTransparency = 1,271 ZIndex = 10,272 Parent = dialogueContainer,273})274275local dialogueBg = createInstance("Frame", {276 Name = "DialogueBg",277 Size = UDim2.fromScale(1, 1),278 BackgroundColor3 = CONFIG.BG_PRIMARY,279 BackgroundTransparency = 0.15,280 BorderSizePixel = 0,281 ZIndex = 10,282 Parent = dialogueBox,283})284createCorner(dialogueBg, 14)285createStroke(dialogueBg, CONFIG.BG_ACCENT, 2)286createGradient(dialogueBg, CONFIG.BG_PRIMARY, CONFIG.BG_SECONDARY, 180)287288local dialogueText = createInstance("TextLabel", {289 Name = "DialogueText",290 Size = UDim2.new(1, -32, 1, -50),291 Position = UDim2.fromOffset(16, 16),292 BackgroundTransparency = 1,293 Text = "",294 TextColor3 = CONFIG.TEXT_WHITE,295 TextSize = 18,296 Font = Enum.Font.GothamBold,297 TextWrapped = true,298 TextXAlignment = Enum.TextXAlignment.Left,299 TextYAlignment = Enum.TextYAlignment.Top,300 LineHeight = 1.4,301 ZIndex = 11,302 Parent = dialogueBg,303})304305-- Continue hint (blinking)306local continueHint = createInstance("TextLabel", {307 Name = "ContinueHint",308 Size = UDim2.fromOffset(160, 20),309 Position = UDim2.new(1, -170, 1, -28),310 BackgroundTransparency = 1,311 Text = "Click to continue >>",312 TextColor3 = CONFIG.TEXT_GRAY,313 TextSize = 11,314 Font = Enum.Font.GothamBold,315 TextXAlignment = Enum.TextXAlignment.Right,316 ZIndex = 11,317 Parent = dialogueBg,318 Visible = false,319})320321-- Typing cursor322local typingCursor = createInstance("Frame", {323 Name = "TypingCursor",324 Size = UDim2.fromOffset(2, 18),325 BackgroundColor3 = CONFIG.TEXT_WHITE,326 BorderSizePixel = 0,327 ZIndex = 12,328 Parent = dialogueBg,329 Visible = false,330})331332-- --------------------------------------------------------333-- NAME TAG334-- --------------------------------------------------------335local nameTag = createInstance("Frame", {336 Name = "NameTag",337 Size = UDim2.fromOffset(140, CONFIG.NAME_TAG_HEIGHT),338 Position = UDim2.fromOffset(CONFIG.PORTRAIT_SIZE - 30, -CONFIG.NAME_TAG_HEIGHT + 10),339 BackgroundTransparency = 1,340 ZIndex = 12,341 Parent = dialogueContainer,342})343344local nameTagBg = createInstance("Frame", {345 Name = "NameTagBg",346 Size = UDim2.fromScale(1, 1),347 BackgroundColor3 = CONFIG.NAME_TAG_BG,348 BorderSizePixel = 0,349 ZIndex = 12,350 Parent = nameTag,351})352createCorner(nameTagBg, 8)353createGradient(nameTagBg, CONFIG.NAME_TAG_BG, Color3.fromRGB(100, 60, 160), 0)354355local nameTagText = createInstance("TextLabel", {356 Name = "NameTagText",357 Size = UDim2.fromScale(1, 1),358 BackgroundTransparency = 1,359 Text = "",360 TextColor3 = CONFIG.TEXT_WHITE,361 TextSize = 14,362 Font = Enum.Font.GothamBlack,363 ZIndex = 13,364 Parent = nameTagBg,365})366367-- --------------------------------------------------------368-- CHOICES CONTAINER369-- --------------------------------------------------------370local choicesContainer = createInstance("Frame", {371 Name = "ChoicesContainer",372 Size = UDim2.new(1, -CONFIG.PORTRAIT_SIZE + 10, 0, 0),373 Position = UDim2.fromOffset(CONFIG.PORTRAIT_SIZE - 10, CONFIG.DIALOGUE_HEIGHT + 8),374 BackgroundTransparency = 1,375 ZIndex = 11,376 Parent = dialogueContainer,377 Visible = false,378 AutomaticSize = Enum.AutomaticSize.Y,379})380381local choicesLayout = createInstance("UIListLayout", {382 SortOrder = Enum.SortOrder.LayoutOrder,383 VerticalAlignment = Enum.VerticalAlignment.Top,384 HorizontalAlignment = Enum.HorizontalAlignment.Right,385 Padding = UDim.new(0, 6),386 Parent = choicesContainer,387})388389-- Choice button template (created dynamically)390local choiceButtonTemplate = createInstance("TextButton", {391 Name = "ChoiceButton",392 Size = UDim2.new(1, 0, 0, CONFIG.CHOICE_HEIGHT),393 BackgroundColor3 = CONFIG.CHOICE_BG,394 Text = "",395 TextColor3 = CONFIG.TEXT_WHITE,396 TextSize = 15,397 Font = Enum.Font.GothamBold,398 BorderSizePixel = 0,399 ZIndex = 11,400 Visible = false,401})402createCorner(choiceButtonTemplate, 8)403createStroke(choiceButtonTemplate, CONFIG.BG_ACCENT, 1)404405-- Store template406choiceButtonTemplate.Parent = screenGui407408-- --------------------------------------------------------409-- SKIP BUTTON410-- --------------------------------------------------------411local skipButton = createInstance("ImageButton", {412 Name = "SkipButton",413 Size = UDim2.fromOffset(80, 28),414 Position = UDim2.new(1, -10, 0, -36),415 AnchorPoint = Vector2.new(1, 0),416 BackgroundColor3 = CONFIG.BG_SECONDARY,417 BackgroundTransparency = 0.3,418 BorderSizePixel = 0,419 ZIndex = 12,420 Parent = dialogueContainer,421})422createCorner(skipButton, 6)423createStroke(skipButton, CONFIG.BG_ACCENT, 1)424425local skipText = createInstance("TextLabel", {426 Name = "SkipText",427 Size = UDim2.fromScale(1, 1),428 BackgroundTransparency = 1,429 Text = "Skip >>",430 TextColor3 = CONFIG.TEXT_GRAY,431 TextSize = 11,432 Font = Enum.Font.GothamBold,433 ZIndex = 13,434 Parent = skipButton,435})436437-- Hover effects438skipButton.MouseEnter:Connect(function()439 TweenService:Create(skipButton, CONFIG.TWEEN_FAST, { BackgroundTransparency = 0.1 }):Play()440 TweenService:Create(skipText, CONFIG.TWEEN_FAST, { TextColor3 = CONFIG.TEXT_WHITE }):Play()441end)442443skipButton.MouseLeave:Connect(function()444 TweenService:Create(skipButton, CONFIG.TWEEN_FAST, { BackgroundTransparency = 0.3 }):Play()445 TweenService:Create(skipText, CONFIG.TWEEN_FAST, { TextColor3 = CONFIG.TEXT_GRAY }):Play()446end)447448-- --------------------------------------------------------449-- DIALOGUE HISTORY450-- --------------------------------------------------------451local historyPanel = createInstance("Frame", {452 Name = "DialogueHistory",453 Size = UDim2.fromOffset(350, 300),454 Position = UDim2.new(1, -370, 0, 20),455 BackgroundColor3 = CONFIG.BG_PRIMARY,456 BackgroundTransparency = 0.1,457 BorderSizePixel = 0,458 ZIndex = 15,459 Parent = dialogueContainer,460 Visible = false,461})462createCorner(historyPanel, 12)463createStroke(historyPanel, CONFIG.BG_ACCENT, 2)464465local historyTitle = createInstance("TextLabel", {466 Name = "HistoryTitle",467 Size = UDim2.new(1, 0, 0, 30),468 BackgroundTransparency = 1,469 Text = "Conversation History",470 TextColor3 = CONFIG.TEXT_YELLOW,471 TextSize = 14,472 Font = Enum.Font.GothamBlack,473 ZIndex = 16,474 Parent = historyPanel,475})476477local historyScrolling = createInstance("ScrollingFrame", {478 Name = "HistoryScrolling",479 Size = UDim2.new(1, -16, 1, -60),480 Position = UDim2.fromOffset(8, 35),481 BackgroundTransparency = 1,482 ScrollBarThickness = 4,483 ScrollBarImageColor3 = CONFIG.BG_ACCENT,484 ZIndex = 16,485 Parent = historyPanel,486})487488local historyLayout = createInstance("UIListLayout", {489 SortOrder = Enum.SortOrder.LayoutOrder,490 Padding = UDim.new(0, 4),491 Parent = historyScrolling,492})493494local closeHistoryButton = createInstance("TextButton", {495 Name = "CloseHistory",496 Size = UDim2.fromOffset(60, 24),497 Position = UDim2.new(1, -10, 0, 4),498 AnchorPoint = Vector2.new(1, 0),499 BackgroundColor3 = CONFIG.CHOICE_BG,500 Text = "Close",501 TextColor3 = CONFIG.TEXT_WHITE,502 TextSize = 11,503 Font = Enum.Font.GothamBold,504 BorderSizePixel = 0,505 ZIndex = 17,506 Parent = historyPanel,507})508createCorner(closeHistoryButton, 4)509510-- ============================================================511-- TYPEWRITER SYSTEM512-- ============================================================513514local typewriterConnection = nil515local continueBlinkConnection = nil516517local function startTypewriter(text)518 state.currentText = ""519 state.targetText = text520 state.canContinue = false521 continueHint.Visible = false522 typingCursor.Visible = true523 dialogueText.Text = ""524525 -- Cancel existing typewriter526 if typewriterConnection then527 typewriterConnection:Disconnect()528 typewriterConnection = nil529 end530531 local charIndex = 0532 local totalChars = #text533534 typewriterConnection = game:GetService("RunService").Heartbeat:Connect(function()535 charIndex += 1536 if charIndex > totalChars then537 -- Typewriter complete538 if typewriterConnection then539 typewriterConnection:Disconnect()540 typewriterConnection = nil541 end542 state.currentText = text543 state.canContinue = true544 dialogueText.Text = text545 continueHint.Visible = true546 typingCursor.Visible = false547548 -- Pulse the continue hint549 if continueBlinkConnection then550 continueBlinkConnection:Disconnect()551 end552 local visible = true553 continueBlinkConnection = game:GetService("RunService").Heartbeat:Connect(function()554 -- Blink every CONTINUE_BLINK_SPEED seconds555 end)556 -- Use a simpler task-based blink557 task.spawn(function()558 while state.canContinue do559 task.wait(CONFIG.CONTINUE_BLINK_SPEED)560 if continueHint and continueHint.Parent then561 continueHint.TextTransparency = continueHint.TextTransparency == 0 and 0.5 or 0562 end563 end564 end)565 return566 end567568 -- Add next character569 state.currentText = string.sub(text, 1, charIndex)570 dialogueText.Text = state.currentText571572 -- Play typewriter sound every few characters573 if charIndex % 2 == 0 then574 -- Sound effect trigger would go here575 end576 end)577end578579local function skipTypewriter()580 if typewriterConnection then581 typewriterConnection:Disconnect()582 typewriterConnection = nil583 end584 state.currentText = state.targetText585 state.canContinue = true586 dialogueText.Text = state.targetText587 continueHint.Visible = true588 typingCursor.Visible = false589end590591-- ============================================================592-- CHOICE MANAGEMENT593-- ============================================================594595local choiceButtons = {}596597local function clearChoices()598 for _, btn in ipairs(choiceButtons) do599 btn:Destroy()600 end601 choiceButtons = {}602 choicesContainer.Visible = false603end604605local function showChoices(choices)606 clearChoices()607 state.choices = choices608 choicesContainer.Visible = true609610 for i, choice in ipairs(choices) do611 local btn = createInstance("TextButton", {612 Name = "ChoiceButton_" .. i,613 Size = UDim2.new(1, 0, 0, CONFIG.CHOICE_HEIGHT),614 BackgroundColor3 = CONFIG.CHOICE_BG,615 Text = choice.text,616 TextColor3 = CONFIG.TEXT_WHITE,617 TextSize = 15,618 Font = Enum.Font.GothamBold,619 BorderSizePixel = 0,620 LayoutOrder = i,621 ZIndex = 11,622 Parent = choicesContainer,623 ClipsDescendants = true,624 })625 createCorner(btn, 8)626 createStroke(btn, CONFIG.BG_ACCENT, 1)627628 -- Hover effects629 btn.MouseEnter:Connect(function()630 TweenService:Create(btn, CONFIG.TWEEN_FAST, {631 BackgroundColor3 = CONFIG.CHOICE_HOVER,632 Position = UDim2.new(0, 8, 0, btn.Position.Y.Offset),633 }):Play()634 end)635636 btn.MouseLeave:Connect(function()637 TweenService:Create(btn, CONFIG.TWEEN_FAST, {638 BackgroundColor3 = CONFIG.CHOICE_BG,639 Position = UDim2.new(0, 0, 0, btn.Position.Y.Offset),640 }):Play()641 end)642643 -- Click handler644 btn.Activated:Connect(function()645 -- Animate selection646 TweenService:Create(btn, CONFIG.TWEEN_FAST, {647 BackgroundColor3 = CONFIG.CHOICE_SELECTED,648 Size = UDim2.new(0.95, 0, 0, CONFIG.CHOICE_HEIGHT + 4),649 }):Play()650651 task.delay(0.2, function()652 selectChoiceEvent:FireServer(choice.id)653 clearChoices()654 end)655 end)656657 -- Entry animation658 btn.BackgroundTransparency = 1659 btn.TextTransparency = 1660 TweenService:Create(btn, TweenInfo.new(0.2, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, (i - 1) * CONFIG.CHOICE_STAGGER), {661 BackgroundTransparency = 0,662 TextTransparency = 0,663 }):Play()664665 table.insert(choiceButtons, btn)666 end667end668669-- ============================================================670-- PORTRAIT MANAGEMENT671-- ============================================================672673local function updatePortrait(portraitId, speakerName_text)674 state.currentPortraitId = portraitId675 state.currentSpeaker = speakerName_text676677 -- Animate portrait change678 TweenService:Create(portraitImage, CONFIG.TWEEN_FAST, {679 ImageTransparency = 1,680 }):Play()681682 task.delay(0.15, function()683 portraitImage.Image = portraitId ~= "" and portraitId or ""684 portraitPlaceholder.Visible = portraitId == ""685686 TweenService:Create(portraitImage, CONFIG.TWEEN_FAST, {687 ImageTransparency = 0,688 }):Play()689 end)690691 -- Update name692 nameTagText.Text = speakerName_text693 speakerName.Text = speakerName_text694695 -- Animate name tag696 nameTagBg.Size = UDim2.fromOffset(20, CONFIG.NAME_TAG_HEIGHT)697 TweenService:Create(nameTagBg, CONFIG.TWEEN_BOUNCE, {698 Size = UDim2.fromOffset(math.max(140, #speakerName_text * 10), CONFIG.NAME_TAG_HEIGHT),699 }):Play()700end701702local function updateEmotion(emotion)703 state.currentEmotion = emotion704 emotionIndicator.Visible = emotion ~= "neutral"705706 local emojiMap = {707 happy = "H",708 sad = "S",709 angry = "A",710 surprised = "!",711 thinking = "?",712 neutral = "N",713 }714715 emotionText.Text = emojiMap[emotion] or "N"716717 local colorMap = {718 happy = Color3.fromRGB(255, 220, 80),719 sad = Color3.fromRGB(100, 150, 255),720 angry = Color3.fromRGB(255, 80, 80),721 surprised = Color3.fromRGB(255, 180, 80),722 thinking = Color3.fromRGB(180, 120, 255),723 neutral = CONFIG.CONTINUE_GLOW,724 }725726 emotionIndicator.BackgroundColor3 = colorMap[emotion] or CONFIG.CONTINUE_GLOW727728 -- Bounce animation729 TweenService:Create(emotionIndicator, CONFIG.TWEEN_BOUNCE, {730 Size = UDim2.fromOffset(28, 28),731 }):Play()732 task.delay(0.2, function()733 TweenService:Create(emotionIndicator, CONFIG.TWEEN_FAST, {734 Size = UDim2.fromOffset(24, 24),735 }):Play()736 end)737end738739-- ============================================================740-- DIALOGUE HISTORY741-- ============================================================742743local function addToHistory(speaker, text)744 local item = createInstance("TextLabel", {745 Name = "HistoryItem",746 Size = UDim2.new(1, 0, 0, 36),747 BackgroundTransparency = 1,748 Text = string.format("<b>%s:</b> %s", speaker, text),749 TextColor3 = CONFIG.TEXT_WHITE,750 TextSize = 12,751 Font = Enum.Font.GothamBold,752 TextWrapped = true,753 TextXAlignment = Enum.TextXAlignment.Left,754 LineHeight = 1.3,755 ZIndex = 16,756 Parent = historyScrolling,757 RichText = true,758 })759760 -- Auto-scroll761 task.delay(0.05, function()762 historyScrolling.CanvasPosition = Vector2.new(0, historyScrolling.AbsoluteCanvasSize.Y)763 end)764765 table.insert(state.dialogueHistory, { speaker = speaker, text = text })766end767768local function toggleHistory()769 state.historyOpen = not state.historyOpen770 historyPanel.Visible = state.historyOpen771772 if state.historyOpen then773 TweenService:Create(historyPanel, CONFIG.TWEEN_NORMAL, {774 BackgroundTransparency = 0.1,775 Size = UDim2.fromOffset(350, 300),776 }):Play()777 end778end779780closeHistoryButton.Activated:Connect(toggleHistory)781782-- ============================================================783-- DIALOGUE OPEN / CLOSE784-- ============================================================785786local function openDialogue(speaker, text, portrait, emotion, choices)787 state.isActive = true788789 -- Show overlay790 darkOverlay.Visible = true791 TweenService:Create(darkOverlay, CONFIG.TWEEN_SLOW, {792 BackgroundTransparency = 0.5,793 }):Play()794795 -- Show container796 dialogueContainer.Visible = true797798 -- Slide in from bottom799 dialogueContainer.Position = UDim2.new(0.5, 0, 1, 100)800 TweenService:Create(dialogueContainer, CONFIG.TWEEN_BOUNCE, {801 Position = UDim2.new(0.5, 0, 1, -60),802 }):Play()803804 -- Slide in portrait805 task.delay(0.1, function()806 TweenService:Create(portraitContainer, CONFIG.TWEEN_BOUNCE, {807 Position = UDim2.fromOffset(-20, -30),808 }):Play()809 end)810811 -- Update portrait and speaker812 if portrait then813 updatePortrait(portrait, speaker)814 end815 if emotion then816 updateEmotion(emotion)817 end818819 -- Start typewriter820 task.delay(0.3, function()821 startTypewriter(text)822 end)823824 -- Show choices if provided825 if choices and #choices > 0 then826 task.delay(0.5 + #text * CONFIG.TYPEWRITER_SPEED, function()827 showChoices(choices)828 end)829 end830end831832local function closeDialogue()833 state.isActive = false834 state.canContinue = false835836 -- Stop typewriter837 if typewriterConnection then838 typewriterConnection:Disconnect()839 typewriterConnection = nil840 end841842 -- Hide choices843 clearChoices()844845 -- Slide out846 TweenService:Create(dialogueContainer, CONFIG.TWEEN_NORMAL, {847 Position = UDim2.new(0.5, 0, 1, 100),848 }):Play()849850 TweenService:Create(portraitContainer, CONFIG.TWEEN_NORMAL, {851 Position = UDim2.fromOffset(-200, -30),852 }):Play()853854 TweenService:Create(darkOverlay, CONFIG.TWEEN_SLOW, {855 BackgroundTransparency = 1,856 }):Play()857858 task.delay(0.5, function()859 dialogueContainer.Visible = false860 darkOverlay.Visible = false861 historyPanel.Visible = false862 end)863end864865local function continueDialogue()866 if not state.canContinue then867 -- Skip typewriter if still typing868 skipTypewriter()869 return870 end871872 -- Send continue to server873 advanceDialogueEvent:FireServer()874end875876-- ============================================================877-- INPUT HANDLING878-- ============================================================879880-- Click on dialogue box to continue881local dialogueClickConnection = nil882883local function setupDialogueClick()884 if dialogueClickConnection then return end885886 dialogueClickConnection = dialogueBg.InputBegan:Connect(function(input)887 if input.UserInputType == Enum.UserInputType.MouseButton1888 or input.UserInputType == Enum.UserInputType.Touch then889 if state.isActive then890 continueDialogue()891 end892 end893 end)894end895896setupDialogueClick()897898-- Skip button899skipButton.Activated:Connect(function()900 if state.isActive then901 endDialogueEvent:FireServer()902 closeDialogue()903 end904end)905906-- Keyboard shortcuts907UserInputService.InputBegan:Connect(function(input, gameProcessed)908 if gameProcessed then return end909910 if not state.isActive then return end911912 if input.KeyCode == Enum.KeyCode.Space913 or input.KeyCode == Enum.KeyCode.Return914 or input.KeyCode == Enum.KeyCode.E then915 continueDialogue()916 elseif input.KeyCode == Enum.KeyCode.H then917 toggleHistory()918 elseif input.KeyCode == Enum.KeyCode.Escape then919 endDialogueEvent:FireServer()920 closeDialogue()921 end922end)923924-- ============================================================925-- REMOTE EVENT CONNECTIONS926-- ============================================================927928startDialogueEvent.OnClientEvent:Connect(function(data)929 -- data: { speaker, text, portrait, emotion, choices }930 openDialogue(931 data.speaker or "NPC",932 data.text or "...",933 data.portrait,934 data.emotion or "neutral",935 data.choices936 )937end)938939advanceDialogueEvent.OnClientEvent:Connect(function(data)940 -- Update with next dialogue segment941 if data.speaker then942 updatePortrait(data.portrait or "", data.speaker)943 end944 if data.emotion then945 updateEmotion(data.emotion)946 end947 if data.text then948 addToHistory(data.speaker or state.currentSpeaker, state.currentText)949 startTypewriter(data.text)950 end951 if data.choices and #data.choices > 0 then952 task.delay(0.3, function()953 showChoices(data.choices)954 end)955 else956 clearChoices()957 end958end)959960endDialogueEvent.OnClientEvent:Connect(function()961 addToHistory(state.currentSpeaker, state.currentText)962 closeDialogue()963end)964965portraitUpdateEvent.OnClientEvent:Connect(function(portraitId, speakerName_text)966 updatePortrait(portraitId, speakerName_text)967end)968969emotionUpdateEvent.OnClientEvent:Connect(function(emotion)970 updateEmotion(emotion)971end)972973-- ============================================================974-- INITIALIZATION975-- ============================================================976977print("[Dialogue System] Initialized successfully")978979-- Continue hint blink (global)980task.spawn(function()981 while true do982 task.wait(CONFIG.CONTINUE_BLINK_SPEED)983 if state.canContinue and continueHint and continueHint.Parent then984 continueHint.TextTransparency = continueHint.TextTransparency == 0 and 0.5 or 0985 end986 end987end)988989-- Demo sequence after 2 seconds990task.delay(2, function()991 if not state.isActive then992 openDialogue(993 "Elder Wizard",994 "Welcome, traveler! I have been expecting you. The kingdom is in grave danger and only you can help us...",995 "",996 "happy",997 {998 { id = "accept", text = "I will help! Tell me more." },999 { id = "decline", text = "I'm busy right now." },1000 { id = "ask", text = "What kind of danger?" },1001 }1002 )1003 end1004end)