• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Dialogue System

426363c67b1787031f031fa16515fc3b.jpg


A remake of a GUI system I uploaded a few years back. This is easier to use and has way more functionality.

Code

Dialogue.wurst | KeyPress.wurst

Wurst:
package Dialogue

import LinkedList
import Table

@configurable
string selectColor = "|c00FFFFFF"
string defaultColor = "|c007d7d7d"
string numberColor = "|c00FFFF00"

integer allowedRows = 6

@endconfigurable

public Dialogue array dialogues
public Option triggeredOption
public player triggeringPlayer

public function getTriggeredOption() returns Option
    return triggeredOption

public function getTriggeringOptionPlayer() returns player
    return triggeringPlayer

public function getTriggeredDialogue() returns Dialogue
    return triggeredOption.parent

public function transmission(unit u, player target, string text, real dur)
    TransmissionFromUnitWithNameBJ(bj_FORCE_PLAYER[target.getId()], u, GetUnitName(u), null, text, bj_TIMETYPE_SET, dur, false)

function transmission(unit u, LinkedList<player> target, string text, real dur)
    force f = CreateForce()
    for player p in target
        f.addPlayer(p)
    TransmissionFromUnitWithNameBJ(f, u, GetUnitName(u), null, text, bj_TIMETYPE_SET, dur, false)
    f.destr()

function unit.transmission(LinkedList<player> target, string text, real dur)
    transmission(this, target, text, dur)

public function unit.transmission(player target, string text, real dur)
    transmission(this, target, text, dur)

public class Option 
    string text
    string resp
    real respDuration
    boolean quit = false
    boolexpr cb

    Dialogue dialogue
    Dialogue parent

    construct(Dialogue parent, string text)
        this.text = text
        this.parent = parent

    function linkTextReply(string s, real duration) returns thistype
        resp = s
        respDuration = duration
        return this

    function linkDialogue(Dialogue d) returns thistype
        dialogue = d
        return this
       
    function makeQuit() returns thistype
        quit = true
        return this

    function callback(code c) returns thistype
        cb = Condition(c)
        return this

public class Dialogue
    Table hash = new Table() //used to contain player specific data

    LinkedList<Option> options = new LinkedList<Option>()
    LinkedList<player> users = new LinkedList<player>()

    unit target

    thistype prev

    construct(unit u)
        target = u

    function getStarterUnit(integer i) returns unit
        return hash.loadUnit(i)

    function getStarterUnit(player p) returns unit
        return getStarterUnit(p.getId())
   
    function setStartUnit(player p, unit u)
        setStartUnit(p.getId(), u)

    function setStartUnit(integer id, unit u)
        hash.saveUnit(id, u)

    function addOption(string text) returns Option
        Option o = new Option(this, text)
        options.add(o)
        return o

    function addPlayer(player p)
        addPlayer(p, null)

    function addPlayer(player p, unit u)
        integer id = p.getId()

        if(u != null)
            setStartUnit(id, u)

        users.push(p)
        dialogues[id] = this
        CinematicModeBJ(true, bj_FORCE_PLAYER[id])
        SetUserControlForceOn(bj_FORCE_PLAYER[id])
        transmission(u, p, toString(p), 9999)
        SetCameraTargetControllerNoZForPlayer(p, target, 0, 0, true)

    function swapTo(thistype d, player p)
        integer id = p.getId()
        unit origin = hash.loadUnit(id)

        users.remove(p)
        dialogues[id] = d

        d.users.push(p)
        d.hash.saveUnit(id, origin)
        transmission(origin, p, d.toString(p), 9999)
        SetCameraTargetControllerNoZForPlayer(p, d.target, 0, 0, true)

    function swapTo(thistype d, player p, boolean forward)
        if(forward)
            d.prev = this

        this.swapTo(d, p)

    function removePlayer(player p)
        users.remove(p)
        dialogues[p.getId()] = null
        CinematicModeBJ(false, bj_FORCE_PLAYER[p.getId()])
        hash.flush()

    function toString(player p) returns string
        string toReturn = ""
        integer selection = hash.loadInt(p.getId())
        int i = options.size() > 5 ? selection : 0
        int endLoop =  i + allowedRows >= options.size() and options.size() >= allowedRows ? i + allowedRows : options.size()
        while i < endLoop
            if i >= this.options.size()
                toReturn += numberColor + (i - options.size() + 1).toString() + "|r. " + (i == selection ? selectColor + options.get(i - options.size()).text + "|r" : defaultColor + options.get(i - options.size()).text + "|r") + "\n"
            else
                toReturn += numberColor + (i + 1).toString() + "|r. " + (i == selection ? selectColor + options.get(i).text + "|r" : defaultColor + options.get(i).text) + "|r\n"
            i++
        return toReturn

