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 skills = new List(); Dictionary _lookup; public IReadOnlyList 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(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(StringComparer.OrdinalIgnoreCase); var byId = new Dictionary(StringComparer.OrdinalIgnoreCase); var axisCounts = new Dictionary(); var channelCounts = new Dictionary(); 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 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 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 newSkills) { skills = newSkills ?? new List(); _lookup = null; } #endif } }