BuildingManager.java
package io.github.unisim.building;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.maps.tiled.TiledMapTile;
import com.badlogic.gdx.maps.tiled.TiledMapTileLayer;
import com.badlogic.gdx.maps.tiled.TiledMapTileLayer.Cell;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector3;
import io.github.unisim.GlobalState;
import io.github.unisim.Point;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Manage the buildings placed in the world and methods common to all buildings.
*/
public class BuildingManager {
// create a list of buildings which will be sorted by a height metric derived from
// the locations of the corners of the buildings.
private final List<Building> buildings = new ArrayList<>();
private final Map<BuildingType, Integer> buildingCounts = new HashMap<>();
private final Matrix4 isoTransform;
private Building previewBuilding;
private float animationTime;
private float animationTimeDirection = 1.0f;
public BuildingManager(Matrix4 isoTransform) {
this.isoTransform = isoTransform;
}
/**
* Determines if a region on the map is composed solely of buildable tiles.
*
* @param btmLeft - The co-ordinates of the bottom left corner of the search region
* @param topRight - The co-ordinates of the top right corner of the search region
* @param tileLayer - A reference to the map layer containing all terrain tiles
* @return - true if the region is made solely of buildable tiles, false otherwise
*/
public boolean isBuildable(Point btmLeft, Point topRight, TiledMapTileLayer tileLayer) {
boolean buildable = true;
// we iterate over each tile within the search region and check
// for any non-buildable tiles.
for (int x = btmLeft.x; x <= topRight.x && buildable; x++) {
for (int y = btmLeft.y; y <= topRight.y && buildable; y++) {
Cell currentCell = tileLayer.getCell(x, y);
if (currentCell == null) {
buildable = false;
continue;
}
TiledMapTile currentTile = currentCell.getTile();
if (!tileBuildable(currentTile)) {
buildable = false;
}
}
}
if (!buildable) {
return false;
}
// Next, iterate over the current buildings to see if any intersect the new building
for (Building building : buildings) {
// Use the seperating axis theorem to detect building overlap
if (!(building.location.x > topRight.x
|| building.location.x + building.size.x - 1 < btmLeft.x
|| building.location.y > topRight.y
|| building.location.y + building.size.y - 1 < btmLeft.y)
) {
if (building == previewBuilding) {
continue;
}
buildable = false;
break;
}
}
return buildable;
}
/**
* Helper method that determines if the provided tile may be built on.
*
* @param tile - A reference to a tile on the terrain layer of the map.
* @return - true if the tile is buildable, false otherwise
*/
private static boolean tileBuildable(TiledMapTile tile) {
return GlobalState.buildableTiles.contains(tile.getId());
}
/**
* Draws each building from the building list onto the map.
*
* @param batch - the SpriteBatch in which to draw
*/
public void render(SpriteBatch batch) {
animationTime += animationTimeDirection * Gdx.graphics.getDeltaTime();
if (animationTime <= 0.0f || animationTime >= 1.0f) {
animationTimeDirection *= -1.0f;
}
for (Building building : buildings) {
drawBuilding(building, batch);
}
}
/**
* Handle placement of a building into the world by determining
* the correct draw order and updating the building counters.
*
* @param building - A reference to a building object to be placed
* @return - The location in the buildings array that the building was placed at
*/
public int placeBuilding(Building building) {
// Insert the building into the correct place in the arrayList to ensure it
// gets rendered in top-down order
// Start by calculating the 'height' values for the left and right corners of the new building
// where height is the taxi-cab distance from the top of the map
int buildingHeightLeftSide = building.location.y - building.location.x;
int buildingHeightRightSide = buildingHeightLeftSide + building.size.y - building.size.x + 1;
Point leftCorner = building.location;
// Move up the array, until the pointer is in the correct place for the new building so the
// array is sorted by height
int i = 0;
while (i < buildings.size()) {
Building other = buildings.get(i);
int otherHeightLeftSide = other.location.y - other.location.x;
// Calculate the taxi-cab distance between the new building's left corner and the other
// building's right corner
int leftDistance = Math.abs(leftCorner.x - other.location.x - other.size.x + 1)
+ Math.abs(leftCorner.y - other.location.y - other.size.y + 1);
// If the distance is small, compare the height of the new buildin'g left corner to the
// height of the other buildings right corner
if (leftDistance < Math.min(building.size.x + building.size.y, other.size.x + other.size.y)) {
int otherHeightRightSide = otherHeightLeftSide + other.size.y - other.size.x + 1;
if (otherHeightRightSide > buildingHeightLeftSide) {
i++;
continue;
} else {
break;
}
}
// Otherwise, compare the distance of the new building's right corner to the other building's
// left corner
if (otherHeightLeftSide > buildingHeightRightSide) {
i++;
} else {
break;
}
}
buildings.add(i, building);
updateCounters(building);
return i;
}
/**
* Creates a counter for the building's type if it is the first to be placed,
* otherwise increments the counter for that type by one.
*
* @param building - A reference to the building object that was placed
*/
private void updateCounters(Building building) {
if (building == previewBuilding) {
return;
}
if (!buildingCounts.containsKey(building.type)) {
buildingCounts.put(building.type, 1);
return;
}
buildingCounts.put(building.type, buildingCounts.get(building.type) + 1);
}
/**
* Returns the number of buildings of a certain type that have been placed
* in the world.
*
* @param type - The type of building
* @return - The number of buildings of that type that have been placed
*/
public int getBuildingCount(BuildingType type) {
if (!buildingCounts.containsKey(type)) {
return 0;
}
return buildingCounts.get(type);
}
/**
* Sets the building to render as a 'preview' on the map prior to placement.
*
* @param previewBuilding - The building to draw as a preview
*/
public void setPreviewBuilding(Building previewBuilding) {
if (this.previewBuilding != null) {
buildings.remove(this.previewBuilding);
}
this.previewBuilding = previewBuilding;
if (previewBuilding != null) {
placeBuilding(previewBuilding);
}
}
/**
* Draw the building texture at the position of the mouse cursor
* when building mode is enabled.
*
* @param building - The building to draw under the mouse cursor
* @param batch - the SpriteBatch to draw into
*/
public void drawBuilding(Building building, SpriteBatch batch) {
if (building.onFire) {
var color = new Color(0.85f, 0.55f, 0.2f, 1.0f);
color.lerp(0.85f, 0.28f, 0.2f, 1.0f, animationTime);
batch.setColor(color);
} else {
batch.setColor(1.0f, 1.0f, 1.0f, 1.0f);
}
Vector3 btmLeftPos = new Vector3(
(float) building.location.x + (
building.flipped ? building.textureOffset.x : building.textureOffset.x
),
(float) building.location.y + (
building.flipped ? building.textureOffset.y : building.textureOffset.y
),
0f
);
Vector3 btmRightPos = new Vector3(btmLeftPos).add(new Vector3(building.size.x - 1, 0f, 0f));
btmLeftPos.mul(isoTransform);
btmRightPos.mul(isoTransform);
batch.draw(
building.texture,
btmLeftPos.x, btmRightPos.y,
building.texture.getWidth() * building.textureScale,
building.texture.getHeight() * building.textureScale,
0, 0, building.texture.getWidth(), building.texture.getHeight(),
building.flipped, false
);
}
public List<Building> getBuildings() {
return buildings;
}
public Building getPreviewBuilding() {
return previewBuilding;
}
}