Files

136 lines
4.4 KiB
C#
Raw Permalink Normal View History

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;
/// <summary>
/// Phase 6 M4 — renders <see cref="NpcActor"/>s on the tactical map (and
/// as muted dots on the world map at low zoom). Mirrors
/// <see cref="PlayerSprite"/>'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.
/// </summary>
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;
}
/// <summary>Draw every live NPC. Caller wraps SpriteBatch.Begin/End around it.</summary>
public void Draw(SpriteBatch sb, Camera2D camera, IEnumerable<NpcActor> 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();
}
}