refactor the skill system to adhere to the lore handbook
This commit is contained in:
@@ -14,10 +14,30 @@ namespace NightclubArcadia.Skills.Tests
|
||||
public int Roll(int sides) => _value;
|
||||
}
|
||||
|
||||
static SkillBibleEntry Entry(string id, int startingRank)
|
||||
{
|
||||
return new SkillBibleEntry
|
||||
{
|
||||
id = id,
|
||||
displayName = id.ToUpperInvariant(),
|
||||
axis = "Self",
|
||||
domain = string.Empty,
|
||||
notes = string.Empty,
|
||||
targetFiringsPerHour = 5f,
|
||||
opposedTo = string.Empty,
|
||||
alliedWith = System.Array.Empty<string>(),
|
||||
primaryChannel = "Behavioural",
|
||||
secondaryChannel = "None",
|
||||
clueYield = "Low",
|
||||
startingRank = startingRank,
|
||||
accentColor = Color.white,
|
||||
};
|
||||
}
|
||||
|
||||
SkillDefinition Make(string id, int startingRank)
|
||||
{
|
||||
var def = ScriptableObject.CreateInstance<SkillDefinition>();
|
||||
def.EditorSet(id, id, SkillDomain.Instinct, string.Empty, startingRank, Color.white);
|
||||
def.EditorSet(Entry(id, startingRank));
|
||||
return def;
|
||||
}
|
||||
|
||||
@@ -37,7 +57,7 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Pick_EmptyCandidates_ReturnsNull()
|
||||
{
|
||||
var skills = SkillsFor(("paranoia", 2));
|
||||
var skills = SkillsFor(("house_pour", 2));
|
||||
var result = CommentarySystem.Pick(
|
||||
new List<SkillDefinition>(),
|
||||
skills,
|
||||
@@ -54,9 +74,9 @@ namespace NightclubArcadia.Skills.Tests
|
||||
// ranks 1 and 3 → weights 1 and 3 → total 4
|
||||
// roll 1 maps to pick index 0 of cumulative: only weight-1 skill
|
||||
// roll 2..4 maps to higher-ranked skill
|
||||
var low = Make("read_room", 1);
|
||||
var high = Make("paranoia", 3);
|
||||
var skills = SkillsFor(("read_room", 1), ("paranoia", 3));
|
||||
var low = Make("facework", 1);
|
||||
var high = Make("house_pour", 3);
|
||||
var skills = SkillsFor(("facework", 1), ("house_pour", 3));
|
||||
|
||||
var highPick = CommentarySystem.Pick(
|
||||
new List<SkillDefinition> { low, high },
|
||||
@@ -65,7 +85,7 @@ namespace NightclubArcadia.Skills.Tests
|
||||
3,
|
||||
new FixedRollSource(2));
|
||||
|
||||
Assert.AreEqual("paranoia", highPick.Id);
|
||||
Assert.AreEqual("house_pour", highPick.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -86,5 +106,25 @@ namespace NightclubArcadia.Skills.Tests
|
||||
|
||||
Assert.AreEqual("b", pick.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsOffCooldown_HighFirings_EligibleImmediately()
|
||||
{
|
||||
// 3600 firings/hour → 1 second interval
|
||||
Assert.IsTrue(CommentarySystem.IsOffCooldown(10f, 11f, 3600f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsOffCooldown_OnePerHour_NotEligibleAfterOneSecond()
|
||||
{
|
||||
Assert.IsFalse(CommentarySystem.IsOffCooldown(10f, 11f, 1f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsOffCooldown_ZeroOrNegative_NeverFires()
|
||||
{
|
||||
Assert.IsFalse(CommentarySystem.IsOffCooldown(float.NegativeInfinity, 100f, 0f));
|
||||
Assert.IsFalse(CommentarySystem.IsOffCooldown(float.NegativeInfinity, 100f, -1f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ namespace NightclubArcadia.Skills.Tests
|
||||
{
|
||||
public class DcBandTests
|
||||
{
|
||||
[TestCase("standard", 11)]
|
||||
[TestCase("STANDARD", 11)]
|
||||
[TestCase("routine", 11)]
|
||||
[TestCase("ROUTINE", 11)]
|
||||
[TestCase("trivial", 8)]
|
||||
[TestCase("13", 13)]
|
||||
[TestCase("-3", -3)]
|
||||
public void TryParse_AcceptsBandNamesAndIntegers(string input, int expected)
|
||||
@@ -15,9 +16,18 @@ namespace NightclubArcadia.Skills.Tests
|
||||
Assert.AreEqual(expected, dc);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryParse_TrivialIsEight_NotFive()
|
||||
{
|
||||
Assert.IsTrue(DcBands.TryParse("trivial", out var dc));
|
||||
Assert.AreEqual(8, dc);
|
||||
Assert.AreEqual(8, (int)DcBand.Trivial);
|
||||
}
|
||||
|
||||
[TestCase("banana")]
|
||||
[TestCase("")]
|
||||
[TestCase(null)]
|
||||
[TestCase("standard")]
|
||||
public void TryParse_RejectsGarbage(string input)
|
||||
{
|
||||
Assert.IsFalse(DcBands.TryParse(input, out _));
|
||||
|
||||
@@ -14,6 +14,26 @@ namespace NightclubArcadia.Skills.Tests
|
||||
public int Roll(int sides) => _value;
|
||||
}
|
||||
|
||||
static SkillBibleEntry Entry(string id, string display, int rank)
|
||||
{
|
||||
return new SkillBibleEntry
|
||||
{
|
||||
id = id,
|
||||
displayName = display,
|
||||
axis = "Social",
|
||||
domain = string.Empty,
|
||||
notes = string.Empty,
|
||||
targetFiringsPerHour = 5f,
|
||||
opposedTo = string.Empty,
|
||||
alliedWith = System.Array.Empty<string>(),
|
||||
primaryChannel = "Testimonial",
|
||||
secondaryChannel = "None",
|
||||
clueYield = "Low",
|
||||
startingRank = rank,
|
||||
accentColor = Color.white,
|
||||
};
|
||||
}
|
||||
|
||||
SkillDatabase CreateDatabase(params (string id, string display, int rank)[] skills)
|
||||
{
|
||||
var db = ScriptableObject.CreateInstance<SkillDatabase>();
|
||||
@@ -21,7 +41,7 @@ namespace NightclubArcadia.Skills.Tests
|
||||
foreach (var (id, display, rank) in skills)
|
||||
{
|
||||
var def = ScriptableObject.CreateInstance<SkillDefinition>();
|
||||
def.EditorSet(id, display, SkillDomain.Presence, string.Empty, rank, Color.white);
|
||||
def.EditorSet(Entry(id, display, rank));
|
||||
list.Add(def);
|
||||
}
|
||||
|
||||
@@ -32,12 +52,12 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_SumsModifiers_AndSucceedsWhenTotalMeetsDc()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 2));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 2));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
|
||||
var result = system.Resolve("persuasion", 12, 1);
|
||||
var result = system.Resolve("placement", 12, 1);
|
||||
|
||||
Assert.AreEqual(13, result.Total);
|
||||
Assert.IsTrue(result.Success);
|
||||
@@ -50,12 +70,12 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_NaturalOne_IsCriticalFailureEvenIfModifiersWouldPass()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 10));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 10));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(1));
|
||||
|
||||
var result = system.Resolve("persuasion", 5, 5);
|
||||
var result = system.Resolve("placement", 5, 5);
|
||||
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(SkillCheckDegree.CriticalFailure, result.Degree);
|
||||
@@ -64,12 +84,12 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_NaturalTwenty_IsCriticalSuccessEvenIfBelowDc()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 0));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 0));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(20));
|
||||
|
||||
var result = system.Resolve("persuasion", 25, 0);
|
||||
var result = system.Resolve("placement", 25, 0);
|
||||
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(SkillCheckDegree.CriticalSuccess, result.Degree);
|
||||
@@ -78,12 +98,12 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_ExactDc_IsSuccess()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 1));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 1));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
|
||||
var result = system.Resolve("persuasion", 11, 0);
|
||||
var result = system.Resolve("placement", 11, 0);
|
||||
|
||||
Assert.AreEqual(11, result.Total);
|
||||
Assert.IsTrue(result.Success);
|
||||
@@ -92,14 +112,14 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_AppliesNegativeAndWildcardStateModifiers()
|
||||
{
|
||||
var db = CreateDatabase(("reflexes", "Reflexes", 1));
|
||||
var db = CreateDatabase(("long_shift", "THE LONG SHIFT", 1));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
state.Set("drunk", "reflexes", -2);
|
||||
state.Set("drunk", "long_shift", -2);
|
||||
state.Set("crowd", "*", -1);
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
|
||||
var result = system.Resolve("reflexes", 5, 0);
|
||||
var result = system.Resolve("long_shift", 5, 0);
|
||||
|
||||
Assert.AreEqual(-3, result.StateMod);
|
||||
Assert.AreEqual(8, result.Total);
|
||||
@@ -108,7 +128,7 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_UnknownSkill_DoesNotThrow_AndUsesZeroRank()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 3));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 3));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
@@ -122,15 +142,15 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_DoesNotMutatePlayerSkills()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 2));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 2));
|
||||
var skills = new PlayerSkills(db);
|
||||
var before = skills.GetRank("persuasion");
|
||||
var before = skills.GetRank("placement");
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
|
||||
system.Resolve("persuasion", 10, 0);
|
||||
system.Resolve("placement", 10, 0);
|
||||
|
||||
Assert.AreEqual(before, skills.GetRank("persuasion"));
|
||||
Assert.AreEqual(before, skills.GetRank("placement"));
|
||||
Assert.AreEqual(1, skills.AllRanks.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using NUnit.Framework;
|
||||
using NightclubArcadia.Skills;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.Skills.Tests
|
||||
{
|
||||
public class SkillRosterTests
|
||||
{
|
||||
const string DatabasePath = "Assets/Skills/SkillDatabase.asset";
|
||||
static readonly Regex IdPattern = new Regex(@"^[a-z][a-z0-9_]*$");
|
||||
|
||||
static readonly (string id, string displayName)[] ExpectedNames =
|
||||
{
|
||||
("provenance", "PROVENANCE"),
|
||||
("confluence", "CONFLUENCE"),
|
||||
("house_pour", "HOUSE POUR"),
|
||||
("long_shift", "THE LONG SHIFT"),
|
||||
("facework", "FACEWORK"),
|
||||
("placement", "PLACEMENT"),
|
||||
("amnesty", "AMNESTY"),
|
||||
("standing_order", "STANDING ORDER"),
|
||||
("undisclosed", "UNDISCLOSED"),
|
||||
("the_float", "THE FLOAT"),
|
||||
("room_tone", "ROOM TONE"),
|
||||
};
|
||||
|
||||
SkillDatabase LoadDatabase()
|
||||
{
|
||||
var db = AssetDatabase.LoadAssetAtPath<SkillDatabase>(DatabasePath);
|
||||
Assert.IsNotNull(db, $"Expected SkillDatabase at {DatabasePath}");
|
||||
return db;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Roster_HasElevenSkills_WithValidIdsAndPairSymmetry()
|
||||
{
|
||||
var db = LoadDatabase();
|
||||
Assert.AreEqual(11, db.All.Count);
|
||||
|
||||
var byId = new Dictionary<string, SkillDefinition>();
|
||||
var axisCounts = new Dictionary<SkillAxis, int>();
|
||||
var channelCounts = new Dictionary<TraceChannel, int>();
|
||||
var firingSum = 0f;
|
||||
|
||||
foreach (var skill in db.All)
|
||||
{
|
||||
Assert.IsNotNull(skill);
|
||||
Assert.IsTrue(IdPattern.IsMatch(skill.Id), $"Bad id '{skill.Id}'");
|
||||
Assert.IsFalse(byId.ContainsKey(skill.Id), $"Duplicate id '{skill.Id}'");
|
||||
byId[skill.Id] = skill;
|
||||
|
||||
firingSum += skill.TargetFiringsPerHour;
|
||||
if (!axisCounts.ContainsKey(skill.Axis))
|
||||
{
|
||||
axisCounts[skill.Axis] = 0;
|
||||
}
|
||||
|
||||
axisCounts[skill.Axis]++;
|
||||
|
||||
if (!channelCounts.ContainsKey(skill.PrimaryChannel))
|
||||
{
|
||||
channelCounts[skill.PrimaryChannel] = 0;
|
||||
}
|
||||
|
||||
channelCounts[skill.PrimaryChannel]++;
|
||||
}
|
||||
|
||||
Assert.AreEqual(71f, firingSum, 0.001f);
|
||||
Assert.AreEqual(2, axisCounts[SkillAxis.Reason]);
|
||||
Assert.AreEqual(2, axisCounts[SkillAxis.Body]);
|
||||
Assert.AreEqual(2, axisCounts[SkillAxis.Social]);
|
||||
Assert.AreEqual(2, axisCounts[SkillAxis.Self]);
|
||||
Assert.AreEqual(3, axisCounts[SkillAxis.Specialist]);
|
||||
|
||||
channelCounts.TryGetValue(TraceChannel.Physical, out var physical);
|
||||
channelCounts.TryGetValue(TraceChannel.Testimonial, out var testimonial);
|
||||
channelCounts.TryGetValue(TraceChannel.Behavioural, out var behavioural);
|
||||
channelCounts.TryGetValue(TraceChannel.Documentary, out var documentary);
|
||||
Assert.AreEqual(3, physical);
|
||||
Assert.AreEqual(3, testimonial);
|
||||
Assert.AreEqual(3, behavioural);
|
||||
Assert.AreEqual(2, documentary);
|
||||
|
||||
foreach (var skill in byId.Values)
|
||||
{
|
||||
Assert.IsTrue(byId.ContainsKey(skill.OpposedTo), $"{skill.Id} opposedTo missing");
|
||||
if (skill.AlliedWith != null)
|
||||
{
|
||||
foreach (var ally in skill.AlliedWith)
|
||||
{
|
||||
Assert.IsTrue(byId.ContainsKey(ally), $"{skill.Id} alliedWith '{ally}' missing");
|
||||
}
|
||||
}
|
||||
|
||||
var opposed = byId[skill.OpposedTo];
|
||||
if (skill.Axis == SkillAxis.Specialist || opposed.Axis == SkillAxis.Specialist)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.AreEqual(
|
||||
skill.Id,
|
||||
opposed.OpposedTo,
|
||||
$"Non-specialist opposedTo asymmetry: {skill.Id} ↔ {opposed.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Roster_DisplayNamesAreUppercaseBibleForms_IdsAreSnakeCase()
|
||||
{
|
||||
var db = LoadDatabase();
|
||||
var byId = new Dictionary<string, SkillDefinition>();
|
||||
foreach (var skill in db.All)
|
||||
{
|
||||
byId[skill.Id] = skill;
|
||||
}
|
||||
|
||||
foreach (var (id, displayName) in ExpectedNames)
|
||||
{
|
||||
Assert.IsTrue(byId.TryGetValue(id, out var skill), $"Missing skill '{id}'");
|
||||
Assert.AreEqual(displayName, skill.DisplayName);
|
||||
Assert.AreNotEqual(displayName, skill.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31c2aa46fda0542dcbe4a2bba026c2c4
|
||||
Reference in New Issue
Block a user