Files

216 lines
7.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
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;
public IReadOnlyList<SkillDefinition> All => skills;
public bool TryGet(string skillId, out SkillDefinition def)
{
EnsureLookup();
if (string.IsNullOrEmpty(skillId))
{
def = null;
return false;
}
return _lookup.TryGetValue(skillId, out def);
}
void EnsureLookup()
{
if (_lookup != null)
{
return;
}
_lookup = new Dictionary<string, SkillDefinition>(StringComparer.OrdinalIgnoreCase);
if (skills == null)
{
return;
}
foreach (var skill in skills)
{
if (skill == null || string.IsNullOrEmpty(skill.Id))
{
continue;
}
_lookup[skill.Id] = skill;
}
}
void OnValidate()
{
_lookup = null;
if (skills == null)
{
return;
}
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)
{
Debug.LogError($"[{nameof(SkillDatabase)}] Null skill entry in {name}.", this);
continue;
}
if (string.IsNullOrEmpty(skill.Id))
{
Debug.LogError($"[{nameof(SkillDatabase)}] Skill '{skill.name}' has an empty Id.", this);
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);
}
}
#if UNITY_EDITOR
public void EditorSetSkills(List<SkillDefinition> newSkills)
{
skills = newSkills ?? new List<SkillDefinition>();
_lookup = null;
}
#endif
}
}