A while back I took a stab at a dialogue system in kotlin, it didn't handle recursion and wouldn't work in several actual use cases so I deleted it a few hours later. Yesterday I tried again and it now works a treat so I thought I'd post the inital draft as it might be of help to someone.
Scripts look like this
Code:
dialogue {
chat("Hi")
options("Hello can you help me") {
id(0)
option("Yes") {
chat("Great thanks, take this...")
action {
println("*Give player item*")
}
redirect(0)
}
option("No") {
chat("Why not?")
}
}
}
The class just prints it so it's readable
Code:
[1] (Chat) Hi -> [0] (Options) Hello can you help me
[0] (Options) Hello can you help me -> [[2] (Option) Yes, [6] (Option) No]
[2] (Option) Yes -> [3] (Chat) Great thanks, take this...
[3] (Chat) Great thanks, take this... -> [4] (Actionable)
[4] (Actionable) -> [5] (Redirect)
[5] (Redirect) -> [1] (Chat) Hi
[6] (Option) No -> [7] (Chat) Why not?
[7] (Chat) Why not? -> null
Obviously this was just the inital test so it doesn't actually support different chat types or running through a dialogue but you get the gist.
DialogueTest.kt Class
Code:
package com.rs.game
class DialogueTest {
class Player
data class Combined(var dialogue: Dialogue, var list: ArrayList<Dialogue>)
private class Builder(action: Combined.() -> Unit) : Dialogue(), Linear {
override var next: Dialogue? = null
init {
val list = ArrayList<Dialogue>()
action(Combined(this, list))
next = list.first()
}
}
private class Actionable(override val action: Player.() -> Unit) : Dialogue(), Linear, Action {
override var next: Dialogue? = null
}
fun Combined.action(action: Player.() -> Unit) {
dialogue.create(Actionable(action))
}
private class Options(text: String) : Chat(text) {
var options = ArrayList<Dialogue>()
}
private class Option(text: String) : Chat(text)
fun Combined.option(text: String, action: Combined.() -> Unit) {
dialogue.apply {
val test = ArrayList<Dialogue>()
val dialogue = add(Option(text))
list.add(dialogue)
action(Combined(dialogue, test))
}
}
fun Combined.options(text: String, action: Combined.() -> Unit) {
dialogue.apply {
val list = ArrayList<Dialogue>()
val dialogue = create(Options(text))
[email protected](dialogue)
action(Combined(dialogue, list))
dialogue.options.addAll(list.filterIsInstance<Option>())
}
}
private class ItemStatement(text: String, val item: Int) : Chat(text)
private class PlayerStatement(text: String, val player: Int) : Chat(text)
private class MobStatement(text: String, val mob: Int) : Chat(text)
private class Redirect(val directId: Int) : Dialogue()
fun Combined.redirect(id: Int) {
//Connect redirect to the previous dialogue
//Don't want it added to the queue as anything following a redirect will be ignored
dialogue.connect(Redirect(id))
}
open class Dialogue {
var id: Int = -1
private var last: Dialogue? = null
fun <T : Dialogue> add(dialogue: T): T {
last = dialogue
return dialogue
}
fun <T : Dialogue> create(dialogue: T): T {
//Connect previous dialogue to current one
connect(dialogue)
//Set current dialogue as the last
add(dialogue)
return dialogue
}
fun connect(dialogue: Dialogue) {
val previous = last ?: this
(previous as? Linear)?.next = dialogue
}
override fun toString(): String {
return "[$id] (${this::class.simpleName})"
}
}
fun Combined.id(id: Int) {
dialogue.id = id
}
open class Chat(val text: String = "") : Dialogue(), Linear {
override var next: Dialogue? = null
override fun toString(): String {
return "${super.toString()} $text"
}
}
interface Linear {
var next: Dialogue?
}
interface Action {
val action: Player.() -> Unit
}
fun Combined.chat(text: String) {
dialogue.apply {
val dialogue = create(Chat(text))
list.add(dialogue)
}
}
private class DialogueBuilder(action: Combined.() -> Unit) {
private val builder = Builder(action)
fun build(): List<Dialogue> {
val all = collect(builder)
redirectCheck(all)
assign(all)
return all
}
/**
* Checks dialogues for redirect formatting issues
* @throws IllegalArgumentException
*/
@Throws(IllegalArgumentException::class)
fun redirectCheck(all: List<Dialogue>) {
//Quick redirect check
val manualIds = all.asSequence().filter { it.id != -1 }
val injects = all.filterIsInstance<Options>().flatMap { it.options.asSequence().filterIsInstance<Redirect>().toList() }
if (manualIds.count() < injects.count()) {
throw IllegalArgumentException("Dialogue script must have id's set for all redirects.")
}
//More lengthy redirect check
val ids = manualIds.map { it.id }
val brokenInject = injects.firstOrNull { !ids.contains(it.id) }
if (brokenInject != null) {
throw IllegalArgumentException("No dialogue id found for redirect: ${brokenInject.id}.")
}
//Duplication check
val distinct = ids.distinct()
if (ids.count() != distinct.count()) {
throw IllegalArgumentException("Duplicate redirect ids found: ${ids.groupingBy { it }.eachCount().filter { it.value > 1 }.keys}")
}
}
/**
* Assigns the remaining dialogues with id's
*/
fun assign(all: List<Dialogue>) {
val ids = all.asSequence().filter { it.id != -1 }.map { it.id }
//Automatically assign the remaining id's
var count = 0
all.asSequence().filter { it.id == -1 }.forEach {
while (ids.contains(count))
count++
it.id = count++
}
}
/**
* Collects all the dialogues into one list
*/
fun collect(dialogue: Dialogue): List<Dialogue> {
val list = ArrayList<Dialogue>()
if (dialogue !is Builder) {
list.add(dialogue)
}
if (dialogue is Linear && dialogue.next != null) {
list.addAll(collect(dialogue.next!!))
} else if (dialogue is Options) {
list.addAll(dialogue.options.flatMap { collect(it) })
}
return list
}
}
private fun dialogue(build: (Combined.() -> Unit)): List<Dialogue> {
return DialogueBuilder(build).build()
}
init {
val start = System.nanoTime()
val script = dialogue {
chat("Hi")
options("Hello can you help me") {
id(0)
option("Yes") {
chat("Great thanks, take this...")
action {
println("*Give player item*")
}
redirect(0)
}
option("No") {
chat("Why not?")
}
}
}
println("Complete in ${(System.nanoTime() - start) / 1000000}ms")
print(script)
}
/**
* Prints dialogues list in readable format
*/
fun print(script: List<Dialogue>) {
//Print results
script.forEach { d ->
println(when (d) {
is Options -> "$d -> ${d.options}"
is Linear -> "$d -> ${d.next}"
is Redirect -> "$d -> ${script[d.directId]}"
else -> "$d -> [] ([])"
})
}
}
}
fun main(args: Array<String>) {
DialogueTest()
}
Although the current version is a bit different from this, code feedback always welcome