refactor the skill system to adhere to the lore handbook

This commit is contained in:
2026-08-15 11:13:15 +02:00
parent 063e94f6eb
commit 9e5c3277bf
70 changed files with 2271 additions and 445 deletions
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
namespace NightclubArcadia.Skills
@@ -7,6 +8,8 @@ namespace NightclubArcadia.Skills
[CreateAssetMenu(menuName = "Nightclub Arcadia/Skill Database", fileName = "SkillDatabase")]
public sealed class SkillDatabase : ScriptableObject
{
static readonly Regex IdPattern = new Regex(@"^[a-z][a-z0-9_]*$", RegexOptions.Compiled);
[SerializeField] List<SkillDefinition> skills = new List<SkillDefinition>();
Dictionary<string, SkillDefinition> _lookup;
@@ -58,6 +61,11 @@ namespace NightclubArcadia.Skills
}
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var byId = new Dictionary<string, SkillDefinition>(StringComparer.OrdinalIgnoreCase);
var axisCounts = new Dictionary<SkillAxis, int>();
var channelCounts = new Dictionary<TraceChannel, int>();
var firingSum = 0f;
foreach (var skill in skills)
{
if (skill == null)
@@ -72,10 +80,127 @@ namespace NightclubArcadia.Skills
continue;
}
if (!IdPattern.IsMatch(skill.Id))
{
Debug.LogError(
$"[{nameof(SkillDatabase)}] Skill Id '{skill.Id}' must match ^[a-z][a-z0-9_]*$.",
this);
}
if (!seen.Add(skill.Id))
{
Debug.LogError($"[{nameof(SkillDatabase)}] Duplicate skill Id '{skill.Id}'.", this);
}
else
{
byId[skill.Id] = skill;
}
if (skill.TargetFiringsPerHour <= 0f)
{
Debug.LogError(
$"[{nameof(SkillDatabase)}] Skill '{skill.Id}' has targetFiringsPerHour <= 0.",
this);
}
else
{
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]++;
}
foreach (var skill in byId.Values)
{
ValidateReference(skill.OpposedTo, skill.Id, "opposedTo", byId);
if (skill.AlliedWith != null)
{
foreach (var ally in skill.AlliedWith)
{
ValidateReference(ally, skill.Id, "alliedWith", byId);
}
}
if (string.IsNullOrEmpty(skill.OpposedTo) || !byId.TryGetValue(skill.OpposedTo, out var opposed))
{
continue;
}
// Symmetry required only when both skills are non-Specialist.
if (skill.Axis == SkillAxis.Specialist || opposed.Axis == SkillAxis.Specialist)
{
continue;
}
if (!string.Equals(opposed.OpposedTo, skill.Id, StringComparison.OrdinalIgnoreCase))
{
Debug.LogError(
$"[{nameof(SkillDatabase)}] opposedTo asymmetry: '{skill.Id}' → '{skill.OpposedTo}' " +
$"but '{opposed.Id}' → '{opposed.OpposedTo}'.",
this);
}
}
AssertAxisCount(axisCounts, SkillAxis.Reason, 2);
AssertAxisCount(axisCounts, SkillAxis.Body, 2);
AssertAxisCount(axisCounts, SkillAxis.Social, 2);
AssertAxisCount(axisCounts, SkillAxis.Self, 2);
AssertAxisCount(axisCounts, SkillAxis.Specialist, 3);
Debug.Log(
$"[{nameof(SkillDatabase)}] targetFiringsPerHour sum = {firingSum} (Bible expects 71).",
this);
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);
Debug.Log(
$"[{nameof(SkillDatabase)}] primary-channel tally: physical {physical} / " +
$"testimonial {testimonial} / behavioural {behavioural} / documentary {documentary} " +
$"(Bible expects 3 / 3 / 3 / 2).",
this);
}
void ValidateReference(
string refId,
string ownerId,
string fieldName,
Dictionary<string, SkillDefinition> byId)
{
if (string.IsNullOrEmpty(refId))
{
return;
}
if (!byId.ContainsKey(refId))
{
Debug.LogError(
$"[{nameof(SkillDatabase)}] Skill '{ownerId}' {fieldName} '{refId}' is not in this database.",
this);
}
}
void AssertAxisCount(Dictionary<SkillAxis, int> counts, SkillAxis axis, int expected)
{
counts.TryGetValue(axis, out var actual);
if (actual != expected)
{
Debug.LogError(
$"[{nameof(SkillDatabase)}] Axis {axis} has {actual} skills; expected {expected}.",
this);
}
}