Files

239 lines
8.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace NightclubArcadia.Skills
{
[CreateAssetMenu(menuName = "Nightclub Arcadia/Candidate Database", fileName = "CandidateDatabase")]
public sealed class CandidateDatabase : ScriptableObject
{
static readonly Regex IdPattern = new Regex(@"^[a-z][a-z0-9_]*$", RegexOptions.Compiled);
static readonly Regex UnlockFlagPattern =
new Regex(@"^\$(visited|errand)_[a-z][a-z0-9_]*$", RegexOptions.Compiled);
const string SkillDatabasePath = "Assets/Skills/SkillDatabase.asset";
[SerializeField] List<CandidateDefinition> candidates = new List<CandidateDefinition>();
Dictionary<string, CandidateDefinition> _lookup;
public IReadOnlyList<CandidateDefinition> All => candidates;
public bool TryGet(string candidateId, out CandidateDefinition def)
{
EnsureLookup();
if (string.IsNullOrEmpty(candidateId))
{
def = null;
return false;
}
return _lookup.TryGetValue(candidateId, out def);
}
void EnsureLookup()
{
if (_lookup != null)
{
return;
}
_lookup = new Dictionary<string, CandidateDefinition>(StringComparer.OrdinalIgnoreCase);
if (candidates == null)
{
return;
}
foreach (var candidate in candidates)
{
if (candidate == null || string.IsNullOrEmpty(candidate.Id))
{
continue;
}
_lookup[candidate.Id] = candidate;
}
}
void OnValidate()
{
_lookup = null;
if (candidates == null)
{
return;
}
var seenCandidates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var seenClues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var axesUsed = new HashSet<SkillAxis>();
var authoredCount = 0;
var plannedCount = 0;
SkillDatabase skillDb = null;
#if UNITY_EDITOR
skillDb = AssetDatabase.LoadAssetAtPath<SkillDatabase>(SkillDatabasePath);
#endif
foreach (var candidate in candidates)
{
if (candidate == null)
{
Debug.LogError($"[{nameof(CandidateDatabase)}] Null candidate entry in {name}.", this);
continue;
}
if (string.IsNullOrEmpty(candidate.Id))
{
Debug.LogError($"[{nameof(CandidateDatabase)}] Candidate '{candidate.name}' has an empty Id.", this);
continue;
}
if (!IdPattern.IsMatch(candidate.Id))
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] Candidate Id '{candidate.Id}' must match ^[a-z][a-z0-9_]*$.",
this);
}
if (!seenCandidates.Add(candidate.Id))
{
Debug.LogError($"[{nameof(CandidateDatabase)}] Duplicate candidate Id '{candidate.Id}'.", this);
}
if (candidate.IntuitionAxis == SkillAxis.Body)
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] Candidate '{candidate.Id}' uses intuitionAxis Body; " +
"Body is deliberately unattached to any candidate.",
this);
}
if (candidate.IntuitionAxis == SkillAxis.Specialist)
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] Candidate '{candidate.Id}' uses intuitionAxis Specialist; " +
"intuition clues attach to Reason / Social / Self only.",
this);
}
if (!axesUsed.Add(candidate.IntuitionAxis))
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] intuitionAxis {candidate.IntuitionAxis} is shared by " +
$"more than one candidate (including '{candidate.Id}').",
this);
}
var typeCounts = new Dictionary<CandidateEvidenceType, int>();
if (candidate.Clues == null)
{
Debug.LogError($"[{nameof(CandidateDatabase)}] Candidate '{candidate.Id}' has no clues.", this);
continue;
}
foreach (var clue in candidate.Clues)
{
if (clue == null)
{
Debug.LogError($"[{nameof(CandidateDatabase)}] Candidate '{candidate.Id}' has a null clue.", this);
continue;
}
if (string.IsNullOrEmpty(clue.Id) || !IdPattern.IsMatch(clue.Id))
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] Candidate '{candidate.Id}' clue Id '{clue.Id}' " +
"must match ^[a-z][a-z0-9_]*$.",
this);
}
else if (!seenClues.Add(clue.Id))
{
Debug.LogError($"[{nameof(CandidateDatabase)}] Duplicate clue Id '{clue.Id}'.", this);
}
if (!typeCounts.ContainsKey(clue.Type))
{
typeCounts[clue.Type] = 0;
}
typeCounts[clue.Type]++;
if (clue.Status == CandidateClueStatus.Authored)
{
authoredCount++;
}
else
{
plannedCount++;
}
if (!string.IsNullOrEmpty(clue.UnlockFlag) && !UnlockFlagPattern.IsMatch(clue.UnlockFlag))
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] Clue '{clue.Id}' unlockFlag '{clue.UnlockFlag}' " +
"must match ^\\$(visited|errand)_[a-z][a-z0-9_]*$ " +
"(exploration-gated, never skill-gated).",
this);
}
if (clue.Type == CandidateEvidenceType.Intuition)
{
if (string.IsNullOrEmpty(clue.SkillId))
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] Intuition clue '{clue.Id}' needs a skillId.",
this);
}
else if (skillDb != null)
{
if (!skillDb.TryGet(clue.SkillId, out var skill))
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] Intuition clue '{clue.Id}' skillId " +
$"'{clue.SkillId}' is not in SkillDatabase.",
this);
}
else if (skill.Axis != candidate.IntuitionAxis)
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] Intuition clue '{clue.Id}' skill " +
$"'{clue.SkillId}' is axis {skill.Axis}, but candidate " +
$"'{candidate.Id}' intuitionAxis is {candidate.IntuitionAxis}.",
this);
}
}
}
}
foreach (CandidateEvidenceType type in Enum.GetValues(typeof(CandidateEvidenceType)))
{
typeCounts.TryGetValue(type, out var count);
if (count != 1)
{
Debug.LogError(
$"[{nameof(CandidateDatabase)}] Candidate '{candidate.Id}' has {count} " +
$"{type} clue(s); expected exactly 1.",
this);
}
}
}
Debug.Log(
$"[{nameof(CandidateDatabase)}] clue coverage: {authoredCount} Authored / {plannedCount} Planned.",
this);
}
#if UNITY_EDITOR
public void EditorSetCandidates(List<CandidateDefinition> newCandidates)
{
candidates = newCandidates ?? new List<CandidateDefinition>();
_lookup = null;
}
#endif
}
}