* @return true if the entity was removed from play
*/
private boolean processSkid(Entity entity, Coords start, int elevation, int direction, int distance, MoveStep step) {
Coords nextPos = start;
Coords curPos = nextPos;
IHex curHex = game.getBoard().getHex(start);
Report r;
int skidDistance = 0; // actual distance moved
ArrayList<Entity> avoidedChargeUnits = new ArrayList<Entity>();
while (!entity.isDoomed() && (distance > 0)) {
nextPos = curPos.translated(direction);
// Is the next hex off the board?
if (!game.getBoard().contains(nextPos)) {
// Can the entity skid off the map?
if (game.getOptions().booleanOption("push_off_board")) {
// Yup. One dead entity.
game.removeEntity(entity.getId(), IEntityRemovalConditions.REMOVE_PUSHED);
send(createRemoveEntityPacket(entity.getId(), IEntityRemovalConditions.REMOVE_PUSHED));
r = new Report(2030, Report.PUBLIC);
r.addDesc(entity);
addReport(r);
for (Entity e : entity.getLoadedUnits()) {
game.removeEntity(e.getId(), IEntityRemovalConditions.REMOVE_PUSHED);
send(createRemoveEntityPacket(e.getId(), IEntityRemovalConditions.REMOVE_PUSHED));
}
Entity swarmer = game.getEntity(entity.getSwarmAttackerId());
if (swarmer != null) {
if (!swarmer.isDone()) {
swarmer.setDone(true);
game.removeTurnFor(swarmer);
send(createTurnVectorPacket());
}
game.removeEntity(swarmer.getId(), IEntityRemovalConditions.REMOVE_PUSHED);
send(createRemoveEntityPacket(swarmer.getId(), IEntityRemovalConditions.REMOVE_PUSHED));
}
// The entity's movement is completed.
return true;
}
// Nope. Update the report.
r = new Report(2035);
r.subject = entity.getId();
r.indent();
addReport(r);
// Stay in the current hex and stop skidding.
break;
}
IHex nextHex = game.getBoard().getHex(nextPos);
distance -= nextHex.movementCost(entity.getMovementMode()) + 1;
// By default, the unit is going to fall to the floor of the next
// hex
int curAltitude = elevation + curHex.getElevation();
int nextAltitude = nextHex.floor();
// but VTOL keep altitude
if (entity.getMovementMode() == IEntityMovementMode.VTOL) {
nextAltitude = Math.max(nextAltitude, curAltitude);
} else {
// Is there a building to "catch" the unit?
if (nextHex.containsTerrain(Terrains.BLDG_ELEV)) {
// unit will land on the roof, if at a higher level,
// otherwise it will skid through the wall onto the same
// floor.
nextAltitude = Math.min(curAltitude, nextHex.getElevation() + nextHex.terrainLevel(Terrains.BLDG_ELEV));
}
// Is there a bridge to "catch" the unit?
if (nextHex.containsTerrain(Terrains.BRIDGE)) {
// unit will land on the bridge, if at a higher level,
// and the bridge exits towards the current hex,
// otherwise the bridge has no effect
int exitDir = (direction + 3) % 6;
exitDir = 1 << exitDir;
if ((nextHex.getTerrain(Terrains.BRIDGE).getExits() & exitDir) == exitDir) {
nextAltitude = Math.min(curAltitude, Math.max(nextAltitude, nextHex.getElevation() + nextHex.terrainLevel(Terrains.BRIDGE_ELEV)));
}
}
if ((nextAltitude <= nextHex.surface()) && (curAltitude >= curHex.surface())) {
// Hovercraft can "skid" over water.
// all units can skid over ice.
if ((entity instanceof Tank) && ((entity.getMovementMode() == IEntityMovementMode.HOVER) || (entity.getMovementMode() == IEntityMovementMode.WIGE))) {
if (nextHex.containsTerrain(Terrains.WATER)) {
nextAltitude = nextHex.surface();
}
} else {
if (nextHex.containsTerrain(Terrains.ICE)) {
nextAltitude = nextHex.surface();
}
}
}
}
// The elevation the skidding unit will occupy in next hex
int nextElevation = nextAltitude - nextHex.surface();
boolean crashedIntoTerrain = curAltitude < nextAltitude;
if (entity.getMovementMode() == IEntityMovementMode.VTOL) {
if ((nextElevation == 0) || ((nextElevation == 1) && (nextHex.containsTerrain(Terrains.WOODS) || nextHex.containsTerrain(Terrains.JUNGLE)))) {
crashedIntoTerrain = true;
}
}
if (nextHex.containsTerrain(Terrains.BLDG_ELEV)) {
Building bldg = game.getBoard().getBuildingAt(nextPos);
if (bldg.getType() == Building.WALL) {
crashedIntoTerrain = true;
}
}
// however WIGE can gain 1 level to avoid crashing into the terrain
if (entity.getMovementMode() == IEntityMovementMode.WIGE) {
if ((nextElevation == 0) && !(nextHex.containsTerrain(Terrains.WOODS) || nextHex.containsTerrain(Terrains.JUNGLE))) {
nextElevation = 1;
crashedIntoTerrain = false;
} else if ((nextElevation == 1) && (nextHex.containsTerrain(Terrains.WOODS) || nextHex.containsTerrain(Terrains.JUNGLE))) {
nextElevation = 2;
crashedIntoTerrain = false;
}
}
if (crashedIntoTerrain) {
if (nextHex.containsTerrain(Terrains.BLDG_ELEV)) {
Building bldg = game.getBoard().getBuildingAt(nextPos);
// If you crash into a wall you want to stop in the hex
// before the wall not in the wall
// Like a building.
if (bldg.getType() == Building.WALL) {
r = new Report(2047);
} else {
r = new Report(2045);
}
} else {
r = new Report(2045);
}
r.subject = entity.getId();
r.indent();
r.add(nextPos.getBoardNum(), true);
addReport(r);
if ((entity.getMovementMode() == IEntityMovementMode.WIGE) || (entity.getMovementMode() == IEntityMovementMode.VTOL)) {
int hitSide = step.getFacing() - direction + 6;
hitSide %= 6;
int table = 0;
switch (hitSide) {// quite hackish...I think it ought to
// work, though.
case 0:// can this happen?
table = ToHitData.SIDE_FRONT;
break;
case 1:
case 2:
table = ToHitData.SIDE_LEFT;
break;
case 3:
table = ToHitData.SIDE_REAR;
break;
case 4:
case 5:
table = ToHitData.SIDE_RIGHT;
break;
}
elevation = nextElevation;
addReport(crashVTOLorWiGE((VTOL) entity, true, distance, curPos, elevation, table));
if ((nextHex.containsTerrain(Terrains.WATER) && !nextHex.containsTerrain(Terrains.ICE)) || nextHex.containsTerrain(Terrains.WOODS) || nextHex.containsTerrain(Terrains.JUNGLE)) {
addReport(destroyEntity(entity, "could not land in crash site"));
} else if (elevation < nextHex.terrainLevel(Terrains.BLDG_ELEV)) {
Building bldg = game.getBoard().getBuildingAt(nextPos);
// If you crash into a wall you want to stop in the hex
// before the wall not in the wall
// Like a building.
if (bldg.getType() == Building.WALL) {
addReport(destroyEntity(entity, "crashed into a wall"));
break;
}
addReport(destroyEntity(entity, "crashed into building"));
} else {
entity.setPosition(nextPos);
entity.setElevation(0);
addReport(doEntityDisplacementMinefieldCheck(entity, curPos, nextPos, nextElevation));
}
curPos = nextPos;
break;
}
// skidding into higher terrain does weight/20
// damage in 5pt clusters to front.
int damage = ((int) entity.getWeight() + 19) / 20;
while (damage > 0) {
addReport(damageEntity(entity, entity.rollHitLocation(ToHitData.HIT_NORMAL, ToHitData.SIDE_FRONT), Math.min(5, damage)));
damage -= 5;
}
// Stay in the current hex and stop skidding.
break;
}
// Have skidding units suffer falls (off a cliff).
else if (curAltitude > nextAltitude + entity.getMaxElevationChange()) {
// WIGE can avoid this too, if they have 2MP to spend
if ((entity.getMovementMode() == IEntityMovementMode.WIGE) && (entity.getRunMP() - 2 >= entity.mpUsed)) {
entity.mpUsed += 2;
nextAltitude = curAltitude;
} else {
addReport(doEntityFallsInto(entity, curPos, nextPos, entity.getBasePilotingRoll(step.getMovementType())));
addReport(doEntityDisplacementMinefieldCheck(entity, curPos, nextPos, nextElevation));
// Stay in the current hex and stop skidding.
break;
}
}
// Get any building in the hex.
Building bldg = null;
if (nextElevation < nextHex.terrainLevel(Terrains.BLDG_ELEV)) {
// We will only run into the building if its at a higher level,
// otherwise we skid over the roof
bldg = game.getBoard().getBuildingAt(nextPos);
}
boolean bldgSuffered = false;
boolean stopTheSkid = false;
// Does the next hex contain an entities?
// ASSUMPTION: hurt EVERYONE in the hex.
Enumeration<Entity> targets = game.getEntities(nextPos);
if (targets.hasMoreElements()) {
boolean skidChargeHit = false;
while (targets.hasMoreElements()) {
Entity target = targets.nextElement();
if ((target.getElevation() > nextElevation + entity.getHeight()) || (target.absHeight() < nextElevation)) {
// target is not in the way
continue;
}
// Can the target avoid the skid?
if (!target.isDone()) {
if (target instanceof Infantry) {
r = new Report(2420);
r.subject = target.getId();
r.addDesc(target);
addReport(r);
continue;
} else if (target instanceof Protomech) {
if (target != Compute.stackingViolation(game, entity, nextPos, null)) {
r = new Report(2420);
r.subject = target.getId();
r.addDesc(target);
addReport(r);
continue;
}
} else {
PilotingRollData psr = target.getBasePilotingRoll();
psr.addModifier(0, "avoiding collision");
int roll = Compute.d6(2);
r = new Report(2425);
r.subject = target.getId();
r.addDesc(target);
r.add(psr.getValue());
r.add(psr.getDesc());
r.add(roll);
addReport(r);
if (roll >= psr.getValue()) {
game.removeTurnFor(target);
avoidedChargeUnits.add(target);
continue;
// TODO: the charge should really be suspended
// and resumed after the target moved.
}
}
}
// Mechs and vehicles get charged,
// but need to make a to-hit roll
if ((target instanceof Mech) || (target instanceof Tank)) {
ChargeAttackAction caa = new ChargeAttackAction(entity.getId(), target.getTargetType(), target.getTargetId(), target.getPosition());
ToHitData toHit = caa.toHit(game, true);
// roll
int roll = Compute.d6(2);
// Update report.
r = new Report(2050);
r.subject = entity.getId();
r.indent();
r.add(target.getShortName(), true);
r.add(nextPos.getBoardNum(), true);
r.newlines = 0;
addReport(r);
if (toHit.getValue() == TargetRoll.IMPOSSIBLE) {
roll = -12;
r = new Report(2055);
r.subject = entity.getId();
r.add(toHit.getDesc());
r.newlines = 0;
addReport(r);
} else if (toHit.getValue() == TargetRoll.AUTOMATIC_SUCCESS) {
r = new Report(2060);
r.subject = entity.getId();
r.add(toHit.getDesc());
r.newlines = 0;
addReport(r);
roll = Integer.MAX_VALUE;
} else {
// report the roll
r = new Report(2065);
r.subject = entity.getId();
r.add(toHit.getValue());
r.add(roll);
r.newlines = 0;
addReport(r);
}
// Resolve a charge against the target.
// ASSUMPTION: buildings block damage for
// *EACH* entity charged.
if (roll < toHit.getValue()) {
r = new Report(2070);
r.subject = entity.getId();
addReport(r);
} else {
// Resolve the charge.
resolveChargeDamage(entity, target, toHit, direction);
// HACK: set the entity's location
// to the original hex again, for the other targets
if (targets.hasMoreElements()) {
entity.setPosition(curPos);
}
bldgSuffered = true;
skidChargeHit = true;
// The skid ends here if the target lives.
if (!target.isDoomed() && !target.isDestroyed() && !game.isOutOfGame(target)) {
stopTheSkid = true;
}
}
// if we don't do this here,
// we can have a mech without a leg
// standing on the field and moving
// as if it still had his leg after
// getting skid-charged.
if (!target.isDone()) {
addReport(resolvePilotingRolls(target));
game.resetPSRs(target);
target.applyDamage();
addNewLines();
}
}
// Resolve "move-through" damage on infantry.
// Infantry inside of a building don't get a
// move-through, but suffer "bleed through"
// from the building.
else if ((target instanceof Infantry) && (bldg != null)) {
// Update report.
r = new Report(2075);
r.subject = entity.getId();
r.indent();
r.add(target.getShortName(), true);
r.add(nextPos.getBoardNum(), true);
r.newlines = 0;
addReport(r);
// Infantry don't have different
// tables for punches and kicks
HitData hit = target.rollHitLocation(ToHitData.HIT_NORMAL, Compute.targetSideTable(entity, target));
hit.setGeneralDamageType(HitData.DAMAGE_PHYSICAL);
// Damage equals tonnage, divided by 5.
// ASSUMPTION: damage is applied in one hit.
addReport(damageEntity(target, hit, Math.round(entity.getWeight() / 5)));
addNewLines();
}
// Has the target been destroyed?
if (target.isDoomed()) {
// Has the target taken a turn?
if (!target.isDone()) {
// Dead entities don't take turns.
game.removeTurnFor(target);
send(createTurnVectorPacket());
} // End target-still-to-move
// Clean out the entity.
target.setDestroyed(true);
game.moveToGraveyard(target.getId());
send(createRemoveEntityPacket(target.getId()));
}
// Update the target's position,
// unless it is off the game map.
if (!game.isOutOfGame(target)) {
entityUpdate(target.getId());
}
} // Check the next entity in the hex.
if (skidChargeHit) {
// HACK: set the entities position to that
// hex's coords, because we had to move the entity
// back earlier for the other targets
entity.setPosition(nextPos);
}
for (Entity e : avoidedChargeUnits) {
GameTurn newTurn = new GameTurn.SpecificEntityTurn(e.getOwner().getId(), e.getId());
game.insertNextTurn(newTurn);
send(createTurnVectorPacket());
}
}
// Handle the building in the hex.
if (bldg != null) {
// Report that the entity has entered the bldg.
r = new Report(2080);
r.subject = entity.getId();
r.indent();
r.add(bldg.getName());
r.add(nextPos.getBoardNum(), true);
addReport(r);
// If the building hasn't already suffered
// damage, then apply charge damage to the
// building and displace the entity inside.
// ASSUMPTION: you don't charge the building
// if Tanks or Mechs were charged.
int chargeDamage = ChargeAttackAction.getDamageFor(entity, game.getOptions().booleanOption("tacops_charge_damage"), entity.delta_distance);
if (!bldgSuffered) {
Vector<Report> reports = damageBuilding(bldg, chargeDamage, nextPos);
for (Report report : reports) {
report.subject = entity.getId();
}
addReport(reports);
// Apply damage to the attacker.
int toAttacker = ChargeAttackAction.getDamageTakenBy(entity, bldg, nextPos);
HitData hit = entity.rollHitLocation(ToHitData.HIT_NORMAL, entity.sideTable(nextPos));
hit.setGeneralDamageType(HitData.DAMAGE_PHYSICAL);
addReport(damageEntity(entity, hit, toAttacker));
addNewLines();
entity.setPosition(nextPos);
entity.setElevation(nextElevation);
addReport(doEntityDisplacementMinefieldCheck(entity, curPos, nextPos, nextElevation));
curPos = nextPos;
} // End buildings-suffer-too
// Any infantry in the building take damage
// equal to the building being charged.
// ASSUMPTION: infantry take no damage from the
// building absorbing damage from
// Tanks and Mechs being charged.
damageInfantryIn(bldg, chargeDamage, nextPos);
// If a building still stands, then end the skid,
// and add it to the list of affected buildings.
if (bldg.getCurrentCF(nextPos) > 0) {
stopTheSkid = true;
addAffectedBldg(bldg, false);
} else {
// otherwise it collapses immediately on our head
checkForCollapse(bldg, game.getPositionMap(), nextPos, true);
}
} // End handle-building.
// Do we stay in the current hex and stop skidding?
if (stopTheSkid) {
break;
}
// Update entity position and elevation
entity.setPosition(nextPos);
entity.setElevation(nextElevation);
addReport(doEntityDisplacementMinefieldCheck(entity, curPos, nextPos, nextElevation));
skidDistance++;
// Check for collapse of any building the entity might be on
Building roof = game.getBoard().getBuildingAt(nextPos);
if (roof != null) {
if (checkForCollapse(roof, game.getPositionMap(), nextPos, true)) {
break; // stop skidding if the building collapsed
}
}
// Can the skiding entity enter the next hex from this?
// N.B. can skid along roads.
if ((entity.isHexProhibited(curHex) || entity.isHexProhibited(nextHex)) && !Compute.canMoveOnPavement(game, curPos, nextPos, step.getParentUpToThisStep())) {
// Update report.
r = new Report(2040);
r.subject = entity.getId();
r.indent();
r.add(nextPos.getBoardNum(), true);
addReport(r);
// If the prohibited terrain is water, entity is destroyed
if ((nextHex.terrainLevel(Terrains.WATER) > 0) && (entity instanceof Tank) && (entity.getMovementMode() != IEntityMovementMode.HOVER) && (entity.getMovementMode() != IEntityMovementMode.WIGE)) {
addReport(destroyEntity(entity, "skidded into a watery grave", false, true));
}
// otherwise, damage is weight/5 in 5pt clusters
int damage = ((int) entity.getWeight() + 4) / 5;
while (damage > 0) {
addReport(damageEntity(entity, entity.rollHitLocation(ToHitData.HIT_NORMAL, ToHitData.SIDE_FRONT), Math.min(5, damage)));
damage -= 5;
}
// and unit is immobile
if (entity instanceof Tank) {
((Tank) entity).immobilize();
}
// Stay in the current hex and stop skidding.
break;
}
if ((nextHex.terrainLevel(Terrains.WATER) > 0) && (entity.getMovementMode() != IEntityMovementMode.HOVER) && (entity.getMovementMode() != IEntityMovementMode.WIGE)) {
// water ends the skid
break;
}
// check for breaking magma crust
if ((nextHex.terrainLevel(Terrains.MAGMA) == 1) && (nextElevation == 0)) {
int roll = Compute.d6(1);
r = new Report(2395);
r.addDesc(entity);
r.add(roll);
r.subject = entity.getId();
addReport(r);
if (roll == 6) {
nextHex.removeTerrain(Terrains.MAGMA);
nextHex.addTerrain(Terrains.getTerrainFactory().createTerrain(Terrains.MAGMA, 2));
sendChangedHex(curPos);
for (Enumeration<Entity> e = game.getEntities(curPos); e.hasMoreElements();) {
Entity en = e.nextElement();
if (en != entity) {
doMagmaDamage(en, false);
}
}
}
}
// check for entering liquid magma
if ((nextHex.terrainLevel(Terrains.MAGMA) == 2) && (nextElevation == 0)) {
doMagmaDamage(entity, false);
}
// is the next hex a swamp?
PilotingRollData rollTarget = entity.checkBogDown(step, nextHex, curPos, nextPos, step.getElevation(), Compute.canMoveOnPavement(game, curPos, nextPos, step.getParentUpToThisStep()));