using System.Collections.Generic; using System.Linq; using Theriapolis.Core.Data; namespace Theriapolis.GodotHost.UI; /// /// Final ability score math — base assignment + clade and species mods. /// Used by StepStats and Aside to render the score breakdown with a /// "+N from Canidae · +2 from Wolf" hover popover. /// /// For hybrids the lineage contribution comes from BOTH parent clades /// (each tagged with its lineage in the source label) and the dominant /// species. Core's CharacterBuilder.TryBuildHybrid is the authority on /// the final mechanical rules at character-finalize time; this is a /// preview-display helper. /// public static class AbilityCalc { public readonly record struct ModSource(string Label, int Value); public static List Sources(string ability, CharacterDraft draft) { var list = new List(); if (draft.IsHybrid) { AddCladeSource(list, ability, CodexContent.Clade(draft.SireCladeId), " (sire)"); AddCladeSource(list, ability, CodexContent.Clade(draft.DamCladeId), " (dam)"); } else { AddCladeSource(list, ability, CodexContent.Clade(draft.CladeId), ""); } var sp = CodexContent.SpeciesById(draft.EffectiveSpeciesId); if (sp is not null) AddDictSource(list, ability, sp.Name, sp.AbilityMods); return list; } public static int TotalBonus(string ability, CharacterDraft draft) => Sources(ability, draft).Sum(s => s.Value); public static int BaseScore(string ability, CharacterDraft draft) => draft.StatAssign.ContainsKey(ability) ? (int)draft.StatAssign[ability] : 0; public static int FinalScore(string ability, CharacterDraft draft) => BaseScore(ability, draft) + TotalBonus(ability, draft); public static int D20Modifier(int score) => (int)System.Math.Floor((score - 10) / 2.0); public static string FormatSigned(int n) => n >= 0 ? $"+{n}" : n.ToString(); /// "+1 from Canidae · +2 from Wolf" — empty when no sources. public static string FormatBreakdown(IEnumerable sources) => string.Join(" · ", sources.Select(s => $"{FormatSigned(s.Value)} from {s.Label}")); private static void AddCladeSource(List list, string ability, CladeDef? clade, string suffix) { if (clade is null) return; AddDictSource(list, ability, clade.Name + suffix, clade.AbilityMods); } private static void AddDictSource(List list, string ability, string sourceName, Dictionary mods) { if (mods.TryGetValue(ability, out int v) && v != 0) list.Add(new ModSource(sourceName, v)); } }