using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Core; using Theriapolis.Core.Entities; using Theriapolis.Core.Rules.Character; namespace Theriapolis.Game.Rendering; /// /// Phase 6 M4 — renders s on the tactical map (and /// as muted dots on the world map at low zoom). Mirrors /// 's counter-scale-by-1/Zoom approach so NPCs /// stay constant-on-screen-size at any zoom level. /// /// Body colour encodes allegiance: /// Hostile → red /// Neutral → grey /// Friendly → green /// Allied → cyan /// Player allegiance never shows here (only the player sprite renders). /// /// A coloured outer ring is drawn beneath each body so NPCs remain /// visible against similar-toned terrain (settlement cobble, rock, /// snow). Generic placeholder art — Phase 9 polish swaps for real /// per-species sprites. /// public sealed class NpcSprite : IDisposable { private readonly GraphicsDevice _gd; private Texture2D _hostile = null!; private Texture2D _neutral = null!; private Texture2D _friendly = null!; private Texture2D _allied = null!; private Texture2D _ring = null!; private bool _disposed; private const int MarkerPx = C.PLAYER_MARKER_SCREEN_PX; public NpcSprite(GraphicsDevice gd) { _gd = gd; BuildTextures(); } private void BuildTextures() { _hostile = BuildBody(new Color(220, 60, 50)); _neutral = BuildBody(new Color(180, 180, 180)); _friendly = BuildBody(new Color( 90, 180, 90)); _allied = BuildBody(new Color( 90, 200, 220)); _ring = BuildRing(); } private Texture2D BuildBody(Color body) { int s = MarkerPx; var pixels = new Color[s * s]; float cx = (s - 1) * 0.5f, cy = (s - 1) * 0.5f, r = s * 0.5f - 1f; // Slightly smaller body than player (0.78 vs 0.85) so player reads as // the protagonist when next to NPCs. for (int y = 0; y < s; y++) for (int x = 0; x < s; x++) { float nx = x - cx, ny = y - cy; float dist = MathF.Sqrt(nx * nx + ny * ny); pixels[y * s + x] = dist <= r * 0.78f ? body : Color.Transparent; } var tex = new Texture2D(_gd, s, s); tex.SetData(pixels); return tex; } private Texture2D BuildRing() { int s = MarkerPx; var pixels = new Color[s * s]; float cx = (s - 1) * 0.5f, cy = (s - 1) * 0.5f, r = s * 0.5f - 1f; for (int y = 0; y < s; y++) for (int x = 0; x < s; x++) { float nx = x - cx, ny = y - cy; float dist = MathF.Sqrt(nx * nx + ny * ny); pixels[y * s + x] = (dist > r * 0.78f && dist <= r) ? new Color(0, 0, 0, 200) : Color.Transparent; } var tex = new Texture2D(_gd, s, s); tex.SetData(pixels); return tex; } /// Draw every live NPC. Caller wraps SpriteBatch.Begin/End around it. public void Draw(SpriteBatch sb, Camera2D camera, IEnumerable npcs) { sb.Begin( transformMatrix: camera.TransformMatrix, samplerState: SamplerState.PointClamp, sortMode: SpriteSortMode.Deferred, blendState: BlendState.AlphaBlend); int s = MarkerPx; var origin = new Vector2(s * 0.5f, s * 0.5f); float screenScale = camera.Zoom > 1e-6f ? 1f / camera.Zoom : 1f; foreach (var npc in npcs) { if (!npc.IsAlive) continue; var pos = new Vector2(npc.Position.X, npc.Position.Y); var body = TextureFor(npc.Allegiance); sb.Draw(_ring, pos, null, Color.White, 0f, origin, screenScale, SpriteEffects.None, 0f); sb.Draw(body, pos, null, Color.White, 0f, origin, screenScale, SpriteEffects.None, 0f); } sb.End(); } private Texture2D TextureFor(Allegiance a) => a switch { Allegiance.Hostile => _hostile, Allegiance.Allied => _allied, Allegiance.Friendly => _friendly, Allegiance.Neutral => _neutral, _ => _neutral, }; public void Dispose() { if (_disposed) return; _disposed = true; _hostile?.Dispose(); _neutral?.Dispose(); _friendly?.Dispose(); _allied?.Dispose(); _ring?.Dispose(); } }