Skillcheck Modifiers

Announcements, support questions, and discussion for the Dialogue System.
NedSanders
Posts: 13
Joined: Tue Jun 06, 2023 5:39 pm

Skillcheck Modifiers

Post by NedSanders »

Hi, I'm pretty new to DS and Unity in general but I've been using it alongside Adventure Creator and I'm really starting to fall in love with it. I'm awful at coding so really appreciate the level of depth I can achieve without having to venture into visual studio.

I've been trying to implement skillchecks in my current project - I've had a look at the example package and feel that I've got a good idea of where to start but haven't implemented anything yet. Specifically, I'm very interested in the idea of skillcheck modifiers as used in games like Disco Elysium or Pentiment. The idea is that players can pick up booleans that, when they go to make a skill check can impact their chances. For example, if you want to convince someone to help you, but earlier in the game, you called them a big idiot, when hovering over the skillcheck option, something like "-5 you called this guy a big idiot" appears and makes it harder to pass the check. I've included examples from the two mentioned games below.

Image

Image

This strikes me as a really nice way to add depth to conversations simply by adding in tags where necessary. How difficult would it be to implement something like this? I'm familiar with the hover text example but not sure how to alter the text so that it specifically shows the modifiers or how to keep track of and use those modifiers?
User avatar
Tony Li
Posts: 21678
Joined: Thu Jul 18, 2013 1:27 pm

Re: Skillcheck Modifiers

Post by Tony Li »

Hi,

Thanks for using the Dialogue System! And good choice with Adventure Creator, too. It's a great asset.

This is unfortunately something that will require a bit of coding -- but perhaps not as much as you'd think. It should only require a small change to one line of the hover text script.

1. Add a field named "Hint" to your dialogue entry template, and tick the Main checkbox:

convince1.png
convince1.png (43.1 KiB) Viewed 1514 times

2. In your player choice dialogue entries, set the Hint field to the text that will show when you hover the mouse over the answer. In the screenshot below, I used the [lua(code)] markup tag to include a Lua expression in the text. This Lua expression uses the Conditional() Lua function to return text only if a condition is true. In this example, it checks if the DS variable "Called_Idiot" is true. If so, it returns the text "-5 you called this guy a big idiot":

convince2.png
convince2.png (43.29 KiB) Viewed 1514 times

Use the hover text example code from the package on the Extras page. Show the Hint field's text -- but run the text through the FormattedText.Parse() method to parse those [lua(code)] tags and any other tags your Hint might use, such as [em#] tags. So replace this:

Code: Select all

if (tooltip != null) tooltip.text = Field.LookupValue(response.destinationEntry.fields, "Description");
with this:

Code: Select all

if (tooltip != null) tooltip.text = FormattedText.Parse(Field.LookupValue(response.destinationEntry.fields, "Hint")).text;

The Lua expression to actually check the skill may be a little complex to read, but you don't need to do any extra C# coding if you don't want to. Say the player's Convince skill is in a DS variable named "Convince_Skill". The dialogue entry requires a skill roll of 11 or higher on a 20-sided die. Normally you could check it by setting the Conditions to:
  • Conditions: (random(20) + Variable["Convince_Skill"]) >= 11
But if you want to factor in Variable["Called_Idiot"], the Lua expression will be a bit longer. It will make use of Lua syntax in this form:

condition and true-value or false-value

The above form evaluates to true-value if condition is true. Otherwise it evaluates to false-value.

So, specifically for this example:

Variable["Called_Idiot"] and -5 or 0

If "Called_Idiot" is true, the value is -5. Otherwise the value is zero.

So the Conditions need to be:

convince3.png
convince3.png (18.22 KiB) Viewed 1511 times
NedSanders
Posts: 13
Joined: Tue Jun 06, 2023 5:39 pm

Re: Skillcheck Modifiers

Post by NedSanders »

Tony Li wrote: Tue Jun 06, 2023 8:17 pm Hi,

Thanks for using the Dialogue System! And good choice with Adventure Creator, too. It's a great asset.

This is unfortunately something that will require a bit of coding -- but perhaps not as much as you'd think. It should only require a small change to one line of the hover text script.

1. Add a field named "Hint" to your dialogue entry template, and tick the Main checkbox:


