using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Core; using Theriapolis.Core.Entities; namespace Theriapolis.Game.Rendering; /// /// Renders the player actor in either view. Both world-map and tactical use /// the same camera (world-pixel space), but the sprite is counter-scaled by /// 1/camera.Zoom so it stays a constant on-screen size at every zoom level. /// Otherwise the marker would become tiny when zoomed out and screen-filling /// at CAMERA_MAX_ZOOM. /// public sealed class PlayerSprite : IDisposable { private readonly GraphicsDevice _gd; private Texture2D _arrow = null!; private Texture2D _ring = null!; private bool _disposed; // Texture size = target on-screen size. Convenient because the // counter-scale formula reduces to (1 / camera.Zoom) at draw time. private const int MarkerPx = C.PLAYER_MARKER_SCREEN_PX; public PlayerSprite(GraphicsDevice gd) { _gd = gd; BuildTextures(); } private void BuildTextures() { // Filled arrow — orientation comes from sprite rotation at draw time. int s = MarkerPx; var arrow = new Color[s * s]; var ring = 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); // Filled disc (body) plus a small notch on the +X side to indicate facing. bool body = dist <= r * 0.85f; bool notch = nx > 0 && MathF.Abs(ny) < (r - nx) * 0.6f; arrow[y * s + x] = body ? (notch ? new Color(255, 230, 180) : new Color(220, 80, 60)) : Color.Transparent; // Outer ring drawn beneath the body so it remains visible against // similarly-coloured terrain at low zoom (settlement icons sit // close to the marker on the world map). ring[y * s + x] = (dist > r * 0.85f && dist <= r) ? new Color(0, 0, 0, 200) : Color.Transparent; } _arrow = new Texture2D(_gd, s, s); _arrow.SetData(arrow); _ring = new Texture2D(_gd, s, s); _ring.SetData(ring); } public void Draw(SpriteBatch sb, Camera2D camera, Actor a) { 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); var pos = new Vector2(a.Position.X, a.Position.Y); // Counter-scale by 1/Zoom so the camera transform's Zoom multiplier // cancels out, leaving a constant MarkerPx-pixel on-screen size. // Guard against zero just in case (shouldn't happen — clamped by C.CAMERA_MIN_ZOOM). float screenScale = camera.Zoom > 1e-6f ? 1f / camera.Zoom : 1f; sb.Draw(_ring, pos, null, Color.White, 0f, origin, screenScale, SpriteEffects.None, 0f); sb.Draw(_arrow, pos, null, Color.White, a.FacingAngleRad, origin, screenScale, SpriteEffects.None, 0f); sb.End(); } public void Dispose() { if (_disposed) return; _disposed = true; _arrow?.Dispose(); _ring?.Dispose(); } }