/******************************************************************************* * Copyright (c) 2015, 2016 * * 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.buildings.military; import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import jsettlers.algorithms.path.IPathCalculatable; import jsettlers.algorithms.path.Path; import jsettlers.algorithms.path.dijkstra.DijkstraAlgorithm.DijkstraContinuableRequest; import jsettlers.common.CommonConstants; import jsettlers.common.buildings.EBuildingType; import jsettlers.common.buildings.IBuilding; import jsettlers.common.buildings.IBuildingOccupier; import jsettlers.common.buildings.OccupierPlace; import jsettlers.common.map.shapes.FreeMapArea; import jsettlers.common.map.shapes.MapCircle; import jsettlers.common.mapobject.EMapObjectType; import jsettlers.common.mapobject.IAttackableTowerMapObject; import jsettlers.common.material.ESearchType; import jsettlers.common.movable.EMovableType; import jsettlers.common.movable.ESoldierClass; import jsettlers.common.movable.ESoldierType; import jsettlers.common.movable.IMovable; import jsettlers.common.position.ShortPoint2D; import jsettlers.common.utils.collections.map.ArrayListMap; import jsettlers.common.utils.collections.map.ArrayListMap.Entry; import jsettlers.graphics.messages.SimpleMessage; import jsettlers.logic.buildings.Building; import jsettlers.logic.buildings.IBuildingsGrid; import jsettlers.logic.constants.Constants; import jsettlers.logic.constants.MatchConstants; import jsettlers.logic.movable.interfaces.ILogicMovable; import jsettlers.logic.movable.interfaces.IAttackable; import jsettlers.logic.movable.interfaces.IAttackableMovable; import jsettlers.logic.objects.StandardMapObject; import jsettlers.logic.player.Player; /** * This is a tower building that can request soldiers and let them defend the building. * * @author Andreas Eberle */ public class OccupyingBuilding extends Building implements IBuilding.IOccupied, IPathCalculatable, IOccupyableBuilding, Serializable { private static final long serialVersionUID = 5267249978497095473L; private static final int TIMER_PERIOD = 500; private final LinkedList<OccupierPlace> emptyPlaces = new LinkedList<>(); private final LinkedList<SoldierRequest> searchedSoldiers = new LinkedList<>(); private final ArrayListMap<IBuildingOccupyableMovable, SoldierRequest> comingSoldiers = new ArrayListMap<>(); private final LinkedList<TowerOccupier> sortedOccupiers = new LinkedList<>(); private final LinkedList<TowerOccupier> toBeReleasedOccupiers = new LinkedList<>(); private DijkstraContinuableRequest dijkstraRequest; private boolean occupiedArea; private float doorHealth = 50f; private boolean inFight = false; private AttackableTowerMapObject attackableTowerObject = null; public OccupyingBuilding(EBuildingType type, Player player, ShortPoint2D position, IBuildingsGrid buildingsGrid) { super(type, player, position, buildingsGrid); initSoldierRequests(); } private void initSoldierRequests() { final OccupierPlace[] occupierPlaces = super.getBuildingType().getOccupierPlaces(); if (occupierPlaces.length > 0) { emptyPlaces.addAll(Arrays.asList(occupierPlaces)); requestSoldiers(); } } @Override protected final int constructionFinishedEvent() { setAttackableTowerObject(true); return TIMER_PERIOD + MatchConstants.random().nextInt(200); // adding random prevents simultaneous scan after map creation } private void setAttackableTowerObject(boolean set) { if (set) { attackableTowerObject = new AttackableTowerMapObject(); super.grid.getMapObjectsManager().addAttackableTowerObject(getDoor(), attackableTowerObject); } else { super.grid.getMapObjectsManager().removeMapObjectType(getDoor().x, getDoor().y, EMapObjectType.ATTACKABLE_TOWER); attackableTowerObject = null; } } void changePlayerTo(ShortPoint2D attackerPos) { assert sortedOccupiers.isEmpty() : "there cannot be any occupiers in the tower when changing the player."; ILogicMovable attacker = super.grid.getMovable(attackerPos); Player newPlayer = attacker.getPlayer(); setAttackableTowerObject(false); super.showFlag(false); resetSoldierSearch(); super.setPlayer(newPlayer); if (occupiedArea) { // free the area if it had been occupied. super.grid.changePlayerOfTower(super.pos, newPlayer, getGroundArea()); } else { occupyAreaIfNeeded(); } initSoldierRequests(); searchedSoldiers.remove(ESearchType.SOLDIER_SWORDSMAN); IBuildingOccupyableMovable newOccupier = attacker.setOccupyableBuilding(this); comingSoldiers.put(newOccupier, searchedSoldiers.pop()); doorHealth = 0.1f; inFight = false; super.showFlag(true); setAttackableTowerObject(true); } private FreeMapArea getGroundArea() { return new FreeMapArea(super.pos, getBuildingType().getProtectedTiles()); } private void resetSoldierSearch() { dijkstraRequest = null; searchedSoldiers.clear(); emptyPlaces.clear(); comingSoldiers.clear(); } @Override protected final void appearedEvent() { occupyAreaIfNeeded(); searchedSoldiers.remove(ESearchType.SOLDIER_SWORDSMAN); } @Override protected final EMapObjectType getFlagType() { return EMapObjectType.FLAG_DOOR; } @Override protected final int subTimerEvent() { increaseDoorHealth(); releaseNextSoldierIfNeeded(); searchSoldiersIfNeeded(); return TIMER_PERIOD; } private void increaseDoorHealth() { if (doorHealth < 1 && !inFight) { doorHealth = Math.min(1, doorHealth + Constants.TOWER_DOOR_REGENERATION); } } private void releaseNextSoldierIfNeeded() { if (!toBeReleasedOccupiers.isEmpty()) { ILogicMovable movableAtDoor = grid.getMovable(super.getDoor()); if (movableAtDoor == null) { TowerOccupier releasedOccupier = toBeReleasedOccupiers.pop(); sortedOccupiers.remove(releasedOccupier); emptyPlaces.add(releasedOccupier.place); IBuildingOccupyableMovable soldier = releasedOccupier.soldier; soldier.leaveOccupyableBuilding(super.getDoor()); } else { movableAtDoor.leavePosition(); } } } private void searchSoldiersIfNeeded() { if (!searchedSoldiers.isEmpty()) { if (dijkstraRequest == null) { dijkstraRequest = new DijkstraContinuableRequest(this, super.pos.x, super.pos.y, (short) 1, Constants.TOWER_SEARCH_RADIUS); } Set<ESearchType> searchTypes = EnumSet.noneOf(ESearchType.class); for (SoldierRequest soldierRequest : searchedSoldiers) { searchTypes.add(soldierRequest.getSearchType()); } dijkstraRequest.setSearchTypes(searchTypes); Path path = super.grid.getDijkstra().find(dijkstraRequest); if (path != null) { ILogicMovable soldier = super.grid.getMovable(path.getTargetPos()); if (soldier != null) { IBuildingOccupyableMovable occupier = soldier.setOccupyableBuilding(this); if (occupier != null) { SoldierRequest soldierRequest = removeMatchingSoldierRequest(occupier.getMovableType().getSoldierType()); comingSoldiers.put(occupier, soldierRequest); dijkstraRequest.reset(); } // else soldier wasn't able to take the job to go to this building } // else { soldier wasn't at the position } } } private SoldierRequest removeMatchingSoldierRequest(ESoldierType soldierType) { for (Iterator<SoldierRequest> iterator = searchedSoldiers.iterator(); iterator.hasNext();) { SoldierRequest soldierRequest = iterator.next(); if (soldierRequest.isOfTypeOrClass(soldierType)) { iterator.remove(); return soldierRequest; } } return null; } @Override protected final void killedEvent() { setSelected(false); if (occupiedArea) { freeArea(); int idx = 0; FreeMapArea buildingArea = super.getBuildingArea(); for (TowerOccupier curr : sortedOccupiers) { addInformableMapObject(curr, false);// if curr is a bowman, this removes the informable map object. curr.getSoldier().leaveOccupyableBuilding(buildingArea.get(idx)); idx++; } sortedOccupiers.clear(); } if (attackableTowerObject != null && attackableTowerObject.currDefender != null) { attackableTowerObject.currDefender.soldier.leaveOccupyableBuilding(attackableTowerObject.getPos()); } setAttackableTowerObject(false); } private void freeArea() { super.grid.freeAreaOccupiedByTower(super.pos); } @Override public final List<? extends IBuildingOccupier> getOccupiers() { return sortedOccupiers; } @Override public final boolean needsPlayersGround() { // soldiers don't need players ground. return false; } @Override public final OccupierPlace addSoldier(IBuildingOccupyableMovable soldier) { SoldierRequest soldierRequest = comingSoldiers.remove(soldier); OccupierPlace place = soldierRequest.place; TowerOccupier towerOccupier = new TowerOccupier(place, soldier); addOccupier(towerOccupier); occupyAreaIfNeeded(); soldier.setSelected(super.isSelected()); addInformableMapObject(towerOccupier, true); return place; } private void addOccupier(TowerOccupier towerOccupier) { sortedOccupiers.add(towerOccupier); Collections.sort(sortedOccupiers, new Comparator<TowerOccupier>() { @Override public int compare(TowerOccupier o1, TowerOccupier o2) { return o1.place.getSoldierClass().ordinal - o2.place.getSoldierClass().ordinal; } }); } @Override public void removeSoldier(IBuildingOccupyableMovable soldier) { TowerOccupier occupier = null; for (TowerOccupier currOccupier : sortedOccupiers) { if (currOccupier.soldier == soldier) { occupier = currOccupier; break; } } // if the soldier is not in the tower, just return if (occupier == null) { return; } // remove the soldier and dijkstraRequest a new one sortedOccupiers.remove(occupier); emptyPlaces.add(occupier.place); requestSoldier(occupier.place.getSoldierClass()); } protected TowerOccupier removeSoldier() { TowerOccupier removedSoldier = sortedOccupiers.removeFirst(); addInformableMapObject(removedSoldier, false); return removedSoldier; } /** * Adds or removes the informable map object for the given soldier. * * @param soldier * @param add * if true, the object is added<br> * if false, the object is removed. */ private void addInformableMapObject(TowerOccupier soldier, boolean add) { if (soldier.place.getSoldierClass() == ESoldierClass.BOWMAN) { ShortPoint2D position = getTowerBowmanSearchPosition(soldier.place); if (add) { super.grid.getMapObjectsManager().addInformableMapObjectAt(position, soldier.getSoldier().getMovable()); } else { super.grid.getMapObjectsManager().removeMapObjectType(position.x, position.y, EMapObjectType.INFORMABLE_MAP_OBJECT); } } } @Override public ShortPoint2D getTowerBowmanSearchPosition(OccupierPlace place) { ShortPoint2D pos = place.getPosition().calculatePoint(super.pos); // FIXME @Andreas Eberle introduce new field in the buildings xml file ShortPoint2D position = new ShortPoint2D(pos.x + 3, pos.y + 6); return position; } private final void occupyAreaIfNeeded() { if (!occupiedArea) { MapCircle occupying = new MapCircle(super.pos, CommonConstants.TOWER_RADIUS); super.grid.occupyAreaByTower(super.getPlayer(), occupying, getGroundArea()); occupiedArea = true; } } @Override public final void requestFailed(IBuildingOccupyableMovable soldier) { SoldierRequest soldierRequest = comingSoldiers.remove(soldier); searchedSoldiers.add(soldierRequest); } @Override public final ShortPoint2D getPosition(IBuildingOccupyableMovable soldier) { for (TowerOccupier curr : sortedOccupiers) { if (curr.getSoldier() == soldier) { return curr.place.getPosition().calculatePoint(super.pos); } } return null; } @Override public final void setSelected(boolean selected) { super.setSelected(selected); for (TowerOccupier curr : sortedOccupiers) { curr.getSoldier().setSelected(selected); } if (attackableTowerObject != null && attackableTowerObject.currDefender != null) { attackableTowerObject.currDefender.getSoldier().setSelected(selected); } } @Override public final boolean isOccupied() { return !sortedOccupiers.isEmpty() || inFight; } @Override public void towerDefended(IBuildingOccupyableMovable soldier) { inFight = false; if (attackableTowerObject.currDefender == null) { System.err.println("ERROR: WHAT? No defender in a defended tower!"); } else { TowerOccupier towerOccupier = new TowerOccupier(attackableTowerObject.currDefender.place, soldier); addOccupier(towerOccupier); attackableTowerObject.currDefender = null; addInformableMapObject(towerOccupier, true); } doorHealth = 0.1f; } @Override public int getSearchedSoldiers(ESoldierClass soldierClass) { int numberOfSearchedSoldiers = 0; for (SoldierRequest searchedSoldier : searchedSoldiers) { if (searchedSoldier.isOfTypeOrClass(soldierClass)) { numberOfSearchedSoldiers++; } } return numberOfSearchedSoldiers; } @Override public int getComingSoldiers(ESoldierClass soldierClass) { int numberOfComingSoldiers = 0; for (Entry<IBuildingOccupyableMovable, SoldierRequest> comingSoldier : comingSoldiers.entrySet()) { if (comingSoldier.getValue().isOfTypeOrClass(soldierClass)) { numberOfComingSoldiers++; } } return numberOfComingSoldiers; } @Override public void requestSoldier(ILogicMovable soldier) { if (!soldier.getMovableType().isSoldier()) { return; } ESoldierClass soldierClass = soldier.getMovableType().getSoldierClass(); OccupierPlace emptyPlace = getEmptyPlaceForSoldierClass(soldierClass); if (emptyPlace == null) { return; } IBuildingOccupyableMovable occupier = soldier.setOccupyableBuilding(this); comingSoldiers.put(occupier, new SoldierRequest(soldierClass, emptyPlace)); } public void requestSoldiers() { for (OccupierPlace emptyPlace : emptyPlaces) { searchedSoldiers.add(new SoldierRequest(emptyPlace.getSoldierClass(), emptyPlace)); } emptyPlaces.clear(); } public void requestSoldier(ESoldierType soldierType) { OccupierPlace emptyPlace = getEmptyPlaceForSoldierClass(soldierType.getSoldierClass()); if (emptyPlace != null) { emptyPlaces.remove(emptyPlace); searchedSoldiers.add(new SoldierRequest(soldierType, emptyPlace)); } } private void requestSoldier(ESoldierClass soldierClass) { OccupierPlace emptyPlace = getEmptyPlaceForSoldierClass(soldierClass); if (emptyPlace != null) { emptyPlaces.remove(emptyPlace); searchedSoldiers.add(new SoldierRequest(soldierClass, emptyPlace)); } } public void releaseSoldiers() { toBeReleasedOccupiers.clear(); toBeReleasedOccupiers.addAll(sortedOccupiers); // release all but first occupier toBeReleasedOccupiers.removeFirst(); for (SoldierRequest searchedSoldier : searchedSoldiers) { emptyPlaces.add(searchedSoldier.place); } searchedSoldiers.clear(); for (Entry<IBuildingOccupyableMovable, SoldierRequest> commingSoldierEntry : comingSoldiers.entrySet()) { commingSoldierEntry.getKey().leaveOccupyableBuilding(super.getDoor()); emptyPlaces.add(commingSoldierEntry.getValue().place); } comingSoldiers.clear(); } public void releaseSoldier(ESoldierType soldierType) { for (Iterator<SoldierRequest> iterator = searchedSoldiers.iterator(); iterator.hasNext();) { SoldierRequest soldierRequest = iterator.next(); if (soldierRequest.isOfTypeOrClass(soldierType)) { iterator.remove(); emptyPlaces.add(soldierRequest.place); } } for (Entry<IBuildingOccupyableMovable, SoldierRequest> commingSoldierEntry : comingSoldiers.entrySet()) { if (commingSoldierEntry.getValue().isOfTypeOrClass(soldierType)) { commingSoldierEntry.getKey().leaveOccupyableBuilding(super.getDoor()); emptyPlaces.add(commingSoldierEntry.getValue().place); comingSoldiers.remove(commingSoldierEntry.getKey()); return; } } if (sortedOccupiers.size() - toBeReleasedOccupiers.size() > 1) { // always keep one soldier inside for (TowerOccupier occupier : sortedOccupiers) { if (occupier.soldier.getMovableType().getSoldierType() == soldierType && !toBeReleasedOccupiers.contains(occupier)) { toBeReleasedOccupiers.add(occupier); return; } } } } private OccupierPlace getEmptyPlaceForSoldierClass(ESoldierClass soldierClass) { for (OccupierPlace place : emptyPlaces) { if (place.getSoldierClass() == soldierClass) { return place; } } return null; } /** * This map object lies at the door of a tower and is used to signal soldiers that there is something to attack. * * @author Andreas Eberle */ public class AttackableTowerMapObject extends StandardMapObject implements IAttackable, IAttackableTowerMapObject { private static final long serialVersionUID = -5137593316096740750L; private TowerOccupier currDefender; public AttackableTowerMapObject() { super(EMapObjectType.ATTACKABLE_TOWER, false, OccupyingBuilding.this.getPlayerId()); } @Override public ShortPoint2D getPos() { return OccupyingBuilding.this.getDoor(); } @Override public void receiveHit(float strength, ShortPoint2D attackerPos, byte attackingPlayer) { if(!OccupyingBuilding.this.isNotDestroyed()){ return; // building is destroyed => do nothing } ILogicMovable attacker = grid.getMovable(attackerPos); if (attacker != null && attacker.getPlayer() == getPlayer()) { return; // this can happen directly after the tower changed its player } if (doorHealth > 0) { doorHealth -= strength / Constants.DOOR_HIT_RESISTENCY_FACTOR; if (doorHealth <= 0) { doorHealth = 0; inFight = true; OccupyingBuilding.this.grid.getMapObjectsManager() .addSelfDeletingMapObject(getPos(), EMapObjectType.GHOST, Constants.GHOST_PLAY_DURATION, getPlayer()); pullNewDefender(attackerPos); } } else if (currDefender != null) { IAttackableMovable movable = currDefender.getSoldier().getMovable(); movable.receiveHit(strength, attackerPos, attackingPlayer); if (movable.getHealth() <= 0) { emptyPlaces.add(currDefender.place); // dijkstraRequest a new soldier. requestSoldier(currDefender.place.getSoldierClass()); pullNewDefender(attackerPos); } } OccupyingBuilding.this.getPlayer().showMessage(SimpleMessage.attacked(attackingPlayer, attackerPos)); } private void pullNewDefender(ShortPoint2D attackerPos) { if (sortedOccupiers.isEmpty()) { currDefender = null; changePlayerTo(attackerPos); } else { currDefender = removeSoldier(); currDefender.getSoldier().setDefendingAt(getPos()); } } @Override public float getHealth() { if (doorHealth > 0) { return doorHealth; } else { return currDefender == null ? 0 : currDefender.getMovable().getHealth(); } } @Override public boolean isAttackable() { return true; } @Override public IMovable getMovable() { return currDefender == null ? null : currDefender.getSoldier().getMovable(); } @Override public EMovableType getMovableType() { assert false : "This should never have been called"; return EMovableType.SWORDSMAN_L1; } @Override public void informAboutAttackable(IAttackable attackable) { } @Override public boolean isTower() { return true; } } private final static class TowerOccupier implements IBuildingOccupier, Serializable { private static final long serialVersionUID = -1491427078923346232L; final OccupierPlace place; final IBuildingOccupyableMovable soldier; TowerOccupier(OccupierPlace place, IBuildingOccupyableMovable soldier) { this.place = place; this.soldier = soldier; } @Override public OccupierPlace getPlace() { return place; } @Override public IMovable getMovable() { return soldier.getMovable(); } public IBuildingOccupyableMovable getSoldier() { return soldier; } } private static class SoldierRequest implements Serializable { final ESoldierClass soldierClass; final ESoldierType soldierType; final OccupierPlace place; SoldierRequest(ESoldierType soldierType, OccupierPlace place) { this.soldierType = soldierType; soldierClass = null; this.place = place; } SoldierRequest(ESoldierClass soldierClass, OccupierPlace place) { this.soldierClass = soldierClass; soldierType = null; this.place = place; } public ESearchType getSearchType() { if (soldierClass != null) { switch (soldierClass) { case INFANTRY: return ESearchType.SOLDIER_INFANTRY; case BOWMAN: return ESearchType.SOLDIER_BOWMAN; } } else { switch (soldierType) { case SWORDSMAN: return ESearchType.SOLDIER_SWORDSMAN; case PIKEMAN: return ESearchType.SOLDIER_PIKEMAN; case BOWMAN: return ESearchType.SOLDIER_BOWMAN; } } throw new RuntimeException("Unknown soldier or search type"); } public boolean isOfTypeOrClass(ESoldierType soldierType) { return this.soldierType == soldierType || soldierClass == soldierType.getSoldierClass(); } public boolean isOfTypeOrClass(ESoldierClass soldierClass) { return this.soldierClass == soldierClass || (this.soldierType != null && this.soldierType.getSoldierClass() == soldierClass); } } }