convince1.png


2. In your player choice dialogue entries, set the Hint field to the text that will show when you hover the mouse over the answer. In the screenshot below, I used the [lua(code)] markup tag to include a Lua expression in the text. This Lua expression uses the Conditional() Lua function to return text only if a condition is true. In this example, it checks if the DS variable "Called_Idiot" is true. If so, it returns the text "-5 you called this guy a big idiot":


convince2.png


Use the hover text example code from the package on the Extras page. Show the Hint field's text -- but run the text through the FormattedText.Parse() method to parse those [lua(code)] tags and any other tags your Hint might use, such as [em#] tags. So replace this:

Code: Select all

if (tooltip != null) tooltip.text = Field.LookupValue(response.destinationEntry.fields, "Description");
with this:

Code: Select all

if (tooltip != null) tooltip.text = FormattedText.Parse(Field.LookupValue(response.destinationEntry.fields, "Hint")).text;

The Lua expression to actually check the skill may be a little complex to read, but you don't need to do any extra C# coding if you don't want to. Say the player's Convince skill is in a DS variable named "Convince_Skill". The dialogue entry requires a skill roll of 11 or higher on a 20-sided die. Normally you could check it by setting the Conditions to:
  • Conditions: (random(20) + Variable["Convince_Skill"]) >= 11
But if you want to factor in Variable["Called_Idiot"], the Lua expression will be a bit longer. It will make use of Lua syntax in this form:

condition and true-value or false-value

The above form evaluates to true-value if condition is true. Otherwise it evaluates to false-value.

So, specifically for this example:

Variable["Called_Idiot"] and -5 or 0

If "Called_Idiot" is true, the value is -5. Otherwise the value is zero.

So the Conditions need to be:


convince3.png
Thanks for the quick and detailed response! I've resorted to your advice on other people's questions a few times in the past so thanks for that as well!

I had some issues with your proposed condition. Entering what you suggested didn't do anything apart from result in an automatic condition-not-met. However, after a bit of snooping, I used "math.random" instead of just "random" and I think it's now working(?).

Will continue implementing and report back if I run into any more issues.
User avatar
Tony Li
Posts: 21678
Joined: Thu Jul 18, 2013 1:27 pm

Re: Skillcheck Modifiers

Post by Tony Li »

Hi,

Oops, you are indeed correct to use math.random. :-)
NedSanders
Posts: 13
Joined: Tue Jun 06, 2023 5:39 pm

Re: Skillcheck Modifiers

Post by NedSanders »

Tony Li wrote: Fri Jun 09, 2023 12:15 pm Hi,

Oops, you are indeed correct to use math.random. :-)
Hey! Adding to this post instead of creating a new topic as I feel that it's an extension of the previous. I've implemented a lot of the above and it's looking great - I've got active checks, passive checks, modifiers and tooltip popups. But the game I'm building has dialogue as its main focus and I'd ideally like to have skillchecks and modifiers as a large part of each dialogue.

Is there any way to streamline the implementation of all these mechanics? Like, iinstead of writing out all the conditions/logic/lua etc. every time, I can create some kind of shorthand functions to put in now that I've nailed down the basic logic - perhaps something like: "Check[skill_name, difficulty], Outcome[failure], Outcome[success], etc."?
User avatar
Tony Li
Posts: 21678
Joined: Thu Jul 18, 2013 1:27 pm

Re: Skillcheck Modifiers

Post by Tony Li »

What would be your ideal workflow?

You can add custom fields to dialogue entries. For example, you could add a Skill Check field that's a custom field type -- maybe a dropdown with values of None, Strength, Intelligence, Wisdom, Dexterity, Constitution, and Charisma -- and a Required Skill Roll value. So maybe to bend the prison bars you'd set Skill Check to Strength and Required Skill Roll to 15. You could add another field to list other variables that would affect the roll.

Then you could use an OnConversationLine() method to check those fields in code and handle it.

If that sounds like a good idea, I could put together a simple example scene. Or, if you have other ideas about what would work better for your needs, feel free to bounce them off me.
NedSanders
Posts: 13
Joined: Tue Jun 06, 2023 5:39 pm

Re: Skillcheck Modifiers

Post by NedSanders »

