implemented skill system according to designdocuments

This commit is contained in:
2026-08-15 08:09:29 +02:00
parent f8def361fa
commit 063e94f6eb
95 changed files with 2924 additions and 4 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c70934ddbc404dfcb0952241bec17b6c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
using System.Collections.Generic;
using NUnit.Framework;
using NightclubArcadia.Skills;
using UnityEngine;
namespace NightclubArcadia.Skills.Tests
{
public class CommentaryPickerTests
{
sealed class FixedRollSource : IRollSource
{
readonly int _value;
public FixedRollSource(int value) => _value = value;
public int Roll(int sides) => _value;
}
SkillDefinition Make(string id, int startingRank)
{
var def = ScriptableObject.CreateInstance<SkillDefinition>();
def.EditorSet(id, id, SkillDomain.Instinct, string.Empty, startingRank, Color.white);
return def;
}
PlayerSkills SkillsFor(params (string id, int rank)[] ranks)
{
var db = ScriptableObject.CreateInstance<SkillDatabase>();
var list = new List<SkillDefinition>();
foreach (var (id, rank) in ranks)
{
list.Add(Make(id, rank));
}
db.EditorSetSkills(list);
return new PlayerSkills(db);
}
[Test]
public void Pick_EmptyCandidates_ReturnsNull()
{
var skills = SkillsFor(("paranoia", 2));
var result = CommentarySystem.Pick(
new List<SkillDefinition>(),
skills,
new List<string>(),
3,
new FixedRollSource(1));
Assert.IsNull(result);
}
[Test]
public void Pick_HigherRankPreferred_GivenLowRoll()
{
// 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 highPick = CommentarySystem.Pick(
new List<SkillDefinition> { low, high },
skills,
new List<string>(),
3,
new FixedRollSource(2));
Assert.AreEqual("paranoia", highPick.Id);
}
[Test]
public void Pick_RecentSpeaker_HasHalvedWeight()
{
// Without recency: weights 2 and 2, total 4
// With "a" recent: weights 1 and 2, total 3 → roll 1 picks a, roll 2+ picks b
var a = Make("a", 2);
var b = Make("b", 2);
var skills = SkillsFor(("a", 2), ("b", 2));
var pick = CommentarySystem.Pick(
new List<SkillDefinition> { a, b },
skills,
new List<string> { "a" },
3,
new FixedRollSource(2));
Assert.AreEqual("b", pick.Id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ba3f2a4a2a44285acd610ab51884753
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,26 @@
using NUnit.Framework;
using NightclubArcadia.Skills;
namespace NightclubArcadia.Skills.Tests
{
public class DcBandTests
{
[TestCase("standard", 11)]
[TestCase("STANDARD", 11)]
[TestCase("13", 13)]
[TestCase("-3", -3)]
public void TryParse_AcceptsBandNamesAndIntegers(string input, int expected)
{
Assert.IsTrue(DcBands.TryParse(input, out var dc));
Assert.AreEqual(expected, dc);
}
[TestCase("banana")]
[TestCase("")]
[TestCase(null)]
public void TryParse_RejectsGarbage(string input)
{
Assert.IsFalse(DcBands.TryParse(input, out _));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9057738b1d8f402d907c1d9dab8d82a2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,24 @@
{
"name": "NightclubArcadia.Skills.Tests",
"rootNamespace": "NightclubArcadia.Skills.Tests",
"references": [
"NightclubArcadia.Skills",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 09d3d01ec5554542b5dd23239ace7a70
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,137 @@
using System.Collections.Generic;
using NUnit.Framework;
using NightclubArcadia.Skills;
using UnityEngine;
namespace NightclubArcadia.Skills.Tests
{
public class SkillCheckSystemTests
{
sealed class FixedRollSource : IRollSource
{
readonly int _value;
public FixedRollSource(int value) => _value = value;
public int Roll(int sides) => _value;
}
SkillDatabase CreateDatabase(params (string id, string display, int rank)[] skills)
{
var db = ScriptableObject.CreateInstance<SkillDatabase>();
var list = new List<SkillDefinition>();
foreach (var (id, display, rank) in skills)
{
var def = ScriptableObject.CreateInstance<SkillDefinition>();
def.EditorSet(id, display, SkillDomain.Presence, string.Empty, rank, Color.white);
list.Add(def);
}
db.EditorSetSkills(list);
return db;
}
[Test]
public void Resolve_SumsModifiers_AndSucceedsWhenTotalMeetsDc()
{
var db = CreateDatabase(("persuasion", "Persuasion", 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);
Assert.AreEqual(13, result.Total);
Assert.IsTrue(result.Success);
Assert.AreEqual(SkillCheckDegree.Success, result.Degree);
Assert.AreEqual(2, result.RankMod);
Assert.AreEqual(1, result.SituationalMod);
Assert.AreEqual(0, result.StateMod);
}
[Test]
public void Resolve_NaturalOne_IsCriticalFailureEvenIfModifiersWouldPass()
{
var db = CreateDatabase(("persuasion", "Persuasion", 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);
Assert.IsFalse(result.Success);
Assert.AreEqual(SkillCheckDegree.CriticalFailure, result.Degree);
}
[Test]
public void Resolve_NaturalTwenty_IsCriticalSuccessEvenIfBelowDc()
{
var db = CreateDatabase(("persuasion", "Persuasion", 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);
Assert.IsTrue(result.Success);
Assert.AreEqual(SkillCheckDegree.CriticalSuccess, result.Degree);
}
[Test]
public void Resolve_ExactDc_IsSuccess()
{
var db = CreateDatabase(("persuasion", "Persuasion", 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);
Assert.AreEqual(11, result.Total);
Assert.IsTrue(result.Success);
}
[Test]
public void Resolve_AppliesNegativeAndWildcardStateModifiers()
{
var db = CreateDatabase(("reflexes", "Reflexes", 1));
var skills = new PlayerSkills(db);
var state = new SkillStateModifiers();
state.Set("drunk", "reflexes", -2);
state.Set("crowd", "*", -1);
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
var result = system.Resolve("reflexes", 5, 0);
Assert.AreEqual(-3, result.StateMod);
Assert.AreEqual(8, result.Total);
}
[Test]
public void Resolve_UnknownSkill_DoesNotThrow_AndUsesZeroRank()
{
var db = CreateDatabase(("persuasion", "Persuasion", 3));
var skills = new PlayerSkills(db);
var state = new SkillStateModifiers();
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
SkillCheckResult result = default;
Assert.DoesNotThrow(() => result = system.Resolve("nosuchskill", 10, 0));
Assert.AreEqual(0, result.RankMod);
Assert.AreEqual("NOSUCHSKILL", result.SkillDisplayName);
}
[Test]
public void Resolve_DoesNotMutatePlayerSkills()
{
var db = CreateDatabase(("persuasion", "Persuasion", 2));
var skills = new PlayerSkills(db);
var before = skills.GetRank("persuasion");
var state = new SkillStateModifiers();
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
system.Resolve("persuasion", 10, 0);
Assert.AreEqual(before, skills.GetRank("persuasion"));
Assert.AreEqual(1, skills.AllRanks.Count);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 78734080b9b5473db4365b8203fc79b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: