b451f83174
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>
337 lines
16 KiB
C#
337 lines
16 KiB
C#
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;
|
||
}
|
||
}
|