NHSE/NHSE.Sprites/Field/ItemLayerSprite.cs
2026-01-25 02:44:50 -06:00

404 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Drawing;
using NHSE.Core;
namespace NHSE.Sprites;
/// <summary>
/// Logic for rendering an <see cref="LayerItem"/>.
/// </summary>
public static class ItemLayerSprite
{
private static readonly Pen Reticle = new(Color.Red);
/// <summary>
/// Populates a bitmap data buffer with color values derived from a collection of items, arranging the colors in column-major order based on the specified width and height.
/// </summary>
/// <remarks>
/// Each item's color is determined using <see cref="FieldItemColor.GetItemColor"/> and stored in ARGB format.
/// Colors are written to bmpData in column-major order, where each column is filled from top to bottom.
/// </remarks>
/// <param name="items">List of items from which color values are extracted. The span must contain at least width × height elements.</param>
/// <param name="bmpData">Pixel data for the bitmap. The span must have a length of at least width × height.</param>
/// <param name="cfg">Configuration for layer positioning.</param>
/// <param name="imgWidth">The number of columns in the bitmap. Must be greater than zero.</param>
/// <param name="imgHeight">The number of rows in the bitmap. Must be greater than zero.</param>
private static void LoadBitmapLayer(ReadOnlySpan<Item> items, Span<int> bmpData, in LayerPositionConfig cfg, int imgWidth, int imgHeight)
{
var (shiftX, shiftY) = cfg.GetCoordinatesAbsolute(0, 0);
// Iterate through the relative positions within the layer.
// Then, map to absolute positions in the bitmap with the configured shift.
for (int x = 0; x < imgWidth; x++)
{
var ix = x * imgHeight;
for (int y = 0; y < imgHeight; y++)
{
// Get the tile at this position.
var index = ix + y;
var tile = items[index];
// Get the actual shifted position in the bitmap.
var offset = ((y + shiftY) * imgWidth) + (x + shiftX);
// Write the color to the bitmap data.
bmpData[offset] = FieldItemColor.GetItemColor(tile).ToArgb();
}
}
}
/// <summary>
/// Loads an item layer into a bitmap, scaling it up to the desired size, drawing special symbols, and overlaying a grid.
/// </summary>
/// <param name="imgScaled">Inflated acre bitmap to write to.</param>
/// <param name="layer">Item layer to draw from.</param>
/// <param name="cfg">Configuration for layer positioning.</param>
/// <param name="absX">Top-left X coordinate to start drawing from, relative to the origin of the map.</param>
/// <param name="absY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgSingle">Pixel data for 1px per tile image.</param>
/// <param name="imgUpscaled">>Pixel data for final inflated image.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
/// <param name="transparency">Optional transparency override color.</param>
/// <param name="gridlineColor">Color to use for gridlines.</param>
public static void LoadViewport(Bitmap imgScaled, LayerItem layer, in LayerPositionConfig cfg, int absX, int absY,
Span<int> imgSingle, Span<int> imgUpscaled, int imgScale, int transparency = -1, int gridlineColor = 0)
{
// Update the 1px view-grid image pixel data.
LoadViewport(layer, cfg, imgSingle, absX, absY);
// Get the final inflated size of the image.
var imgWidth = imgScaled.Width;
var imgHeight = imgScaled.Height;
// Inflate to the final size storage.
ImageUtil.ScalePixelImage(imgSingle, imgUpscaled, imgWidth, imgHeight, imgScale);
// Optional transparency clamping to make image fainter.
if (transparency >>> 24 != 0xFF)
ImageUtil.ClampAllTransparencyTo(imgUpscaled, transparency);
// Draw symbols over special items now?
DrawDirectionals(layer, cfg, imgUpscaled, absX, absY, imgWidth, imgScale);
// Apply gridlines to visually separate each cell.
DrawGrid(imgUpscaled, imgWidth, imgHeight, gridlineColor, imgScale);
// Update the bitmap, final data.
imgScaled.SetBitmapData(imgUpscaled);
}
/// <summary>
/// Loads pixel data from the specified layer into the provided span, using the given starting coordinates and the layer's view dimensions.
/// </summary>
/// <param name="layer">The layer from which to load pixel data.</param>
/// <param name="cfg">Configuration for layer positioning.</param>
/// <param name="data">Pixel data of the final image.</param>
/// <param name="absX">The x-coordinate of the upper-left corner in the map from which to start loading pixels.</param>
/// <param name="absY">The y-coordinate of the upper-left corner in the map from which to start loading pixels.</param>
private static void LoadViewport(LayerItem layer, in LayerPositionConfig cfg, Span<int> data, int absX, int absY)
{
var width = layer.TileInfo.ViewWidth;
var height = layer.TileInfo.ViewHeight;
var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY);
for (int y = 0; y < height; y++)
{
var baseIndex = (y * width);
for (int x = 0; x < width; x++)
{
var tileX = relX + x;
var tileY = relY + y;
if (!cfg.IsCoordinateValidRelative(tileX, tileY))
continue;
var tile = layer.GetTile(tileX, tileY);
var color = FieldItemColor.GetItemColor(tile).ToArgb();
var index = baseIndex + x;
data[index] = color;
}
}
}
/// <summary>
/// Draws extension tile info for the tiles in the specified layer onto the provided pixel data.
/// </summary>
/// <param name="layer">The layer containing the tiles to process.</param>
/// <param name="cfg">Configuration for layer positioning.</param>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="absX">Top-left X coordinate to start drawing from, relative to the origin of the map.</param>
/// <param name="absY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgWidth">Width of the entire image.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
private static void DrawDirectionals(LayerItem layer, in LayerPositionConfig cfg,
Span<int> data,
int absX, int absY, int imgWidth, int imgScale)
{
var width = layer.TileInfo.ViewWidth;
var height = layer.TileInfo.ViewHeight;
var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY);
for (int viewX = 0; viewX < width; viewX++)
{
for (int viewY = 0; viewY < height; viewY++)
{
var pX = relX + viewX;
var pY = relY + viewY;
if (!cfg.IsCoordinateValidRelative(pX, pY))
continue;
var tile = layer.GetTile(pX, pY);
if (tile.IsNone)
continue;
// Apply cosmetic details based on the tile's details.
if (tile.IsBuried)
DrawX(data, viewX * imgScale, viewY * imgScale, imgScale, imgWidth);
else if (tile.IsDropped)
DrawPlus(data, viewX * imgScale, viewY * imgScale, imgScale, imgWidth);
else if (tile.IsExtension)
DrawDirectional(data, tile, viewX * imgScale, viewY * imgScale, imgScale, imgWidth);
// Based on the display item, apply details.
var id = tile.DisplayItemId;
var kind = ItemInfo.GetItemKind(id);
if (kind.IsFlowerGene(id))
{
var geneIndex = GetGeneIndex(ref tile, layer, pX, pY);
var genes = ((uint)tile.Genes ^ 0b00_11_00_00); // invert W bits
var geneValue = (genes >> (geneIndex * 2)) & 3;
if (geneValue == 0)
continue;
DrawGene(data, viewX * imgScale, viewY * imgScale, imgScale, imgWidth, geneValue, geneIndex);
}
}
}
}
/// <summary>
/// Gets the index of the gene based on the specified extension value. Repoints the tile to root node if it is an extension tile.
/// </summary>
private static int GetGeneIndex(ref Item tile, LayerItem layer, int relX, int relY)
{
if (tile.IsRoot)
return 0;
// Sanity check: can only extend by 1 in either direction, and must extend in same direction as relative position.
// Ignore bad extension values; we know the gene index from position alone.
var eX = relX & 1;
var eY = relY & 1;
var geneIndex = (eY << 1) | eX;
tile = layer.GetTile(relX - eX, relY - eY);
return geneIndex;
}
/// <summary>
/// Draws a flower gene on the item cell.
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="viewX">Top-left X coordinate to start drawing from.</param>
/// <param name="viewY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scale of the entire cell.</param>
/// <param name="imgWidth">Width of the entire image.</param>
/// <param name="geneValue">Value of the gene (0-3).</param>
/// <param name="geneIndex">0-3 value indicating which gene (quadrant) to draw.</param>
private static void DrawGene(Span<int> data, int viewX, int viewY, int imgScale, int imgWidth, uint geneValue, int geneIndex)
{
var c = ShiftToGeneCoordinate(ref viewX, ref viewY, imgScale, geneIndex);
FillSquare(data, viewX, viewY, imgScale / 2, imgWidth, c, geneValue == 3 ? 1 : 2);
}
/// <summary>
/// Fills a square region within a one-dimensional span with the specified color value, using a given increment to control the fill step.
/// </summary>
/// <remarks>This method assumes that the specified region fits within the bounds of the provided span.
/// No bounds checking is performed.
/// The increment parameter can be used to fill a subset with the square's elements, which may be useful for performance or pattern effects.
/// </remarks>
/// <param name="data">The span representing the target buffer to fill. Each element corresponds to a pixel or cell in a row-major order grid.</param>
/// <param name="viewX">Top-left X coordinate to start drawing from.</param>
/// <param name="viewY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scale of the entire cell.</param>
/// <param name="imgWidth">Width of the entire image.</param>
/// <param name="color">The color value to assign to each filled element in the square region.</param>
/// <param name="increment">The step size to use when filling elements. Must be positive. A larger increment skips more elements within the square.</param>
private static void FillSquare(Span<int> data, int viewX, int viewY, int imgScale, int imgWidth, int color, int increment)
{
var baseIndex = (viewY * imgWidth) + viewX;
for (int i = 0; i < imgScale * imgScale; i += increment)
{
var x = i % imgScale;
var y = i / imgScale;
var index = (y * imgWidth) + x;
data[baseIndex + index] = color;
}
}
/// <summary>
/// Adjusts the specified coordinates to the center of a gene region based on the given gene index and scale, and returns the corresponding ARGB color value for that region.
/// </summary>
/// <remarks>
/// The method modifies the input coordinates in place according to the specified gene region.
/// The returned color value corresponds to the region: Red for bottom right, Yellow for bottom left, AntiqueWhite for top right, and Black for top left or any other index.
/// </remarks>
/// <param name="absX">Top-left X coordinate to start drawing from.</param>
/// <param name="absY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scale of the entire cell.</param>
/// <param name="geneIndex">The index of the gene region to shift to. Valid values are 0 (bottom right), 1 (bottom left), 2 (top right), and any other value for top left.</param>
/// <returns>An integer representing the ARGB color value associated with the specified gene region.</returns>
private static int ShiftToGeneCoordinate(ref int absX, ref int absY, int imgScale, int geneIndex)
{
switch (geneIndex)
{
case 0: // bottom right
absX += imgScale / 2;
absY += imgScale / 2;
return Color.Red.ToArgb();
case 1: // bottom left
absY += imgScale / 2;
return Color.Yellow.ToArgb();
case 2: // top right
absX += imgScale / 2;
return Color.AntiqueWhite.ToArgb();
default: // top left
return Color.Black.ToArgb();
}
}
/// <summary>
/// Updates the pixel data to draw a `+`, indicating the item as "dropped".
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="viewX">Top-left X coordinate to start drawing from.</param>
/// <param name="viewY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
/// <param name="imgWidth">Width of the entire image.</param>
private static void DrawPlus(Span<int> data, int viewX, int viewY, int imgScale, int imgWidth)
{
var x0y0 = (imgWidth * viewY) + viewX;
var s2 = imgScale / 2;
var ws2 = imgWidth * s2;
var v0 = x0y0 + s2;
var h0 = x0y0 + ws2;
for (int x = imgScale / 4; x <= 3 * (imgScale / 4); x++)
{
var vert = v0 + (imgWidth * x);
data[vert] ^= 0x808080;
var hori = h0 + x;
data[hori] ^= 0x808080;
}
}
/// <summary>
/// Updates the pixel data to draw an `X`, indicating the item as "buried".
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="viewX">Top-left X coordinate to start drawing from.</param>
/// <param name="viewY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
/// <param name="imgWidth">Width of the entire image.</param>
private static void DrawX(Span<int> data, int viewX, int viewY, int imgScale, int imgWidth)
{
var opposite = imgScale - 1;
var wo = imgWidth * opposite;
// Starting offsets for each of the slashes
var bBackward = (imgWidth * viewY) + viewX; // Backwards \
var bForward = bBackward + wo; // Forwards /
for (int x = 0; x < imgScale; x++)
{
var wx = imgWidth * x;
var backward = bBackward + x + wx;
data[backward] ^= 0x808080;
var forward = bForward + x - wx;
data[forward] ^= 0x808080;
}
}
/// <summary>
/// Updates the pixel data to draw a directional, indicating the item as an extension of a root item node.
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="tile">Extension tile data.</param>
/// <param name="viewX">Top-left X coordinate to start drawing from.</param>
/// <param name="viewY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
/// <param name="imgWidth">Width of the entire image.</param>
private static void DrawDirectional(Span<int> data, Item tile, int viewX, int viewY, int imgScale, int imgWidth)
{
var eX = tile.ExtensionX;
var eY = tile.ExtensionY;
if (eX == 0 && eY == 0)
return;
var sum = eX + eY;
var start = imgScale / (sum + 1);
var startX = eX >= eY ? 0 : start;
var startY = eX <= eY ? 0 : start;
var baseIndex = (imgWidth * viewY) + viewX;
for (int x = startX, y = startY; x < imgScale && y < imgScale; x += eX, y += eY)
{
var index = baseIndex + (imgWidth * y) + x;
if (index >= data.Length) // Since we can't guarantee valid extension values, just skip bad ones.
continue;
data[index] ^= 0x00_808080;
}
}
/// <summary>
/// Draws gridlines on the provided pixel data at the specified scale.
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="imgWidth">Width of the entire image.</param>
/// <param name="imgHeight">Height of the entire image.</param>
/// <param name="gridlineColor">Color to use for gridlines.</param>
/// <param name="gridlineInterval">Pixel interval to draw gridlines.</param>
public static void DrawGrid(Span<int> data, int imgWidth, int imgHeight, int gridlineColor, int gridlineInterval)
{
// Horizontal Lines
for (int y = gridlineInterval; y < imgHeight; y += gridlineInterval)
{
var baseIndex = y * imgWidth;
for (int x = 0; x < imgWidth; x++)
{
var index = baseIndex + x;
data[index] = gridlineColor;
}
}
// Vertical Lines
for (int y = 0; y < imgHeight; y++)
{
var baseIndex = y * imgWidth;
for (int x = gridlineInterval; x < imgWidth; x += gridlineInterval)
{
var index = baseIndex + x;
data[index] = gridlineColor;
}
}
}
/// <summary>
/// Loads an item layer into a viewport bitmap, drawing a view reticle over it.
/// </summary>
/// <param name="cfg">Configuration for layer positioning.</param>
/// <param name="layer">Item layer to draw from.</param>
/// <param name="data">Pixel data of the final image.</param>
/// <param name="transparency">Optional transparency override color.</param>
public static void LoadItemLayerDrawReticle(LayerPositionConfig cfg, LayerItem layer, Span<int> data, int transparency = -1)
{
LoadBitmapLayer(layer.Tiles, data, cfg, layer.TileInfo.TotalWidth, layer.TileInfo.TotalHeight);
if (transparency >>> 24 != 0xFF)
ImageUtil.ClampAllTransparencyTo(data, transparency);
}
public static void DrawViewReticle(Bitmap map, TileGridViewport g, int absX, int absY, int scale = 1)
{
using var gfx = Graphics.FromImage(map);
var reticleWidth = g.ViewWidth * scale;
var reticleHeight = g.ViewHeight * scale;
gfx.DrawRectangle(Reticle, absX * scale, absY * scale, reticleWidth, reticleHeight);
}
}