Thread: [KT] Dialogue System

Page 1 of 2 12 LastLast
Results 1 to 10 of 17
  1. #1 [KT] Dialogue System 
    Renown Programmer
    Greg's Avatar
    Join Date
    Jun 2010
    Posts
    1,179
    Thanks given
    260
    Thanks received
    1,012
    Rep Power
    2003
    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
    Attached imageAttached image
    Reply With Quote  
     

  2. Thankful users:


  3. #2  
    Blurite

    Corey's Avatar
    Join Date
    Feb 2012
    Age
    26
    Posts
    1,491
    Thanks given
    1,245
    Thanks received
    1,729
    Rep Power
    5000
    Good release!

    Your show off thread actually inspired me to make my own for Gielinor which ended up looking extremely similar:
    Code:
    override fun dialogue(player: Player, npc: NPC) = player.createDialogue {
            npc chat "Good day, how may I help you?"
            options {
                option("I'd like to access my bank account, please.") {
                    action {
                        player.interfaceHandler.sendInterface(ComponentType.CENTRAL, InterfaceConstants.BANK)
                    }
                }
                option("I'd like to check my PIN settings.") {
                    action {
                        player.actionSender.sendMessage("This would normally open your PIN settings")
                    }
                }
                option("I'd like to collect items.") {
                    action {
                        player.interfaceHandler.sendInterface(ComponentType.CENTRAL, 402)
                    }
                }
                option("What is this place?") {
                    chat("What is this place?")
                    npc chat "This is a branch of the Bank of Gielinor. We have<br>branches in many towns."
                    options {
                        option("And what do you do?") {
                            chat("And what do you do?")
                            npc chat "We will look after your items and money for you.<br>Leave your valuables with us if you want to keep them<br>safe."
                        }
                        option("Didn't you used to be called the Bank of Varrock?") {
                            chat("Didn't you used to be called the Bank of Varrock?")
                            npc chat "Yes we did, but people kept on coming into our<br>branches outside of Varrock and telling us that our<br>signs were wrong. They acted as if we didn't know<br>what town we were in or something."
                        }
                    }
                }
            }
        }
    Spoiler for .:
    Attached image

    (Just noticed we had the exact same idea with action too!)

    Nice to see that my back-end code ended up being similar too:
    Code:
    inline fun Player.createDialogue(init: DialogueBuilder.() -> Unit): DialogueBuilder {
        val dialogueSet = DialogueBuilder(this)
        dialogueSet.init()
        return dialogueSet
    }
    Attached image

    I initially added a similar system to your id/redirect system but never got round to readding it after implementing options.


    Hope people use this and make beautiful dialogues from it, not like the shit every other source has these days!
    Last edited by Corey; 02-25-2019 at 07:30 PM.
    Attached image
    Reply With Quote  
     

  4. Thankful user:


  5. #3  
    Renown Programmer
    Greg's Avatar
    Join Date
    Jun 2010
    Posts
    1,179
    Thanks given
    260
    Thanks received
    1,012
    Rep Power
    2003
    Quote Originally Posted by thot View Post
    Good release!

    Your show off thread actually inspired me to make my own for Gielinor which ended up looking extremely similar:
    Code:
    override fun dialogue(player: Player, npc: NPC) = player.createDialogue {
            npc chat "Good day, how may I help you?"
            options {
                option("I'd like to access my bank account, please.") {
                    action {
                        player.interfaceHandler.sendInterface(ComponentType.CENTRAL, InterfaceConstants.BANK)
                    }
                }
                option("I'd like to check my PIN settings.") {
                    action {
                        player.actionSender.sendMessage("This would normally open your PIN settings")
                    }
                }
                option("I'd like to collect items.") {
                    action {
                        player.interfaceHandler.sendInterface(ComponentType.CENTRAL, 402)
                    }
                }
                option("What is this place?") {
                    chat("What is this place?")
                    npc chat "This is a branch of the Bank of Gielinor. We have<br>branches in many towns."
                    options {
                        option("And what do you do?") {
                            chat("And what do you do?")
                            npc chat "We will look after your items and money for you.<br>Leave your valuables with us if you want to keep them<br>safe."
                        }
                        option("Didn't you used to be called the Bank of Varrock?") {
                            chat("Didn't you used to be called the Bank of Varrock?")
                            npc chat "Yes we did, but people kept on coming into our<br>branches outside of Varrock and telling us that our<br>signs were wrong. They acted as if we didn't know<br>what town we were in or something."
                        }
                    }
                }
            }
        }
    Spoiler for .:

    (Just noticed we had the exact same idea with action too!)

    Nice to see that my code back-end code ended up being similar too:
    Code:
    inline fun Player.createDialogue(init: DialogueBuilder.() -> Unit): DialogueBuilder {
        val dialogueSet = DialogueBuilder(this)
        dialogueSet.init()
        return dialogueSet
    }


    I initially added a similar system to your id/redirect system but never got round to readding it after implementing options.


    Hope people use this and make beautiful dialogues from it, not like the shit every other source has these days!
    Looks good, I'd recommend using extension functions for Player if you can; things like send message & interface look a lot nicer when they're
    Code:
    player.message("This would normally open your PIN settings")
    player.openInterface(402)
    Than
    Code:
    player.actionSender.sendMessage("This would normally open your PIN settings")
    player.interfaceHandler.sendInterface(ComponentType.CENTRAL, 402)
    Attached imageAttached image
    Reply With Quote  
     

  6. Thankful users:


  7. #4  
    Donator


    Join Date
    Jan 2010
    Age
    29
    Posts
    4,122
    Thanks given
    274
    Thanks received
    551
    Rep Power
    738
    Looks clean greg gj
    Reply With Quote  
     

  8. #5  
    Blurite

    Corey's Avatar
    Join Date
    Feb 2012
    Age
    26
    Posts
    1,491
    Thanks given
    1,245
    Thanks received
    1,729
    Rep Power
    5000
    Quote Originally Posted by Greg View Post
    Looks good, I'd recommend using extension functions for Player if you can; things like send message & interface look a lot nicer when they're
    Code:
    player.message("This would normally open your PIN settings")
    player.openInterface(402)
    Than
    Code:
    player.actionSender.sendMessage("This would normally open your PIN settings")
    player.interfaceHandler.sendInterface(ComponentType.CENTRAL, 402)
    For sure. There were a lot of things I had planned and in my backlog to do (both with dialogues and the rest of the source in general), however I'm no longer working on it.
    Attached image
    Reply With Quote  
     

  9. #6  
    [KT] Dialogue System



    Scu11's Avatar
    Join Date
    Aug 2007
    Age
    30
    Posts
    16,307
    Thanks given
    7,215
    Thanks received
    12,308
    Rep Power
    5000
    here's mine

    Code:
    on<OperateNpc>()
        .where { operation == "Talk-to" && target.def.name == "Banker" }
        .then { player.talkToBanker(target) }
    
    fun Player.talkToBanker(target: Npc) = queue {
        chatNpc(target, ChatAnimation.Quiz, "Good day. How may I help you?")
    
        var option = promptOptionsTitled(
            "What would you like to say?",
            "I'd like to access my bank account, please.",
            "I'd like to set/change my PIN please.",
            "I'd like to see my collection box.",
            "What is this place?"
        )
    
        if (option == 1) {
            // TODO: open bank
        } else if (option == 2) {
            // TODO: open bank pin
        } else if (option == 3) {
            // TODO: open collection box
        } else if (option == 4) {
            chatPlayer("What is this place?")
            chatNpc(target, ChatAnimation.Happy, """
                This is a branch of the Bank of RuneScape. We have
                branches in many towns.
            """)
    
            option = promptOptionsTitled(
                "What would you like to say?",
                "And what do you do?",
                "Didn't you used to be called the Bank of Varrock?"
            )
    
            if (option == 1) {
                chatPlayer("And what do you do?")
                chatNpc(target, ChatAnimation.Happy, """
                    We will look after your items and money for you.
                    Leave your valuables with us if you want to keep them
                    safe.
                """)
            } else if (option == 2) {
                chatPlayer("Didn't you used to be called the Bank of Varrock?")
                chatNpc(target, """
                    Yes we did, but people kept on coming into our
                    branches outside of Varrock and telling us that our
                    signs were wrong. They acted as if we didn't know
                    what town we were in or something.
                """)
            }
        }
    }

    Attached image
    Reply With Quote  
     

  10. Thankful users:


  11. #7  
    Renown Programmer
    Greg's Avatar
    Join Date
    Jun 2010
    Posts
    1,179
    Thanks given
    260
    Thanks received
    1,012
    Rep Power
    2003
    Quote Originally Posted by Scu11 View Post
    here's mine

    Code:
    on<OperateNpc>()
        .where { operation == "Talk-to" && target.def.name == "Banker" }
        .then { player.talkToBanker(target) }
    
    fun Player.talkToBanker(target: Npc) = queue {
        chatNpc(target, ChatAnimation.Quiz, "Good day. How may I help you?")
    
        var option = promptOptionsTitled(
            "What would you like to say?",
            "I'd like to access my bank account, please.",
            "I'd like to set/change my PIN please.",
            "I'd like to see my collection box.",
            "What is this place?"
        )
    
        if (option == 1) {
            // TODO: open bank
        } else if (option == 2) {
            // TODO: open bank pin
        } else if (option == 3) {
            // TODO: open collection box
        } else if (option == 4) {
            chatPlayer("What is this place?")
            chatNpc(target, ChatAnimation.Happy, """
                This is a branch of the Bank of RuneScape. We have
                branches in many towns.
            """)
    
            option = promptOptionsTitled(
                "What would you like to say?",
                "And what do you do?",
                "Didn't you used to be called the Bank of Varrock?"
            )
    
            if (option == 1) {
                chatPlayer("And what do you do?")
                chatNpc(target, ChatAnimation.Happy, """
                    We will look after your items and money for you.
                    Leave your valuables with us if you want to keep them
                    safe.
                """)
            } else if (option == 2) {
                chatPlayer("Didn't you used to be called the Bank of Varrock?")
                chatNpc(target, """
                    Yes we did, but people kept on coming into our
                    branches outside of Varrock and telling us that our
                    signs were wrong. They acted as if we didn't know
                    what town we were in or something.
                """)
            }
        }
    }
    Bit messy if you ask me, converted just to prove my superiority
    Still could use some improvements but it's getting there
    Code:
    override val dialogue: Dialogues.() -> Unit = {
            mob("Good day. How may I help you?") anim 9878
            options("What would you like to say?") {
                option("I'd like to access my bank account, please.") {
                    // TODO: open bank
                }
                option("I'd like to set/change my PIN please.") {
                    // TODO: open bank pin
                }
                option("I'd like to see my collection box.") {
                    // TODO: open collection box
                }
                option("What is this place?") {
                    player("What is this place?")
                    mob("This is a branch of the Bank of RuneScape. We have branches in many towns.")
                    options( "What would you like to say?") {
                        option("And what do you do?") {
                            player("And what do you do?")
                            mob("We will look after your items and money for you. Leave your valuables with us if you want to keep them safe.") anim 9878
                            redirect(0)
                        }
                        option("Didn't you used to be called the Bank of Varrock?") {
                            player("Didn't you used to be called the Bank of Varrock?")
                            mob("Yes we did, but people kept on coming into our branches outside of Varrock and telling us that our signs were wrong. They acted as if we didn't know what town we were in or something.")
                            redirect(0)
                        }
                    }
                }
            } redirectId 0
        }
    But seriously, how does your
    Code:
    on<OperateNpc>()
    work, is it an event bus?
    Attached imageAttached image
    Reply With Quote  
     

  12. #8  
    [KT] Dialogue System



    Scu11's Avatar
    Join Date
    Aug 2007
    Age
    30
    Posts
    16,307
    Thanks given
    7,215
    Thanks received
    12,308
    Rep Power
    5000
    Quote Originally Posted by Greg View Post
    Bit messy if you ask me, converted just to prove my superiority
    Still could use some improvements but it's getting there
    Nesting the options is awful, you're gonna end up with 10+ levels of indentation for the more complex dialogues. The snippet of the bank dialogue you posted already goes up to 5 levels of indentation in your example vs a max of 3 on mine...

    You're using magic numbers for the chat animations instead of defined constants? You can't really think that "anim 9878" is better than "ChatAnimation.Quiz"?

    I really don't like the magic "redirect 0" which is presumably redirecting you to "some" step in the dialogue? would be better if they're named at least

    Given you're storing this all in some "Dialogue" object, how can you do arbitrary function calls like adding things to the players backpack or teleporting them somewhere (for example, in a cutscene dialogue)...?

    But seriously, how does your
    Code:
    on<OperateNpc>()
    work, is it an event bus?
    ya
    Last edited by Scu11; 10-10-2018 at 03:39 PM.

    Attached image
    Reply With Quote  
     

  13. #9  
    Blurite

    Corey's Avatar
    Join Date
    Feb 2012
    Age
    26
    Posts
    1,491
    Thanks given
    1,245
    Thanks received
    1,729
    Rep Power
    5000
    Quote Originally Posted by Scu11 View Post
    Given you're storing this all in some "Dialogue" object, how can you do arbitrary function calls like adding things to the players backpack or teleporting them somewhere (for example, in a cutscene dialogue)...?
    Can't say the same for Greg, but the way mine worked was by capturing everything inside an 'action' tag as a Unit lambda (stored inside a dialogue component) which is then executed as and when it's needed.
    Attached image
    Reply With Quote  
     

  14. #10  
    Discord Johnyblob22#7757


    Join Date
    Mar 2016
    Posts
    1,384
    Thanks given
    365
    Thanks received
    575
    Rep Power
    5000
    why not use javber : (
    Attached image
    Reply With Quote  
     

Page 1 of 2 12 LastLast

Thread Information
Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)


User Tag List

Similar Threads

  1. Dialogue System(Easy usage)
    By Nemmyz in forum Tutorials
    Replies: 46
    Last Post: 12-12-2010, 06:41 PM
  2. Dialogue System
    By Profesor Oak in forum Snippets
    Replies: 2
    Last Post: 03-11-2010, 02:49 AM
  3. My Improved Dialogue System
    By Vastiko in forum Snippets
    Replies: 6
    Last Post: 03-10-2010, 05:25 PM
  4. Dialogue System :)
    By Ultimate in forum Snippets
    Replies: 6
    Last Post: 09-12-2009, 10:28 PM
  5. Best Dialogue System{3x better than yours}
    By wizzyt21 in forum Tutorials
    Replies: 14
    Last Post: 11-28-2008, 12:41 AM
Tags for this Thread

View Tag Cloud

Posting Permissions
  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •