Files
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

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();
}
}