Files
TheriapolisV3/Theriapolis.Game/CodexUI/Core/CodexAtlas.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

337 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Theriapolis.Game.CodexUI.Core;
/// <summary>
/// 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. <see cref="LoadAll"/> looks for PNGs under
/// <c>Content/Gfx/codex/</c> 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.
/// </summary>
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<Texture2D> 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;
}
}