Tony Li wrote: Thu Jun 15, 2023 4:49 pm What would be your ideal workflow?

You can add custom fields to dialogue entries. For example, you could add a Skill Check field that's a custom field type -- maybe a dropdown with values of None, Strength, Intelligence, Wisdom, Dexterity, Constitution, and Charisma -- and a Required Skill Roll value. So maybe to bend the prison bars you'd set Skill Check to Strength and Required Skill Roll to 15. You could add another field to list other variables that would affect the roll.

Then you could use an OnConversationLine() method to check those fields in code and handle it.

If that sounds like a good idea, I could put together a simple example scene. Or, if you have other ideas about what would work better for your needs, feel free to bounce them off me.
That sounds perfect! I'd really appreciate a sample scene. I'm using my own skills but I'd imagine I'll be able to tweak details like that in the templates window. Two questions:

1. Would this OnConversationLine() method be able to factor in modifiers? Ideally, multiple modifiers can stack so that checks can be affected by multiple.
2. Would the logic be different in passive checks to active checks? Currently, passive checks are set to passthrough if not met, whilst active block off the high priority success if not met and force the player down the failure route.
User avatar
Tony Li
Posts: 21678
Joined: Thu Jul 18, 2013 1:27 pm

Re: Skillcheck Modifiers

Post by Tony Li »

Hi,

I'll put together an example this weekend.
NedSanders wrote: Sat Jun 17, 2023 5:27 am1. Would this OnConversationLine() method be able to factor in modifiers? Ideally, multiple modifiers can stack so that checks can be affected by multiple.
Yes, I'll add that to the example.
NedSanders wrote: Sat Jun 17, 2023 5:27 am2. Would the logic be different in passive checks to active checks? Currently, passive checks are set to passthrough if not met, whilst active block off the high priority success if not met and force the player down the failure route.
Yes. I think in this case you'd just put the checks on NPC nodes or passthrough group nodes so they don't appear as response menu options.
User avatar
Tony Li
Posts: 21678
Joined: Thu Jul 18, 2013 1:27 pm

Re: Skillcheck Modifiers

Post by Tony Li »

Hi,

I changed the approach to something that may be simpler. Here's an example:

DS_AdvancedSkillCheckExample_2023-06-18.unitypackage

There are multiple moving parts.

First, there's a Skill ScriptableObject asset that defines a single skill such as Strength or Perception.
Skill

Code: Select all

using UnityEngine;

/// <summary>
/// Defines a single skill such as Strength, Perception, etc.
/// </summary>
[CreateAssetMenu]
public class Skill : ScriptableObject
{
    public int value;
}
Then there's a SkillCheck ScriptableObject asset, which should be in a folder named Resources so the code can easily access it at runtime. It defines a single skill check, such as trying to pry open the bars of a specific gate.
SkillCheck

Code: Select all

using System;
using System.Collections.Generic;
using UnityEngine;
using PixelCrushers.DialogueSystem;

/// <summary>
/// Defines a skill check such as picking the lock of a particular door.
/// </summary>
[CreateAssetMenu]
public class SkillCheck : ScriptableObject
{
    [Tooltip("The skill that's being checked.")]
    public Skill skill;

    [Tooltip("The required value that must be met by the skill's value + a d20 roll + all applicable modifiers.")]
    public int requiredValue;

    [Tooltip("Modifiers that affect the check.")]
    public List<SkillCheckModifier> modifiers;

    /// <summary>
    /// Gets the text describing the skill and difficulty.
    /// </summary>
    public string GetText()
    {
        int requiredRoll = requiredValue - skill.value;
        foreach (var modifier in modifiers)
        {
            if (DialogueLua.GetVariable(modifier.variable).asBool)
            {
                requiredRoll -= modifier.modifierValue;
            }
        }
        return $"[{skill.name} {requiredRoll}]";
    }

    /// <summary>
    /// Given a d20 roll, does a skill check and returns true if successful.
    /// </summary>
    public bool Check(int roll)
    {
        var modifiedRoll = roll;
        foreach (var modifier in modifiers)
        {
            if (DialogueLua.GetVariable(modifier.variable).asBool)
            {
                modifiedRoll += modifier.modifierValue;
            }
        }
        return (modifiedRoll + skill.value) > requiredValue;
    }
}

