136 lines
4.4 KiB
C#
136 lines
4.4 KiB
C#
|
|
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();
|
||
|
|
}
|
||
|
|
}
|