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>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user