/*******************************************************************************
* Copyright (c) 2015 - 2017
* <p/>
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
* <p/>
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
* <p/>
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*******************************************************************************/
package jsettlers.logic.buildings;
import jsettlers.algorithms.fogofwar.IViewDistancable;
import jsettlers.common.buildings.EBuildingType;
import jsettlers.common.buildings.IBuilding;
import jsettlers.common.buildings.IBuildingMaterial;
import jsettlers.common.buildings.RelativeBricklayer;
import jsettlers.common.buildings.stacks.ConstructionStack;
import jsettlers.common.buildings.stacks.RelativeStack;
import jsettlers.common.map.shapes.FreeMapArea;
import jsettlers.common.mapobject.EMapObjectType;
import jsettlers.common.material.EPriority;
import jsettlers.common.movable.EDirection;
import jsettlers.common.player.IPlayerable;
import jsettlers.common.position.RelativePoint;
import jsettlers.common.position.ShortPoint2D;
import jsettlers.common.selectable.ESelectionType;
import jsettlers.logic.buildings.military.Barrack;
import jsettlers.logic.buildings.military.OccupyingBuilding;
import jsettlers.logic.buildings.others.DefaultBuilding;
import jsettlers.logic.buildings.others.StockBuilding;
import jsettlers.logic.buildings.others.TempleBuilding;
import jsettlers.logic.buildings.spawn.BigLivinghouse;
import jsettlers.logic.buildings.spawn.BigTemple;
import jsettlers.logic.buildings.spawn.MediumLivinghouse;
import jsettlers.logic.buildings.spawn.SmallLivinghouse;
import jsettlers.logic.buildings.stack.IRequestStack;
import jsettlers.logic.buildings.stack.RequestStack;
import jsettlers.logic.buildings.trading.MarketBuilding;
import jsettlers.logic.buildings.trading.TradingBuilding;
import jsettlers.logic.buildings.workers.MillBuilding;
import jsettlers.logic.buildings.workers.MineBuilding;
import jsettlers.logic.buildings.workers.ResourceBuilding;
import jsettlers.logic.buildings.workers.WorkerBuilding;
import jsettlers.logic.constants.Constants;
import jsettlers.logic.map.grid.objects.AbstractHexMapObject;
import jsettlers.logic.map.grid.partition.manager.manageables.interfaces.IConstructableBuilding;
import jsettlers.logic.map.grid.partition.manager.manageables.interfaces.IDiggerRequester;
import jsettlers.logic.movable.interfaces.IDebugable;
import jsettlers.logic.player.Player;
import jsettlers.logic.timer.IScheduledTimerable;
import jsettlers.logic.timer.RescheduleTimer;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
public abstract class Building extends AbstractHexMapObject implements IConstructableBuilding, IPlayerable, IBuilding, IScheduledTimerable,
IDebugable, IDiggerRequester, IViewDistancable {
private static final long serialVersionUID = 4379555028512391595L;
private static final float BUILDING_DESTRUCTION_SMOKE_DURATION = 1.2f;
private static final short UNOCCUPIED_VIEW_DISTANCE = 5;
private static final short UNCONSTRUCTED_VIEW_DISTANCE = 0;
private static final int IS_UNSTOPPED_RECHECK_PERIOD = 1000;
private static final int IS_FLATTENED_RECHECK_PERIOD = 1000;
private static final int WAITING_FOR_MATERIAL_PERIOD = 1000;
private static final EPriority[] SUPPORTED_PRIORITIES_FOR_CONSTRUCTION = new EPriority[] { EPriority.LOW, EPriority.HIGH, EPriority.STOPPED };
private static final EPriority[] SUPPORTED_PRIORITIES_FOR_NON_WORKERS = new EPriority[0];
private static final ConcurrentLinkedQueue<Building> allBuildings = new ConcurrentLinkedQueue<Building>();
protected final EBuildingType type;
protected final ShortPoint2D pos;
protected final IBuildingsGrid grid;
private Player player;
private EBuildingState state = EBuildingState.CREATED;
private EPriority priority = EPriority.DEFAULT;
private float constructionProgress = 0.0f;
private byte heightAvg;
private short remainingMaterialActions = 0;
private List<? extends IRequestStack> stacks;
private transient boolean selected;
protected Building(EBuildingType type, Player player, ShortPoint2D position, IBuildingsGrid buildingsGrid) {
this.type = type;
this.player = player;
this.pos = position;
this.grid = buildingsGrid;
allBuildings.add(this);
}
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
allBuildings.add(this);
}
@Override
public EMapObjectType getObjectType() {
return EMapObjectType.BUILDING;
}
@Override
public boolean cutOff() {
return false;
}
@Override
public boolean canBeCut() {
return false;
}
public final void construct(boolean fullyConstructed) {
assert state == EBuildingState.CREATED : "building can not be constructed in this state";
boolean itWorked = grid.setBuilding(pos, this);
if (itWorked) {
if (getFlagType() == EMapObjectType.FLAG_DOOR) {
showFlag(true);
}
placedAtEvent(pos);
if (fullyConstructed) {
appearFullyConstructed();
} else {
initConstruction();
}
} else {
kill();
}
}
protected void placedAtEvent(ShortPoint2D pos) {
}
private void appearFullyConstructed() {
this.state = EBuildingState.CONSTRUCTED;
grid.setBlocked(getBuildingArea(), true);
finishConstruction();
appearedEvent();
}
protected void appearedEvent() {
}
private void initConstruction() {
stacks = createConstructionStacks();
placeAdditionalMapObjects(grid, pos, true);
this.state = EBuildingState.CREATED;
RescheduleTimer.add(this, IS_UNSTOPPED_RECHECK_PERIOD);
}
private List<IRequestStack> createConstructionStacks() {
List<IRequestStack> result = new LinkedList<IRequestStack>();
for (ConstructionStack stack : type.getConstructionStacks()) {
result.add(new RequestStack(grid.getRequestStackGrid(), stack.calculatePoint(this.pos), stack.getMaterialType(), type, priority,
stack.requiredForBuild()));
}
return result;
}
protected final void initWorkStacks() {
this.stacks = createWorkStacks();
}
protected List<? extends IRequestStack> createWorkStacks() {
List<RequestStack> newStacks = new LinkedList<>();
for (RelativeStack stack : type.getRequestStacks()) {
newStacks.add(new RequestStack(grid.getRequestStackGrid(), stack.calculatePoint(this.pos), stack.getMaterialType(), type, priority));
}
return newStacks;
}
protected void placeAdditionalMapObjects(IBuildingsGrid grid, ShortPoint2D pos, boolean place) {
if (place) {
grid.getMapObjectsManager().addSimpleMapObject(pos, EMapObjectType.BUILDINGSITE_SIGN, false, null);
} else {
grid.getMapObjectsManager().removeMapObjectType(pos.x, pos.y, EMapObjectType.BUILDINGSITE_SIGN);
}
for (RelativePoint curr : type.getBuildMarks()) {
if (place) {
grid.getMapObjectsManager().addSimpleMapObject(curr.calculatePoint(pos), EMapObjectType.BUILDINGSITE_POST, false, null);
} else {
ShortPoint2D postPos = curr.calculatePoint(pos);
grid.getMapObjectsManager().removeMapObjectType(postPos.x, postPos.y, EMapObjectType.BUILDINGSITE_POST);
}
}
}
/**
* Used to set or clear the small red flag atop a building to indicate it is occupied.
*
* @param place
* specifies whether the flag should appear or not.
*/
protected void showFlag(boolean place) {
ShortPoint2D flagPosition = type.getFlag().calculatePoint(pos);
if (place) {
grid.getMapObjectsManager().addSimpleMapObject(flagPosition, getFlagType(), false, player);
} else {
grid.getMapObjectsManager().removeMapObjectType(flagPosition.x, flagPosition.y, getFlagType());
}
}
private void requestDiggers() {
if (shouldBeFlatened()) {
RelativePoint[] protectedTiles = getFlattenTiles();
int heightSum = 0;
for (RelativePoint curr : protectedTiles) {
ShortPoint2D currPos = curr.calculatePoint(this.pos);
heightSum += this.grid.getHeightAt(currPos);
}
this.heightAvg = (byte) (heightSum / protectedTiles.length);
byte numberOfDiggers = (byte) Math.ceil(((float) protectedTiles.length) / Constants.TILES_PER_DIGGER);
grid.requestDiggers(this, numberOfDiggers);
}
}
private void requestBricklayers() {
RelativeBricklayer[] bricklayers = type.getBricklayers();
for (RelativeBricklayer curr : bricklayers) {
grid.requestBricklayer(this, curr.calculatePoint(pos), curr.getDirection());
}
}
protected boolean shouldBeFlatened() {
return true;
}
private boolean isFlatened() {
if (shouldBeFlatened()) {
return grid.isAreaFlattenedAtHeight(pos, getFlattenTiles(), heightAvg);
} else {
return true;
}
}
@Override
public int timerEvent() {
switch (state) {
case CREATED:
if (priority == EPriority.STOPPED) {
return IS_UNSTOPPED_RECHECK_PERIOD;
} else {
state = EBuildingState.IN_FLATTERNING;
requestDiggers();
}
case IN_FLATTERNING:
if (!isFlatened()) {
return IS_FLATTENED_RECHECK_PERIOD;
} else {
placeAdditionalMapObjects(grid, pos, false);
grid.setBlocked(getBuildingArea(), true);
this.state = EBuildingState.WAITING_FOR_MATERIAL;
// directly go into the next case!
}
case WAITING_FOR_MATERIAL:
if (priority != EPriority.STOPPED && (isMaterialAvailable() || remainingMaterialActions > 0)) {
state = EBuildingState.BRICKLAYERS_REQUESTED;
requestBricklayers();
return -1; // no new scheduling
} else {
return WAITING_FOR_MATERIAL_PERIOD;
}
case BRICKLAYERS_REQUESTED: // the state changes are handled by tryToTakeMaterial()
assert false : "Building.timerEvent() should not be called in state: " + state;
return -1;
case CONSTRUCTED:
return subTimerEvent();
case DESTROYED:
default:
return -1;
}
}
/**
* This method will be called when the building has finished construction and is not destroyed yet.
*
* @return Gives the number of milliseconds when this method should be called again. Return -1 to unschedule the building.
*/
protected abstract int subTimerEvent();
private boolean isMaterialAvailable() {
if (stacks == null)
return true;
for (IRequestStack stack : stacks) {
if (stack.hasMaterial())
return true;
}
return false;
}
@Override
public final EBuildingType getBuildingType() {
return type;
}
@Override
public byte getPlayerId() {
return player.playerId;
}
public void setPlayer(Player player) {
this.player = player;
}
public final Player getPlayer() {
return player;
}
@Override
public boolean tryToTakeMaterial() {
if (state != EBuildingState.BRICKLAYERS_REQUESTED) {
return false;
}
remainingMaterialActions--;
constructionProgress += 1f / (Constants.BRICKLAYER_ACTIONS_PER_MATERIAL * getBuildingType().getNumberOfConstructionMaterials());
if (remainingMaterialActions > 0) {
return true;
} else {
IRequestStack stack = getStackWithMaterial();
if (stack != null) {
stack.pop();
remainingMaterialActions = Constants.BRICKLAYER_ACTIONS_PER_MATERIAL;
return true;
} else {
if (areAllStacksFullfilled()) {
finishConstruction();
} else {
state = EBuildingState.WAITING_FOR_MATERIAL;
RescheduleTimer.add(this, WAITING_FOR_MATERIAL_PERIOD);
}
return false;
}
}
}
private boolean areAllStacksFullfilled() {
for (IRequestStack curr : stacks) {
if (!curr.isFulfilled()) {
return false;
}
}
return true;
}
protected IRequestStack getStackWithMaterial() {
for (IRequestStack curr : stacks) {
if (curr.hasMaterial()) {
return curr;
}
}
return null;
}
private void finishConstruction() {
constructionProgress = 1;
this.setPriority(EPriority.DEFAULT);
this.state = EBuildingState.CONSTRUCTED;
if (getFlagType() == EMapObjectType.FLAG_DOOR) { // this building has no worker
stacks = createWorkStacks();
} else {
stacks = new LinkedList<>(); // create a new stacks list
}
int timerPeriod = constructionFinishedEvent();
RescheduleTimer.add(this, timerPeriod);
}
protected abstract int constructionFinishedEvent();
@Override
public float getStateProgress() {
return constructionProgress;
}
@Override
public ShortPoint2D getPos() {
return pos;
}
@Override
public boolean isSelected() {
return this.selected;
}
@Override
public void setSelected(boolean selected) {
this.selected = selected;
}
public final boolean isConstructionFinished() {
return state == EBuildingState.CONSTRUCTED || state == EBuildingState.DESTROYED;
}
protected abstract EMapObjectType getFlagType();
public final ShortPoint2D getDoor() {
return getBuildingType().getDoorTile().calculatePoint(pos);
}
@Override
public void kill() {
if (this.state == EBuildingState.DESTROYED) {
return;
}
System.out.println("building killed");
if (grid != null) {
grid.removeBuildingAt(pos);
grid.getMapObjectsManager().addSelfDeletingMapObject(pos,
EMapObjectType.BUILDING_DECONSTRUCTION_SMOKE, BUILDING_DESTRUCTION_SMOKE_DURATION, player);
placeAdditionalMapObjects(grid, pos, false);
showFlag(false);
placeReusableMaterials();
killedEvent();
}
releaseRequestStacks();
allBuildings.remove(this);
this.state = EBuildingState.DESTROYED;
this.selected = false;
}
private void placeReusableMaterials() {
int posIdx = 0;
FreeMapArea buildingArea = new FreeMapArea(this.pos, type.getBlockedTiles());
if (isConstructionFinished()) {
for (ConstructionStack curr : type.getConstructionStacks()) {
byte paybackAmount = (byte) (curr.requiredForBuild() * Constants.BUILDINGS_DESTRUCTION_MATERIALS_PAYBACK_FACTOR);
while (paybackAmount > 0) {
byte paybackForStack = (byte) Math.min(Constants.STACK_SIZE, paybackAmount);
ShortPoint2D position = buildingArea.get(posIdx);
grid.pushMaterialsTo(position, curr.getMaterialType(), paybackForStack);
paybackAmount -= paybackForStack;
posIdx += 4;
}
}
} else {
for (IRequestStack stack : stacks) {
posIdx += 4;
int paybackAmount = (int) (stack.getNumberOfPopped() * Constants.BUILDINGS_DESTRUCTION_MATERIALS_PAYBACK_FACTOR);
if (paybackAmount > 0) {
ShortPoint2D position = buildingArea.get(posIdx);
grid.pushMaterialsTo(position, stack.getMaterialType(), (byte) Math.min(paybackAmount, Constants.STACK_SIZE));
}
}
}
}
protected void killedEvent() {
}
protected void releaseRequestStacks() {
if (stacks != null) {
for (IRequestStack curr : stacks) {
curr.releaseRequests();
}
stacks = Collections.emptyList();
}
}
public void setWorkAreaCenter(ShortPoint2D workAreaCenter) {
}
@Override
public void debug() {
System.out.println("debug: building at " + pos);
}
@Override
public EPriority[] getSupportedPriorities() {
if (!isConstructionFinished()) {
return SUPPORTED_PRIORITIES_FOR_CONSTRUCTION;
} else {
return SUPPORTED_PRIORITIES_FOR_NON_WORKERS;
}
}
@Override
public EPriority getPriority() {
return priority;
}
@Override
public boolean isDiggerRequestActive() {
return state == EBuildingState.IN_FLATTERNING;
}
@Override
public void diggerRequestFailed() {
if (isDiggerRequestActive()) {
grid.requestDiggers(this, (byte) 1);
}
}
@Override
public boolean isBricklayerRequestActive() {
return state == EBuildingState.BRICKLAYERS_REQUESTED;
}
@Override
public void bricklayerRequestFailed(ShortPoint2D bricklayerTargetPos, EDirection lookDirection) {
if (isBricklayerRequestActive()) {
grid.requestBricklayer(this, bricklayerTargetPos, lookDirection);
}
}
protected FreeMapArea getBuildingArea() {
return new FreeMapArea(this.pos, type.getBlockedTiles());
}
@Override
public byte getAverageHeight() {
return this.heightAvg;
}
public final boolean isNotDestroyed() {
return state != EBuildingState.DESTROYED;
}
protected List<? extends IRequestStack> getStacks() {
return stacks;
}
public static ConcurrentLinkedQueue<Building> getAllBuildings() {
return allBuildings;
}
public static void clearState() {
allBuildings.clear();
}
@Override
public final short getViewDistance() {
if (isConstructionFinished()) {
if (isOccupied()) {
return type.getViewDistance();
} else {
return UNOCCUPIED_VIEW_DISTANCE;
}
} else {
return UNCONSTRUCTED_VIEW_DISTANCE;
}
}
@Override
public ESelectionType getSelectionType() {
return ESelectionType.BUILDING;
}
@Override
public List<IBuildingMaterial> getMaterials() {
ArrayList<IBuildingMaterial> materials = new ArrayList<IBuildingMaterial>();
for (IRequestStack stack : stacks) {
if (state == EBuildingState.CONSTRUCTED) {
materials.add(new BuildingMaterial(stack.getMaterialType(), stack.getStackSize(), false));
} else { // during construction
materials.add(new BuildingMaterial(stack.getMaterialType(), stack.getStillRequired()));
}
}
if (state == EBuildingState.CONSTRUCTED) {
for (RelativeStack offerStack : type.getOfferStacks()) {
byte stackSize = grid.getRequestStackGrid().getStackSize(offerStack.calculatePoint(pos), offerStack.getMaterialType());
materials.add(new BuildingMaterial(offerStack.getMaterialType(), stackSize, true));
}
}
return materials;
}
public void setPriority(EPriority newPriority) {
this.priority = newPriority;
if (stacks != null) {
for (IRequestStack curr : stacks) {
curr.setPriority(newPriority);
}
}
if (newPriority == EPriority.STOPPED) {
switch (state) {
case IN_FLATTERNING:
state = EBuildingState.CREATED; // we're still scheduled in this state => no rescheduling!
break;
case BRICKLAYERS_REQUESTED:
state = EBuildingState.WAITING_FOR_MATERIAL;
RescheduleTimer.add(this, WAITING_FOR_MATERIAL_PERIOD); // we're not scheduled atm => reschedule!
break;
}
}
}
public final RelativePoint[] getFlattenTiles() {
if (shouldBeFlatened()) {
return type.getProtectedTiles();
} else {
return new RelativePoint[0];
}
}
public short getPartitionId() {
return grid.getPartitionIdAt(pos);
}
@Override
public boolean cannotWork() {
return false;
}
public static Building createBuilding(EBuildingType type, Player player, ShortPoint2D position, IBuildingsGrid buildingsGrid) {
switch (type) {
case BIG_LIVINGHOUSE:
return new BigLivinghouse(player, position, buildingsGrid);
case MEDIUM_LIVINGHOUSE:
return new MediumLivinghouse(player, position, buildingsGrid);
case SMALL_LIVINGHOUSE:
return new SmallLivinghouse(player, position, buildingsGrid);
case CHARCOAL_BURNER:
case BAKER:
case DONKEY_FARM:
case FARM:
case FORESTER:
case GOLDMELT:
case IRONMELT:
case LUMBERJACK:
case PIG_FARM:
case SAWMILL:
case SLAUGHTERHOUSE:
case STONECUTTER:
case TOOLSMITH:
case WEAPONSMITH:
case WATERWORKS:
case WINEGROWER:
return new WorkerBuilding(type, player, position, buildingsGrid);
case MILL:
return new MillBuilding(type, player, position, buildingsGrid);
case TOWER:
case BIG_TOWER:
case CASTLE:
return new OccupyingBuilding(type, player, position, buildingsGrid);
case BARRACK:
return new Barrack(player, position, buildingsGrid);
case IRONMINE:
case GOLDMINE:
case COALMINE:
return new MineBuilding(type, player, position, buildingsGrid);
case FISHER:
return new ResourceBuilding(EBuildingType.FISHER, player, position, buildingsGrid);
case STOCK:
return new StockBuilding(player, position, buildingsGrid);
case TEMPLE:
return new TempleBuilding(player, position, buildingsGrid);
case MARKET_PLACE:
return new MarketBuilding(type, player, position, buildingsGrid);
case HARBOR:
return new TradingBuilding(type, player, position, buildingsGrid, true);
case BIG_TEMPLE:
return new BigTemple(player, position, buildingsGrid);
case HOSPITAL:
case LOOKOUT_TOWER:
case DOCKYARD:
return new DefaultBuilding(type, player, position, buildingsGrid);
default:
System.err.println("ERROR: couldn't create new building, because type is unknown: " + type);
return null;
}
}
private enum EBuildingState {
CREATED,
IN_FLATTERNING,
WAITING_FOR_MATERIAL,
CONSTRUCTED,
DESTROYED,
BRICKLAYERS_REQUESTED
}
}