b451f83174
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>
71 lines
2.8 KiB
C#
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;
|
|
}
|
|
}
|