Created
August 4, 2022 04:43
-
-
Save addie-lombardo/f19dd7746eb41cd2b25718b2df118e7f to your computer and use it in GitHub Desktop.
A robust dice roll Lua command for Dialogue System for Unity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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