Readability and Maintainability of a Chained Dialogue Builder

hey- for people with experience in making dialogue systems, may i have some feedback on this dialogue structure i came up with

return option.build()
    .speak('hey')
	.with({
		speaker='man',
		delay=3
	})
	.iff(function(player, info)
		return workspace:GetAttribute('hey')
	end)

	.reply('hey man hows vit going')
		.speak('im doin fine, how about yourself?')
			.with({
				delay=3
			})
			
		.speak('i heard you\'ve been doing some cool things?')
			.label('coolthings')
			
			.reply('yeah yeah for sure')
				.speak('yeah i mean all i\'ve been really doing is killing mobs.')
					.with({
						speaker='myself'
					})

				.speak('nothing spectacular, yk?')
					.reply('i mean taking lives, even a mob\'s, requires some serious resolve man.')
					
					.tie()
			.tie()
	.tie()

	.reply('hey! just stopping by')
		.speak('oh dude you\'re too kind :)')
		.jump('coolthings')
	.tie()

	.reply('wait ill be back')
	
	.tie()

(for my 1-life hardcore desert true-survival game neniadea which takes inspiration from PZ)

theres nothing wrong with this code nor the option code; i really just want advice or feedback on the scalability of this dialogue syntax, and any alternative ways to structurue the dialogue.

i’m avoiding writing in raw data tables because they dont scale at all; this option metatable will be converted to a raw data table so it can be flattened and compressed for the client

few things to note, that i havent included here:

  • when a player gets near an npc the dialogue will be replicated to them in full.
  • iff (if and only if) accpets 3 argumetns- the latter two will bind to a player so the dialogue tree updates if a player satisfies a specific condition
  • chatgpt suggested i have a option:validate() function to make sure replies and ties are consistent, which is what ill do
  • i will probably implement a preset ‘with operation’, e.g. option.definePreset and option.withPreset
  • each quote will have an inline ‘with’ operation, done with a lexer. for example, if i wanted to give a phrase more expression, i could do:
"i heard you've been doing some<period_length=0.8>... <wavy=true,underline=true,speech_rate=0.1> cool things?"

it would probably be strucutred or expressed differently in the final draft.
‘with’ lets me add these parameters in a clean-ish way. for one: say im copy and pasting a bunch of raw dialogue from an odt text file; then, if i were to replace the currently existing version of the text in the games dialogue data, i wouldn’t have to erase the whole thing and reimplement the inline ‘with’ data, etc. additionally, it allows greater readability and intuition.


the syntax itself is very self-explanatory, almost lua-like, but ill give an overview:

option.build(text, parent) builds the dialogue node. option.reply(text) also builds a dialogue node and is just a readability-focused wrapper to option.build

option.speak({text...} or text) basically gives the current node some text to go through. you can chain multiple together- dialogue in this game, much like other roblox games, is not a ‘you say 1 thing, i say 1 thing’ type of system.

option.iff(deterministicConditionPerDialogueReplicationToPlayer, bindChangeFunction, unbindChangedFunction) ive already described it earlier and the parameters should be clear enough. a dialogue option only appears IF AND ONLY IF the condition is true.

option.with applies data to a dialogue node (either a speak or a reply)

option.tie() is like lua’s end keyword

option.label and option.jump is just like what it is in assembly


i was thinking about making a language similar to ink

not sure. because making my own language would strip me of being able to directly have functions like .iff and would require a lot of effort


here’s my untested draft of the option class

Summary
local option = {}
option.__index = option

function option.build(text, parent, isReply)
	local node = {parent=parent, text=text, isReply=(isReply or false), branches={}}
	return setmetatable({}, {
		__index = function(self, key)
			if (option[key]) then
				return function(...)
					return option[key](node, ...)
				end
			end
		end,
	})
end

function option:reply(text)
	return option.build(self, text, true)
end

function option:speak(text)
	table.insert(self.branches, {text=text})
	return self
end

function option:tie()
	return self.parent.parent or self
end

