using System.Diagnostics.CodeAnalysis; namespace NHSE.Core; /// /// Configures how a layer rests within the Map's grid, relative to a "chunk" or "acre". /// /// Number of acres in the width direction. /// Number of acres in the height direction. /// Horizontal acre shift from the map's origin. /// Vertical acre shift from the map's origin. /// Number of tiles per acre in one dimension (16 or 32). /// Bit shift value to convert between tiles and acres (4 for 16 tiles, 5 for 32 tiles). /// Size of tile compared to the smallest tile possible (2 for 16 tiles, 1 for 32 tiles). public readonly record struct LayerPositionConfig( byte CountWidth, byte CountHeight, byte ShiftWidth, byte ShiftHeight, [ConstantExpected] byte TilesPerAcre, byte TileBitShift, [ConstantExpected(Max = 2, Min = 1)] byte MetaTileSize) { // Maps in Animal Crossing: New Horizons are made up of acres that are 9 tiles wide and 8 tiles high. // 5 columns in the center are land, surrounded by 2 tiles of beach and 2 tiles of sea on each side. // 4 rows in the center are land, surrounded by 2 rows of beach and 2 rows of sea on each side. // +-----------+ // | ~~~~~~~~~ | // | ~*******~ | // | ~*=====*~ | // | ~*=====*~ | // | ~*=====*~ | // | ~*=====*~ | // | ~*******~ | // | ~~~~~~~~~ | // +-----------+ // Main Island Map Config - True Dimensions private const byte MapAcreWidth = 9; // 2 sea, 2 beach, 5 land private const byte MapAcreHeight = 8; // 2 sea, 2 beach, 4 land // Optimize some calculations away by using bit-shift instead of mul/div, as we're always a multiple of 2. private const byte Grid32 = 32; private const byte Grid16 = 16; private const byte Shift32 = 5; // div32 is same as sh 5 private const byte Shift16 = 4; // div16 is same as sh 4 /// /// Creates a new instance, centering the layer within the acre. /// /// Width of the layer in acres. /// Height of the layer in acres. /// Number of tiles per acre (16 or 32). /// Size of tile compared to the smallest tile possible (2 for 16 tiles, 1 for 32 tiles). /// A new instance. public static LayerPositionConfig Create(byte width, byte height, [ConstantExpected(Min = Grid16, Max = Grid32)] byte tilesPerAcre, [ConstantExpected(Min = 1, Max = 2)] byte metaTileSize) { var shiftW = (byte)((MapAcreWidth - width) / 2); // centered var shiftH = (byte)((MapAcreHeight - height) / 2); // centered var bitShift = tilesPerAcre == Grid16 ? Shift16 : Shift32; #pragma warning disable CA1857 return new LayerPositionConfig(width, height, shiftW, shiftH, tilesPerAcre, bitShift, metaTileSize); #pragma warning restore CA1857 } public (int X, int Y) GetCoordinatesAbsolute(int relX, int relY) { var absX = relX + ((ShiftWidth * MetaTileSize) << TileBitShift); var absY = relY + ((ShiftHeight * MetaTileSize) << TileBitShift); return (absX, absY); } public (int X, int Y) GetCoordinatesRelative(int absX, int absY) { var relX = absX - ((ShiftWidth * MetaTileSize) << TileBitShift); var relY = absY - ((ShiftHeight * MetaTileSize) << TileBitShift); return (relX, relY); } /// /// Determines whether the specified absolute X and Y coordinates are within the valid bounds of the map. /// /// The absolute X coordinate to validate. Must be within the horizontal bounds of the map. /// The absolute Y coordinate to validate. Must be within the vertical bounds of the map. /// if the coordinates are valid; otherwise, . public bool IsCoordinateValidRelative(int relX, int relY) { if ((uint)relX >= CountWidth << TileBitShift) return false; if ((uint)relY >= CountHeight << TileBitShift) return false; return true; } public bool IsCoordinateValidAbsolute(int absX, int absY) { var (relX, relY) = GetCoordinatesRelative(absX, absY); return IsCoordinateValidRelative(relX, relY); } }