function disableTransmissionSkip()
    if bj_cineSceneBeingSkipped == null
        TryInitCinematicBehaviorBJ()
   
    DisableTrigger(bj_cineSceneBeingSkipped)

init
    disableTransmissionSkip()
Wurst:
package KeyPress

import Dialogue
import TimerUtils

trigger callback
enum Key
    up
    down
    right
    left
    invalid

function boolexpr.execute()
    TriggerClearConditions(callback)
    TriggerAddCondition(callback, this)
    TriggerEvaluate(callback)

function eventid.toKey() returns Key
    Key toReturn = invalid
    if this == ConvertPlayerEvent(267)
        toReturn = up
    else if this == ConvertPlayerEvent(265)
        toReturn = down
    else if this == ConvertPlayerEvent(263)
        toReturn = right
    else if this == ConvertPlayerEvent(261)
        toReturn = left
    return toReturn

function trigger.regKeyEventForPlayers(integer id)
    int i = 0
    while i < bj_MAX_PLAYERS
        TriggerRegisterPlayerKeyEventBJ(this, players[i], bj_KEYEVENTTYPE_DEPRESS, id)
        i++

function unit.makeResponse(player target, string msg, real duration)
    this.transmission(target, msg, duration)
    CreateTimer()
    ..setData(target.getId())
    ..start(duration) ->
        timer t = GetExpiredTimer()
        integer id = t.getData()
        Dialogue d = dialogues[id]
        transmission(getOrigin(d, players[id]), players[id], d.toString(players[id]), 999)
        t.release()

function getOrigin(Dialogue d, player p) returns unit
    return d.hash.loadUnit(p.getId())

function getSelection(Dialogue d, player p) returns integer
    return d.hash.loadInt(p.getId())

init
    callback = CreateTrigger()
    CreateTrigger()
    ..regKeyEventForPlayers(bj_KEYEVENTKEY_UP)
    ..regKeyEventForPlayers(bj_KEYEVENTKEY_DOWN)
    ..regKeyEventForPlayers(bj_KEYEVENTKEY_RIGHT)
    ..regKeyEventForPlayers(bj_KEYEVENTKEY_LEFT)
    ..addAction() ->

        eventid id = GetTriggerEventId()
        player p = GetTriggerPlayer()
        Dialogue d = dialogues[GetPlayerId(p)]

        if(d != null)
            integer selection = getSelection(d, p)

            switch id.toKey()
                case up
                    selection  = selection <= 0 ? d.options.size() -1 : selection - 1
                    d.hash.saveInt(p.getId(), selection)
                    transmission(getOrigin(d, p), p, d.toString(p), 999)

                case down
                    selection = selection >= d.options.size() -1 ? 0 : selection + 1
                    d.hash.saveInt(p.getId(), selection)
                    transmission(getOrigin(d, p), p, d.toString(p), 999)

                case right
                    Option o = d.options.get(selection)
                    triggeredOption = o
                    triggeringPlayer = p
                    o.cb.execute()
                    if(o.resp != null and o.resp != "")
                        d.target.makeResponse(p, o.resp, o.respDuration)

                    else if o.quit
                        d.removePlayer(p)

                    else if o.dialogue != null
                        d.swapTo(o.dialogue, p, true)
                case left
                    if(d.prev != null)
                        d.swapTo(d.prev, p, false)
                case invalid
                    print("Error") //Should never happen

Demo

Wurst:
package Hello

import Dialogue
import Talk

Dialogue array dialogues

function giveItem()
    Dialogue d = getTriggeredDialogue()
    player p   = getTriggeringOptionPlayer()
    unit u     = d.getStarterUnit(p)

    u.addItem('bgst')