function option:with(datum)
	if (self.isReply) then
		self.datum = datum
	else
		self.branches[#self.branches].datum = datum
	end
	
	return self
end

function option:iff(fn, onPlayerAdded, onPlayerRemoval)
	local branch = self.branches[#self.branches]
	branch.iff = fn
	branch.onPlayerAdded = onPlayerAdded
	branch.onPlayerRemoval = onPlayerRemoval
	return self
end

return option

but yeah, overall how is the structure of this dialogue system. what would you change, remove, add, etc? what do you think of my stylistic choices, and what do you think of the creation of a scripting language like ink?

I unfortunately don’t have a lot of experience with dialogue systems, but I think I do have enough credibility to ask: Wouldn’t this get really messy?
I mean, if you are confident you wouldn’t get frustrated then go ahead, but this really seems like it’ll be a pain to deal with.
I obviously can’t suggest anything better since I don’t have a lot of experience, but I still think my point stands.

i mean thats exactly my concern, will it get messy? i cant tell.
i had ai just now write me some dialogue

simpler one:

Summary
local require = shared.require

local option = require('server@dialogueHandler/option')

return option.build()

-- ENTRY
.speak("...Oh. A traveler.")
.with({
	speaker = "stranger",
	delay = 2
})
.iff(function(player, info)
	return not player:GetAttribute("MetStranger")
end)

.speak("I don't get many visitors down here.")
.with({ delay = 1 })

.reply("Who are you?")
	.speak("Names don't carry much weight anymore.")
	.speak("But once, they called me Caelum.")
	.with({ speaker = "Caelum" })
	.label("intro_name")
	.tie()

.reply("Where am I?")
	.speak("Below the old roads.")
	.speak("Where decisions come to rest.")
	.with({ delay = 2 })
	.tie()

.reply("I should go.")
	.speak("You could.")
	.speak("But something tells me you won't.")
	.tie()

-- HUB
.speak("So. What do you want to know?")
.label("hub")

.reply("Tell me about this place.")
	.speak("This place exists between moments.")
	.speak("When people hesitate...")
	.speak("It grows.")
	.with({ delay = 1 })
	.reply("That doesn't make sense.")
		.speak("It doesn't have to.")
		.tie()
	.reply("Sounds dangerous.")
		.speak("Only if you plan to leave unchanged.")
		.tie()
	.tie()

.reply("Tell me about yourself.")
	.speak("I was once like you.")
	.speak("Curious. Armed. Certain.")
	.reply("What happened?")
		.speak("I chose poorly.")
		.speak("Then I chose again.")
		.speak("And again.")
		.tie()
	.reply("Do you regret it?")
		.speak("Regret is just memory with teeth.")
		.tie()
	.tie()

.reply("I'm looking for work.")
	.speak("Work?")
	.speak("Hmm.")
	.speak("There *is* something.")
	.label("offer_job")
	.reply("I'm listening.")
		.speak("A fracture has formed nearby.")
		.speak("Something is leaking through.")
		.reply("What kind of something?")
			.speak("The kind that remembers being human.")
			.tie()
		.reply("I'll handle it.")
			.speak("Good.")
			.speak("Return if you survive.")
			.with({ speaker = "Caelum", delay = 2 })
			.tie()
		.tie()
	.reply("Not interested.")
		.speak("Few are.")
		.speak("Fewer last.")
		.tie()
	.tie()

.reply("This is getting weird.")
	.speak("You noticed.")
	.tie()

.reply("Let's talk about something else.")
	.jump("hub")
	.tie()

-- SECRET PATH
.speak("Wait.")
.iff(function(player, info)
	return player:GetAttribute("KilledManyMobs") == true
end)

.speak("Your hands are loud.")
.with({ speaker = "Caelum" })

.reply("What does that mean?")
	.speak("Violence echoes.")
	.speak("Some of us can hear it.")
	.tie()

.reply("They deserved it.")
	.speak("Maybe.")
	.speak("So did I, once.")
	.tie()

.reply("You're judging me?")
	.speak("No.")
	.speak("I'm measuring you.")
	.label("measurement")
	.tie()

-- LOOPING MORAL TEST
.speak("Answer honestly.")
.with({ delay = 1 })

.reply("I do what I must to survive.")
	.speak("A common answer.")
	.speak("And an incomplete one.")
	.jump("measurement")
	.tie()

.reply("I enjoy the fight.")
	.speak("Honest.")
	.speak("Concerning.")
	.jump("measurement")
	.tie()

.reply("I hate it.")
	.speak("Good.")
	.speak("That means you're still human.")
	.tie()

-- ENDING BRANCHES
.speak("We've talked long enough.")

.reply("So what happens now?")
	.speak("Now you leave.")
	.speak("Or you stay.")
	.tie()

.reply("I'll leave.")
	.speak("Most do.")
	.speak("Few return.")
	.with({ delay = 2 })
	.tie()

.reply("I'll stay.")
	.speak("Then listen carefully.")
	.speak("This place will change you.")
	.speak("Whether you want it to or not.")
	.tie()

.reply("Can we talk again?")
	.speak("We already are.")
	.jump("hub")
	.tie()

-- FINAL
.speak("Until next time, traveler.")
.with({
	speaker = "Caelum",
	delay = 3
})

some of the simpler ones lines are cool asf what the hell :joy:

.reply("Tell me about this place.")
	.speak("This place exists between moments.")
	.speak("When people hesitate...")
	.speak("It grows.")
	.with({ delay = 1 })
	.reply("That doesn't make sense.")
		.speak("It doesn't have to.")
---
.reply("That doesn't make sense.")
		.speak("It doesn't have to.")
---
I was once like you.
Curious. Armed. Certain.

I chose poorly.
Then I chose again.
And again.

Regret is just memory with teeth.

complexer one:

Summary
return option.build()

	-- ENTRY POINT
	.speak("...Hey. You came back.")
	.with({
		speaker = "man",
		delay = 2
	})
	.iff(function(player, info)
		return workspace:GetAttribute("hey") == true
	end)

	.speak("Most people don't. They say they will, but they never do.")
	.with({ delay = 2 })

	.reply("Yeah. I figured I'd check in.")
		.speak("That figures.")
		.speak("You always struck me as the type who can't quite leave things unfinished.")
		.with({ delay = 3 })
		.tie()

	.reply("I didn't really have a choice.")
		.speak("Funny thing about choices.")
		.speak("We only notice them once they're gone.")
		.tie()

	.reply("Let's skip the small talk.")
		.speak("Heh.")
		.speak("Straight to business, then.")
		.tie()

	-- HUB
	.speak("So. What are we doing this time?")
	.label("main_hub")

	.reply("Ask about you.")
		.speak("About me?")
		.speak("That's a deep hole to start digging.")
		.reply("Try me.")
			.speak("Alright.")
			.speak("I used to run jobs for people who didn't want to get their hands dirty.")
			.speak("Scouting. Cleanup. Making sure problems stayed quiet.")
			.with({ delay = 2 })

			.reply("Sounds familiar.")
				.speak("Yeah.")
				.speak("That's what scares me.")
				.tie()

			.reply("Why'd you stop?")
				.speak("Because eventually you realize you're not solving problems.")
				.speak("You're just making sure someone else never has to see them.")
				.tie()

			.reply("Do you regret it?")
				.speak("Regret implies I had better options.")
				.speak("I'm not convinced I did.")
				.tie()
			.tie()
		.tie()

	.reply("Ask about the area.")
		.speak("This place?")
		.speak("It's a crossroads.")
		.speak("Not on any map, but plenty of people pass through it anyway.")
		.reply("Doing what?")
			.speak("Running from something.")
			.speak("Or toward it.")
			.tie()

		.reply("Is it dangerous?")
			.speak("Only if you stay long enough to get comfortable.")
			.tie()
		.tie()

	.reply("Looking for work.")
		.speak("Of course you are.")
		.speak("You have that look.")
		.iff(function(player, info)
			return player:GetAttribute("CombatLevel") >= 5
		end)

		.reply("What look?")
			.speak("Someone who's survived enough fights to stop being scared of the next one.")
			.tie()

		.reply("What've you got?")
			.speak("There's something nesting under the old transit tunnel.")
			.speak("It's not supposed to be alive.")
			.speak("And it's definitely not supposed to be learning.")
			.with({ delay = 2 })

			.reply("Learning?")
				.speak("Adapting.")
				.speak("Remembering.")
				.speak("That's usually when things go wrong.")
				.tie()

			.reply("I'll handle it.")
				.speak("I thought you'd say that.")
				.speak("Try not to make it angry before you understand it.")
				.player(function(player)
					player:SetAttribute("TunnelQuest", true)
				end)
				.tie()

			.reply("Not my problem.")
				.speak("It will be.")
				.speak("Just not today.")
				.tie()
			.tie()
		.tie()

	.reply("About the killing.")
		.iff(function(player, info)
			return player:GetAttribute("KillCount") and player:GetAttribute("KillCount") > 20
		end)

		.speak("Ah.")
		.speak("So you noticed.")
		.speak("Most don't.")
		.with({ delay = 2 })

		.reply("You judging me?")
			.speak("No.")
			.speak("I'm trying to understand whether you still hear it.")
			.tie()

		.reply("Hear what?")
			.speak("The part of you that hesitates.")
			.speak("Once that goes quiet, it never comes back.")
			.tie()

		.reply("They deserved it.")
			.speak("Maybe.")
			.speak("That answer gets easier every time, doesn't it?")
			.tie()
		.tie()

	.reply("I should go.")
		.speak("Yeah.")
		.speak("You probably should.")
		.reply("Will I see you again?")
			.speak("Depends.")
			.speak("On the choices you make when no one's watching.")
			.tie()

		.reply("You make it sound ominous.")
			.speak("I prefer 'honest'.")
			.tie()
		.tie()

	.reply("Let's start over.")
		.jump("main_hub")
		.tie()

	-- SECRET LOOP
	.speak("One more thing.")
	.iff(function(player, info)
		return player:GetAttribute("TunnelQuest") == true
	end)

	.speak("If it talks to you...")
	.speak("Don't answer immediately.")
	.with({ delay = 2 })

	.reply("Why?")
		.speak("Because the first answer is never really yours.")
		.tie()

	.reply("You're not helping.")
		.speak("I know.")
		.speak("Good luck.")
		.tie()

do you think this is sustainable in the long run? do you have any ideas on improving this, or do you have any ideas for an alternative structure? etc. like just any thoughts. like how would this be a pain to deal with, and do you think something like raw data tables, e.g. (i had ai turn ‘complexer one’ into a raw datatable below)

data table
return {
	entry = "start",

	nodes = {

		start = {
			type = "speak",
			text = "...Hey. You came back.",
			with = { speaker = "man", delay = 2 },
			iff = function(player, info)
				return workspace:GetAttribute("hey") == true
			end,
			next = "came_back_1"
		},

		came_back_1 = {
			type = "speak",
			text = "Most people don't. They say they will, but they never do.",
			with = { delay = 2 },
			replies = {
				{
					text = "Yeah. I figured I'd check in.",
					next = "check_in"
				},
				{
					text = "I didn't really have a choice.",
					next = "no_choice"
				},
				{
					text = "Let's skip the small talk.",
					next = "skip_smalltalk"
				}
			}
		},

		check_in = {
			type = "speak",
			text = "That figures. You always struck me as the type who can't quite leave things unfinished.",
			with = { delay = 3 },
			next = "main_hub"
		},

		no_choice = {
			type = "speak",
			text = "Funny thing about choices. We only notice them once they're gone.",
			next = "main_hub"
		},

		skip_smalltalk = {
			type = "speak",
			text = "Heh. Straight to business, then.",
			next = "main_hub"
		},

		-- HUB
		main_hub = {
			type = "speak",
			label = "main_hub",
			text = "So. What are we doing this time?",
			replies = {

				-- ABOUT NPC
				{
					text = "Ask about you.",
					next = "about_you"
				},

				-- AREA
				{
					text = "Ask about the area.",
					next = "about_area"
				},

				-- WORK (STATE GATED)
				{
					text = "Looking for work.",
					iff = function(player)
						return (player:GetAttribute("CombatLevel") or 0) >= 5
					end,
					next = "work_intro"
				},

				-- MORAL CHECK
				{
					text = "About the killing.",
					iff = function(player)
						return (player:GetAttribute("KillCount") or 0) > 20
					end,
					next = "killing_intro"
				},

				-- EXIT
				{
					text = "I should go.",
					next = "exit"
				},

				-- LOOP
				{
					text = "Let's start over.",
					jump = "main_hub"
				}
			}
		},

		-- ABOUT YOU BRANCH
		about_you = {
			type = "speak",
			text = "About me? That's a deep hole to start digging.",
			replies = {
				{ text = "Try me.", next = "about_you_deep" }
			}
		},

		about_you_deep = {
			type = "speak",
			text = "I used to run jobs for people who didn't want to get their hands dirty. Scouting. Cleanup. Making sure problems stayed quiet.",
			with = { delay = 2 },
			replies = {
				{
					text = "Sounds familiar.",
					next = "about_you_familiar"
				},
				{
					text = "Why'd you stop?",
					next = "about_you_stop"
				},
				{
					text = "Do you regret it?",
					next = "about_you_regret"
				}
			}
		},

		about_you_familiar = {
			type = "speak",
			text = "Yeah. That's what scares me.",
			next = "main_hub"
		},

		about_you_stop = {
			type = "speak",
			text = "Because eventually you realize you're not solving problems. You're just making sure someone else never has to see them.",
			next = "main_hub"
		},

		about_you_regret = {
			type = "speak",
			text = "Regret implies I had better options. I'm not convinced I did.",
			next = "main_hub"
		},

		-- AREA
		about_area = {
			type = "speak",
			text = "This place? It's a crossroads. Not on any map, but plenty of people pass through it anyway.",
			replies = {
				{ text = "Doing what?", next = "area_doing" },
				{ text = "Is it dangerous?", next = "area_danger" }
			}
		},

		area_doing = {
			type = "speak",
			text = "Running from something. Or toward it.",
			next = "main_hub"
		},

		area_danger = {
			type = "speak",
			text = "Only if you stay long enough to get comfortable.",
			next = "main_hub"
		},

		-- WORK
		work_intro = {
			type = "speak",
			text = "There's something nesting under the old transit tunnel. It's not supposed to be alive. And it's definitely not supposed to be learning.",
			with = { delay = 2 },
			replies = {
				{ text = "Learning?", next = "work_learning" },
				{
					text = "I'll handle it.",
					next = "work_accept",
					onSelect = function(player)
						player:SetAttribute("TunnelQuest", true)
					end
				},
				{ text = "Not my problem.", next = "work_decline" }
			}
		},

		work_learning = {
			type = "speak",
			text = "Adapting. Remembering. That's usually when things go wrong.",
			next = "main_hub"
		},

		work_accept = {
			type = "speak",
			text = "I thought you'd say that. Try not to make it angry before you understand it.",
			next = "main_hub"
		},

		work_decline = {
			type = "speak",
			text = "It will be. Just not today.",
			next = "main_hub"
		},

		-- MORAL CHECK
		killing_intro = {
			type = "speak",
			text = "So you noticed. Most don't.",
			with = { delay = 2 },
			replies = {
				{ text = "You judging me?", next = "killing_judge" },
				{ text = "Hear what?", next = "killing_hear" },
				{ text = "They deserved it.", next = "killing_deserved" }
			}
		},

		killing_judge = {
			type = "speak",
			text = "No. I'm trying to understand whether you still hear it.",
			next = "main_hub"
		},

		killing_hear = {
			type = "speak",
			text = "The part of you that hesitates. Once that goes quiet, it never comes back.",
			next = "main_hub"
		},

		killing_deserved = {
			type = "speak",
			text = "Maybe. That answer gets easier every time, doesn't it?",
			next = "main_hub"
		},

		-- EXIT
		exit = {
			type = "speak",
			text = "Depends. On the choices you make when no one's watching."
		}
	}
}

would be easier?

and ai says

:check_mark: No chaining → easier serialization
:check_mark: Explicit graph → easy jumps, loops, rewinds
:check_mark: State logic isolated → testable conditions
:check_mark: Works for editor tooling / visualizers
:check_mark: Can be compiled back into fluent syntax if needed

which is true but im not sure i agree with syntactically, because theres a lot of key-pair values that need to be named manually, and the ‘next’ index wouldn’t scale well most likely

i really need this system to scale well because there’s going to be a ridiculous amount of dialogue

1 Like

I don’t really think the data table option that the AI provided is good.
Like you said, there’s way too much key setting.
I quickly thought up a different approach though (emphasis on “quickly”, meaning it might not be that good).
My approach would also involve data tables, but in my opinion it’s better (because of course it is).
So, here’s the strategy:

Main Module

This module is what will read other modules, because we’ll be storing dialogues in modules since that let’s you organize how you’d like.

Dialogue Modules

They return a table which of this type: {[number]: string | () -> ModuleScript?}
My plan is that every dialogue module is a linear line of speech formatted like this: "speaker|text"
Then, the last index of that table is a function which will return which dialogue module to go to next, or none if the dialogue ends.

Example

local choiceMod = require(game.ReplicatedStorage.Modules.ChoiceModule)
return {
    "Joe|Hi!",
    "Doe|Sup.",
    "You|Hello!",
    function()
        if choiceMod("How are you?", "Didn't see you in a while") == 1 then
            return script.Parent:FindFirstChild("HowAreYouDialogue")
        else
            return script.Parent:FindFirstChild("InAWhileDialogue")
        end
    end
}

Why I think it’s good

  • You don’t have to set any key-value pairs
  • Easy to read (meaning it’s easy to make the main module and modify it)
  • Can freely organize it

Why I think it’s bad

  • I didn’t think of a way to pass in arguments for the functions, meaning you’ll rely on instances
  • You might have to redeclare variables a lot of times if you have a lot of branching
  • If you hate making a lot of modulescripts, then this is your hell

Conclusion

I thought this type of thing would be simple and efficient enough, which is all that matters in game development when it comes to code.
What do you think of it?

1 Like

yeah ive thought about that module approach, and i think i used it like 4yrs ago in a really old game of mine, but immediately i dismissed it as infeasible

with your apporach, and its defintiely not a terrible way of doing it, but with your approach

  1. i’m forced with crazy tunnel vision since im only able to view one dialogue branch / linear line of speech at a time
  2. each module would have a lot of boilerplate code
  3. theres a lot of instances and managing those is especially infeasible. for example, when i spam add a sound from robloxs sound library


sure thats feasible; however my game is super dialogue focused
so realistically this is what itd look like

and with multiple equally complex npcs it becomes a child-opening nightmare since dropdowns are closed by default.

this wouldn’t be very fun and doesn’t scale well. and my pc only has 32gb of ram so it wouldn’t be able to fully expand all the modules in explorer

  1. its not my style in general. i tend to keep things pretty streamlined as a programmer- minimal instances and modules

i really appreciate your insight but this wouldn’t work in my game since scalability is so important

i might honestly stick with my initial approach. it might not get better than that- i think dialogue branching is just some unavoidable complexity. of course im still seeking imrpvoement for my structure

1 Like

Yeah, huh.
That’s not wrong!
I do think it can be improved to need less instances and all that, but if you like your approach then it’d be better if you stuck to it (since you’ll eventually master it).

1 Like
finalized syntactic structure
local require = shared.require

local love = require('shared@love')
local option = require('server@dialogueHandler/option')

return option.build()
	.speak('heyyyy')
		.reply('hello')
			.speak('hows it going?')
				.reply('great').tie()
				.reply('hey, uh yeah im doing well i guess')
					.jump('greetings')
					.tie()
			.tie()
		.tie()
		
		.reply('whats up?')
			.label('greetings')
			.speak('nothing crazy, yfm')
				.with({speaker='jane doe'})
			
			.tie()
		.tie()

any suggestions?

I would suggest not having to use .tie() every time you want to close off a command. Even though the formatting of your code makes it more legible, in the end it is still something like .speak("hi").reply("hi").tie().tie() I would look into it so it’s structure is more akin to a tree (dialogues being a subset of another dialogue, search up dialogue trees, you could get a sense of an idea from other games that make their own dialogue system):

2 Likes

thanks for your advice
yeah i’m aware it looks like .speak("hi").reply("hi").tie().tie(), however i dont see too significnat of a scalability issue. it allows for dialogues to be a subset of another dialogue, i.e. it follows a tree-like strucure especially with the label/jump mechanism i implemented.

	.speak('what are you?')
		.reply('tide to an ocean.')
			.speak('and i?')
				.reply('the same.')
					.speak('we must go then.')
						.speak('for there exists ebbs.')
							.reply('what is the point?')
								.reply('for they will come soon').tie()
								.reply('for tides must have ebbs.').tie()
							.tie()
							
							.reply('i do not see any.')
								.reply('i am cunning.').tie()
							.tie()
						.tie()
					.tie()
				.tie()
			.tie()
		.tie()
		
		.reply('love')
			.iff(function(player, p2)
				return player.Name == 'Ilucere1'
			end)
			
			.speak('then i must be ego?')
				.reply('for we are opposites- the tension of them is lovely, morrie')
			
				.tie()
			.tie()
		.tie()
	.tie()

in my old game cinder, dialogue looked like:

local opening = option.New('Yeah?') do
	local questionCindraName = option.New("Your name is very feminine, yet you\'re male?", opening)
	questionCindraName:SetVisibilityCondition(function(player)
		local datum = global.playerData[player]
		local info = datum.information
		
		return not info.dialogue.dialogueData.questionedCindrasName
	end) do
		local responseA = option.New("I see your desire for peace. This isn't known by many, but I lack identity. My brother, on the contrary, has identity. When he died, I stole his identity.", questionCindraName)
		local responseE = option.New("Alright, so, this isn't known by many, but I lack identity. My brother, on the contrary, has identity. When he died, I stole his identity.")
		
		local playerResponseA = option.New('Still, was your brothers name Cindra?', responseA, responseE)
		local playerResponseB = option.New('Okay?', responseA, responseE)
		do
			local responseB = option.New("No, sorry for not elaborating further. My 'identity', if you can call it that, was of the name Cindra. I took my brothers body and changed his name.", playerResponseA)
			local responseC = option.New("My 'identity', if you can call it that, was of the name Cindra. I took my brothers body yet changed the name.", playerResponseB)
		end
		
		local playerResponse3 = option.New("Sorry. I don't have time for a story, could you tell me later?", responseA) do
			local responseB = option.New('No.', playerResponse3) do
				local responseC = option.New('Fine, elaborate further please.', responseB)
				responseE:AddParent(responseC)
				local responseD = option.New("Then I'm leaving.", responseB)
			end
		end
	end
	
	local leave = option.New('I have nothing to say, sorry.', opening) do
		local response = option.New("It's fine.", leave)
		local farewell = option.New('Alright.', response)
	end
end

opening:SetName('cindras speech')

return opening

which followed a similar structure to what you’re showing me; but it’s not scalable. i also tried tables a few years ago- it didn’t work out.
in the end i settled on my approacch. it works perfectly.
one issue i have is the amount of ‘branches’, like the ties. i can jujst add a .tie(n) functionality but im not sure

if you could show me an example of what you mean i would appreciate that.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.