Files
TheriapolisV3/Theriapolis.Core/Rules/Character/SubclassResolver.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

71 lines
2.8 KiB
C#

using Theriapolis.Core.Data;
namespace Theriapolis.Core.Rules.Character;
/// <summary>
/// Phase 6.5 M2 — given a character's <see cref="ClassDef"/> + a chosen
/// <c>subclassId</c>, look up the subclass-feature ids unlocked at a
/// specific level. Used by <see cref="LevelUpFlow.Compute"/> to populate
/// <see cref="LevelUpResult.SubclassFeaturesUnlocked"/>.
///
/// The resolver does NOT mutate state — it's a pure lookup. The
/// <see cref="FeatureProcessor"/> takes the resulting feature ids and
/// applies their mechanical effects at combat-resolution time.
/// </summary>
public static class SubclassResolver
{
/// <summary>
/// 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).
/// </summary>
public static SubclassDef? TryFindSubclass(
IReadOnlyDictionary<string, SubclassDef> subclasses,
string? subclassId)
{
if (string.IsNullOrEmpty(subclassId)) return null;
return subclasses.TryGetValue(subclassId, out var def) ? def : null;
}
/// <summary>
/// Feature ids unlocked by the chosen subclass at <paramref name="level"/>.
/// Returns an empty array if no subclass is picked, the subclass def is
/// missing, or the level has no entry in <see cref="SubclassDef.LevelFeatures"/>.
/// </summary>
public static string[] UnlockedFeaturesAt(
IReadOnlyDictionary<string, SubclassDef> subclasses,
string? subclassId,
int level)
{
var def = TryFindSubclass(subclasses, subclassId);
if (def is null) return Array.Empty<string>();
foreach (var entry in def.LevelFeatures)
if (entry.Level == level)
return entry.Features ?? Array.Empty<string>();
return Array.Empty<string>();
}
/// <summary>
/// Resolve a feature description (for display in the level-up screen
/// and combat HUD tooltips). Looks first in the subclass's
/// <see cref="SubclassDef.FeatureDefinitions"/>, then falls through to
/// the parent class's
/// <see cref="ClassDef.FeatureDefinitions"/> (in case the feature id is
/// shared — e.g. <c>asi</c>, <c>extra_attack</c>). Returns null if neither
/// has it.
/// </summary>
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;
}
}