[Serializable]
public class SkillCheckModifier
{
    [VariablePopup] public string variable;
    public int modifierValue;
}
In an Editor folder, there's a custom field drawer for SkillChecks. To use this, inspect a dialogue entry node and add a field named "Skill Check". Set the type dropdown the "SkillCheck". Then select the skill check asset from the dropdown.
CustomFieldType_SkillCheck

Code: Select all

using UnityEngine;
using UnityEditor;

namespace PixelCrushers.DialogueSystem
{

    /// <summary>
    /// Shows a field as a dropdown of names of SkillCheck ScriptableObject asset
    /// files in Resources folders.
    /// </summary>
    [CustomFieldTypeService.Name("SkillCheck")]
    public class CustomFieldType_SkillCheck : CustomFieldType
    {

        private static string[] skillCheckNames = null;

        public override string Draw(string currentValue, DialogueDatabase database)
        {
            GetSkillCheckNames();
            var index = EditorGUILayout.Popup(NameToIndex(currentValue), skillCheckNames);
            return IndexToName(index);
        }

        public override string Draw(Rect rect, string currentValue, DialogueDatabase database)
        {
            GetSkillCheckNames();
            var index = EditorGUI.Popup(rect, NameToIndex(currentValue), skillCheckNames);
            return IndexToName(index);
        }

        private void GetSkillCheckNames()
        {
            if (skillCheckNames != null) return;
            var list = Resources.LoadAll<SkillCheck>("");
            skillCheckNames = new string[list.Length];
            for (int i = 0; i < list.Length; i++)
            {
                skillCheckNames[i] = list[i].name;
            }
        }

        private int NameToIndex(string skillCheckName)
        {
            for (int i = 0; i < skillCheckNames.Length; i++)
            {
                if (skillCheckName == skillCheckNames[i]) return i;
            }
            return -1;
        }

        private string IndexToName(int index)
        {
            if (0 <= index && index < skillCheckNames.Length) return skillCheckNames[index];
            return "";
        }

    }
}
Finally, there's a script to add to the Dialogue Manager GameObject. As conversations prepare to show a subtitle or a response menu, this script checks the subtitle or response menu nodes for Skill Check fields. If so, it performs the skill check so the conversation will know where to proceed from there.
AdvancedSkillCheckExample

Code: Select all

using UnityEngine;
using PixelCrushers.DialogueSystem;

public class AdvancedSkillCheckExample : MonoBehaviour
{

    /// <summary>
    /// When showing the response menu, check any response nodes for a field that
    /// has the SkillCheck type. If found:
    /// - Add the skill check info to the response button text.
    /// - Run the skill check and a variable "Success_ID" to true or false, where 
    ///   ID is the response node's ID.
    /// - Assume the response links to a blank node, and the blank node links to
    ///   a success node (first) and a failure node (second). Set the success
    ///   node's Conditions to require that Success_ID is true.
    /// </summary>
    private void OnConversationResponseMenu(Response[] responses)
    {
        foreach (Response response in responses)
        {
            string text;
            HandleSkillCheck(response.destinationEntry, out text);
            if (!string.IsNullOrEmpty(text))
            { 
                response.formattedText.text = $"{text} {response.formattedText.text}";
            }
        }
    }

    /// <summary>
    /// When showing a subtitle, check for a field that has the SkillCheck type. 
    /// If found:
    /// - Run the skill check and a variable "Success_ID" to true or false, where 
    ///   ID is the response node's ID.
    /// - Assume the node links to a blank node, and the blank node links to
    ///   a success node (first) and, optionally, a failure node (second). 
    ///   Set the success node's Conditions to require that Success_ID is true.
    /// </summary>
    /// <param name="subtitle"></param>
    private void OnConversationLine(Subtitle subtitle)
    {
        string text;
        HandleSkillCheck(subtitle.dialogueEntry, out text);
    }

