90 lines
3.4 KiB
C#
90 lines
3.4 KiB
C#
|
|
using Microsoft.Xna.Framework;
|
||
|
|
using Microsoft.Xna.Framework.Graphics;
|
||
|
|
using Theriapolis.Core;
|
||
|
|
using Theriapolis.Core.Entities;
|
||
|
|
|
||
|
|
namespace Theriapolis.Game.Rendering;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 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.
|
||
|
|
/// </summary>
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
}
|