package com.lyndir.omicron.api;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import static com.lyndir.omicron.api.error.ExceptionUtils.*;
import static com.lyndir.omicron.api.util.PathUtils.*;
import com.lyndir.lhunath.opal.system.util.*;
import com.lyndir.omicron.api.error.*;
import java.util.*;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class MobilityModule extends Module implements IMobilityModule, IMobilityModuleController {
private final int movementSpeed;
private final Map<LevelType, Double> movementCost = Collections.synchronizedMap( new EnumMap<>( LevelType.class ) );
private final Map<LevelType, Double> levelingCost = Collections.synchronizedMap( new EnumMap<>( LevelType.class ) );
private double remainingSpeed;
protected MobilityModule(final ImmutableResourceCost resourceCost, final int movementSpeed, final Map<LevelType, Double> movementCost,
final Map<LevelType, Double> levelingCost) {
super( resourceCost );
this.movementSpeed = movementSpeed;
this.movementCost.putAll( movementCost );
this.levelingCost.putAll( levelingCost );
}
static Builder0 createWithStandardResourceCost() {
return createWithExtraResourceCost( ResourceCost.immutable() );
}
static Builder0 createWithExtraResourceCost(final ImmutableResourceCost resourceCost) {
return new Builder0( ModuleType.MOBILITY.getStandardCost().add( resourceCost ) );
}
@Override
public double getRemainingSpeed()
throws NotAuthenticatedException, NotObservableException {
assertObservable();
return remainingSpeed;
}
@Override
public double getMovementSpeed() {
assertObservable();
return movementSpeed;
}
/**
* Get the speed cost related to moving around in the given level.
*
* @param levelType The level to move around in.
*
* @return The speed cost.
*/
@Override
public double costForMovingInLevel(final LevelType levelType)
throws NotAuthenticatedException, NotObservableException {
assertObservable();
return ifNotNullElse( movementCost.get( levelType ), Double.MAX_VALUE );
}
@Override
public double costForLevelingToLevel(final LevelType levelType)
throws NotAuthenticatedException, NotObservableException {
assertObservable();
Tile location = getGameObject().getLocation().get();
// Level up until we reach the target level.
double cost = 0;
LevelType currentLevel = location.getLevel().getType();
if (levelType == currentLevel)
return 0;
while (true) {
Optional<LevelType> newLevel = currentLevel.up();
if (!newLevel.isPresent())
break;
Double currentLevelCost = levelingCost.get( EnumUtils.min( currentLevel, newLevel.get() ) );
if (currentLevelCost == null)
// Cannot level to this level.
return Double.MAX_VALUE;
currentLevel = newLevel.get();
cost += currentLevelCost;
if (currentLevel == levelType)
return cost;
}
// Level down until we reach the target level.
cost = 0;
currentLevel = location.getLevel().getType();
while (true) {
Optional<LevelType> newLevel = currentLevel.down();
if (!newLevel.isPresent())
break;
Double currentLevelCost = levelingCost.get( EnumUtils.min( currentLevel, newLevel.get() ) );
if (currentLevelCost == null)
// Cannot level to this level.
return Double.MAX_VALUE;
currentLevel = newLevel.get();
cost += currentLevelCost;
if (currentLevel == levelType)
return cost;
}
// Unreachable code.
throw new IllegalArgumentException( "Unsupported level type: " + levelType );
}
/**
* Move the unit to the given level.
*
* @param levelType The side of the adjacent tile relative to the current.
*/
@Override
public Leveling leveling(final LevelType levelType)
throws NotAuthenticatedException, NotOwnedException, NotObservableException {
assertOwned();
Tile currentLocation = getGameObject().getLocation().get();
if (levelType == currentLocation.getLevel().getType())
// Already in the destination level.
return Leveling.possible( this, currentLocation, 0 );
double cost = costForLevelingToLevel( levelType );
if (cost > remainingSpeed)
// Cannot move: insufficient speed remaining this turn.
return Leveling.impossible( this, cost );
return Leveling.possible( this, getGameObject().getGame().getLevel( levelType ).getTile( currentLocation.getPosition() ).get(),
cost );
}
/**
* Move the unit to an adjacent tile.
*
* @param target The side of the adjacent tile relative to the current.
*/
@Override
public Movement movement(final ITile target)
throws NotAuthenticatedException, NotOwnedException, NotObservableException {
assertOwned();
Leveling leveling = leveling( target.getLevel().getType() );
if (!leveling.isPossible())
// Cannot move because we can't level to the target's level.
return Movement.impossible( this, leveling.getCost() );
// Initialize cost calculation.
ITile currentLocation = leveling.getTarget();
final double stepCost = costForMovingInLevel( currentLocation.getLevel().getType() );
// Initialize path finding data functions.
PredicateNN<ITile> foundFunction = tile -> tile.equals( target );
NNFunctionNN<Step<ITile>, Double> costFunction = tileStep -> {
if (!tileStep.getTo().isAccessible().isTrue())
return Double.MAX_VALUE;
return stepCost;
};
NNFunctionNN<ITile, Stream<? extends ITile>> neighboursFunction = (input) -> input.neighbours().stream();
// Find the path!
Optional<Path<ITile>> path = find( currentLocation, foundFunction, costFunction, remainingSpeed - leveling.getCost(),
neighboursFunction );
return Movement.possible( this, leveling.getCost() + (path.isPresent()? path.get().getCost(): 0), leveling, path );
}
@Override
protected void onReset() {
remainingSpeed = movementSpeed;
}
@Override
protected void onNewTurn() {
}
@Override
public IMobilityModuleController getController() {
return this;
}
@Override
public IMobilityModule getModule() {
return this;
}
public static class Leveling extends MetaObject implements IMobilityModuleController.ILeveling {
private final MobilityModule module;
private final double cost;
private final Optional<ITile> target;
private Leveling(final MobilityModule module, final Optional<ITile> target, final double cost) {
this.module = module;
this.cost = cost;
this.target = target;
}
static Leveling impossible(final MobilityModule module, final double cost) {
return new Leveling( module, Optional.empty(), cost );
}
static Leveling possible(final MobilityModule module, final ITile target, final double cost) {
return new Leveling( module, Optional.of( target ), cost );
}
@Override
public boolean isPossible() {
return target.isPresent();
}
/**
* The cost for executing the leveling. If not possible, the cost is either zero if unknown or the cost for the action that
* exceeded the module's remaining speed (not the cost of getting to the target).
*
* @return An amount of speed.
*/
@Override
public double getCost() {
return cost;
}
/**
* @return The target tile after leveling.
*
* @throws IllegalStateException if the leveling is not possible ({@link #isPossible()} returns {@code false})
*/
@Override
public ITile getTarget() {
return target.get();
}
@Override
public void execute()
throws NotAuthenticatedException, NotOwnedException, ImpossibleException, InvalidatedException {
module.assertOwned();
assertState( isPossible(), ImpossibleException.class );
assertState( cost <= module.remainingSpeed, InvalidatedException.class );
// TODO: No target.isAccessible check: Most units that level cannot see other levels before they go there.
// TODO: Should we disallow leveling until you can see the level above you like we do with movement and the tile you move to?
Change.From<ITile> locationChange = Change.<ITile>from( module.getGameObject().getLocation().get() );
ChangeDbl.From remainingSpeedChange = ChangeDbl.from( module.remainingSpeed );
// Execute the leveling.
module.getGameObject().getController().setLocation( Tile.cast( target.get() ) );
module.remainingSpeed -= cost;
module.getGameObject()
.getGame()
.getController()
.fireIfObservable( module.getGameObject() )
.onMobilityLeveled( module, locationChange.to( module.getGameObject().getLocation().get() ),
remainingSpeedChange.to( module.remainingSpeed ) );
}
}
public static class Movement extends MetaObject implements IMovement {
private final MobilityModule module;
private final double cost;
private final Leveling leveling;
private final Optional<Path<ITile>> path;
private Movement(final MobilityModule module, final double cost, @Nullable final Leveling leveling,
final Optional<Path<ITile>> path) {
this.module = module;
this.cost = cost;
this.leveling = leveling;
this.path = path;
}
static Movement impossible(final MobilityModule module, final double cost) {
return new Movement( module, cost, null, Optional.empty() );
}
static Movement possible(final MobilityModule module, final double cost, @Nonnull final Leveling leveling,
final Optional<Path<ITile>> path) {
return new Movement( module, cost, leveling, path );
}
/**
* The cost for executing the movement. If not possible, the cost is either zero if unknown or the cost for the action that
* exceeded the module's remaining speed (not the cost of getting to the target).
*
* @return An amount of speed.
*/
@Override
public double getCost() {
return cost;
}
/**
* @return The target tile after leveling.
*
* @throws IllegalStateException if the leveling is not possible ({@link #isPossible()} returns {@code false})
*/
@Override
public Path<ITile> getPath() {
return path.get();
}
@Override
public boolean isPossible() {
return path.isPresent();
}
@Override
public void execute()
throws NotAuthenticatedException, NotOwnedException, ImpossibleException, InvalidatedException {
module.assertOwned();
assertState( isPossible(), ImpossibleException.class );
assertState( cost <= module.remainingSpeed, InvalidatedException.class );
assert leveling != null;
Change.From<ITile> locationChange = Change.<ITile>from( module.getGameObject().getLocation().get() );
ChangeDbl.From remainingSpeedChange = ChangeDbl.from( module.remainingSpeed );
// Check that the path can still be walked.
Path<ITile> tracePath = path.get();
while (true) {
assertState( tracePath.getTarget().isAccessible().isTrue() || //
module.getGameObject().getLocation().get().equals( tracePath.getTarget() ), //
PathInvalidatedException.class, tracePath );
Optional<Path<ITile>> parent = tracePath.getParent();
if (!parent.isPresent())
break;
tracePath = parent.get();
}
// Execute the leveling.
leveling.execute();
// Execute the path.
module.getGameObject().getController().setLocation( Tile.cast( path.get().getTarget() ) );
module.remainingSpeed -= path.get().getCost();
module.getGameObject()
.getGame()
.getController()
.fireIfObservable( module.getGameObject() )
.onMobilityMoved( module, locationChange.to( module.getGameObject().getLocation().get() ),
remainingSpeedChange.to( module.remainingSpeed ) );
}
}
@SuppressWarnings({ "ParameterHidesMemberVariable", "InnerClassFieldHidesOuterClassField" })
static class Builder0 {
private final ImmutableResourceCost resourceCost;
private Builder0(final ImmutableResourceCost resourceCost) {
this.resourceCost = resourceCost;
}
Builder1 movementSpeed(final int movementSpeed) {
return new Builder1( movementSpeed );
}
class Builder1 {
private final int movementSpeed;
private Builder1(final int movementSpeed) {
this.movementSpeed = movementSpeed;
}
Builder2 movementCost(final Map<LevelType, Double> movementCost) {
return new Builder2( movementCost );
}
class Builder2 {
private final Map<LevelType, Double> movementCost;
private Builder2(final Map<LevelType, Double> movementCost) {
this.movementCost = movementCost;
}
MobilityModule levelingCost(final Map<LevelType, Double> levelingCost) {
return new MobilityModule( resourceCost, movementSpeed, movementCost, levelingCost );
}
}
}
}
}