    private void HandleSkillCheck(DialogueEntry entry, out string text)
    {
        var skillCheckField = entry.fields.Find(field => field.typeString == "CustomFieldType_SkillCheck");
        if (skillCheckField != null)
        {
            var skillCheck = Resources.Load<SkillCheck>(skillCheckField.value);

            text = skillCheck.GetText();

            var d20 = Random.Range(0, 20);
            var result = skillCheck.Check(d20);
            Lua.Run($"Success_{entry.id} = {result.ToString().ToLower()}");

            var skillCheckEntry = DialogueManager.masterDatabase.GetDialogueEntry(entry.outgoingLinks[0]);
            var successEntry = DialogueManager.masterDatabase.GetDialogueEntry(skillCheckEntry.outgoingLinks[0]);
            successEntry.conditionsString = $"Success_{entry.id}";
        }
        else
        {
            text = string.Empty;
        }
    }
}
NedSanders
Posts: 13
Joined: Tue Jun 06, 2023 5:39 pm

Re: Skillcheck Modifiers

Post by NedSanders »

Tony Li wrote: Sun Jun 18, 2023 12:25 pm Hi,

I changed the approach to something that may be simpler. Here's an example:

DS_AdvancedSkillCheckExample_2023-06-18.unitypackage

There are multiple moving parts.

First, there's a Skill ScriptableObject asset that defines a single skill such as Strength or Perception.
Skill

Code: Select all

using UnityEngine;

/// <summary>
/// Defines a single skill such as Strength, Perception, etc.
/// </summary>
[CreateAssetMenu]
public class Skill : ScriptableObject
{
    public int value;
}
Then there's a SkillCheck ScriptableObject asset, which should be in a folder named Resources so the code can easily access it at runtime. It defines a single skill check, such as trying to pry open the bars of a specific gate.
SkillCheck

Code: Select all

using System;
using System.Collections.Generic;
using UnityEngine;
using PixelCrushers.DialogueSystem;

/// <summary>
/// Defines a skill check such as picking the lock of a particular door.
/// </summary>
[CreateAssetMenu]
public class SkillCheck : ScriptableObject
{
    [Tooltip("The skill that's being checked.")]
    public Skill skill;

    [Tooltip("The required value that must be met by the skill's value + a d20 roll + all applicable modifiers.")]
    public int requiredValue;

    [Tooltip("Modifiers that affect the check.")]
    public List<SkillCheckModifier> modifiers;

    /// <summary>
    /// Gets the text describing the skill and difficulty.
    /// </summary>
    public string GetText()
    {
        int requiredRoll = requiredValue - skill.value;
        foreach (var modifier in modifiers)
        {
            if (DialogueLua.GetVariable(modifier.variable).asBool)
            {
                requiredRoll -= modifier.modifierValue;
            }
        }
        return $"[{skill.name} {requiredRoll}]";
    }

    /// <summary>
    /// Given a d20 roll, does a skill check and returns true if successful.
    /// </summary>
    public bool Check(int roll)
    {
        var modifiedRoll = roll;
        foreach (var modifier in modifiers)
        {
            if (DialogueLua.GetVariable(modifier.variable).asBool)
            {
                modifiedRoll += modifier.modifierValue;
            }
        }
        return (modifiedRoll + skill.value) > requiredValue;
    }
}

[Serializable]
public class SkillCheckModifier
{
    [VariablePopup] public string variable;
    public int modifierValue;
}
In an Editor folder, there's a custom field drawer for SkillChecks. To use this, inspect a dialogue entry node and add a field named "Skill Check". Set the type dropdown the "SkillCheck". Then select the skill check asset from the dropdown.
CustomFieldType_SkillCheck

Code: Select all

using UnityEngine;
using UnityEditor;

namespace PixelCrushers.DialogueSystem
{

    /// <summary>
    /// Shows a field as a dropdown of names of SkillCheck ScriptableObject asset
    /// files in Resources folders.
    /// </summary>
    [CustomFieldTypeService.Name("SkillCheck")]
    public class CustomFieldType_SkillCheck : CustomFieldType
    {

        private static string[] skillCheckNames = null;

        public override string Draw(string currentValue, DialogueDatabase database)
        {
            GetSkillCheckNames();
            var index = EditorGUILayout.Popup(NameToIndex(currentValue), skillCheckNames);
            return IndexToName(index);
        }

        public override string Draw(Rect rect, string currentValue, DialogueDatabase database)
        {
            GetSkillCheckNames();
            var index = EditorGUI.Popup(rect, NameToIndex(currentValue), skillCheckNames);
            return IndexToName(index);
        }

