using Godot; using Theriapolis.Core.Data; using Theriapolis.GodotHost.Scenes.Widgets; using Theriapolis.GodotHost.UI; namespace Theriapolis.GodotHost.Scenes.Steps; /// /// Step V — History. Direct port of StepBackground in /// src/steps.jsx: card per background showing flavor, granted /// skill / tool proficiencies, and a unique feature with a description /// popover. /// public partial class StepBackground : VBoxContainer, IStep { private CharacterDraft _draft = null!; private GridContainer _grid = null!; public void Bind(CharacterDraft draft) { _draft = draft; _draft.Changed += () => Callable.From(Refresh).CallDeferred(); Build(); } public string? Validate() => string.IsNullOrEmpty(_draft?.BackgroundId) ? "Pick a history." : null; private void Build() { AddThemeConstantOverride("separation", 16); var intro = new VBoxContainer(); intro.AddThemeConstantOverride("separation", 6); AddChild(intro); intro.AddChild(new Label { Text = "FOLIO V · HISTORY" }); intro.AddChild(new Label { Text = "Choose a History" }); intro.AddChild(new Label { Text = "Where your character came from before the wandering began. " + "History grants extra skill and tool proficiencies and a unique " + "feature that resolves in-fiction more often than in combat.", AutowrapMode = TextServer.AutowrapMode.WordSmart, }); _grid = new GridContainer { Columns = 3, SizeFlagsHorizontal = SizeFlags.ExpandFill }; _grid.AddThemeConstantOverride("h_separation", 16); _grid.AddThemeConstantOverride("v_separation", 16); AddChild(_grid); Refresh(); } /// /// Background-availability rules. The backgrounds.json schema doesn't /// carry restriction fields — the gating lives in flavor text only — /// so they're hardcoded here. Each predicate returns true when the /// background is available to the given draft; missing entries are /// universally available. /// /// If backgrounds.json ever gains structured restriction fields, /// swap these out for a property-driven check. /// private static readonly System.Collections.Generic.Dictionary> AvailabilityRules = new() { // Hybrid-only backgrounds — flavor text explicitly hybrid. { "passer", d => d.IsHybrid }, { "hybrid_underground", d => d.IsHybrid }, { "former_chattel", d => d.IsHybrid }, // Clade-restricted backgrounds. { "warren_runner", d => d.HasClade("leporidae") }, { "pack_raised", d => d.HasClade("canidae") }, { "herd_city_born", d => d.HasAnyCladeOfKind("prey") }, }; private void Refresh() { if (_grid is null) return; foreach (var c in _grid.GetChildren()) c.QueueFree(); foreach (var bg in CodexContent.Backgrounds) { if (AvailabilityRules.TryGetValue(bg.Id, out var rule) && !rule(_draft)) continue; _grid.AddChild(BuildCard(bg)); } } private Control BuildCard(BackgroundDef bg) { bool selected = _draft.BackgroundId == bg.Id; var card = new PanelContainer { CustomMinimumSize = new Vector2(200, 0), MouseFilter = MouseFilterEnum.Stop, }; if (selected) card.Modulate = new Color(1f, 0.95f, 0.85f); card.GuiInput += (InputEvent e) => { if (e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left) _draft.Patch(new Godot.Collections.Dictionary { { "background_id", bg.Id } }); }; var box = new VBoxContainer { MouseFilter = MouseFilterEnum.Pass }; box.AddThemeConstantOverride("separation", 6); card.AddChild(box); box.AddChild(new Label { Text = bg.Name }); if (!string.IsNullOrEmpty(bg.Flavor)) { box.AddChild(new Label { Text = bg.Flavor, AutowrapMode = TextServer.AutowrapMode.WordSmart, }); } // Skill + tool prof chips. if (bg.SkillProficiencies.Length > 0 || bg.ToolProficiencies.Length > 0) { var profs = new HFlowContainer { MouseFilter = MouseFilterEnum.Pass }; profs.AddThemeConstantOverride("h_separation", 6); profs.AddThemeConstantOverride("v_separation", 4); box.AddChild(profs); foreach (var s in bg.SkillProficiencies) profs.AddChild(new TraitChip { TraitName = s, Description = "Skill proficiency", Tag = "skill" }); foreach (var t in bg.ToolProficiencies) profs.AddChild(new TraitChip { TraitName = t, Description = "Tool proficiency", Tag = "tool" }); } // Background feature — chip whose popover shows the description. if (!string.IsNullOrEmpty(bg.FeatureName)) { var featRow = new HBoxContainer(); featRow.AddThemeConstantOverride("separation", 6); box.AddChild(featRow); featRow.AddChild(new Label { Text = "FEATURE" }); featRow.AddChild(new TraitChip { TraitName = bg.FeatureName, Description = bg.FeatureDescription, }); } return card; } }