using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Theriapolis.Core;
using Theriapolis.Core.Tactical;
namespace Theriapolis.Game.Rendering;
///
/// Renders the streamed tactical view: per-tile surface + decoration sprites,
/// with a soft edge-blend pass between dissimilar adjacent surfaces.
///
/// Each tactical tile occupies 1×1 world pixel (the canonical coord system).
/// Sprites authored at ² are drawn at
/// scale = 1 / TACTICAL_TILE_SPRITE_PX so the source texture fits inside that
/// 1×1 cell.
///
/// Render order, per visible chunk:
/// 1. Base surface tile (its own texture).
/// 2. Edge blend overlays — for each cardinal neighbour with a different
/// surface, draw a per-edge alpha-gradient mask tinted with the
/// neighbour's average color. This softens the otherwise hard seam
/// between dissimilar surfaces (the "wallpaper grid" look).
/// 3. Decoration sprite, if any.
///
/// This is "Option B" autotiling per the Phase 4 plan — short-term smoothing
/// using flat color blends. The longer-term plan is "Option C": full Wang
/// corner-based autotiling driven by Pixellab's `create_topdown_tileset`,
/// where each cell picks one of 16 transition tiles based on its 4-corner
/// terrain sample. Tracked in `theriapolis-tactical-tile-art-request.md`.
///
/// Rivers/roads/rail are NOT redrawn here; the polyline burn-in already
/// embedded them in the chunk's surface tiles, and LineFeatureRenderer keeps
/// drawing the source polylines on top so the shared visual is unbroken.
///
public sealed class TacticalRenderer : IMapView, IDisposable
{
private readonly GraphicsDevice _gd;
private readonly ChunkStreamer _streamer;
private readonly TacticalAtlas _atlas;
private bool _disposed;
// 1/SpritePx — multiplied by sprite source size (32) to land at 1×1 world pixel.
private static readonly Vector2 SpriteScale =
new(1f / C.TACTICAL_TILE_SPRITE_PX, 1f / C.TACTICAL_TILE_SPRITE_PX);
// Toggle for the Option B edge-blend pass. Currently OFF — the first
// tuning produced washed-out tiles when many neighbouring surfaces had
// saturated placeholder colours (snow≈white, sand=cream). Two issues to
// fix before re-enabling:
// 1. 4 overlapping masks compound into ~4× alpha at tile corners with
// 4 different neighbours — needs cap or non-overlapping geometry.
// 2. Tint alpha (0.55) and 16-px falloff are both too aggressive when
// the neighbour colour is nothing like our own.
// Defer re-tuning until the per-tile art set is filled in; the placeholder
// colour palette isn't a fair test bed.
// static readonly (not const) so the guard evaluates at runtime — avoids
// CS0162 unreachable-code warnings on the gated branch.
private static readonly bool EdgeBlendEnabled = false;
// Edge masks — 32×32 textures with white RGB and an alpha gradient that
// fades from the named edge inward. Drawn over a tile (tinted with the
// neighbour's avg color) to bleed neighbour colour over our own.
private Texture2D _edgeN = null!, _edgeE = null!, _edgeS = null!, _edgeW = null!;
public TacticalRenderer(GraphicsDevice gd, ChunkStreamer streamer, TacticalAtlas atlas)
{
_gd = gd;
_streamer = streamer;
_atlas = atlas;
BuildEdgeMasks();
}
private void BuildEdgeMasks()
{
_edgeN = MakeMask((x, y, sz) => 1f - (float)y / (sz / 2));
_edgeS = MakeMask((x, y, sz) => 1f - (float)(sz - 1 - y) / (sz / 2));
_edgeW = MakeMask((x, y, sz) => 1f - (float)x / (sz / 2));
_edgeE = MakeMask((x, y, sz) => 1f - (float)(sz - 1 - x) / (sz / 2));
}
private Texture2D MakeMask(Func alphaAt)
{
int sz = C.TACTICAL_TILE_SPRITE_PX;
var pixels = new Color[sz * sz];
for (int y = 0; y < sz; y++)
for (int x = 0; x < sz; x++)
{
float a = MathHelper.Clamp(alphaAt(x, y, sz), 0f, 1f);
// Quadratic falloff feels softer than linear at the seam itself.
a = a * a;
pixels[y * sz + x] = new Color((byte)255, (byte)255, (byte)255, (byte)(a * 255));
}
var tex = new Texture2D(_gd, sz, sz);
tex.SetData(pixels);
return tex;
}
public void Draw(SpriteBatch sb, Camera2D camera, GameTime gameTime)
{
// Visible AABB in tactical-tile (world-pixel) coords.
var tl = camera.ScreenToWorld(Vector2.Zero);
var br = camera.ScreenToWorld(new Vector2(camera.ScreenWidth, camera.ScreenHeight));
int x0 = (int)MathF.Floor(tl.X) - 1;
int y0 = (int)MathF.Floor(tl.Y) - 1;
int x1 = (int)MathF.Ceiling(br.X) + 1;
int y1 = (int)MathF.Ceiling(br.Y) + 1;
// Clamp to the world.
int worldPxW = C.WORLD_WIDTH_TILES * C.TACTICAL_PER_WORLD_TILE;
int worldPxH = C.WORLD_HEIGHT_TILES * C.TACTICAL_PER_WORLD_TILE;
x0 = Math.Clamp(x0, 0, worldPxW);
y0 = Math.Clamp(y0, 0, worldPxH);
x1 = Math.Clamp(x1, 0, worldPxW);
y1 = Math.Clamp(y1, 0, worldPxH);
if (x0 >= x1 || y0 >= y1) return;
sb.Begin(
transformMatrix: camera.TransformMatrix,
samplerState: SamplerState.PointClamp,
sortMode: SpriteSortMode.Deferred,
blendState: BlendState.AlphaBlend);
// Iterate chunk-by-chunk so we touch each cached chunk array directly.
var ccTl = ChunkCoord.ForTactical(x0, y0);
var ccBr = ChunkCoord.ForTactical(x1 - 1, y1 - 1);
// Pass 1: base surfaces.
for (int cy = ccTl.Y; cy <= ccBr.Y; cy++)
for (int cx = ccTl.X; cx <= ccBr.X; cx++)
{
var chunk = _streamer.Get(new ChunkCoord(cx, cy));
DrawChunkSurfaces(sb, chunk, x0, y0, x1, y1);
}
// Pass 2: edge blends — gated on EdgeBlendEnabled (currently false).
if (EdgeBlendEnabled)
{
for (int cy = ccTl.Y; cy <= ccBr.Y; cy++)
for (int cx = ccTl.X; cx <= ccBr.X; cx++)
{
var chunk = _streamer.Get(new ChunkCoord(cx, cy));
DrawChunkEdgeBlends(sb, chunk, x0, y0, x1, y1);
}
}
// Pass 3: decorations.
for (int cy = ccTl.Y; cy <= ccBr.Y; cy++)
for (int cx = ccTl.X; cx <= ccBr.X; cx++)
{
var chunk = _streamer.Get(new ChunkCoord(cx, cy));
DrawChunkDecos(sb, chunk, x0, y0, x1, y1);
}
sb.End();
}
private void DrawChunkSurfaces(SpriteBatch sb, TacticalChunk chunk, int vx0, int vy0, int vx1, int vy1)
{
int ox = chunk.OriginX;
int oy = chunk.OriginY;
int sx = Math.Max(0, vx0 - ox);
int sy = Math.Max(0, vy0 - oy);
int ex = Math.Min(C.TACTICAL_CHUNK_SIZE, vx1 - ox);
int ey = Math.Min(C.TACTICAL_CHUNK_SIZE, vy1 - oy);
for (int ly = sy; ly < ey; ly++)
for (int lx = sx; lx < ex; lx++)
{
ref var t = ref chunk.Tiles[lx, ly];
var tex = _atlas.GetSurface(t.Surface, t.Variant);
sb.Draw(tex,
position: new Vector2(ox + lx, oy + ly),
sourceRectangle: null,
color: Color.White,
rotation: 0f,
origin: Vector2.Zero,
scale: SpriteScale,
effects: SpriteEffects.None,
layerDepth: 0f);
}
}
private void DrawChunkEdgeBlends(SpriteBatch sb, TacticalChunk chunk, int vx0, int vy0, int vx1, int vy1)
{
int ox = chunk.OriginX;
int oy = chunk.OriginY;
int sx = Math.Max(0, vx0 - ox);
int sy = Math.Max(0, vy0 - oy);
int ex = Math.Min(C.TACTICAL_CHUNK_SIZE, vx1 - ox);
int ey = Math.Min(C.TACTICAL_CHUNK_SIZE, vy1 - oy);
for (int ly = sy; ly < ey; ly++)
for (int lx = sx; lx < ex; lx++)
{
ref var t = ref chunk.Tiles[lx, ly];
int tx = ox + lx, ty = oy + ly;
// Sample 4 cardinal neighbours via the streamer (handles cross-chunk).
var nN = _streamer.SampleTile(tx, ty - 1).Surface;
var nS = _streamer.SampleTile(tx, ty + 1).Surface;
var nW = _streamer.SampleTile(tx - 1, ty ).Surface;
var nE = _streamer.SampleTile(tx + 1, ty ).Surface;
if (nN != t.Surface) BlendEdge(sb, tx, ty, _edgeN, _atlas.GetSurfaceAverageColor(nN));
if (nS != t.Surface) BlendEdge(sb, tx, ty, _edgeS, _atlas.GetSurfaceAverageColor(nS));
if (nW != t.Surface) BlendEdge(sb, tx, ty, _edgeW, _atlas.GetSurfaceAverageColor(nW));
if (nE != t.Surface) BlendEdge(sb, tx, ty, _edgeE, _atlas.GetSurfaceAverageColor(nE));
}
}
private void BlendEdge(SpriteBatch sb, int tx, int ty, Texture2D mask, Color tint)
{
if (tint.A == 0) return;
// Cap blend strength so the seam softens but we don't drown out our own
// surface. 0.55 is a comfortable mid-point — stronger than a hint, weaker
// than a 50/50 blend.
var c = new Color(tint.R, tint.G, tint.B, (byte)(0.55f * 255));
sb.Draw(mask,
position: new Vector2(tx, ty),
sourceRectangle: null,
color: c,
rotation: 0f,
origin: Vector2.Zero,
scale: SpriteScale,
effects: SpriteEffects.None,
layerDepth: 0f);
}
private void DrawChunkDecos(SpriteBatch sb, TacticalChunk chunk, int vx0, int vy0, int vx1, int vy1)
{
int ox = chunk.OriginX;
int oy = chunk.OriginY;
int sx = Math.Max(0, vx0 - ox);
int sy = Math.Max(0, vy0 - oy);
int ex = Math.Min(C.TACTICAL_CHUNK_SIZE, vx1 - ox);
int ey = Math.Min(C.TACTICAL_CHUNK_SIZE, vy1 - oy);
for (int ly = sy; ly < ey; ly++)
for (int lx = sx; lx < ex; lx++)
{
ref var t = ref chunk.Tiles[lx, ly];
var tex = _atlas.GetDeco(t.Deco, t.Variant);
if (tex is null) continue;
sb.Draw(tex,
position: new Vector2(ox + lx, oy + ly),
sourceRectangle: null,
color: Color.White,
rotation: 0f,
origin: Vector2.Zero,
scale: SpriteScale,
effects: SpriteEffects.None,
layerDepth: 0f);
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_edgeN?.Dispose();
_edgeE?.Dispose();
_edgeS?.Dispose();
_edgeW?.Dispose();
// _atlas is owned by PlayScreen; do not dispose here.
}
}