Stat checks: check command with stat roll outcomes and its schema

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cthangoverandClaude Opus 5.5 committed 2026-10-24 10:01:15 +02:00
commit 50be9ad273
18 files changed
+589

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
# Line endings: LF everywhere, on every OS, whatever core.autocrlf says.
* text=auto eol=lf
# cmd.exe misreads labels and goto in LF batch files, so they stay CRLF.
*.bat text eol=crlf
*.cmd text eol=crlf
# Binaries: never converted, never diffed as text.
*.png binary
*.jpg binary
*.jpeg binary
*.webp binary
*.svg text
*.ogg binary
*.wav binary
*.mp3 binary
*.ttf binary
*.otf binary
*.woff binary
*.woff2 binary
*.dll binary
*.so binary
*.dylib binary
*.pdb binary
*.exe binary
*.zip binary
*.pck binary
*.moc3 binary
*.res binary
*.scn binary
+37
View File
@@ -0,0 +1,37 @@
# Godot 4.x
.godot/
.idea/
sdk/
bin/
obj/
#ai
.omo/
.opencode/
gen/
obj/
bin/
*.tres~
*.tscn~
# GDScript LSP
.godot/
# Export
*.exe
*.dmg
*.apk
*.aab
*.pck
*.zip
*.log
# OS
.DS_Store
Thumbs.db
# Editor
*.import
*.godot.uid
+17
View File
@@ -0,0 +1,17 @@
mod_stat_checks_name = Stat Checks
mod_stat_checks_desc = Stat checks in any scenario: “check cha 12 -> :success” rolls a stat plus dice against a difficulty, shows the outcome line and branches the story — talking a guard round in a novel, forcing a door in an RPG. The roll stays in dialog variables for lines of your own. Dice default to the game setting checks.dice, else d20; dice=none makes a plain threshold.
checks/line = [${check_stat}: ${check_total} vs ${check_dc}] ${check_result}
checks/success = Success
checks/failure = Failure
checks/schema/checks/check = Stat check
checks/schema/checks/check_desc = Stat + dice + bonus against a difficulty: a line with the outcome and a jump
checks/schema/checks/arrow_success = On success, go to
checks/schema/field/stat = Stat
checks/schema/field/difficulty = Difficulty
checks/schema/field/who = Who
checks/schema/field/dice = Dice
checks/schema/checks/dice_desc = d20, 2d6, none — without dice the check is a threshold
checks/schema/field/bonus = Bonus
checks/schema/field/else = Else label
checks/schema/field/quiet = Quiet
checks/schema/checks/quiet_desc = Do not show the roll line
+17
View File
@@ -0,0 +1,17 @@
mod_stat_checks_name = Tiradas de característica
mod_stat_checks_desc = Tiradas de característica en cualquier escenario: «check cha 12 -> :éxito» tira una estadística más dados contra una dificultad, muestra la línea con el resultado y ramifica la historia: convencer a un guardia en una novela, forzar una puerta en un RPG. La tirada queda en variables del diálogo para tus propias líneas. Los dados por defecto son el ajuste checks.dice, si no d20; dice=none es un umbral simple.
checks/line = [${check_stat}: ${check_total} contra ${check_dc}] ${check_result}
checks/success = Éxito
checks/failure = Fallo
checks/schema/checks/check = Tirada de característica
checks/schema/checks/check_desc = Estadística + dados + bonificación contra una dificultad: una línea con el resultado y un salto
checks/schema/checks/arrow_success = Si hay éxito, ir a
checks/schema/field/stat = Estadística
checks/schema/field/difficulty = Dificultad
checks/schema/field/who = Quién
checks/schema/field/dice = Dados
checks/schema/checks/dice_desc = d20, 2d6, none: sin dados la tirada es un umbral
checks/schema/field/bonus = Bonificación
checks/schema/field/else = Etiqueta si no
checks/schema/field/quiet = Silencioso
checks/schema/checks/quiet_desc = No mostrar la línea de la tirada
+17
View File
@@ -0,0 +1,17 @@
mod_stat_checks_name = 能力値判定
mod_stat_checks_desc = どのシナリオでも使える能力値判定:「check cha 12 -> :成功」はステータスとダイスを難易度と比べ、結果の行を出して物語を分岐させる——ノベルで門番を説得し、RPG で扉をこじ開ける。出目は会話変数に残り、自分の行で使える。ダイスの既定はゲーム設定 checks.dice、なければ d20。dice=none なら単純なしきい値。
checks/line = [${check_stat}:${check_total} 対 ${check_dc}] ${check_result}
checks/success = 成功
checks/failure = 失敗
checks/schema/checks/check = 能力値判定
checks/schema/checks/check_desc = ステータス + ダイス + ボーナス 対 難易度:結果の行とジャンプ
checks/schema/checks/arrow_success = 成功時のラベル
checks/schema/field/stat = ステータス
checks/schema/field/difficulty = 難易度
checks/schema/field/who = 誰
checks/schema/field/dice = ダイス
checks/schema/checks/dice_desc = d20、2d6、none——ダイスなしなら判定はしきい値
checks/schema/field/bonus = ボーナス
checks/schema/field/else = それ以外のラベル
checks/schema/field/quiet = 結果を表示しない
checks/schema/checks/quiet_desc = ダイスの行を表示しない
+17
View File
@@ -0,0 +1,17 @@
mod_stat_checks_name = 능력치 판정
mod_stat_checks_desc = 모든 시나리오에서 쓰는 능력치 판정: “check cha 12 -> :성공”은 능력치와 주사위를 난이도와 겨루고, 결과 줄을 보여 주며 이야기를 갈래로 나눕니다 — 노벨에서 경비병 설득, RPG에서 문 부수기. 굴림 결과는 대화 변수에 남아 직접 쓴 줄에 쓸 수 있습니다. 주사위 기본값은 게임 설정 checks.dice, 없으면 d20. dice=none은 단순 기준값.
checks/line = [${check_stat}: ${check_total} 대 ${check_dc}] ${check_result}
checks/success = 성공
checks/failure = 실패
checks/schema/checks/check = 능력치 판정
checks/schema/checks/check_desc = 능력치 + 주사위 + 보너스 대 난이도: 결과 줄과 이동
checks/schema/checks/arrow_success = 성공 시 이동
checks/schema/field/stat = 능력치
checks/schema/field/difficulty = 난이도
checks/schema/field/who = 누구
checks/schema/field/dice = 주사위
checks/schema/checks/dice_desc = d20, 2d6, none — 주사위가 없으면 판정은 기준값
checks/schema/field/bonus = 보너스
checks/schema/field/else = 아니면 라벨
checks/schema/field/quiet = 결과 숨김
checks/schema/checks/quiet_desc = 굴림 줄을 표시하지 않음
+17
View File
@@ -0,0 +1,17 @@
mod_stat_checks_name = Проверки характеристик
mod_stat_checks_desc = Проверки характеристик в любом сценарии: «check cha 12 -> :успех» бросает стат плюс кубики против сложности, показывает строку с исходом и ветвит историю — уговорить стражника в новелле, выбить дверь в RPG. Бросок остаётся в переменных диалога для своих строк. Кубики по умолчанию — настройка игры checks.dice, иначе d20; dice=none — простой порог.
checks/line = [${check_stat}: ${check_total} против ${check_dc}] ${check_result}
checks/success = Успех
checks/failure = Провал
checks/schema/checks/check = Проверка характеристики
checks/schema/checks/check_desc = Стат + кубики + бонус против сложности: строка с исходом и переход
checks/schema/checks/arrow_success = При успехе — метка
checks/schema/field/stat = Стат
checks/schema/field/difficulty = Сложность
checks/schema/field/who = Кто
checks/schema/field/dice = Кубики
checks/schema/checks/dice_desc = d20, 2d6, none — без кубиков проверка становится порогом
checks/schema/field/bonus = Бонус
checks/schema/field/else = Иначе — метка
checks/schema/field/quiet = Без строки исхода
checks/schema/checks/quiet_desc = Не показывать строку с броском
+17
View File
@@ -0,0 +1,17 @@
mod_stat_checks_name = Kiểm tra chỉ số
mod_stat_checks_desc = Kiểm tra chỉ số trong mọi kịch bản: «check cha 12 -> :thành_công» tung chỉ số cộng xúc xắc so với độ khó, hiện dòng kết quả và rẽ nhánh câu chuyện — thuyết phục lính gác trong tiểu thuyết, phá cửa trong RPG. Kết quả tung còn lại trong biến hội thoại cho dòng của riêng bạn. Xúc xắc mặc định theo cài đặt checks.dice, không thì d20; dice=none là ngưỡng đơn giản.
checks/line = [${check_stat}: ${check_total} so với ${check_dc}] ${check_result}
checks/success = Thành công
checks/failure = Thất bại
checks/schema/checks/check = Kiểm tra chỉ số
checks/schema/checks/check_desc = Chỉ số + xúc xắc + thưởng so với độ khó: một dòng kết quả và bước nhảy
checks/schema/checks/arrow_success = Khi thành công, đến
checks/schema/field/stat = Chỉ số
checks/schema/field/difficulty = Độ khó
checks/schema/field/who = Ai
checks/schema/field/dice = Xúc xắc
checks/schema/checks/dice_desc = d20, 2d6, none — không xúc xắc thì kiểm tra là ngưỡng
checks/schema/field/bonus = Thưởng
checks/schema/field/else = Nhãn nếu không
checks/schema/field/quiet = Không hiện kết quả
checks/schema/checks/quiet_desc = Không hiện dòng tung xúc xắc
+17
View File
@@ -0,0 +1,17 @@
mod_stat_checks_name = 属性检定
mod_stat_checks_desc = 任何剧本中的属性检定:“check cha 12 -> :成功”用属性加骰子对抗难度,显示结果行并让故事分支——在小说里说服守卫,在 RPG 里撬开门。掷骰结果留在对话变量里,供你自己的台词使用。骰子默认取游戏设置 checks.dice,否则为 d20;dice=none 即简单门槛。
checks/line = [${check_stat}:${check_total} 对 ${check_dc}] ${check_result}
checks/success = 成功
checks/failure = 失败
checks/schema/checks/check = 属性检定
checks/schema/checks/check_desc = 属性 + 骰子 + 加成对抗难度:显示结果并跳转
checks/schema/checks/arrow_success = 成功时跳转
checks/schema/field/stat = 属性
checks/schema/field/difficulty = 难度
checks/schema/field/who = 谁
checks/schema/field/dice = 骰子
checks/schema/checks/dice_desc = d20、2d6、none——没有骰子时检定就是门槛
checks/schema/field/bonus = 加成
checks/schema/field/else = 否则跳转
checks/schema/field/quiet = 不显示结果
checks/schema/checks/quiet_desc = 不显示掷骰行
+18
View File
@@ -0,0 +1,18 @@
{
"id": "stat_checks",
"name": "Stat Checks",
"version": "1.0.0",
"author": "ct",
"description": "Stat checks for any scenario: `check cha 12 [dice=d20] [bonus=2] [else=label] -> :success` rolls a stat plus dice against a difficulty, shows the outcome line and branches (a novel's persuasion, a tile RPG's locked door). The roll is left in dialog variables (check_total, check_roll, …) for ptext lines. Dice default to the game setting checks.dice, else d20; dice=none makes a plain threshold. The conditional jump `when` and option conditions live in dialog_base.",
"category": "gameplay",
"sources": [
"src/**/*.cs"
],
"test_sources": [
"tests/*.cs"
],
"depends": [
"stats_base>=1",
"dialog_base>=1"
]
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+56
View File
@@ -0,0 +1,56 @@
{
"Commands": [
{
"Id": "check",
"Name": "checks/schema/checks/check",
"Description": "checks/schema/checks/check_desc",
"Category": "flow",
"Arrow": "checks/schema/checks/arrow_success",
"Positional": [
{
"Key": "stat",
"Type": "ref",
"Kind": "stat",
"Required": true,
"Name": "checks/schema/field/stat"
},
{
"Key": "difficulty",
"Type": "int",
"Required": true,
"Name": "checks/schema/field/difficulty"
}
],
"Params": [
{
"Key": "who",
"Type": "who",
"Name": "checks/schema/field/who"
},
{
"Key": "dice",
"Type": "text",
"Default": "d20",
"Name": "checks/schema/field/dice",
"Description": "checks/schema/checks/dice_desc"
},
{
"Key": "bonus",
"Type": "int",
"Name": "checks/schema/field/bonus"
},
{
"Key": "else",
"Type": "label",
"Name": "checks/schema/field/else"
},
{
"Key": "quiet",
"Type": "bool",
"Name": "checks/schema/field/quiet",
"Description": "checks/schema/checks/quiet_desc"
}
]
}
]
}
+95
View File
@@ -0,0 +1,95 @@
namespace Cthangover.Mods.StatChecks
{
using System.Collections.Generic;
using System.Globalization;
using Cthangover.Core.Games;
using Cthangover.Core.Localization;
using Cthangover.Mods.Dialog;
using Cthangover.Mods.Dialog.Scenarios;
using Cthangover.Mods.Stats;
using Godot;
/// <summary>
/// The DSL command of a stat check:
/// <code>check cha 12 [who=player] [dice=d20] [bonus=2] [else=fail] [quiet=1] -> :success</code>
/// rolls stat + dice + bonus against the difficulty, shows one line with the outcome
/// ("[Charisma 15 / 12] Success") unless <c>quiet</c>, and jumps to the arrow label on
/// success, to <c>else</c> on failure (or goes on with the next line).
/// <para/>
/// The same check serves a visual novel ("persuade the guard"), a tile RPG ("force the
/// rusty door") and any other game with stats: the command only knows the stats
/// facade. The dice default is the game's setting <c>checks.dice</c>, else <c>d20</c>;
/// <c>dice=none</c> makes it a plain threshold. After the line the dialog variables
/// <c>check_value</c>, <c>check_roll</c>, <c>check_bonus</c>, <c>check_total</c>,
/// <c>check_dc</c>, <c>check_stat</c> and <c>check_result</c> hold the roll for
/// <c>ptext</c> lines that want to tell it their own way.
/// </summary>
public class CheckCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
{
/// <summary>Default line when the game has no <c>checks/line</c> translation.</summary>
public const string DefaultLine = "[${check_stat} ${check_total} / ${check_dc}] ${check_result}";
/// <inheritdoc />
public string Command => "check";
/// <inheritdoc />
public int MinPositional => 2;
/// <inheritdoc />
public int MaxPositional => 2;
/// <inheritdoc />
public IReadOnlyList<string> AllowedNamed => new[] { "who", "dice", "bonus", "else", "quiet" };
/// <inheritdoc />
public string JumpTargetNamedKey => "else";
/// <inheritdoc />
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
{
named.TryGetValue("who", out var who);
named.TryGetValue("dice", out var dice);
named.TryGetValue("bonus", out var bonus);
named.TryGetValue("else", out var otherwise);
var check = new StatCheck
{
Who = who,
Stat = positional.Count > 0 ? positional[0].ToLowerInvariant() : string.Empty,
Difficulty = Int(positional.Count > 1 ? positional[1] : null),
Dice = dice ?? DefaultDice(),
Bonus = Int(bonus),
};
dlg.Run(check.Perform);
dlg.Set("check_stat", () => StatName(check.Stat));
dlg.Set("check_value", () => check.Value.ToString(CultureInfo.InvariantCulture));
dlg.Set("check_roll", () => check.Roll.ToString(CultureInfo.InvariantCulture));
dlg.Set("check_bonus", () => check.Bonus.ToString(CultureInfo.InvariantCulture));
dlg.Set("check_total", () => check.Total.ToString(CultureInfo.InvariantCulture));
dlg.Set("check_dc", () => check.Difficulty.ToString(CultureInfo.InvariantCulture));
dlg.Set("check_result", () => Tr(check.Success ? "checks/success" : "checks/failure"));
if (!named.ContainsKey("quiet"))
dlg.PText(locale?.Get("checks/line") ?? DefaultLine);
dlg.IfGoTo(() => check.Success, Label(arrowTarget), Label(otherwise));
}
private static string DefaultDice()
{
var setting = ActiveGame.GetSetting("checks.dice");
return string.IsNullOrWhiteSpace(setting) ? "d20" : setting;
}
private static int Int(string text)
=> int.TryParse(text?.Trim(), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var value) ? value : 0;
private static string Label(string target) => string.IsNullOrWhiteSpace(target) ? null : target.Trim().TrimStart(':');
private static string StatName(string stat)
{
var key = StatsService.Definition(stat)?.Name;
return string.IsNullOrEmpty(key) ? stat : Tr(key);
}
private static string Tr(string key) => TranslationServer.Translate(key).ToString();
}
}
+50
View File
@@ -0,0 +1,50 @@
namespace Cthangover.Mods.StatChecks
{
using System;
using System.Globalization;
/// <summary>
/// Dice in tabletop notation: <c>d20</c>, <c>2d6</c>, <c>1d100</c>; <c>none</c> (or
/// empty, or <c>0</c>) rolls nothing, which makes a check deterministic — a novel's
/// "you are brave enough, or you are not". A malformed text rolls nothing too, so a
/// typo never breaks a scene; the validator reports it.
/// </summary>
public static class DiceRoll
{
/// <summary>Whether <paramref name="dice"/> is valid notation (or a "no dice" word).</summary>
public static bool IsValid(string dice) => TryParse(dice, out _, out _);
/// <summary>Rolls <paramref name="dice"/>: the sum of the faces, 0 for no dice.</summary>
public static int Roll(string dice, Random random = null)
{
if (!TryParse(dice, out var count, out var sides) || count == 0)
return 0;
var rng = random ?? Random.Shared;
var sum = 0;
for (var i = 0; i < count; i++)
sum += rng.Next(1, sides + 1);
return sum;
}
/// <summary>Parses <c>NdM</c>; <paramref name="count"/> is 0 for "no dice".</summary>
public static bool TryParse(string dice, out int count, out int sides)
{
count = 0;
sides = 0;
var text = dice?.Trim().ToLowerInvariant();
if (string.IsNullOrEmpty(text) || text is "none" or "0")
return true;
var d = text.IndexOf('d');
if (d < 0)
return false;
var countText = text.Substring(0, d);
if (countText.Length == 0)
count = 1;
else if (!int.TryParse(countText, NumberStyles.None, CultureInfo.InvariantCulture, out count))
return false;
if (!int.TryParse(text.Substring(d + 1), NumberStyles.None, CultureInfo.InvariantCulture, out sides))
return false;
return count is >= 1 and <= 100 && sides >= 2;
}
}
}
+53
View File
@@ -0,0 +1,53 @@
namespace Cthangover.Mods.StatChecks
{
using System;
using Cthangover.Mods.Stats;
/// <summary>
/// One check of a <c>check</c> line: stat value + dice + bonus against a difficulty.
/// The line is parsed once and the check object lives in the dialog queue, so every
/// pass through the line (a <c>goto</c> loop, a replay) rolls again and the dialog
/// variables show the latest roll.
/// </summary>
public class StatCheck
{
/// <summary>Whose stat (a character id or <c>player</c>).</summary>
public string Who { get; init; }
/// <summary>The stat id.</summary>
public string Stat { get; init; }
/// <summary>The total needed to succeed.</summary>
public int Difficulty { get; init; }
/// <summary>Dice notation (<see cref="DiceRoll"/>).</summary>
public string Dice { get; init; }
/// <summary>A flat bonus (or malus) of this check only.</summary>
public int Bonus { get; init; }
/// <summary>The stat value of the last roll.</summary>
public int Value { get; private set; }
/// <summary>The dice of the last roll.</summary>
public int Roll { get; private set; }
/// <summary>Value + dice + bonus of the last roll.</summary>
public int Total { get; private set; }
/// <summary>Whether the last roll reached the difficulty.</summary>
public bool Success { get; private set; }
/// <summary>Rolls the check now (reads the stat as it is at this moment).</summary>
public void Perform() => Perform(StatsService.Sheet(Who).Rounded(Stat), null);
/// <summary>Rolls with a given stat value and dice generator (tests, previews).</summary>
public void Perform(int value, Random random)
{
Value = value;
Roll = DiceRoll.Roll(Dice, random);
Total = Value + Roll + Bonus;
Success = Total >= Difficulty;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
namespace Cthangover.Mods.StatChecks.Tests
{
using Cthangover.Core.Mods.Testing;
using Cthangover.Mods.Dialog;
using Cthangover.Mods.Dialog.Scenarios;
/// <summary>
/// <c>check</c> lines in a real scenario text: the queue rolls when it reaches the line,
/// jumps to the arrow label on success and to <c>else</c> (or the next line) on failure,
/// and leaves the roll in dialog variables.
/// </summary>
public class CheckCommandStrategyTests
{
private static DialogRuntime Run(string scenario)
{
var dlg = ScenarioParser.Parse(scenario);
var runtime = new DialogRuntime();
runtime.SetDialogQueueAndRun(null, dlg.Queue, null);
runtime.Run();
return runtime;
}
// A stat no mod defines reads 0 for anyone, so the outcomes below are fixed whatever packs are loaded.
private const string Nobody = "who=test_nobody_has_stats";
[ModTest("command/success_jumps_to_the_arrow")]
static void SuccessJumps(ModTestContext ctx)
{
var runtime = Run($"check test_undefined_stat 0 {Nobody} dice=none bonus=3 quiet=1 -> :ok\nset outcome failed\nend\n:ok\nset outcome passed\nend\n");
ModAssert.Equal("passed", runtime.GetVariable("outcome"), "jumped to :ok");
ModAssert.Equal("3", runtime.GetVariable("check_total"), "0 + 0 + 3");
ModAssert.Equal("0", runtime.GetVariable("check_dc"), "difficulty");
}
[ModTest("command/failure_falls_through_or_takes_else")]
static void FailureFallsThroughOrTakesElse(ModTestContext ctx)
{
var through = Run($"check test_undefined_stat 5 {Nobody} dice=none quiet=1 -> :ok\nset outcome failed\nend\n:ok\nset outcome passed\nend\n");
ModAssert.Equal("failed", through.GetVariable("outcome"), "fell through to the next line");
var other = Run($"check test_undefined_stat 5 {Nobody} dice=none quiet=1 else=no -> :ok\nset outcome next\nend\n:ok\nset outcome passed\nend\n:no\nset outcome else\nend\n");
ModAssert.Equal("else", other.GetVariable("outcome"), "jumped to else");
}
[ModTest("command/spec_for_the_validator")]
static void SpecForTheValidator(ModTestContext ctx)
{
var spec = new CheckCommandStrategy();
ModAssert.Equal(2, spec.MinPositional, "stat and difficulty");
ModAssert.Equal(2, spec.MaxPositional, "nothing more");
ModAssert.Equal("else", spec.JumpTargetNamedKey, "else is a label");
ModAssert.NotNull(ScenarioCommandStrategyFactory.Get("check"), "registered as a DSL command");
}
}
}
+38
View File
@@ -0,0 +1,38 @@
namespace Cthangover.Mods.StatChecks.Tests
{
using System;
using Cthangover.Core.Mods.Testing;
/// <summary>Tabletop dice notation: valid forms roll within their range, "no dice" rolls 0, typos roll 0 and are invalid.</summary>
public class DiceRollTests
{
[ModTest("dice/notation")]
static void Notation(ModTestContext ctx)
{
ModAssert.True(DiceRoll.TryParse("2d6", out var count, out var sides) && count == 2 && sides == 6, "2d6");
ModAssert.True(DiceRoll.TryParse("D20", out count, out sides) && count == 1 && sides == 20, "d20 in any case");
ModAssert.True(DiceRoll.TryParse("none", out count, out _) && count == 0, "none");
ModAssert.True(DiceRoll.TryParse("", out count, out _) && count == 0, "empty");
foreach (var bad in new[] { "d1", "2x6", "-1d6", "d", "3d", "1000d6", "1.5d6" })
ModAssert.False(DiceRoll.IsValid(bad), $"'{bad}' is not dice");
}
[ModTest("dice/rolls_stay_in_range")]
static void RollsStayInRange(ModTestContext ctx)
{
var random = new Random(3);
var seenLow = false;
var seenHigh = false;
for (var i = 0; i < 400; i++)
{
var roll = DiceRoll.Roll("2d6", random);
ModAssert.True(roll >= 2 && roll <= 12, $"2d6 gave {roll}");
seenLow |= roll == 2;
seenHigh |= roll == 12;
}
ModAssert.True(seenLow && seenHigh, "both ends of 2d6 come up in 400 rolls");
ModAssert.Equal(0, DiceRoll.Roll("none"), "no dice");
ModAssert.Equal(0, DiceRoll.Roll("oops"), "a typo rolls nothing");
}
}
}
+37
View File
@@ -0,0 +1,37 @@
namespace Cthangover.Mods.StatChecks.Tests
{
using System;
using Cthangover.Core.Mods.Testing;
/// <summary>A check is value + dice + bonus against the difficulty; reaching it exactly succeeds.</summary>
public class StatCheckTests
{
[ModTest("check/total_and_threshold")]
static void TotalAndThreshold(ModTestContext ctx)
{
var check = new StatCheck { Stat = "cha", Difficulty = 12, Dice = "none", Bonus = 2 };
check.Perform(10, null);
ModAssert.Equal(12, check.Total, "10 + 0 + 2");
ModAssert.True(check.Success, "reaching the difficulty exactly succeeds");
check.Perform(9, null);
ModAssert.False(check.Success, "one short fails");
ModAssert.Equal(0, check.Roll, "no dice");
}
[ModTest("check/dice_add_to_the_total")]
static void DiceAddToTheTotal(ModTestContext ctx)
{
var check = new StatCheck { Stat = "str", Difficulty = 100, Dice = "1d6", Bonus = -1 };
var random = new Random(11);
for (var i = 0; i < 50; i++)
{
check.Perform(5, random);
ModAssert.True(check.Roll is >= 1 and <= 6, $"1d6 gave {check.Roll}");
ModAssert.Equal(5 + check.Roll - 1, check.Total, "value + roll + bonus");
ModAssert.False(check.Success, "100 is out of reach");
}
}
}
}