using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Theriapolis.Game.CodexUI.Core; /// /// Strongly-typed asset table for the codex aesthetic. Each property is a /// texture used by one or more widgets — see §6 of the implementation plan /// for the full list. looks for PNGs under /// Content/Gfx/codex/ first; any missing slots fall back to a /// procedural placeholder generated in code so the screen still renders /// before art is authored. /// /// Procedural placeholders aim to capture *structure* (9-slice shape, /// corner/edge sizes, broad colour) so layout work can proceed against /// them. Final art replaces them one by one. /// public sealed class CodexAtlas { public Texture2D Pixel = null!; // 1×1 white — flat-fill helper public Texture2D ParchmentBg = null!; // 256×256 tileable public Texture2D ParchmentCard = null!; // 96×96 9-slice (24 px corners) public Texture2D GildFrame = null!; // 64×64 9-slice card border public Texture2D GildFrameSelected = null!; // 64×64 9-slice — heavier gild public Texture2D GildButtonPrimary = null!; // 96×32 9-slice public Texture2D InkButtonGhost = null!; // 96×32 9-slice public Texture2D WaxSeal = null!; // 64×64 sprite public Texture2D OrnamentDiamond = null!; // 16×16 sprite public Texture2D StepperLocked = null!; public Texture2D StepperActive = null!; public Texture2D StepperDone = null!; public Texture2D ChipTrait = null!; public Texture2D ChipSkillBg = null!; public Texture2D ChipSkillClass = null!; public Texture2D ChipLanguage = null!; public Texture2D PoolDie = null!; public Texture2D SlotEmpty = null!; public Texture2D SlotFilled = null!; public Texture2D BarTrack = null!; public Texture2D BarFill = null!; public Texture2D PopoverBg = null!; public Texture2D CladeSigilCanidae = null!; public Texture2D CladeSigilFelidae = null!; public Texture2D CladeSigilMustelidae = null!; public Texture2D CladeSigilUrsidae = null!; public Texture2D CladeSigilCervidae = null!; public Texture2D CladeSigilBovidae = null!; public Texture2D CladeSigilLeporidae = null!; public void LoadAll(GraphicsDevice gd, string contentRoot) { Pixel = new Texture2D(gd, 1, 1); Pixel.SetData(new[] { Color.White }); string codexDir = System.IO.Path.Combine(contentRoot, "codex"); ParchmentBg = LoadOrFallback(gd, codexDir, "parchment_bg.png", () => MakeParchmentTile(gd, 256, 256)); ParchmentCard = LoadOrFallback(gd, codexDir, "parchment_card.png", () => MakeParchmentCard(gd, 96, 96)); GildFrame = LoadOrFallback(gd, codexDir, "gild_frame.png", () => MakeGildFrame(gd, 64, 64, selected: false)); GildFrameSelected = LoadOrFallback(gd, codexDir, "gild_frame_selected.png",() => MakeGildFrame(gd, 64, 64, selected: true)); GildButtonPrimary = LoadOrFallback(gd, codexDir, "gild_button_primary.png",() => MakeButton(gd, 96, 32, fill: CodexColors.Seal, border: CodexColors.Seal2)); InkButtonGhost = LoadOrFallback(gd, codexDir, "ink_button_ghost.png", () => MakeButton(gd, 96, 32, fill: CodexColors.Bg, border: CodexColors.Ink)); WaxSeal = LoadOrFallback(gd, codexDir, "wax_seal.png", () => MakeWaxSeal(gd, 64, 64)); OrnamentDiamond = LoadOrFallback(gd, codexDir, "ornament_diamond.png", () => MakeOrnamentDiamond(gd, 16, 16)); StepperLocked = LoadOrFallback(gd, codexDir, "stepper_bullet_locked.png", () => MakeStepperBullet(gd, 24, 24, CodexColors.InkMute, CodexColors.Bg)); StepperActive = LoadOrFallback(gd, codexDir, "stepper_bullet_active.png", () => MakeStepperBullet(gd, 24, 24, CodexColors.Gild, CodexColors.Bg)); StepperDone = LoadOrFallback(gd, codexDir, "stepper_bullet_done.png", () => MakeStepperBullet(gd, 24, 24, CodexColors.Seal, CodexColors.Bg)); ChipTrait = LoadOrFallback(gd, codexDir, "chip_trait.png", () => MakeChip(gd, 96, 24, CodexColors.Bg, CodexColors.Gild)); ChipSkillBg = LoadOrFallback(gd, codexDir, "chip_skill_bg.png", () => MakeChip(gd, 96, 24, CodexColors.Bg, CodexColors.Gild)); ChipSkillClass = LoadOrFallback(gd, codexDir, "chip_skill_class.png", () => MakeChip(gd, 96, 24, CodexColors.Bg, CodexColors.Seal)); ChipLanguage = LoadOrFallback(gd, codexDir, "chip_language.png", () => MakeChip(gd, 96, 24, CodexColors.Bg, CodexColors.Rule)); PoolDie = LoadOrFallback(gd, codexDir, "pool_die.png", () => MakeButton(gd, 56, 56, CodexColors.Bg, CodexColors.Rule)); SlotEmpty = LoadOrFallback(gd, codexDir, "slot_empty.png", () => MakeSlot(gd, 64, 44, dashed: true)); SlotFilled = LoadOrFallback(gd, codexDir, "slot_filled.png",() => MakeSlot(gd, 64, 44, dashed: false)); BarTrack = LoadOrFallback(gd, codexDir, "bar_track.png", () => MakeBar(gd, 16, 8, fill: false)); BarFill = LoadOrFallback(gd, codexDir, "bar_fill.png", () => MakeBar(gd, 16, 8, fill: true)); PopoverBg = LoadOrFallback(gd, codexDir, "popover_bg.png", () => MakePopoverBg(gd, 96, 96)); // Clade sigils — placeholder = circular badge with stylised initial CladeSigilCanidae = LoadOrFallback(gd, codexDir, "clade_sigil_canidae.png", () => MakeSigil(gd, 48, 'C')); CladeSigilFelidae = LoadOrFallback(gd, codexDir, "clade_sigil_felidae.png", () => MakeSigil(gd, 48, 'F')); CladeSigilMustelidae = LoadOrFallback(gd, codexDir, "clade_sigil_mustelidae.png", () => MakeSigil(gd, 48, 'M')); CladeSigilUrsidae = LoadOrFallback(gd, codexDir, "clade_sigil_ursidae.png", () => MakeSigil(gd, 48, 'U')); CladeSigilCervidae = LoadOrFallback(gd, codexDir, "clade_sigil_cervidae.png", () => MakeSigil(gd, 48, 'D')); CladeSigilBovidae = LoadOrFallback(gd, codexDir, "clade_sigil_bovidae.png", () => MakeSigil(gd, 48, 'B')); CladeSigilLeporidae = LoadOrFallback(gd, codexDir, "clade_sigil_leporidae.png", () => MakeSigil(gd, 48, 'L')); } public Texture2D SigilFor(string cladeId) => cladeId switch { "canidae" => CladeSigilCanidae, "felidae" => CladeSigilFelidae, "mustelidae" => CladeSigilMustelidae, "ursidae" => CladeSigilUrsidae, "cervidae" => CladeSigilCervidae, "bovidae" => CladeSigilBovidae, "leporidae" => CladeSigilLeporidae, _ => CladeSigilCanidae, }; private static Texture2D LoadOrFallback(GraphicsDevice gd, string dir, string name, System.Func fallback) { string path = System.IO.Path.Combine(dir, name); if (System.IO.File.Exists(path)) { using var fs = System.IO.File.OpenRead(path); return Texture2D.FromStream(gd, fs); } return fallback(); } // ── Procedural placeholder generators ──────────────────────────────── // Each returns a Texture2D the matching widget can use. They aim for // structural correctness (right size, right 9-slice insets) rather // than the final illuminated-codex aesthetic. private static Texture2D MakeParchmentTile(GraphicsDevice gd, int w, int h) { var pixels = new Color[w * h]; var rng = new System.Random(0xC0DE); for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { // Two layered radial gradients + grain noise for parchment feel. float dx1 = (x - w * 0.3f) / (w * 0.6f); float dy1 = (y - h * 0.2f) / (h * 0.4f); float light = MathHelper.Clamp(1f - (dx1 * dx1 + dy1 * dy1), 0f, 1f) * 0.18f; float dx2 = (x - w * 0.8f) / (w * 0.45f); float dy2 = (y - h * 0.8f) / (h * 0.35f); float shade = MathHelper.Clamp(1f - (dx2 * dx2 + dy2 * dy2), 0f, 1f) * 0.10f; float grain = (float)(rng.NextDouble() - 0.5) * 0.04f; float r = CodexColors.Bg.R / 255f + light - shade + grain; float g = CodexColors.Bg.G / 255f + light - shade + grain; float b = CodexColors.Bg.B / 255f + light - shade * 1.3f + grain; pixels[y * w + x] = new Color(MathHelper.Clamp(r, 0f, 1f), MathHelper.Clamp(g, 0f, 1f), MathHelper.Clamp(b, 0f, 1f)); } var tex = new Texture2D(gd, w, h); tex.SetData(pixels); return tex; } private static Texture2D MakeParchmentCard(GraphicsDevice gd, int w, int h) { var pixels = new Color[w * h]; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { // Slight top-down lighten as in `linear-gradient(180deg, rgba(255,250,235,0.05), transparent 30%)`. float topLift = MathHelper.Clamp(1f - y / (h * 0.3f), 0f, 1f) * 0.04f; var c = CodexColors.Bg2; pixels[y * w + x] = new Color( MathHelper.Clamp(c.R / 255f + topLift, 0f, 1f), MathHelper.Clamp(c.G / 255f + topLift, 0f, 1f), MathHelper.Clamp(c.B / 255f + topLift, 0f, 1f)); } var tex = new Texture2D(gd, w, h); tex.SetData(pixels); return tex; } private static Texture2D MakeGildFrame(GraphicsDevice gd, int w, int h, bool selected) { var pixels = new Color[w * h]; var border = selected ? CodexColors.Seal : CodexColors.Rule; var glow = selected ? CodexColors.CardSelectedHalo : CodexColors.CardHoverHalo; int thickness = selected ? 2 : 1; // 1-px border around the edge, plus an inner glow stripe when selected. for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { int distFromEdge = System.Math.Min(System.Math.Min(x, y), System.Math.Min(w - 1 - x, h - 1 - y)); if (distFromEdge < thickness) pixels[y * w + x] = border; else if (selected && distFromEdge < thickness + 2) pixels[y * w + x] = glow; else pixels[y * w + x] = Color.Transparent; } var tex = new Texture2D(gd, w, h); tex.SetData(pixels); return tex; } private static Texture2D MakeButton(GraphicsDevice gd, int w, int h, Color fill, Color border) { var pixels = new Color[w * h]; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { int distFromEdge = System.Math.Min(System.Math.Min(x, y), System.Math.Min(w - 1 - x, h - 1 - y)); pixels[y * w + x] = distFromEdge < 1 ? border : fill; } var tex = new Texture2D(gd, w, h); tex.SetData(pixels); return tex; } private static Texture2D MakeWaxSeal(GraphicsDevice gd, int w, int h) { var pixels = new Color[w * h]; float cx = w / 2f, cy = h / 2f, r = w / 2f - 2; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { float dx = x - cx, dy = y - cy; float d = (float)System.Math.Sqrt(dx * dx + dy * dy); if (d > r) pixels[y * w + x] = Color.Transparent; else if (d > r - 1.5) pixels[y * w + x] = CodexColors.Seal2; else pixels[y * w + x] = Color.Lerp(CodexColors.Seal, CodexColors.Seal2, d / r); } var tex = new Texture2D(gd, w, h); tex.SetData(pixels); return tex; } private static Texture2D MakeOrnamentDiamond(GraphicsDevice gd, int w, int h) { var pixels = new Color[w * h]; float cx = w / 2f, cy = h / 2f; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { float manhat = System.Math.Abs(x - cx) + System.Math.Abs(y - cy); pixels[y * w + x] = manhat <= cx ? CodexColors.Gild : Color.Transparent; } var tex = new Texture2D(gd, w, h); tex.SetData(pixels); return tex; } private static Texture2D MakeStepperBullet(GraphicsDevice gd, int size, int _, Color border, Color fill) { var pixels = new Color[size * size]; float cx = size / 2f, cy = size / 2f, r = size / 2f - 1; for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) { float dx = x - cx, dy = y - cy; float d = (float)System.Math.Sqrt(dx * dx + dy * dy); if (d > r) pixels[y * size + x] = Color.Transparent; else if (d > r - 1.4) pixels[y * size + x] = border; else pixels[y * size + x] = fill; } var tex = new Texture2D(gd, size, size); tex.SetData(pixels); return tex; } private static Texture2D MakeChip(GraphicsDevice gd, int w, int h, Color fill, Color border) => MakeButton(gd, w, h, fill, border); private static Texture2D MakeSlot(GraphicsDevice gd, int w, int h, bool dashed) { var pixels = new Color[w * h]; var border = dashed ? CodexColors.InkMute : CodexColors.InkSoft; var fill = dashed ? Color.Transparent : new Color(180, 138, 60, 16); for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { int distFromEdge = System.Math.Min(System.Math.Min(x, y), System.Math.Min(w - 1 - x, h - 1 - y)); bool onEdge = distFromEdge < 1; if (onEdge) { if (dashed) { int along = (x == 0 || x == w - 1) ? y : x; pixels[y * w + x] = (along / 3) % 2 == 0 ? border : Color.Transparent; } else { pixels[y * w + x] = border; } } else pixels[y * w + x] = fill; } var tex = new Texture2D(gd, w, h); tex.SetData(pixels); return tex; } private static Texture2D MakeBar(GraphicsDevice gd, int w, int h, bool fill) { var pixels = new Color[w * h]; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { int distFromEdge = System.Math.Min(System.Math.Min(x, y), System.Math.Min(w - 1 - x, h - 1 - y)); if (distFromEdge < 1) pixels[y * w + x] = CodexColors.Rule; else pixels[y * w + x] = fill ? CodexColors.Gild : CodexColors.Bg; } var tex = new Texture2D(gd, w, h); tex.SetData(pixels); return tex; } private static Texture2D MakePopoverBg(GraphicsDevice gd, int w, int h) { var pixels = new Color[w * h]; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { int distFromEdge = System.Math.Min(System.Math.Min(x, y), System.Math.Min(w - 1 - x, h - 1 - y)); if (distFromEdge < 1) pixels[y * w + x] = CodexColors.Gild; else pixels[y * w + x] = CodexColors.Bg2; } var tex = new Texture2D(gd, w, h); tex.SetData(pixels); return tex; } private static Texture2D MakeSigil(GraphicsDevice gd, int size, char letter) { var pixels = new Color[size * size]; float cx = size / 2f, cy = size / 2f, r = size / 2f - 1; for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) { float dx = x - cx, dy = y - cy; float d = (float)System.Math.Sqrt(dx * dx + dy * dy); if (d > r) pixels[y * size + x] = Color.Transparent; else if (d > r - 1.5) pixels[y * size + x] = CodexColors.Rule; else pixels[y * size + x] = Color.Lerp(CodexColors.Bg, CodexColors.Bg2, d / r); } var tex = new Texture2D(gd, size, size); tex.SetData(pixels); // Note: the letter glyph itself is drawn by the widget on top of this circle (CodexCard). return tex; } }