init
    // Unit Preperation
    unit questNPC = createUnit(players[0], 'hfoo', vec2(100, 100), angle(0))
    ..setColor(PLAYER_COLOR_BLUE)

    // Dialogue setup
    dialogues[0] = new Dialogue(questNPC)
    dialogues[1] = new Dialogue(questNPC)

    dialogues[0].addOption("Hello, fellow soldier!").linkTextReply("Dues vult, brother. Here is a free item, check your inventory.", 4).callback(function giveItem)
    dialogues[0].addOption("Chaosy is the best").linkTextReply("Nah.", 2)
    dialogues[0].addOption("I have another question").linkDialogue(dialogues[1])
    dialogues[0].addOption("Quit").makeQuit()

    dialogues[1].addOption("Where am I?").linkTextReply("Life is full of mysteries.", 2)
    dialogues[1].addOption("Help, this system does not work in my map").linkTextReply("Chances are that your editor does not support wurst.", 2)
    dialogues[1].addOption("Back").linkDialogue(dialogues[0])

    // Optional for demo
    setupQuestGiver(dialogues[0])
Contents

Dialogue System (Map)

Reviews
MyPad
This has been overlooked for quite some time. Approved

Nitpicks:


These points are disputable and are aligned with the user's views:​
  • There are a lot of spaces in the package? Seems kinda counterproductive for coding.
  • @configigurable should be @configurable, though they are annotations and can easily be fixed.
  • What about the capability to go left?

Notes:

  • trigger.regKeyForPlayers should loop over all players. Ideally, the bj_MAX_PLAYERS constant is used over the literal 12.
As stated in the nitpicks section, there are a lot of gaps in the package. Otherwise, the package seems well enough on its own.
Quite nice: 3.5 / 5
 
Last edited:

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,182
1. I cannot copy from visual studio code for some reason, I need to copy it into word first. It fucks up the indenting a bit, but it is better than 0 indenting. @Ralle plz fix
2. Wops.
3. I'll look into it
4. Currently you dedicate a option in order to go back, I did not consider using the left key for that.. note taken. I will probably include it whenever I decide to update this.
 
Level 28
Joined
Nov 12, 2007
Messages
2,340
This is really cool. I noticed two things though:
  • If the user press Esc, it clears the all the messages and he does not see what's written/what option he is selecting.
  • After pressing right arrow key, the knight portrait disappears, at least until I do another action again.
I found this upload really nice nonetheless
 
Last edited:
Your resource has been set to Awaiting Update for the following reasons:

Reason:

  • A 7-day period shall be enacted. During that time, if there are any bugs reported within the time period, the author will have the liberty to fix it.
  • If, after 7 days, the resource is found to be bug-free, or at least good enough to work, it shall be approved.
  • However, if the condition above is not satisfied, the resource shall remain in pending until it satisfies the condition or 6 months have elapsed.
  • After 6 months have elapsed, if the resource is still in Awaiting Update, the moderator may have the liberty to place it under Substandard.
 

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,516
This is a comment about the API only, as demonstrated in the demo:

- API looks great overall, but how do I add arbitrary closure effects to a dialogue option, or chain multiple messages to one dialogue option? I'm thinking something like

Wurst:
dialogues[1].addOption("Where am I?").linkTextReply("Life is full of mysteries.", 2)
    ..andThen("I suppose if you wanted to investigate, you could try exploring the north...")
    ..andThen() d ->
        enableExploreNorthQuest(d.owner)
        disableWhereAmIDialogue()
        doAfter(1.) () ->
            playSound(Sound.hint, d.initiator.getPos())
            print("|cffffcc00Hint:|r you've enabled a quest. Try communicating with other NPCs to discover more quests.")

- What about the name Dialogue? Not a big fan - jass has dialog. Maybe better, "Conversation"?

- I care less in the case of the script code, but for demo API I think you should use high-level standard library tools. Can you change your `Table` to a `HashMap`?
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,182
Yeah I realized that some sort of callback functionality was needed.

My quick-fix was to add a callback to each option.
dialog.addOption("Cool message").makeQuit().callback(function onOptionSelect)

all option functions (on my local version) returns the instance meaning you can chain the functions together. I think it gives better readability than mashing everything into the constructor.

And in the callback you can use

public function getTriggeredOption() returns Option
public function getTriggeringOptionPlayer() returns player

having access to the Option instance gives access to most of the relevant information
 
Top