using Godot; using System.Collections.Generic; using Theriapolis.Core.Data; using Theriapolis.GodotHost.Platform; namespace Theriapolis.GodotHost.UI; /// /// In-progress character record. Resource (not Node) per /// GODOT_PORTING_GUIDE.md §2.1: serializable via ResourceSaver.save, /// inspectable in the editor, doesn't need tree presence. /// /// Mirrors app.jsx's state shape — clade/species/class/etc., /// stat assignment, chosen skills, name. Mutate via so /// every change emits ; the Aside and Wizard listen /// and re-render on that single signal. /// [GlobalClass] public partial class CharacterDraft : Resource { // NB: Resource defines its own `Changed` signal (emitted via EmitChanged()). // We piggyback on it instead of declaring our own to avoid a name clash — // listeners do `draft.Changed += ...` exactly the same way either way. [Export] public string CladeId { get; set; } = ""; [Export] public string SpeciesId { get; set; } = ""; [Export] public string ClassId { get; set; } = ""; [Export] public string SubclassId { get; set; } = ""; [Export] public string BackgroundId { get; set; } = ""; /// "array" or "roll" — assignment method for ability scores. [Export] public string StatMethod { get; set; } = "array"; [Export] public Godot.Collections.Array StatPool { get; set; } = new() { 15, 14, 13, 12, 10, 8 }; [Export] public Godot.Collections.Dictionary StatAssign { get; set; } = new(); [Export] public Godot.Collections.Array ChosenSkills { get; set; } = new(); [Export] public string CharacterName { get; set; } = ""; /// /// Mutate one or more fields, then emit Changed once. Mirrors /// the JS prototype's set(patch) from app.jsx. /// public void Patch(Godot.Collections.Dictionary patch) { foreach (var key in patch.Keys) { string k = (string)key; switch (k) { case "clade_id": CladeId = (string)patch[key]; break; case "species_id": SpeciesId = (string)patch[key]; break; case "class_id": ClassId = (string)patch[key]; break; case "subclass_id": SubclassId = (string)patch[key]; break; case "background_id": BackgroundId = (string)patch[key]; break; case "stat_method": StatMethod = (string)patch[key]; break; case "stat_pool": StatPool = (Godot.Collections.Array)patch[key]; break; case "stat_assign": StatAssign = (Godot.Collections.Dictionary)patch[key]; break; case "chosen_skills": ChosenSkills = (Godot.Collections.Array)patch[key]; break; case "character_name":CharacterName = (string)patch[key]; break; default: GD.PushWarning($"[CharacterDraft] unknown patch key: {k}"); break; } } EmitChanged(); } } /// /// Lazy-loaded immutable content tables. Replaces the React prototype's /// data.jsx + autoload Content per GODOT_PORTING_GUIDE.md §2.2 — same /// purpose, idiomatic C#. /// public static class CodexContent { private static CladeDef[]? _clades; private static SpeciesDef[]? _species; private static ClassDef[]? _classes; private static SubclassDef[]? _subclasses; private static BackgroundDef[]? _backgrounds; public static CladeDef[] Clades => _clades ??= Load(l => l.LoadClades()); public static SpeciesDef[] Species => _species ??= Load(l => l.LoadSpecies(Clades)); public static ClassDef[] Classes => _classes ??= Load(l => l.LoadClasses()); public static SubclassDef[] Subclasses => _subclasses ??= Load(l => l.LoadSubclasses(Classes)); public static BackgroundDef[] Backgrounds => _backgrounds ??= Load(l => l.LoadBackgrounds()); public static CladeDef? Clade(string id) => System.Array.Find(Clades, c => c.Id == id); public static SpeciesDef? SpeciesById(string id) => System.Array.Find(Species, s => s.Id == id); public static ClassDef? Class(string id) => System.Array.Find(Classes, c => c.Id == id); public static BackgroundDef? Background(string id) => System.Array.Find(Backgrounds, b => b.Id == id); public static IEnumerable SpeciesOfClade(string cladeId) { foreach (var s in Species) if (string.Equals(s.CladeId, cladeId, System.StringComparison.OrdinalIgnoreCase)) yield return s; } private static T Load(System.Func fn) { var loader = new Theriapolis.Core.Data.ContentLoader(ContentPaths.DataDir); return fn(loader); } }