using Godot; using System; using System.Collections.Generic; using Theriapolis.Core.Tactical; using Theriapolis.GodotHost.Platform; namespace Theriapolis.GodotHost.Rendering; /// /// Loads and caches PNG textures for every TacticalSurface and TacticalDeco /// value, with optional per-variant alternates (name_0.png, name_1.png, ...). /// Falls back to a procedurally generated solid-colour image when no art is /// on disk so the renderer always has something to draw. /// /// Mirrors Theriapolis.Game/Rendering/TacticalAtlas.cs lookup contract. /// PNGs are loaded via ContentLoader, which caches at the file level — this /// class just adds the per-enum dispatch and variant arrays. /// public static class TacticalAtlas { private static readonly Dictionary _surfaces = new(); private static readonly Dictionary _decos = new(); private static bool _loaded; public static void EnsureLoaded() { if (_loaded) return; _loaded = true; foreach (TacticalSurface s in Enum.GetValues()) _surfaces[s] = LoadVariants("surface", s.ToString().ToLowerInvariant(), () => SurfacePlaceholder(s)); foreach (TacticalDeco d in Enum.GetValues()) { if (d == TacticalDeco.None) continue; _decos[d] = LoadVariants("deco", d.ToString().ToLowerInvariant(), () => DecoPlaceholder(d)); } } public static Texture2D GetSurface(TacticalSurface s, byte variant) { EnsureLoaded(); if (!_surfaces.TryGetValue(s, out var arr) || arr.Length == 0) arr = _surfaces[TacticalSurface.None]; return arr[variant % arr.Length]; } public static Texture2D? GetDeco(TacticalDeco d, byte variant) { EnsureLoaded(); if (d == TacticalDeco.None) return null; if (!_decos.TryGetValue(d, out var arr) || arr.Length == 0) return null; return arr[variant % arr.Length]; } private static Texture2D[] LoadVariants(string subdir, string name, Func placeholder) { var found = new List(); // Variant suffix files first: name_0.png, name_1.png, ... for (int i = 0; ; i++) { var tex = ContentLoader.LoadGfx($"tactical/{subdir}/{name}_{i}.png"); if (tex is null) break; found.Add(tex); } // Fallback to a single name.png if no _N variants exist. if (found.Count == 0) { var tex = ContentLoader.LoadGfx($"tactical/{subdir}/{name}.png"); if (tex is not null) found.Add(tex); } if (found.Count == 0) found.Add(placeholder()); return found.ToArray(); } private static Texture2D SurfacePlaceholder(TacticalSurface s) => SolidTexture(SurfaceColor(s), Theriapolis.Core.C.TACTICAL_TILE_SPRITE_PX); private static Texture2D DecoPlaceholder(TacticalDeco d) { var (color, fillFraction) = DecoStyle(d); int px = Theriapolis.Core.C.TACTICAL_TILE_SPRITE_PX; var image = Image.CreateEmpty(px, px, false, Image.Format.Rgba8); float cx = (px - 1) * 0.5f; float r = px * 0.5f * fillFraction; for (int y = 0; y < px; y++) { for (int x = 0; x < px; x++) { float dx = x - cx, dy = y - cx; image.SetPixel(x, y, (dx * dx + dy * dy) <= r * r ? color : new Color(0, 0, 0, 0)); } } return ImageTexture.CreateFromImage(image); } private static Texture2D SolidTexture(Color c, int size) { var image = Image.CreateEmpty(size, size, false, Image.Format.Rgba8); image.Fill(c); return ImageTexture.CreateFromImage(image); } // Mirrors Theriapolis.Game/Rendering/TacticalAtlas.SurfaceColor for placeholder parity. private static Color SurfaceColor(TacticalSurface s) => s switch { TacticalSurface.DeepWater => ColorByte(20, 60, 130), TacticalSurface.ShallowWater => ColorByte(60, 120, 180), TacticalSurface.Marsh => ColorByte(70, 100, 80), TacticalSurface.Mud => ColorByte(100, 80, 60), TacticalSurface.Sand => ColorByte(220, 200, 150), TacticalSurface.Snow => ColorByte(230, 235, 240), TacticalSurface.Rock => ColorByte(120, 115, 110), TacticalSurface.Cobble => ColorByte(170, 150, 120), TacticalSurface.Gravel => ColorByte(150, 140, 110), TacticalSurface.Wall => ColorByte(60, 55, 50), TacticalSurface.Floor => ColorByte(180, 160, 130), TacticalSurface.Dirt => ColorByte(120, 95, 60), TacticalSurface.TroddenDirt => ColorByte(140, 110, 70), TacticalSurface.TallGrass => ColorByte(80, 140, 60), TacticalSurface.Grass => ColorByte(110, 160, 70), TacticalSurface.DungeonFloor => ColorByte(110, 90, 70), TacticalSurface.DungeonRubble=> ColorByte(80, 70, 60), TacticalSurface.DungeonTile => ColorByte(140, 130, 110), TacticalSurface.Cave => ColorByte(70, 60, 55), TacticalSurface.MineFloor => ColorByte(95, 85, 70), _ => ColorByte(255, 0, 255), }; private static (Color color, float fillFraction) DecoStyle(TacticalDeco d) => d switch { TacticalDeco.Tree => (ColorByte(20, 80, 30), 0.85f), TacticalDeco.Bush => (ColorByte(70, 110, 50), 0.55f), TacticalDeco.Boulder => (ColorByte(110, 100, 90), 0.65f), TacticalDeco.Rock => (ColorByte(140, 130, 110), 0.35f), TacticalDeco.Flower => (ColorByte(220, 180, 210), 0.25f), TacticalDeco.Crop => (ColorByte(180, 160, 60), 0.40f), TacticalDeco.Reed => (ColorByte(120, 140, 60), 0.40f), TacticalDeco.Snag => (ColorByte(80, 60, 40), 0.45f), _ => (ColorByte(255, 0, 255), 0.5f), }; private static Color ColorByte(byte r, byte g, byte b) => new(r / 255f, g / 255f, b / 255f); }