Skip to content

Commit

Permalink
Merge pull request #135 from Fundynamic/feature/restrict-placement-bu…
Browse files Browse the repository at this point in the history
…ildings

Feature/restrict placement buildings
  • Loading branch information
stefanhendriks authored Oct 8, 2016
2 parents ce45e92 + 18eacbc commit 5343991
Show file tree
Hide file tree
Showing 27 changed files with 468 additions and 100 deletions.
1 change: 1 addition & 0 deletions src/main/java/com/fundynamic/d2tm/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class Game extends StateBasedGame {
public static final int SCREEN_HEIGHT = 600;

public static final int TILE_SIZE = 32;
public static final int HALF_TILE = TILE_SIZE / 2;

public static final boolean DEBUG_INFO = false;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,53 @@
package com.fundynamic.d2tm.game.controls.battlefield;


import com.fundynamic.d2tm.game.entities.Entity;
import com.fundynamic.d2tm.game.entities.EntityData;
import com.fundynamic.d2tm.Game;
import com.fundynamic.d2tm.game.entities.*;
import com.fundynamic.d2tm.game.entities.entitybuilders.PlacementBuildableEntity;
import com.fundynamic.d2tm.game.entities.predicates.PredicateBuilder;
import com.fundynamic.d2tm.game.map.Cell;
import com.fundynamic.d2tm.game.rendering.gui.battlefield.BattleField;
import com.fundynamic.d2tm.game.terrain.ConstructionGround;
import com.fundynamic.d2tm.math.Coordinate;
import org.newdawn.slick.Color;
import com.fundynamic.d2tm.math.MapCoordinate;
import com.fundynamic.d2tm.math.Vector2D;
import com.fundynamic.d2tm.utils.Colors;
import com.fundynamic.d2tm.utils.SlickUtils;
import org.newdawn.slick.Graphics;

import java.util.ArrayList;
import java.util.List;

import static java.util.stream.Collectors.toList;

public class PlacingStructureMouse extends AbstractBattleFieldMouseBehavior {

private EntityData entityToPlace;
private EntityData entityDataToPlace;
private Entity entityWhoConstructsIt;
private List<PlaceableMapCoordinateCandidate> mapCoordinatesForEntityToPlace;
private EntityRepository entityRepository;

public PlacingStructureMouse(BattleField battleField, EntityData entityData) {
public PlacingStructureMouse(BattleField battleField, PlacementBuildableEntity placementBuildableEntity) {
super(battleField);
this.entityToPlace = entityData;
this.entityRepository = battleField.getEntityRepository();

this.entityDataToPlace = placementBuildableEntity.getEntityData();
this.entityWhoConstructsIt = placementBuildableEntity.getEntityWhoConstructsThis();
this.mapCoordinatesForEntityToPlace = new ArrayList<>();
}

@Override
public void leftClicked() {
Cell hoverCell = getHoverCell();
// TODO: Check if it may be placed or not...
Entity entity = entityRepository.placeOnMap(hoverCell.getCoordinates(), entityToPlace, mouse.getControllingPlayer());
battleField.entityPlacedOnMap(entity);
if (!canPlaceEntity()) return;

// tell battlefield of the created entity
battleField.entityPlacedOnMap(
entityRepository.placeOnMap( // place entity on map
getAbsoluteCoordinateTopLeftOfStructureToPlace(),
entityDataToPlace,
mouse.getControllingPlayer()
)
);
}

@Override
Expand All @@ -40,22 +64,184 @@ public void mouseMovedToCell(Cell cell) {
public void render(Graphics graphics) {
Cell hoverCell = getHoverCell();
if (hoverCell == null) return;
if (mapCoordinatesForEntityToPlace.isEmpty()) return;

MapCoordinate topLeftMapCoordinate = mapCoordinatesForEntityToPlace.get(0).mapCoordinate;

Coordinate coordinateTopLeft = battleField.translateMapCoordinateToViewportCoordinate(topLeftMapCoordinate);
SlickUtils.drawImage(graphics, entityDataToPlace.getFirstImage(), coordinateTopLeft);

// Now, do checks if the structure may be placed
for (PlaceableMapCoordinateCandidate placeableMapCoordinateCandidate : mapCoordinatesForEntityToPlace) {
Coordinate absoluteMapCoordinate = placeableMapCoordinateCandidate.mapCoordinate.toCoordinate().addHalfTile();
Coordinate coordinate = battleField.translateAbsoluteMapCoordinateToViewportCoordinate(absoluteMapCoordinate);

Cell cell = battleField.getCellByAbsoluteViewportCoordinate(coordinate);

// first check if it is visible (easiest check)
boolean isPlaceable = cell.isVisibleFor(player);

// visible? then check if it may be constructed (is it construction ground?)
if (isPlaceable && !(cell.getTerrain() instanceof ConstructionGround)) {
isPlaceable = false;
}

// Calculate distance first, later do checking
Entity closestFriendlyStructure = findClosestStructureOfPlayer(battleField.translateViewportCoordinateToAbsoluteMapCoordinate(coordinate));
Coordinate constructingEntityCoordinate = battleField.translateAbsoluteMapCoordinateToViewportCoordinate(closestFriendlyStructure.getCenteredCoordinate());

Coordinate absoluteCoordinates = hoverCell.getCoordinates();
Coordinate viewportCoordinate = battleField.translateAbsoluteMapCoordinateToViewportCoordinate(absoluteCoordinates);
placeableMapCoordinateCandidate.distance = constructingEntityCoordinate.distance(coordinate);

graphics.setColor(Color.green);
float lineWidth = graphics.getLineWidth();
graphics.setLineWidth(1.1f);
graphics.drawImage(entityToPlace.getFirstImage(), viewportCoordinate.getXAsInt(), viewportCoordinate.getYAsInt());
graphics.drawRect(viewportCoordinate.getXAsInt(), viewportCoordinate.getYAsInt(), entityToPlace.getWidth(), entityToPlace.getHeight());
graphics.setLineWidth(lineWidth);
// render the lines when debug info is true
if (Game.DEBUG_INFO) {
graphics.setColor(Colors.WHITE);
SlickUtils.drawLine(graphics, coordinate, constructingEntityCoordinate);
}

// still placeable? good. Final (expensive) check -> any other units that may block this?
if (isPlaceable) {
EntitiesSet entitiesAtMapCoordinate = entityRepository.findAliveEntitiesOfTypeAtVector(absoluteMapCoordinate, EntityType.STRUCTURE, EntityType.UNIT);
isPlaceable = entitiesAtMapCoordinate.isEmpty();
}

if (!isPlaceable) {
placeableMapCoordinateCandidate.placeableState = PlaceableState.BLOCKED;
}
}

float maxDistance = entityWhoConstructsIt.getEntityData().buildRange; // from center of the structure that built this entity (to place)

for (PlaceableMapCoordinateCandidate pmcc : mapCoordinatesForEntityToPlace) {
if (pmcc.placeableState != PlaceableState.PLACEABLE) continue;
if (pmcc.distance > maxDistance) {
pmcc.placeableState = PlaceableState.OUT_OF_REACH;
}
}

boolean canPlaceEntity = canPlaceEntity();

// Render stuff!
for (PlaceableMapCoordinateCandidate placeableMapCoordinateCandidate : mapCoordinatesForEntityToPlace) {
Coordinate absoluteMapCoordinate = placeableMapCoordinateCandidate.mapCoordinate.toCoordinate();
Coordinate coordinate = battleField.translateAbsoluteMapCoordinateToViewportCoordinate(absoluteMapCoordinate);

if (canPlaceEntity) {
graphics.setColor(Colors.GREEN_ALPHA_128);
} else {
switch (placeableMapCoordinateCandidate.placeableState) {
case PLACEABLE:
graphics.setColor(Colors.GREEN_ALPHA_128);
break;
case BLOCKED:
graphics.setColor(Colors.RED_ALPHA_128);
break;
case OUT_OF_REACH:
graphics.setColor(Colors.YELLOW_ALPHA_128);
break;
}
}


graphics.fillRect(coordinate.getXAsInt(), coordinate.getYAsInt(), Game.TILE_SIZE, Game.TILE_SIZE);
}
}

@Override
public void movedTo(Vector2D coordinates) {
super.movedTo(coordinates);

// TODO: move this to an update method thing? because it is possible that state changes without us
// moving the mouse. So this won't last unfortunately. (or we should do some kind of message thing so this
// class knows the state changed and should re-calculate or something like that)

Coordinate absoluteMapCoordinateOfTopleftOfStructure = getAbsoluteCoordinateTopLeftOfStructureToPlace();

// first determine all cells that will be occupied
mapCoordinatesForEntityToPlace = new ArrayList<>();

List<MapCoordinate> allMapCoordinates = this.entityDataToPlace.getAllCellsAsCoordinates(absoluteMapCoordinateOfTopleftOfStructure);

mapCoordinatesForEntityToPlace =
allMapCoordinates
.stream()
.map(mapCoordinate -> new PlaceableMapCoordinateCandidate(mapCoordinate, PlaceableState.PLACEABLE))
.collect(toList());

}

public Coordinate getAbsoluteCoordinateTopLeftOfStructureToPlace() {
// first get absolute viewport coordinates, we can calculate on the battlefield with that
Coordinate viewportCoordinate = battleField.translateScreenToViewportCoordinate(mouseCoordinates);

// now substract half of the structure to place, so we make the structure to place center beneath the mouse
Vector2D halfSize = entityDataToPlace.getHalfSize();
Coordinate topLeftOfStructure = viewportCoordinate.min(halfSize);

Cell topLeftCellOfStructure = battleField.getCellByAbsoluteViewportCoordinate(topLeftOfStructure);
return topLeftCellOfStructure.getCoordinates();
}

@Override
public String toString() {
return "PlacingStructureMouse{" +
"entityToPlace=" + entityToPlace +
"entityDataToPlace=" + entityDataToPlace +
'}';
}

public boolean canPlaceEntity() {
// blocked == not good
// passable == good
// out of reach, but one passable == good
boolean blocked = this.mapCoordinatesForEntityToPlace.stream().anyMatch(c -> c.placeableState == PlaceableState.BLOCKED);
if (blocked) return false;

return this.mapCoordinatesForEntityToPlace.stream().anyMatch(c -> c.placeableState == PlaceableState.PLACEABLE);
}

/**
* This method finds a closest player Structure to the given coordinate.
* @param coordinate
* @return
*/
private Entity findClosestStructureOfPlayer(Coordinate coordinate) {
// TODO: Optimize
// Perhaps have a 'satisfying distance' ? ie, whenever it finds one within this distance, stop searching?
// Do not loop over everything?
// Move to EntityRepository?
PredicateBuilder predicateBuilder = Predicate.builder().forPlayer(player).ofType(EntityType.STRUCTURE);
EntitiesSet allStructuresForPlayer = entityRepository.filter(predicateBuilder.build());

float closestDistanceFoundSoFar = 320000; // Get from Map!? (width * height) -> get rid of magic number
Entity closestEntityFoundSoFar = null;

for (Entity entity : allStructuresForPlayer) {
float distance = entity.getCenteredCoordinate().distance(coordinate);
if (distance < closestDistanceFoundSoFar) {
closestEntityFoundSoFar = entity;
closestDistanceFoundSoFar = distance;
}
}
return closestEntityFoundSoFar;
}

/**
* A class that holds state per map coordinate needed for placement. Mostly used for rendering.
*/
private class PlaceableMapCoordinateCandidate {

public MapCoordinate mapCoordinate;
public PlaceableState placeableState;
public float distance = 99999;

public PlaceableMapCoordinateCandidate(MapCoordinate mapCoordinate, PlaceableState state) {
this.mapCoordinate = mapCoordinate;
this.placeableState = state;
}
}

enum PlaceableState {
PLACEABLE,
BLOCKED,
OUT_OF_REACH
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class EntityData {
private float chop = -1f;
private float halfChop = -1f;
public float buildTimeInSeconds = 5.0F;
public float buildRange = 0F;

public EntityType type;

Expand Down Expand Up @@ -241,8 +242,8 @@ public List<MapCoordinate> getAllCellsAsCoordinates(Coordinate coordinate) {
List<MapCoordinate> result = new ArrayList<>(widthInCells * heightInCells);
for (int x = 0; x < widthInCells; x++) {
for (int y = 0; y < heightInCells; y++) {
int vecX = coordinate.getXAsInt() + x * Game.TILE_SIZE;
int vecY = coordinate.getYAsInt() + y * Game.TILE_SIZE;
int vecX = coordinate.getXAsInt() + (x * Game.TILE_SIZE);
int vecY = coordinate.getYAsInt() + (y * Game.TILE_SIZE);
result.add(Coordinate.create(vecX, vecY).toMapCoordinate());
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.fundynamic.d2tm.game.entities.entitiesdata;


import com.fundynamic.d2tm.Game;
import com.fundynamic.d2tm.game.entities.EntityData;
import com.fundynamic.d2tm.game.entities.EntityNotFoundException;
import com.fundynamic.d2tm.game.entities.EntityType;
Expand Down Expand Up @@ -87,9 +88,9 @@ public void addParticle(String id, String pathToImage, int widthInPixels, int he
*
* @throws SlickException
*/
public EntityData addStructure(IniDataStructure iniDataStructure) throws SlickException {
public EntityData addStructure(String id, IniDataStructure iniDataStructure) throws SlickException {
EntityData entityData = createEntity(
iniDataStructure.id,
id,
iniDataStructure.image,
null,
iniDataStructure.width,
Expand All @@ -100,13 +101,42 @@ public EntityData addStructure(IniDataStructure iniDataStructure) throws SlickEx
iniDataStructure.hitpoints
);

// The buildRange is (for now) determined by the size of the structure.
// Because the range is calculated from the center of the structure.
// In order to make it 'fair' for larger structures (if any would appear),
// we add 'half' of the structure to the range.
//
// For a constyard it is a square, so 64x64 pixels = 1 'half', meaning:
//
// (64/64)/2 = 0,5 * 32 (tile width) = 16 pixels
//
// A larger structure would be (width/height), ie a heavy factory thing would be:
// (96/64)/2 = ,75 * 32 = 24 pixels.
//
// Although on every squared structure this would even out fine, but then the
// 'buildRange' should have one tile extra because (again) it is calculated
// from the center
//
// this is all weird , then again, fixing this would require to check for every cell
// on the structure which basically makes calculating the 'can I place it in this distance' logic
// Width*height times more consuming.
//
// Unless....
//
// We do the 'computed map' thing (where we have all entity data on a map attached), so we don't
// need to do an expensive lookup in the EntityRepository
//TODO: Do something about the above, for now accept its quirks
float someRatio = (float)entityData.getWidth() / (float)entityData.getHeight();
int extraFromCenter = (int)((someRatio / 2) * Game.TILE_SIZE); // this is seriously flawed :/ (too tired to fix now)
// add additional '1' to get 'past the center and occupy one cell'.
entityData.buildRange = extraFromCenter + ((1 + iniDataStructure.buildRangeInTiles) * Game.TILE_SIZE);
entityData.entityBuilderType = iniDataStructure.getEntityBuilderType();
entityData.buildTimeInSeconds = iniDataStructure.buildTimeInSeconds;
entityData.buildList = iniDataStructure.buildList;

if (!idProvided(iniDataStructure.explosion)) {
if (!tryGetEntityData(EntityType.PARTICLE, iniDataStructure.explosion)) {
throw new IllegalArgumentException("structure " + iniDataStructure.id + " [explosion] refers to non-existing [EXPLOSIONS/" + iniDataStructure.explosion + "]");
throw new IllegalArgumentException("structure " + id + " [explosion] refers to non-existing [EXPLOSIONS/" + iniDataStructure.explosion + "]");
}
entityData.explosionId = iniDataStructure.explosion;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class EntitiesDataReader {
public static final String INI_KEYWORD_BUILD_ICON = "BuildIcon";
public static final String INI_KEYWORD_BUILDS = "Builds";
public static final String INI_KEYWORD_BUILD_TIME = "BuildTime";
public static final String INI_KEYWORD_BUILD_RANGE = "BuildRange";
public static final String INI_KEYWORD_BUILD_LIST = "BuildList";
public static final String INI_KEYWORD_FPS = "Fps";
public static final String INI_KEYWORD_RECOLOR = "Recolor";
Expand Down Expand Up @@ -81,19 +82,8 @@ public void readStructures(EntitiesData entitiesData, Ini ini) throws SlickExcep
for (String id : strings) {
Profile.Section struct = structures.getChild(id);
entitiesData.addStructure(
new IniDataStructure(
id,
struct.get(INI_KEYWORD_IMAGE, String.class, null),
struct.get(INI_KEYWORD_WIDTH, Integer.class),
struct.get(INI_KEYWORD_HEIGHT, Integer.class),
struct.get(INI_KEYWORD_SIGHT, Integer.class),
struct.get(INI_KEYWORD_HIT_POINTS, Integer.class),
struct.get(INI_KEYWORD_EXPLOSION, String.class, EntitiesData.UNKNOWN),
struct.get(INI_KEYWORD_BUILD_ICON, String.class, null),
struct.get(INI_KEYWORD_BUILDS, String.class, ""),
struct.get(INI_KEYWORD_BUILD_TIME, Float.class, 0F),
struct.get(INI_KEYWORD_BUILD_LIST, String.class, "")
)
id,
new IniDataStructure(struct)
);
}
}
Expand Down
Loading

0 comments on commit 5343991

Please sign in to comment.