        private void GetSkillCheckNames()
        {
            if (skillCheckNames != null) return;
            var list = Resources.LoadAll<SkillCheck>("");
            skillCheckNames = new string[list.Length];
            for (int i = 0; i < list.Length; i++)
            {
                skillCheckNames[i] = list[i].name;
            }
        }

        private int NameToIndex(string skillCheckName)
        {
            for (int i = 0; i < skillCheckNames.Length; i++)
            {
                if (skillCheckName == skillCheckNames[i]) return i;
            }
            return -1;
        }

        private string IndexToName(int index)
        {
            if (0 <= index && index < skillCheckNames.Length) return skillCheckNames[index];
            return "";
        }

    }
}
Finally, there's a script to add to the Dialogue Manager GameObject. As conversations prepare to show a subtitle or a response menu, this script checks the subtitle or response menu nodes for Skill Check fields. If so, it performs the skill check so the conversation will know where to proceed from there.
AdvancedSkillCheckExample

Code: Select all

using UnityEngine;
using PixelCrushers.DialogueSystem;

public class AdvancedSkillCheckExample : MonoBehaviour
{

    /// <summary>
    /// When showing the response menu, check any response nodes for a field that
    /// has the SkillCheck type. If found:
    /// - Add the skill check info to the response button text.
    /// - Run the skill check and a variable "Success_ID" to true or false, where 
    ///   ID is the response node's ID.
    /// - Assume the response links to a blank node, and the blank node links to
    ///   a success node (first) and a failure node (second). Set the success
    ///   node's Conditions to require that Success_ID is true.
    /// </summary>
    private void OnConversationResponseMenu(Response[] responses)
    {
        foreach (Response response in responses)
        {
            string text;
            HandleSkillCheck(response.destinationEntry, out text);
            if (!string.IsNullOrEmpty(text))
            { 
                response.formattedText.text = $"{text} {response.formattedText.text}";
            }
        }
    }

    /// <summary>
    /// When showing a subtitle, check for a field that has the SkillCheck type. 
    /// If found:
    /// - Run the skill check and a variable "Success_ID" to true or false, where 
    ///   ID is the response node's ID.
    /// - Assume the node links to a blank node, and the blank node links to
    ///   a success node (first) and, optionally, a failure node (second). 
    ///   Set the success node's Conditions to require that Success_ID is true.
    /// </summary>
    /// <param name="subtitle"></param>
    private void OnConversationLine(Subtitle subtitle)
    {
        string text;
        HandleSkillCheck(subtitle.dialogueEntry, out text);
    }

    private void HandleSkillCheck(DialogueEntry entry, out string text)
    {
        var skillCheckField = entry.fields.Find(field => field.typeString == "CustomFieldType_SkillCheck");
        if (skillCheckField != null)
        {
            var skillCheck = Resources.Load<SkillCheck>(skillCheckField.value);

            text = skillCheck.GetText();

            var d20 = Random.Range(0, 20);
            var result = skillCheck.Check(d20);
            Lua.Run($"Success_{entry.id} = {result.ToString().ToLower()}");

            var skillCheckEntry = DialogueManager.masterDatabase.GetDialogueEntry(entry.outgoingLinks[0]);
            var successEntry = DialogueManager.masterDatabase.GetDialogueEntry(skillCheckEntry.outgoingLinks[0]);
            successEntry.conditionsString = $"Success_{entry.id}";
        }
        else
        {
            text = string.Empty;
        }
    }
}
Thanks for putting this all together! I'm a little confused but playing around to try and figure it out - as I said, I'm new to Unity and Scriptable objects are something I've not encountered before. I'd appreciate you walking through what the process would be for setting these up - adding a new skill or skill check, etc. Would it be best for to just duplicate one of the existing ones in the resources folder and edit the details?

One more question: Are the strings for the text that shows up for modifiers just the name of variable? As in, will I have to make sure that the variable name for each one is the exact text I want to pop up on the tool tip? Is it not possible to have a separate string value to customise each one so that I can organise my variables without having to consider them as UI? i.e. the variable is "called_john_idiot" but the tooltip is "-5 You called this guy an idiot"?
Post Reply