using Theriapolis.Core.Data;
namespace Theriapolis.Core.Rules.Character;
///
/// Phase 6.5 M2 — given a character's + a chosen
/// subclassId, look up the subclass-feature ids unlocked at a
/// specific level. Used by to populate
/// .
///
/// The resolver does NOT mutate state — it's a pure lookup. The
/// takes the resulting feature ids and
/// applies their mechanical effects at combat-resolution time.
///
public static class SubclassResolver
{
///
/// Look up a subclass def by id from a content collection. Returns
/// null if the id is empty or unknown — callers should treat that as
/// "no subclass picked yet" (pre-L3) or "subclass content missing"
/// (data error, log it).
///
public static SubclassDef? TryFindSubclass(
IReadOnlyDictionary subclasses,
string? subclassId)
{
if (string.IsNullOrEmpty(subclassId)) return null;
return subclasses.TryGetValue(subclassId, out var def) ? def : null;
}
///
/// Feature ids unlocked by the chosen subclass at .
/// Returns an empty array if no subclass is picked, the subclass def is
/// missing, or the level has no entry in .
///
public static string[] UnlockedFeaturesAt(
IReadOnlyDictionary subclasses,
string? subclassId,
int level)
{
var def = TryFindSubclass(subclasses, subclassId);
if (def is null) return Array.Empty();
foreach (var entry in def.LevelFeatures)
if (entry.Level == level)
return entry.Features ?? Array.Empty();
return Array.Empty();
}
///
/// Resolve a feature description (for display in the level-up screen
/// and combat HUD tooltips). Looks first in the subclass's
/// , then falls through to
/// the parent class's
/// (in case the feature id is
/// shared — e.g. asi, extra_attack). Returns null if neither
/// has it.
///
public static ClassFeatureDef? ResolveFeatureDef(
ClassDef classDef,
SubclassDef? subclass,
string featureId)
{
if (subclass is not null
&& subclass.FeatureDefinitions.TryGetValue(featureId, out var sdef))
return sdef;
if (classDef.FeatureDefinitions.TryGetValue(featureId, out var cdef))
return cdef;
return null;
}
}