updated Candidate System with Voice System
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user