namespace AcDream.Core.CharGen;
///
/// Retail's four skill states. Wire values match ACE's
/// ACE.Entity.Enum.SkillAdvancementClass exactly (0=Inactive,
/// 1=Untrained, 2=Trained, 3=Specialized) — ACE unpacks the 0xF656
/// CharacterCreateInfo.SkillAdvancementClasses list with this same
/// numbering, and retail's CharGenState::UpdateRemainingSkillCredits @
/// 0x005C37C0 only charges credits for Trained (2) and Specialized (3).
///
public enum ChargenSkillAdvancementClass : uint
{
Inactive = 0,
Untrained = 1,
Trained = 2,
Specialized = 3,
}
///
/// One skill's retail training cost for a heritage. Retail schema:
/// SkillCG, entries of HeritageGroupCG.Skills.
/// PrimaryCost is the TOTAL cost to reach Specialized (not an
/// increment on top of NormalCost) — retail's
/// UpdateRemainingSkillCredits adds exactly one of the two per
/// skill, never both.
///
public readonly record struct ChargenSkillCost(uint SkillId, int NormalCost, int PrimaryCost);
///
/// Retail's fixed-size per-character skill-advancement array
/// (CharGenState.skillLevels). ACE's CharacterCreateInfo.Unpack
/// terminates the connection if the wire's numSkills count is not
/// exactly (55): retail's own loop in
/// UpdateRemainingSkillCredits walks indices 1..totalNumSkills
/// (skipping reserved slot 0), and Chorizite's SkillId enum runs
/// 1..54 — 54 real skills plus the reserved slot 0 is exactly 55. This type
/// makes that shape structural: it always holds exactly 55 slots, so a
/// caller building the 0xF656 body (CC2) cannot accidentally send a
/// different count.
///
public sealed class ChargenSkillAdvancementSet
{
/// Slot 0 is reserved (unused by retail); slots 1..54 map 1:1
/// to Chorizite's DatReaderWriter.Enums.SkillId values.
public const int SlotCount = 55;
private readonly ChargenSkillAdvancementClass[] _slots = new ChargenSkillAdvancementClass[SlotCount];
/// Skill state by raw skill id. Ids outside 1..54 read
/// as and cannot be
/// set.
public ChargenSkillAdvancementClass this[uint skillId]
{
get => skillId >= 1 && skillId < SlotCount
? _slots[skillId]
: ChargenSkillAdvancementClass.Inactive;
set
{
if (skillId < 1 || skillId >= SlotCount)
throw new ArgumentOutOfRangeException(
nameof(skillId),
skillId,
$"Skill id must be in 1..{SlotCount - 1}.");
_slots[skillId] = value;
}
}
///
/// Materializes the wire body shape: exactly
/// entries, slot 0 first, matching ACE's
/// CharacterCreateInfo.SkillAdvancementClasses read order.
///
public IReadOnlyList ToWireClasses()
{
var wire = new uint[SlotCount];
for (int i = 0; i < SlotCount; i++)
wire[i] = (uint)_slots[i];
return wire;
}
}