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);
///
/// gmCGSkillsPage::MakeSkillFormula @0x00480e10's six raw inputs —
/// retail's SkillFormula struct (acclient.h) verbatim field
/// order/shape: _w=,
/// _x=,
/// _y=, _z=,
/// _attr1=, _attr2=.
/// / are the raw 1-6 retail
/// attribute id (matching AcDream.Runtime.Session.ChargenAttributeId's
/// own numbering exactly — Strength=1..Self=6) rather than that enum type
/// itself, since Core does not (and must not) reference Runtime; the App
/// layer, which already references both, does the enum cast at the one
/// call site that needs an attribute NAME.
///
public readonly record struct ChargenSkillFormula(
int AdditiveBonus,
int Attribute1Multiplier,
int Attribute2Multiplier,
int Divisor,
uint Attribute1,
uint Attribute2);
///
/// One skill's GLOBAL (heritage-independent) presentation data — retail's
/// SkillBase._min_level/_description/_formula fields,
/// sourced ONLY from the portal.dat SkillTable. Distinct from
/// (which exists BOTH per-heritage
/// (SkillCG) AND globally) because these three fields have NO
/// per-heritage override in retail at all — SkillCG (the per-
/// heritage cost record HeritageGroupCG.Skills projects) carries
/// only Id/NormalCost/PrimaryCost, verified against the
/// DatReaderWriter binding.
///
///
/// Retail's _min_level is typed SKILL_ADVANCEMENT_CLASS, not a
/// character level — gmCGSkillsPage::UpdateSkillEntry @0x00480bf0's
/// own bucket test (arg2->iMinlevel <= 1) reads it as "the
/// lowest at which this skill is
/// USEABLE" — <= 1 (Inactive/Untrained) means useable while
/// untrained, == 2 (Trained) means training is required first.
///
public readonly record struct ChargenSkillDetail(
uint SkillId,
uint MinLevel,
string Description,
ChargenSkillFormula Formula);
///
/// 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;
}
}