// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2022 // www.magicbane.com package engine.InterestManagement; import engine.Enum; import engine.gameManager.ConfigManager; import engine.gameManager.DbManager; import engine.gameManager.ZoneManager; import engine.math.Bounds; import engine.math.FastMath; import engine.math.Vector2f; import engine.math.Vector3fImmutable; import engine.objects.Zone; import org.pmw.tinylog.Logger; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; import static java.lang.Math.abs; public class HeightMap { // Class variables public static final HashMap heightmapByLoadNum = new HashMap<>(); // Heightmap data for all zones. public static float SCALEVALUE = 1.0f / 255; // Bootstrap Tracking public static int heightMapsCreated = 0; public static HeightMap PlayerCityHeightMap; // Heightmap data for this heightmap public final int heightMapID; public final int maxHeight; public final int fullExtentsX; public final int fullExtentsY; public final int zoneLoadID; public BufferedImage heightmapImage; public float bucketWidthX; public float bucketWidthY; public float seaLevel = 0; public int[][] pixelColorValues; public float zone_minBlend; public float zone_maxBlend; public HeightMap(ResultSet rs) throws SQLException { this.heightMapID = rs.getInt("heightMapID"); this.maxHeight = rs.getInt("maxHeight"); int halfExtentsX = rs.getInt("xRadius"); int halfExtentsY = rs.getInt("zRadius"); this.zoneLoadID = rs.getInt("zoneLoadID"); this.seaLevel = rs.getFloat("seaLevel"); this.zone_minBlend = rs.getFloat("outsetZ"); this.zone_maxBlend = rs.getFloat("outsetX"); // Cache the full extents to avoid the calculation this.fullExtentsX = halfExtentsX * 2; this.fullExtentsY = halfExtentsY * 2; this.heightmapImage = null; File imageFile = new File(ConfigManager.DEFAULT_DATA_DIR + "heightmaps/" + this.heightMapID + ".bmp"); // early exit if no image file was found. Will log in caller. if (!imageFile.exists()) return; // load the heightmap image. try { this.heightmapImage = ImageIO.read(imageFile); } catch (IOException e) { Logger.error("***Error loading heightmap data for heightmap " + this.heightMapID + e); } // Calculate the data we do not load from table float numOfBucketsX = this.heightmapImage.getWidth() - 1; float calculatedWidthX = this.fullExtentsX / numOfBucketsX; this.bucketWidthX = calculatedWidthX; float numOfBucketsY = this.heightmapImage.getHeight() - 1; float calculatedWidthY = this.fullExtentsY / numOfBucketsY; this.bucketWidthY = calculatedWidthY; // Generate pixel array from image data generatePixelData(this); HeightMap.heightmapByLoadNum.put(this.zoneLoadID, this); heightMapsCreated++; } //Created for PlayerCities public HeightMap() { this.heightMapID = 999999; this.maxHeight = 5; // for real... int halfExtentsX = (int) Enum.CityBoundsType.ZONE.halfExtents; int halfExtentsY = (int) Enum.CityBoundsType.ZONE.halfExtents; this.zoneLoadID = 0; this.seaLevel = 0; this.zone_minBlend = 0; this.zone_maxBlend = 0; // Cache the full extents to avoid the calculation this.fullExtentsX = halfExtentsX * 2; this.fullExtentsY = halfExtentsY * 2; this.heightmapImage = null; // Calculate the data we do not load from table this.bucketWidthX = halfExtentsX; this.bucketWidthY = halfExtentsY; this.pixelColorValues = new int[this.fullExtentsX][this.fullExtentsY]; for (int y = 0; y < this.fullExtentsY; y++) { for (int x = 0; x < this.fullExtentsX; x++) { pixelColorValues[x][y] = 255; } } } public HeightMap(Zone zone) { this.heightMapID = 999999; this.maxHeight = 0; int halfExtentsX = (int) zone.getBounds().getHalfExtents().x; int halfExtentsY = (int) zone.getBounds().getHalfExtents().y; this.zoneLoadID = 0; this.seaLevel = 0; // Cache the full extents to avoid the calculation this.fullExtentsX = halfExtentsX * 2; this.fullExtentsY = halfExtentsY * 2; this.heightmapImage = null; // Calculate the data we do not load from table this.bucketWidthX = halfExtentsX; this.bucketWidthY = halfExtentsY; this.pixelColorValues = new int[this.fullExtentsX][this.fullExtentsY]; for (int y = 0; y < this.fullExtentsY; y++) { for (int x = 0; x < this.fullExtentsX; x++) { pixelColorValues[x][y] = 0; } } } public static void GeneratePlayerCityHeightMap() { HeightMap.PlayerCityHeightMap = new HeightMap(); } public static void GenerateCustomHeightMap(Zone zone) { HeightMap heightMap = new HeightMap(zone); HeightMap.heightmapByLoadNum.put(zone.getLoadNum(), heightMap); } public static Zone getNextZoneWithTerrain(Zone zone) { Zone nextZone = zone; if (zone.getHeightMap() != null) return zone; if (zone.equals(ZoneManager.getSeaFloor())) return zone; while (nextZone.getHeightMap() == null) nextZone = nextZone.getParent(); return nextZone; } public static float getWorldHeight(Zone currentZone, Vector3fImmutable worldLoc) { Zone heightMapZone; Zone parentZone; float interpolatedParentTerrainHeight; // Seafloor is rather flat. if (currentZone == ZoneManager.getSeaFloor()) return currentZone.worldAltitude; // Retrieve the next zone with a heightmap attached. // Zones without a heightmap use the next zone up the // tree to calculate heights from. heightMapZone = getNextZoneWithTerrain(currentZone); // Transform world loc into zone space coordinate system Vector2f zoneLoc = ZoneManager.worldToZoneSpace(worldLoc, heightMapZone); // Interpolate height for this position using pixel array. float interpolatedTerrainHeight = heightMapZone.getHeightMap().getInterpolatedTerrainHeight(zoneLoc); interpolatedTerrainHeight += heightMapZone.worldAltitude; // Heightmap blending is based on distance to edge of zone. if (Bounds.collide(worldLoc, heightMapZone.maxBlend) == true) return interpolatedTerrainHeight; // We will need the parent height if we got this far into the method parentZone = HeightMap.getNextZoneWithTerrain(heightMapZone.getParent()); interpolatedParentTerrainHeight = HeightMap.getWorldHeight(parentZone, worldLoc); interpolatedParentTerrainHeight += parentZone.worldAltitude; Bounds blendBounds = Bounds.borrow(); zoneLoc.x = abs(zoneLoc.x); zoneLoc.y = abs(zoneLoc.x); blendBounds.setBounds(new Vector2f(heightMapZone.absX, heightMapZone.absZ), zoneLoc, 0.0f); float maxBlendArea = (heightMapZone.maxBlend.getHalfExtents().x) * (heightMapZone.maxBlend.getHalfExtents().y); float currentArea = (blendBounds.getHalfExtents().x) * (blendBounds.getHalfExtents().y); float zoneArea = (heightMapZone.getBounds().getHalfExtents().x) * (heightMapZone.getBounds().getHalfExtents().y); float blendDelta = zoneArea - maxBlendArea; float currentDelta = zoneArea - currentArea; float percentage; if (currentDelta != 0 && blendDelta != 0) percentage = currentDelta / blendDelta; else percentage = 0.0f; interpolatedTerrainHeight = FastMath.LERP(percentage, interpolatedTerrainHeight, interpolatedParentTerrainHeight); return interpolatedTerrainHeight; } public static float getWorldHeight(Vector3fImmutable worldLoc) { Zone currentZone = ZoneManager.findSmallestZone(worldLoc); if (currentZone == null) return 0; return getWorldHeight(currentZone, worldLoc); } public static void loadAlHeightMaps() { // Load the heightmaps into staging hashmap keyed by HashMapID DbManager.HeightMapQueries.LOAD_ALL_HEIGHTMAPS(); //generate static player city heightmap. HeightMap.GeneratePlayerCityHeightMap(); // Clear all heightmap image data as it's no longer needed. for (HeightMap heightMap : HeightMap.heightmapByLoadNum.values()) { heightMap.heightmapImage = null; } Logger.info(HeightMap.heightmapByLoadNum.size() + " Heightmaps cached."); } public static boolean isLocUnderwater(Vector3fImmutable currentLoc) { float localAltitude = HeightMap.getWorldHeight(currentLoc); Zone zone = ZoneManager.findSmallestZone(currentLoc); return localAltitude < zone.getSeaLevel(); } private static void generatePixelData(HeightMap heightMap) { Color color; // Generate altitude lookup table for this heightmap heightMap.pixelColorValues = new int[heightMap.heightmapImage.getWidth()][heightMap.heightmapImage.getHeight()]; for (int y = 0; y < heightMap.heightmapImage.getHeight(); y++) { for (int x = 0; x < heightMap.heightmapImage.getWidth(); x++) { color = new Color(heightMap.heightmapImage.getRGB(x, y)); heightMap.pixelColorValues[x][y] = color.getRed(); } } } public Vector2f getGridSquare(Vector2f zoneLoc) { // Clamp values. if (zoneLoc.x < 0) zoneLoc.setX(0); if (zoneLoc.x >= this.fullExtentsX) zoneLoc.setX(this.fullExtentsX); if (zoneLoc.y < 0) zoneLoc.setY(0); if (zoneLoc.y > this.fullExtentsY) zoneLoc.setY(this.fullExtentsY); // Flip Y coordinates zoneLoc.setY(this.fullExtentsY - zoneLoc.y); float xBucket = (zoneLoc.x / this.bucketWidthX); float yBucket = (zoneLoc.y / this.bucketWidthY); return new Vector2f(xBucket, yBucket); } public float getInterpolatedTerrainHeight(Vector2f zoneLoc) { Vector2f gridSquare; gridSquare = getGridSquare(zoneLoc); int gridX = (int) gridSquare.x; int gridY = (int) gridSquare.y; float offsetX = (gridSquare.x - gridX); float offsetY = gridSquare.y - gridY; //get height of the 4 vertices. float topLeftHeight = 0; float topRightHeight = 0; float bottomLeftHeight = 0; float bottomRightHeight = 0; int nextY = gridY + 1; int nextX = gridX + 1; topLeftHeight = pixelColorValues[gridX][gridY]; topRightHeight = pixelColorValues[nextX][gridY]; bottomLeftHeight = pixelColorValues[gridX][nextY]; bottomRightHeight = pixelColorValues[nextX][nextY]; float interpolatedHeight; interpolatedHeight = topRightHeight * (1 - offsetY) * (offsetX); interpolatedHeight += (bottomRightHeight * offsetY * offsetX); interpolatedHeight += (bottomLeftHeight * (1 - offsetX) * offsetY); interpolatedHeight += (topLeftHeight * (1 - offsetX) * (1 - offsetY)); interpolatedHeight *= (float) this.maxHeight * SCALEVALUE; // Scale height return interpolatedHeight; } }