Skip to content

Instantly share code, notes, and snippets.

@addie-lombardo
Created August 4, 2022 04:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save addie-lombardo/f19dd7746eb41cd2b25718b2df118e7f to your computer and use it in GitHub Desktop.
Save addie-lombardo/f19dd7746eb41cd2b25718b2df118e7f to your computer and use it in GitHub Desktop.
A robust dice roll Lua command for Dialogue System for Unity
// Requires Dialogue System for Unity: https://www.pixelcrushers.com/dialogue-system/
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using PixelCrushers.DialogueSystem;
using UnityEngine;
/// <summary>
/// IMPORTANT: C# methods registered with Lua must use double for numbers, not float or int.
/// You can cast to (int), (float), etc inside your function if necessary.
/// </summary>
[SuppressMessage("ReSharper", "UnusedMethodReturnValue.Local")]
public class DiceLuaCommands : MonoBehaviour
{
// you could easily make this a bool parameter of the RollDice method instead and toggle it on a per roll basis!
public bool AllowLuckySave = true;
private static readonly RNGCryptoServiceProvider generator = new();
private void OnEnable()
{
Lua.RegisterFunction("RollDice", this, SymbolExtensions.GetMethodInfo(() => RollDice(String.Empty, 0, string.Empty)));
}
/// <summary>
/// Rolls a given number of die, applies modifiers if given, and returns the result.
/// IMPORTANT: The result is not the total rolled but instead a double which represents the result of the roll, See Returns for return values.
/// </summary>
/// <param name="diceStr">Each integer stands for a new dice with x number of sides. 1|2|3</param>
/// <param name="success">Threshold for a successful roll</param>
/// <param name="modifiersStr">Number Lua variables to apply to the total of the roll. Modifier1|Modifier2|Modifier3</param>
/// <example>
/// "Variable["lastDiceRollResult"] = RollDice(6|6,8,strength|charisma)"
/// Roll 2 6-sided dice, apply 2 Number variables (strength and charisma) as modifiers, and return the result of if it passed the success threshold of 8 to a variable named "lastDiceRollResult".
/// </example>
/// <returns>
/// -1 = Error
/// 0 = Critical Failure
/// 1 = Failure
/// 2 = Success
/// 3 = Critical Success
/// 4 = Lucky Save (Optional: make sure the AllowLuckySave variable is true to offer as a return value)
/// </returns>
private double RollDice(string diceStr, double success, string modifiersStr)
{
if (string.IsNullOrWhiteSpace(diceStr))
return -1;
#if UNITY_EDITOR
if (DialogueDebug.logInfo)
{
Debug.Log(!string.IsNullOrWhiteSpace(modifiersStr)
? $"[ROLL DICE] Rolling dice ({diceStr}) with modifiers ({modifiersStr}). Aiming for a total of {success} or greater."
: $"[ROLL DICE] Rolling dice ({diceStr}). Aiming for a total of {success} or greater.");
}
var rollDebug = "";
#endif
int[] dice = DeconstructIntArrayString(diceStr);
int rolledTotal = 0;
for (int i = 0; i < dice.Length; i++)
{
var roll = MoreRandomInt(1, dice[i]);
rolledTotal += roll;
#if UNITY_EDITOR
rollDebug += roll;
if (i != dice.Length - 1)
rollDebug += ",";
#endif
}
var modifierDebug = ""; // i gave up putting this behind UNITY_EDITOR, so it's here!
int modifiersTotal = CalculateModifiers(modifiersStr, out modifierDebug);
rolledTotal += modifiersTotal;
#if UNITY_EDITOR
if (DialogueDebug.logInfo)
{
Debug.Log(modifiersTotal > 0
? $"[ROLL DICE] Rolled a total of {rolledTotal} ({rollDebug}) with a modifer of {modifiersTotal} ({modifierDebug})"
: $"[ROLL DICE] Rolled a total of {rolledTotal} ({rollDebug})");
}
#endif
// calculate total possible value of all the dice together
var totalPossibleValue = 0;
foreach (var die in dice)
totalPossibleValue += die;
if (totalPossibleValue >= (int) success) // check if we've reached the min for success
return totalPossibleValue <= rolledTotal ? 3 : 2; // calculate if we achieved a crit success or a regular success
// check if we've made a lucky save
if (AllowLuckySave && dice.Length > 1)
{
// a "lucky save" is when we roll 2 or more dice less than the success threshold
// but we roll all matching values that aren't 1. idk it's a fun idea, but feel free to disable it.
int toMatch = dice[0];
bool allMatch = true;
for (var i = 1; i < dice.Length; i++)
{
var die = dice[i];
if (die != toMatch)
{
allMatch = false;
break;
}
}
if (allMatch)
return 4;
}
return totalPossibleValue == dice.Length ? 0 : 1; // calculate if we achieved a crit failure or regular failure
// deconstruct a string formatted as integers with the deliminator "|" (ex. "1|2|3")
int[] DeconstructIntArrayString(string str)
{
List<int> output = new List<int>();
var splitStr = str.Split('|');
foreach (var s in splitStr)
{
if (int.TryParse(s, out var result))
output.Add(result);
else if (DialogueDebug.logWarnings)
Debug.LogWarning($"[ROLL DICE] Unable to parse the given value from the int array as an int ({s}). Continuing.");
}
return output.ToArray();
}
// split the modifiers string, get the individual variable names, and attempt to parse each as an int
int CalculateModifiers(string str, out string debugResult)
{
debugResult = "";
if (string.IsNullOrWhiteSpace(str))
return 0;
int modifiersTotal = 0;
var splitStr = str.Split('|');
for (var i = 0; i < splitStr.Length; i++)
{
var s = splitStr[i];
var result = DialogueLua.GetVariable(s);
if (result.hasReturnValue && result.isNumber)
{
var value = result.asInt;
modifiersTotal += value;
#if UNITY_EDITOR
modifierDebug += value;
if (i != splitStr.Length - 1)
modifierDebug += ",";
#endif
}
else if (DialogueDebug.logWarnings)
{
Debug.LogWarning(!result.hasReturnValue
? $"[ROLL DICE] Unable to find a variable named ({s}). Continuing."
: $"[ROLL DICE] The given variable ({s}) isn't a Number variable. Continuing.");
}
}
return modifiersTotal;
}
}
/// <summary>
/// Returns a more random integer than System.Random or UnityEngine.Random between two values, both params are inclusive.
/// </summary>
private static int MoreRandomInt(int min, int max)
{
byte[] randomNumber = new byte[1];
generator.GetBytes(randomNumber);
double asciiValueOfRandomCharacter = Convert.ToDouble(randomNumber[0]);
// We are using Math.Max, and substracting 0.00000000001,
// to ensure "multiplier" will always be between 0.0 and .99999999999
// Otherwise, it's possible for it to be "1", which causes problems in our rounding.
double multiplier = Math.Max(0, asciiValueOfRandomCharacter / 255d - 0.00000000001d);
// We need to add one to the range, to allow for the rounding done with Math.Floor
int range = max - min + 1;
double randomValueInRange = Math.Floor(multiplier * range);
return (int)(min + randomValueInRange);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment