Files
TheriapolisV3/Theriapolis.Game/CodexUI/Core/CodexAtlas.cs
T

337 lines
16 KiB
C#
Raw Normal View History

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;
}
}