/*
* MegaMek -
* Copyright (C) 2000,2001,2002,2003,2004,2005 Ben Mazur (bmazur@sev.org)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package megamek.server;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Vector;
import megamek.common.Building;
import megamek.common.Compute;
import megamek.common.Coords;
import megamek.common.Entity;
import megamek.common.IBoard;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.PlanetaryConditions;
import megamek.common.Report;
import megamek.common.TargetRoll;
import megamek.common.Terrains;
public class FireProcessor extends DynamicTerrainProcessor {
private IGame game;
Vector<Report> vPhaseReport;
public FireProcessor(Server server) {
super(server);
}
@Override
void doEndPhaseChanges(Vector<Report> vPhaseReport) {
game = server.getGame();
if (game.getOptions().booleanOption("tacops_start_fire")) {
this.vPhaseReport = vPhaseReport;
resolveFire();
this.vPhaseReport = null;
}
}
/**
* This debug/profiling function will print the current time (in
* milliseconds) to the log. If the boolean is true, the garbage collector
* will be called in an attempt to minimize timing errors. You should try
* and minimize applications being run in the background when using this
* function. Note that MS Windows only has 10 milisecond resolution. The
* function should be optimized completely out of the code when the first
* if-statement below reads "if (false)...", so performance shouldn't be
* impacted if you leave calls to this function in the code (I think).
*/
private void debugTime(String s, boolean collectGarbage) {
// Change the "false" below to "true" to enable this function
if (false) {
if (collectGarbage) {
System.gc();
}
System.out.println(s + ": " + System.currentTimeMillis());
}
}
/**
* Make fires spread, smoke spread, and make sure that all fires started
* this turn are marked as "burning" for next turn. What turn the fire startd on is no
* longer determined by level but is rather a characteristic of the hex.
* Level now denotes standard and inferno fires.
*/
private void resolveFire() {
IBoard board = game.getBoard();
int width = board.getWidth();
int height = board.getHeight();
int windDirection = game.getPlanetaryConditions().getWindDirection();
int windStrength = game.getPlanetaryConditions().getWindStrength();
Report r;
// Get the position map of all entities in the game.
Hashtable<Coords, Vector<Entity>> positionMap = game.getPositionMap();
// process smoke FIRST, before any fires spread or
// smoke is produced.
resolveSmoke();
// Cycle through all buildings, checking for fire.
// ASSUMPTION: buildings don't lose 2 CF on the turn a fire starts.
// ASSUMPTION: multi-hex buildings lose 2 CF in each burning hex
Enumeration<Building> buildings = game.getBoard().getBuildings();
while (buildings.hasMoreElements()) {
Building bldg = buildings.nextElement();
Enumeration<Coords> bldgCoords = bldg.getCoords();
while (bldgCoords.hasMoreElements()) {
Coords coords = bldgCoords.nextElement();
if (bldg.isBurning(coords)) {
int cf = Math.max(bldg.getCurrentCF(coords) - 2, 0);
bldg.setCurrentCF(cf, coords);
// Does the building burn down?
if (cf == 0) {
r = new Report(5120, Report.PUBLIC);
r.add(bldg.getName());
vPhaseReport.addElement(r);
}
// If it doesn't collapse under its load, mark it for update.
else if (!server.checkForCollapse(bldg, positionMap, coords, false)) {
bldg.setPhaseCF(cf, coords);
}
}
}
}
debugTime("resolve fire 1", true);
// Cycle through all hexes, checking for fire and the spread of fire
for (int currentXCoord = 0; currentXCoord < width; currentXCoord++) {
for (int currentYCoord = 0; currentYCoord < height; currentYCoord++) {
Coords currentCoords = new Coords(currentXCoord, currentYCoord);
IHex currentHex = board.getHex(currentXCoord, currentYCoord);
if(currentHex.containsTerrain(Terrains.FIRE)) {
//If the woods has been cleared, or the building
// has collapsed put non-inferno fires out.
if ((currentHex.terrainLevel(Terrains.FIRE) == 1) && !currentHex.isIgnitable()) {
server.removeFire(currentCoords, "lack of fuel");
continue;
}
//only check spread for fires that didn't start this turn
if(currentHex.getFireTurn() > 0) {
//optional rule, woods burn down
if ((currentHex.containsTerrain(Terrains.WOODS) || currentHex
.containsTerrain(Terrains.JUNGLE))
&& game.getOptions().booleanOption("woods_burn_down")) {
burnDownWoods(currentCoords);
}
//report and check for fire spread
r = new Report(5125, Report.PUBLIC);
if (currentHex.terrainLevel(Terrains.FIRE) == 2) {
r.messageId = 5130;
}
r.add(currentCoords.getBoardNum());
vPhaseReport.addElement(r);
spreadFire(currentXCoord, currentYCoord, windDirection, windStrength);
}
}
}
}
//Cycle through all hexes again, reporting new fires, spreading smoke, and incrementing the fire turn.
//Can't do this in first loop because new fires may be spread
for (int currentXCoord = 0; currentXCoord < width; currentXCoord++) {
for (int currentYCoord = 0; currentYCoord < height; currentYCoord++) {
Coords currentCoords = new Coords(currentXCoord, currentYCoord);
IHex currentHex = board.getHex(currentXCoord, currentYCoord);
if(currentHex.containsTerrain(Terrains.FIRE)) {
//was the fire started this turn?
if(currentHex.getFireTurn() == 0) {
//report fire started this round
r = new Report(5135, Report.PUBLIC);
r.add(currentCoords.getBoardNum());
vPhaseReport.addElement(r);
// If the hex contains a building, set it on fire.
Building bldg = game.getBoard().getBuildingAt(
currentCoords);
if (bldg != null) {
bldg.setBurning(true, currentCoords);
}
}
//check for any explosions
server.checkExplodeIndustrialZone(currentCoords, vPhaseReport);
//Add smoke (unless we are in a tornado)
boolean bInferno = currentHex.terrainLevel(Terrains.FIRE) == 2;
if (game.getPlanetaryConditions().getWindStrength() < PlanetaryConditions.WI_TORNADO_F13) {
ArrayList<Coords> smokeList = new ArrayList<Coords>();
smokeList.add(new Coords(Coords.xInDir(currentXCoord, currentYCoord, windDirection), Coords.yInDir(currentXCoord, currentYCoord, windDirection)));
smokeList.add(new Coords(Coords.xInDir(currentXCoord, currentYCoord, (windDirection+1)%6), Coords.yInDir(currentXCoord, currentYCoord, (windDirection+1)%6)));
smokeList.add(new Coords(Coords.xInDir(currentXCoord, currentYCoord, (windDirection+5)%6), Coords.yInDir(currentXCoord, currentYCoord, (windDirection+5)%6)));
server.addSmoke(smokeList, windDirection, bInferno);
board.initializeAround(currentXCoord, currentYCoord);
}
//increment the fire turn counter
currentHex.incrementFireTurn();
server.sendChangedHex(currentCoords);
}
}
}
} // End the ResolveFire() method
public void burnDownWoods(Coords coords) {
int burnDamage = 5;
try {
burnDamage = game.getOptions().intOption("woods_burn_down_amount");
} catch (Exception ex) {
ex.printStackTrace();
}
vPhaseReport.addAll(server.tryClearHex(coords, burnDamage, Entity.NONE));
}
/**
* Spreads the fire around the specified coordinates.
*/
public void spreadFire(int x, int y, int windDir, int windStr) {
Coords src = new Coords(x, y);
Coords nextCoords = src.translated(windDir);
//check for height differences between hexes
//TODO: until further clarification only the heights matter (not the base elevation)
//This means that a fire cannot spread from a level 6 building at base level 0 to
//a level 1 building at base level 0, for example.
int curHeight = game.getBoard().getHex(src).ceiling();
TargetRoll directroll = new TargetRoll(9, "spread downwind");
TargetRoll obliqueroll = new TargetRoll(11, "spread 60 degrees to downwind");
if((windStr > PlanetaryConditions.WI_NONE) && (windStr < PlanetaryConditions.WI_STRONG_GALE)) {
directroll.addModifier(-2, "light/moderate gale");
obliqueroll.addModifier(-1, "light/moderate gale");
}
else if(windStr > PlanetaryConditions.WI_MOD_GALE) {
directroll.addModifier(-3, "strong gale+");
directroll.addModifier(-2, "strong gale+");
}
spreadFire(nextCoords, directroll, curHeight);
// Spread to the next hex downwind on a 12 if the first hex wasn't
// burning...
// unless a higher hex intervenes
IHex nextHex = game.getBoard().getHex(nextCoords);
IHex jumpHex = game.getBoard().getHex(nextCoords.translated(windDir));
if ((nextHex != null) && (jumpHex != null) && !(nextHex.containsTerrain(Terrains.FIRE))
&& ((curHeight >= nextHex.ceiling()) || (jumpHex.ceiling() >= nextHex.ceiling()))) {
// we've already gone one step in the wind direction, now go another
directroll.addModifier(3, "crossing non-burning hex");
spreadFire(nextCoords.translated(windDir), directroll, curHeight);
}
// spread fire 60 degrees clockwise....
spreadFire(src.translated((windDir + 1) % 6), obliqueroll, curHeight);
// spread fire 60 degrees counterclockwise
spreadFire(src.translated((windDir + 5) % 6), obliqueroll, curHeight);
}
/**
* Spreads the fire, and reports the spread, to the specified hex, if
* possible, if the hex isn't already on fire, and the fire roll is made.
*/
public void spreadFire(Coords coords, TargetRoll roll, int height) {
IHex hex = game.getBoard().getHex(coords);
if (hex == null) {
// Don't attempt to spread fire off the board.
return;
}
if(Math.abs(hex.ceiling() - height) > 4) {
return;
}
if (!(hex.containsTerrain(Terrains.FIRE)) && server.checkIgnition(coords, roll)) {
Report r = new Report(5150, Report.PUBLIC);
r.add(coords.getBoardNum());
vPhaseReport.addElement(r);
}
}
/**
* Under L3 rules, smoke drifts in the direction of the wind and has a
* chance to dissipate. This function will keep track of hexes to have smoke
* removed and added, since there's no other way to tell if a certain smoke
* cloud has drifted that turn. This method creates the class SmokeDrift to
* store hex and size data for the smoke clouds. This method calls functions
* driftAddSmoke, driftSmokeDissipate, driftSmokeReport
*/
private void resolveSmoke() {
IBoard board = game.getBoard();
int windDir = game.getPlanetaryConditions().getWindDirection();
int windStr = game.getPlanetaryConditions().getWindStrength();
ArrayList<Coords> smokeToAdd;
HashMap<SmokeCloud, ArrayList<Coords>> smokeCloudData = new HashMap<SmokeCloud, ArrayList<Coords>>();
// Cycle through all smokeclouds
for ( SmokeCloud cloud : server.getSmokeCloudList() ){
smokeToAdd = new ArrayList<Coords>();
for ( Coords currentCoords : cloud.getCoordsList() ){
Coords smokeCoords = driftAddSmoke(currentCoords, windDir, windStr);
//Smoke has Dissipated by moving into a hex with a greater then 4 elevation drop.
if ( smokeCoords == null ){
Report r = new Report(5220, Report.PUBLIC);
r.add(currentCoords.getBoardNum());
vPhaseReport.addElement(r);
r = new Report(5222,Report.PUBLIC);
vPhaseReport.addElement(r);
}
else if (board.contains(smokeCoords) && !currentCoords.equals(smokeCoords) ) {
// don't add it to the vector if it's not on board!
smokeToAdd.add(smokeCoords);
cloud.setDrift(true);
} else if ( !board.contains(smokeCoords) ) {
// report that the smoke has blown off the map
Report r = new Report(5230, Report.PUBLIC);
r.add(currentCoords.getBoardNum());
vPhaseReport.addElement(r);
}
} // end the loop through Cloud coordinates
if ( smokeToAdd.size() > 0 ) {
smokeCloudData.put(cloud,smokeToAdd);
}
} // end the loop through clouds
//update all the new coords for the smoke cloud.
for ( SmokeCloud cloud : smokeCloudData.keySet() ){
smokeToAdd = smokeCloudData.get(cloud);
server.updateSmoke(cloud, smokeToAdd);
}
// Cycle through the vector again and dissipate the smoke, then
// reporting it
for ( SmokeCloud cloud : server.getSmokeCloudList() ){
int roll = Compute.d6(2);
boolean dissipated = driftSmokeDissipate(cloud, roll, windStr);
if ( dissipated || cloud.didDrift() ){
driftSmokeReport(cloud,dissipated);
if ( dissipated ) {
cloud.setSmokeLevel(cloud.getSmokeLevel()-1);
}
}
cloud.setDrift(false);
}
} // end smoke resolution
/**
* Override for the main driftAddSmoke to allow for 0 direction changes
* @param x
* @param y
* @param windDir
* @param windStr
* @return
*/
public Coords driftAddSmoke(Coords coords, int windDir, int windStr) {
return driftAddSmoke(coords, windDir, windStr, 0);
}
/**
* Smoke cannot climb more then 4 hexes if the next hex is more then 4 in elevation then
* The smoke will try to go right. If it cannot go right it'll try to go left
* if it cannot go left it'll stay put.
*
* @param x
* @param y
* @param windDir
* @param windStr
* @param directionChanges How many times the smoke has tried to change directions to get around an obsticle.
* @return
*/
public Coords driftAddSmoke(Coords src, int windDir, int windStr, int directionChanges) {
//Coords src = new Coords(x, y);
Coords nextCoords = src.translated(windDir);
IBoard board = game.getBoard();
//if the wind conditions are calm, then don't drift it
if(windStr == PlanetaryConditions.WI_NONE) {
return src;
}
//if it is no longer on the board then return it now to avoid getting null arguments later
if(!board.contains(nextCoords)) {
return nextCoords;
}
int hexElevation = board.getHex(src).getElevation();
int nextElevation = board.getHex(nextCoords).getElevation();
if ( board.getHex(nextCoords).containsTerrain(Terrains.BUILDING) ) {
nextElevation += board.getHex(nextCoords).terrainLevel(Terrains.BLDG_ELEV);
}
if ( board.getHex(src).containsTerrain(Terrains.BUILDING) ) {
hexElevation += board.getHex(src).terrainLevel(Terrains.BLDG_ELEV);
}
//If the smoke moves into a hex that has a greater then 4 elevation drop it dissipates.
if ( hexElevation - nextElevation > 4 ) {
return null;
}
if ( hexElevation - nextElevation < -4 ){
//Try Right
if ( directionChanges == 0 ){
return driftAddSmoke(src, (windDir + 1) % 6, windStr, ++directionChanges);
}
//Try Left
else if ( directionChanges == 1) {
return driftAddSmoke(src, (windDir - 2 ) % 6, windStr, ++directionChanges);
//Stay put
} else {
return src;
}
}
// stronger wind causes smoke to drift farther
if (windStr > PlanetaryConditions.WI_MOD_GALE) {
return driftAddSmoke(nextCoords, windDir, --windStr);
}
return nextCoords;
}
/**
* Diisipates Smoke clouds instead of indivdiual smoke hexes
* @param cloud
* @param roll
* @param windStr
* @return
*/
public boolean driftSmokeDissipate(SmokeCloud cloud, int roll, int windStr) {
//HVAC Heavy smoke dissipation
if ( (cloud.getDuration() > 0) && (cloud.getDuration()-1 == 0) ) {
cloud.setDuration(0);
cloud.setSmokeLevel(0);
return true;
}
if ( (cloud.getDuration() > 0) && (cloud.getDuration()-1 > 0) ) {
cloud.setDuration(cloud.getDuration()-1);
}
// Dissipate in various winds
if ((roll > 10) || ((roll > 9) && (windStr == PlanetaryConditions.WI_MOD_GALE))
|| ((roll > 7) && (windStr == PlanetaryConditions.WI_STRONG_GALE))
|| ((roll > 5) && (windStr == PlanetaryConditions.WI_STORM))) {
return true;
}
//All smoke goes bye bye in Tornados
if ( windStr > PlanetaryConditions.WI_STORM ) {
cloud.setSmokeLevel(0);
return true;
}
return false;
}
public void driftSmokeReport(SmokeCloud cloud, boolean dis) {
Report r;
int size = cloud.getSmokeLevel();
if ((size == 2) && (dis == true)) {
// heavy smoke drifts and dissipates to light
for ( int pos = 0; pos < cloud.getCoordsList().size(); pos++ ) {
if ( pos == 0 ) {
r = new Report(5210, Report.PUBLIC);
} else {
r = new Report(5211, Report.PUBLIC);
}
r.add(cloud.getCoordsList().get(pos).getBoardNum());
r.newlines = 0;
vPhaseReport.addElement(r);
}
r = new Report(5212, Report.PUBLIC);
vPhaseReport.addElement(r);
} else if ((size == 2) && (dis == false)) {
// heavy smoke drifts
for ( int pos = 0; pos < cloud.getCoordsList().size(); pos++ ) {
if ( pos == 0 ) {
r = new Report(5210, Report.PUBLIC);
} else {
r = new Report(5211, Report.PUBLIC);
}
r.add(cloud.getCoordsList().get(pos).getBoardNum());
r.newlines = 0;
vPhaseReport.addElement(r);
}
r = new Report(5213, Report.PUBLIC);
vPhaseReport.addElement(r);
} else if ((size == 1) && (dis == true)) {
// light smoke drifts and dissipates
for ( int pos = 0; pos < cloud.getCoordsList().size(); pos++ ) {
if ( pos == 0 ) {
r = new Report(5220, Report.PUBLIC);
} else {
r = new Report(5211, Report.PUBLIC);
}
r.add(cloud.getCoordsList().get(pos).getBoardNum());
r.newlines = 0;
vPhaseReport.addElement(r);
}
r = new Report(5222, Report.PUBLIC);
vPhaseReport.addElement(r);
} else if ((size == 1) && (dis == false)) {
// light smoke drifts
for ( int pos = 0; pos < cloud.getCoordsList().size(); pos++ ) {
if ( pos == 0) {
r = new Report(5220, Report.PUBLIC);
} else {
r = new Report(5211, Report.PUBLIC);
}
r.add(cloud.getCoordsList().get(pos).getBoardNum());
r.newlines = 0;
vPhaseReport.addElement(r);
}
r = new Report(5213, Report.PUBLIC);
vPhaseReport.addElement(r);
}else if (size < 1 ) {
// light smoke drifts and dissipates
for ( int pos = 0; pos < cloud.getCoordsList().size(); pos++ ) {
if ( pos == 0 ) {
r = new Report(5223, Report.PUBLIC);
} else {
r = new Report(5211, Report.PUBLIC);
}
r.add(cloud.getCoordsList().get(pos).getBoardNum());
r.newlines = 0;
vPhaseReport.addElement(r);
}
r = new Report(5224, Report.PUBLIC);
vPhaseReport.addElement(r);
}
}
}