/*******************************************************************************
* Copyright (c) 2015 - 2017
*
* 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:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* 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.map.grid.objects;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.PriorityQueue;
import jsettlers.common.landscape.EResourceType;
import jsettlers.common.map.shapes.HexGridArea;
import jsettlers.common.mapobject.EMapObjectType;
import jsettlers.common.mapobject.IAttackableTowerMapObject;
import jsettlers.common.material.EMaterialType;
import jsettlers.common.material.ESearchType;
import jsettlers.common.position.RelativePoint;
import jsettlers.common.position.ShortPoint2D;
import jsettlers.logic.buildings.stack.IStackSizeSupplier;
import jsettlers.logic.constants.Constants;
import jsettlers.logic.constants.MatchConstants;
import jsettlers.logic.movable.interfaces.IInformable;
import jsettlers.logic.objects.DonkeyMapObject;
import jsettlers.logic.objects.PigObject;
import jsettlers.logic.objects.RessourceSignMapObject;
import jsettlers.logic.objects.SelfDeletingMapObject;
import jsettlers.logic.objects.SoundableSelfDeletingObject;
import jsettlers.logic.objects.StandardMapObject;
import jsettlers.logic.objects.WineBowlMapObject;
import jsettlers.logic.objects.arrow.ArrowObject;
import jsettlers.logic.objects.building.BuildingWorkAreaMarkObject;
import jsettlers.logic.objects.building.ConstructionMarkObject;
import jsettlers.logic.objects.building.InformableMapObject;
import jsettlers.logic.objects.growing.Corn;
import jsettlers.logic.objects.growing.Wine;
import jsettlers.logic.objects.growing.tree.AdultTree;
import jsettlers.logic.objects.growing.tree.Tree;
import jsettlers.logic.objects.stack.StackMapObject;
import jsettlers.logic.objects.stone.Stone;
import jsettlers.logic.player.Player;
import jsettlers.logic.timer.IScheduledTimerable;
import jsettlers.logic.timer.RescheduleTimer;
import java8.util.Optional;
/**
* This class manages the MapObjects on the grid. It handles timed events like growth interrupts of a tree or deletion of arrows.
*
* @author Andreas Eberle
*
*/
public final class MapObjectsManager implements IScheduledTimerable, Serializable {
private static final long serialVersionUID = 1833055351956872224L;
private final IMapObjectsManagerGrid grid;
private final PriorityQueue<TimeEvent> timingQueue = new PriorityQueue<TimeEvent>();
private boolean killed = false;
public MapObjectsManager(IMapObjectsManagerGrid grid) {
this.grid = grid;
RescheduleTimer.add(this, 100);
}
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
}
@Override
public int timerEvent() {
if (killed) {
return -1;
}
int gameTime = MatchConstants.clock().getTime();
TimeEvent curr = null;
curr = timingQueue.peek();
while (curr != null && curr.isOutDated(gameTime)) {
timingQueue.poll();
if (curr.shouldRemoveObject()) {
removeMapObject(curr.mapObject.getX(), curr.mapObject.getY(), curr.mapObject);
} else {
curr.getMapObject().changeState();
}
curr = timingQueue.peek();
}
return 100;
}
@Override
public void kill() {
killed = true;
}
public boolean executeSearchType(ShortPoint2D pos, ESearchType type) {
switch (type) {
case PLANTABLE_TREE:
return plantTree(new ShortPoint2D(pos.x, pos.y + 1));
case CUTTABLE_TREE:
return cutTree(pos);
case CUTTABLE_STONE:
cutStone(pos);
return true;
case PLANTABLE_CORN:
return plantCorn(pos);
case CUTTABLE_CORN:
return cutCorn(pos);
case PLANTABLE_WINE:
return plantWine(pos);
case HARVESTABLE_WINE:
return harvestWine(pos);
case RESOURCE_SIGNABLE:
return addRessourceSign(pos);
default:
System.err.println("ERROR: Can't handle search type in executeSearchType(): " + type);
return false;
}
}
private boolean addRessourceSign(ShortPoint2D pos) {
EResourceType resourceType = grid.getRessourceTypeAt(pos.x, pos.y);
byte resourceAmount = grid.getRessourceAmountAt(pos.x, pos.y);
RessourceSignMapObject object = new RessourceSignMapObject(pos, resourceType, resourceAmount
/ ((float) Constants.MAX_RESOURCE_AMOUNT_PER_POSITION));
addMapObject(pos, object);
timingQueue.add(new TimeEvent(object, RessourceSignMapObject.getLivetime(), true));
return true;
}
private void cutStone(ShortPoint2D pos) {
short x = (short) (pos.x - 1);
short y = (short) (pos.y + 1);
AbstractHexMapObject stone = grid.getMapObject(x, y, EMapObjectType.STONE);
if (stone != null) {
stone.cutOff();
if (!stone.canBeCut()) {
addSelfDeletingMapObject(new ShortPoint2D(x, y), EMapObjectType.CUT_OFF_STONE, Stone.DECOMPOSE_DELAY, null);
removeMapObjectType(x, y, EMapObjectType.STONE);
}
}
}
private boolean plantTree(ShortPoint2D pos) {
Tree tree = new Tree(pos);
addMapObject(pos, tree);
schedule(tree, Tree.GROWTH_DURATION, false);
return true;
}
private boolean cutTree(ShortPoint2D pos) {
short x = (short) (pos.x - 1);
short y = (short) (pos.y - 1);
if (grid.isInBounds(x, y)) {
AbstractObjectsManagerObject tree = (AbstractObjectsManagerObject) grid.getMapObject(x, y, EMapObjectType.TREE_ADULT);
if (tree != null && tree.cutOff()) {
schedule(tree, Tree.DECOMPOSE_DURATION, true);
return true;
}
}
return false;
}
private boolean plantCorn(ShortPoint2D pos) {
Corn corn = new Corn(pos);
addMapObject(pos, corn);
schedule(corn, Corn.GROWTH_DURATION, false);
schedule(corn, Corn.GROWTH_DURATION + Corn.DECOMPOSE_DURATION, false);
schedule(corn, Corn.GROWTH_DURATION + Corn.DECOMPOSE_DURATION + Corn.REMOVE_DURATION, true);
return true;
}
private boolean cutCorn(ShortPoint2D pos) {
short x = pos.x;
short y = pos.y;
if (grid.isInBounds(x, y)) {
AbstractObjectsManagerObject corn = (AbstractObjectsManagerObject) grid.getMapObject(x, y, EMapObjectType.CORN_ADULT);
if (corn != null && corn.cutOff()) {
schedule(corn, Corn.REMOVE_DURATION, true);
return true;
}
}
return false;
}
private boolean plantWine(ShortPoint2D pos) {
Wine wine = new Wine(pos);
addMapObject(pos, wine);
schedule(wine, Wine.GROWTH_DURATION, false);
schedule(wine, Wine.GROWTH_DURATION + Wine.DECOMPOSE_DURATION, false);
schedule(wine, Wine.GROWTH_DURATION + Wine.DECOMPOSE_DURATION + Wine.REMOVE_DURATION, true);
return true;
}
private boolean harvestWine(ShortPoint2D pos) {
short x = pos.x;
short y = pos.y;
if (grid.isInBounds(x, y)) {
AbstractObjectsManagerObject wine = (AbstractObjectsManagerObject) grid.getMapObject(x, y, EMapObjectType.WINE_HARVESTABLE);
if (wine != null && wine.cutOff()) {
schedule(wine, Wine.REMOVE_DURATION, true);
return true;
}
}
return false;
}
private final boolean addMapObject(ShortPoint2D pos, AbstractHexMapObject mapObject) {
return addMapObject(pos.x, pos.y, mapObject);
}
private final boolean addMapObject(int x, int y, AbstractHexMapObject mapObject) {
for (RelativePoint point : mapObject.getBlockedTiles()) {
int currX = point.calculateX(x);
int currY = point.calculateY(y);
if (!grid.isInBounds(currX, currY) || grid.isBlocked(currX, currY)) {
return false;
}
}
grid.addMapObject(x, y, mapObject);
mapObject.handlePlacement(x, y, this, grid);
return true;
}
public void removeMapObjectType(int x, int y, EMapObjectType mapObjectType) {
removeMapObject(x, y, grid.getMapObject(x, y, mapObjectType));
}
public void removeMapObject(int x, int y, AbstractHexMapObject mapObject) {
boolean removed = grid.removeMapObject(x, y, mapObject);
if (removed) {
mapObject.handleRemove(x, y, this, grid);
}
}
public void addStone(ShortPoint2D pos, int capacity) {
if (capacity > 0) {
addMapObject(pos, new Stone(capacity));
} else {
addSelfDeletingMapObject(pos, EMapObjectType.CUT_OFF_STONE, Stone.DECOMPOSE_DELAY, null);
}
}
public void plantAdultTree(ShortPoint2D pos) {
addMapObject(pos, new AdultTree(pos));
}
/**
* Adds an arrow object to the map flying from
*
* @param attackedPos
* Attacked position.
* @param shooterPos
* Position of the shooter.
* @param shooterPlayerId
* The player of the shooter.
* @param hitStrength
* Strength of the hit.
*/
public void addArrowObject(ShortPoint2D attackedPos, ShortPoint2D shooterPos, byte shooterPlayerId, float hitStrength) {
ArrowObject arrow = new ArrowObject(grid, attackedPos, shooterPos, shooterPlayerId, hitStrength);
addMapObject(attackedPos, arrow);
schedule(arrow, arrow.getEndTime(), false);
schedule(arrow, arrow.getEndTime() + ArrowObject.MIN_DECOMPOSE_DELAY * (1 + MatchConstants.random().nextFloat()), true);
}
public void addSimpleMapObject(ShortPoint2D pos, EMapObjectType objectType, boolean blocking, Player player) {
addMapObject(pos, new StandardMapObject(objectType, blocking, player != null ? player.playerId : -1));
}
public void addBuildingWorkAreaObject(int x, int y, float radius) {
addMapObject(x, y, new BuildingWorkAreaMarkObject(radius));
}
public void addWineBowl(ShortPoint2D pos, IStackSizeSupplier wineStack) {
addMapObject(pos, new WineBowlMapObject(wineStack));
}
public void addSelfDeletingMapObject(ShortPoint2D pos, EMapObjectType mapObjectType, float duration, Player player) {
SelfDeletingMapObject object;
byte playerId = player != null ? player.playerId : -1;
switch (mapObjectType) {
case GHOST:
case BUILDING_DECONSTRUCTION_SMOKE:
object = new SoundableSelfDeletingObject(pos, mapObjectType, duration, playerId);
break;
default:
object = new SelfDeletingMapObject(pos, mapObjectType, duration, playerId);
break;
}
addMapObject(pos, object);
timingQueue.add(new TimeEvent(object, duration, true));
}
public void setConstructionMarking(int x, int y, byte value) {
if (value >= 0) {
ConstructionMarkObject markObject = (ConstructionMarkObject) grid.getMapObject(x, y, EMapObjectType.CONSTRUCTION_MARK);
if (markObject == null) {
addMapObject(x, y, new ConstructionMarkObject(value));
} else {
markObject.setConstructionValue(value);
}
} else {
removeMapObjectType(x, y, EMapObjectType.CONSTRUCTION_MARK);
}
}
public boolean canPush(ShortPoint2D position) {
StackMapObject stackObject = (StackMapObject) grid.getMapObject(position.x, position.y, EMapObjectType.STACK_OBJECT);
int sum = 0;
while (stackObject != null) { // find correct stack
sum += stackObject.getSize();
stackObject = getNextStackObject(stackObject);
}
return sum < Constants.STACK_SIZE;
}
public final boolean canPop(short x, short y, EMaterialType materialType) {
StackMapObject stackObject = getStackAtPosition(x, y, materialType);
return stackObject != null && (stackObject.getMaterialType() == materialType || materialType == null) && !stackObject.isEmpty();
}
public final void markStolen(short x, short y, boolean mark) {
if (mark) {
StackMapObject stack = getStackAtPosition(x, y, null);
while (stack != null) {
if (stack.hasUnstolen()) {
stack.incrementStolenMarks();
break;
}
stack = getNextStackObject(stack);
}
} else {
StackMapObject stack = getStackAtPosition(x, y, null);
while (stack != null) {
if (stack.hasStolenMarks()) {
stack.decrementStolenMarks();
break;
}
stack = getNextStackObject(stack);
}
}
}
private final static StackMapObject getNextStackObject(StackMapObject stack) {
AbstractHexMapObject next = stack.getNextObject();
if (next != null) {
return (StackMapObject) next.getMapObject(EMapObjectType.STACK_OBJECT);
} else {
return null;
}
}
public boolean pushMaterial(int x, int y, EMaterialType materialType) {
assert materialType != null : "material type can never be null here";
StackMapObject stackObject = getStackAtPosition(x, y, materialType);
if (stackObject == null) {
grid.addMapObject(x, y, new StackMapObject(materialType, (byte) 1));
grid.setProtected(x, y, true);
return true;
} else {
if (stackObject.isFull()) {
return false;
} else {
stackObject.increment();
return true;
}
}
}
public ShortPoint2D pushMaterialForced(int x, int y, EMaterialType materialType) {
return HexGridArea.stream(x, y, 0, 200)
.filterBounds(grid.getWidth(), grid.getHeight())
.filter((currX, currY) -> canForcePushMaterial(currX, currY, materialType))
.iterateForResult((currX, currY) -> {
pushMaterial(currX, currY, materialType);
return Optional.of(new ShortPoint2D(currX, currY));
}).orElse(null);
}
/**
* Checks if there is no stack of another material at the given location and there is space on a stack of this material type.
*
* @param x
* @param y
* @param materialType
* @return
*/
private boolean canForcePushMaterial(int x, int y, EMaterialType materialType) {
if (grid.isBlocked(x, y)) {
return false;
}
byte size = 0;
StackMapObject stackObject = (StackMapObject) grid.getMapObject(x, y, EMapObjectType.STACK_OBJECT);
while (stackObject != null) {
if (stackObject.getMaterialType() != materialType) {
return false;
} else {
size += stackObject.getSize();
}
stackObject = getNextStackObject(stackObject);
}
if (size == 0) {
return !grid.isProtected(x, y); // if there is no stack yet, don't create a stack on protected locations
} else {
return size < Constants.STACK_SIZE; // if there is a stack of this material already, check if it is not full
}
}
public final boolean popMaterial(short x, short y, EMaterialType materialType) {
return popMaterialTypeAt(x, y, materialType) != null;
}
private EMaterialType popMaterialTypeAt(short x, short y, EMaterialType materialType) {
StackMapObject stackObject = getStackAtPosition(x, y, materialType);
if (stackObject == null) {
return null;
} else {
if (stackObject.isEmpty()) {
removeStackObject(x, y, stackObject);
return null;
} else {
stackObject.decrement();
if (stackObject.isEmpty()) { // remove empty stack object
removeStackObject(x, y, stackObject);
}
return stackObject.getMaterialType();
}
}
}
public final EMaterialType getMaterialTypeAt(short x, short y) {
StackMapObject stackObject = (StackMapObject) grid.getMapObject(x, y, EMapObjectType.STACK_OBJECT);
if (stackObject == null) {
return null;
} else {
return stackObject.getMaterialType();
}
}
private final void removeStackObject(short x, short y, StackMapObject stackObject) {
removeMapObject(x, y, stackObject);
if (!grid.isBuildingAreaAt(x, y) && grid.getMapObject(x, y, EMapObjectType.STACK_OBJECT) == null) {
grid.setProtected(x, y, false); // no other stack, so remove protected
}
}
public final byte getStackSize(short x, short y, EMaterialType materialType) {
StackMapObject stackObject = getStackAtPosition(x, y, materialType);
if (stackObject == null) {
return 0;
} else {
return stackObject.getSize();
}
}
public final boolean hasStealableMaterial(int x, int y) {
StackMapObject stackObject = (StackMapObject) grid.getMapObject(x, y, EMapObjectType.STACK_OBJECT);
while (stackObject != null) { // find all stacks
if (stackObject.hasUnstolen()) {
return true;
}
stackObject = getNextStackObject(stackObject);
}
return false;
}
public final EMaterialType stealMaterialAt(short x, short y) {
return popMaterialTypeAt(x, y, null);
}
private StackMapObject getStackAtPosition(int x, int y, EMaterialType materialType) {
StackMapObject stackObject = (StackMapObject) grid.getMapObject(x, y, EMapObjectType.STACK_OBJECT);
while (stackObject != null && stackObject.getMaterialType() != materialType && materialType != null) { // find correct stack
stackObject = getNextStackObject(stackObject);
}
return stackObject;
}
public void addBuildingTo(ShortPoint2D position, AbstractHexMapObject newBuilding) {
addMapObject(position, newBuilding);
}
public void placePig(ShortPoint2D pos, boolean place) {
if (place) {
AbstractHexMapObject pig = grid.getMapObject(pos.x, pos.y, EMapObjectType.PIG);
if (pig == null) {
addMapObject(pos, new PigObject());
}
} else {
removeMapObjectType(pos.x, pos.y, EMapObjectType.PIG);
}
}
public boolean isPigThere(ShortPoint2D pos) {
AbstractHexMapObject pig = grid.getMapObject(pos.x, pos.y, EMapObjectType.PIG);
return pig != null;
}
public boolean isPigAdult(ShortPoint2D pos) {
AbstractHexMapObject pig = grid.getMapObject(pos.x, pos.y, EMapObjectType.PIG);
return pig != null && pig.canBeCut();
}
public boolean feedDonkeyAt(ShortPoint2D position, byte playerId) {
AbstractHexMapObject object = grid.getMapObject(position.x, position.y, EMapObjectType.DONKEY);
DonkeyMapObject donkey;
boolean result;
if (object != null) {
donkey = (DonkeyMapObject) object;
result = donkey.feed();
} else {
donkey = new DonkeyMapObject(position, playerId);
addMapObject(position, donkey);
result = true;
}
if (donkey.isFullyFed()) {
// release it to the world.
grid.spawnDonkey(position, playerId);
removeMapObjectType(position.x, position.y, EMapObjectType.DONKEY);
} else {
timingQueue.add(new TimeEvent(donkey, DonkeyMapObject.FEED_TIME, false));
}
return result;
}
public void addWaves(short x, short y) {
grid.addMapObject(x, y, new DecorationMapObject(EMapObjectType.WAVES));
}
public void addFish(short x, short y) {
grid.addMapObject(x, y, new DecorationMapObject(EMapObjectType.FISH_DECORATION));
}
private static class TimeEvent implements Comparable<TimeEvent>, Serializable {
private static final long serialVersionUID = -4439126418530597713L;
private final AbstractObjectsManagerObject mapObject;
private final int eventTime;
private final boolean shouldRemove;
/**
*
* @param mapObject
* @param duration
* in seconds
* @param shouldRemove
* if true, the map object will be removed after this event
*/
protected TimeEvent(AbstractObjectsManagerObject mapObject, float duration, boolean shouldRemove) {
this.mapObject = mapObject;
this.shouldRemove = shouldRemove;
this.eventTime = (int) (MatchConstants.clock().getTime() + duration * 1000);
}
public boolean isOutDated(int gameTime) {
return gameTime > eventTime;
}
private AbstractObjectsManagerObject getMapObject() {
return mapObject;
}
public boolean shouldRemoveObject() {
return shouldRemove;
}
@Override
public int compareTo(TimeEvent o) {
return this.eventTime - o.eventTime;
}
}
private boolean schedule(AbstractObjectsManagerObject object, float duration, boolean remove) {
return timingQueue.offer(new TimeEvent(object, duration, remove));
}
/**
* Adds an attackable tower map object to the grid.
*
* @param position
* Position the map object will be added.
* @param attackableTowerMapObject
* The object to be added. NOTE: This object must be an instance of {@link IAttackableTowerMapObject}!
*/
public void addAttackableTowerObject(ShortPoint2D position, AbstractHexMapObject attackableTowerMapObject) {
assert attackableTowerMapObject instanceof IAttackableTowerMapObject;
this.addMapObject(position, attackableTowerMapObject);
}
/**
* Adds a map object that informs the given {@link IInformable} about attackable enemies in the area.
*
* @param position
* The position the object should be added.
* @param informable
* The {@link IInformable} that will be informed of enemies.
*/
public void addInformableMapObjectAt(ShortPoint2D position, IInformable informable) {
this.addMapObject(position, new InformableMapObject(informable));
}
}