216 lines
7.1 KiB
C#
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
|
|
}
|
|
}
|