This repository has been archived on 2025-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
citylimitsj/src/micropolisj/engine/Micropolis.java
2013-10-04 12:53:59 +00:00

2682 lines
57 KiB
Java

// This file is part of MicropolisJ.
// Copyright (C) 2013 Jason Long
// Portions Copyright (C) 1989-2007 Electronic Arts Inc.
//
// MicropolisJ is free software; you can redistribute it and/or modify
// it under the terms of the GNU GPLv3, with additional terms.
// See the README file, included in this distribution, for details.
package micropolisj.engine;
import java.io.*;
import java.util.*;
import static micropolisj.engine.TileConstants.*;
/**
* The main simulation engine for Micropolis.
* The front-end should call animate() periodically
* to move the simulation forward in time.
*/
public class Micropolis
{
static final Random DEFAULT_PRNG = new Random();
Random PRNG;
// full size arrays
char [][] map;
boolean [][] powerMap;
// half-size arrays
/**
* For each 2x2 section of the city, the land value of the city (0-250).
* 0 is lowest land value; 250 is maximum land value.
* Updated each cycle by ptlScan().
*/
int [][] landValueMem;
/**
* For each 2x2 section of the city, the pollution level of the city (0-255).
* 0 is no pollution; 255 is maximum pollution.
* Updated each cycle by ptlScan(); affects land value.
*/
public int [][] pollutionMem;
/**
* For each 2x2 section of the city, the crime level of the city (0-250).
* 0 is no crime; 250 is maximum crime.
* Updated each cycle by crimeScan(); affects land value.
*/
public int [][] crimeMem;
/**
* For each 2x2 section of the city, the population density (0-?).
* Used for map overlays and as a factor for crime rates.
*/
public int [][] popDensity;
/**
* For each 2x2 section of the city, the traffic density (0-255).
* If less than 64, no cars are animated.
* If between 64 and 192, then the "light traffic" animation is used.
* If 192 or higher, then the "heavy traffic" animation is used.
*/
int [][] trfDensity;
// quarter-size arrays
/**
* For each 4x4 section of the city, an integer representing the natural
* land features in the vicinity of this part of the city.
*/
int [][] terrainMem;
// eighth-size arrays
/**
* For each 8x8 section of the city, the rate of growth.
* Capped to a number between -200 and 200.
* Used for reporting purposes only; the number has no affect.
*/
public int [][] rateOGMem; //rate of growth?
int [][] fireStMap; //firestations- cleared and rebuilt each sim cycle
public int [][] fireRate; //firestations reach- used for overlay graphs
int [][] policeMap; //police stations- cleared and rebuilt each sim cycle
public int [][] policeMapEffect;//police stations reach- used for overlay graphs
/** For each 8x8 section of city, this is an integer between 0 and 64,
* with higher numbers being closer to the center of the city. */
int [][] comRate;
static final int DEFAULT_WIDTH = 120;
static final int DEFAULT_HEIGHT = 100;
public final CityBudget budget = new CityBudget(this);
public boolean autoBulldoze = true;
public boolean autoBudget = false;
public Speed simSpeed = Speed.NORMAL;
public boolean noDisasters = false;
public int gameLevel;
boolean autoGo;
// census numbers, reset in phase 0 of each cycle, summed during map scan
int poweredZoneCount;
int unpoweredZoneCount;
int roadTotal;
int railTotal;
int firePop;
int resZoneCount;
int comZoneCount;
int indZoneCount;
int resPop;
int comPop;
int indPop;
int hospitalCount;
int churchCount;
int policeCount;
int fireStationCount;
int stadiumCount;
int coalCount;
int nuclearCount;
int seaportCount;
int airportCount;
int totalPop;
int lastCityPop;
// used in generateBudget()
int lastRoadTotal;
int lastRailTotal;
int lastTotalPop;
int lastFireStationCount;
int lastPoliceCount;
int trafficMaxLocationX;
int trafficMaxLocationY;
int pollutionMaxLocationX;
int pollutionMaxLocationY;
int crimeMaxLocationX;
int crimeMaxLocationY;
public int centerMassX;
public int centerMassY;
CityLocation meltdownLocation; //may be null
CityLocation crashLocation; //may be null
int needHospital; // -1 too many already, 0 just right, 1 not enough
int needChurch; // -1 too many already, 0 just right, 1 not enough
int crimeAverage;
int pollutionAverage;
int landValueAverage;
int trafficAverage;
int resValve; // ranges between -2000 and 2000, updated by setValves
int comValve; // ranges between -1500 and 1500
int indValve; // ranges between -1500 and 1500
boolean resCap; // residents demand a stadium, caps resValve at 0
boolean comCap; // commerce demands airport, caps comValve at 0
boolean indCap; // industry demands sea port, caps indValve at 0
int crimeRamp;
int polluteRamp;
//
// budget stuff
//
public int cityTax = 7;
public double roadPercent = 1.0;
public double policePercent = 1.0;
public double firePercent = 1.0;
int taxEffect = 7;
int roadEffect = 32;
int policeEffect = 1000;
int fireEffect = 1000;
int cashFlow; //net change in totalFunds in previous year
boolean newPower;
int floodCnt; //number of turns the flood will last
int floodX;
int floodY;
public int cityTime; //counts "weeks" (actually, 1/48'ths years)
int scycle; //same as cityTime, except mod 1024
int fcycle; //counts simulation steps (mod 1024)
int acycle; //animation cycle (mod 960)
public CityEval evaluation;
ArrayList<Sprite> sprites = new ArrayList<Sprite>();
static final int VALVERATE = 2;
public static final int CENSUSRATE = 4;
static final int TAXFREQ = 48;
public void spend(int amount)
{
budget.totalFunds -= amount;
fireFundsChanged();
}
public Micropolis()
{
this(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
public Micropolis(int width, int height)
{
PRNG = DEFAULT_PRNG;
evaluation = new CityEval(this);
init(width, height);
initTileBehaviors();
}
protected void init(int width, int height)
{
map = new char[height][width];
powerMap = new boolean[height][width];
int hX = (width+1)/2;
int hY = (height+1)/2;
landValueMem = new int[hY][hX];
pollutionMem = new int[hY][hX];
crimeMem = new int[hY][hX];
popDensity = new int[hY][hX];
trfDensity = new int[hY][hX];
int qX = (width+3)/4;
int qY = (height+3)/4;
terrainMem = new int[qY][qX];
int smX = (width+7)/8;
int smY = (height+7)/8;
rateOGMem = new int[smY][smX];
fireStMap = new int[smY][smX];
policeMap = new int[smY][smX];
policeMapEffect = new int[smY][smX];
fireRate = new int[smY][smX];
comRate = new int[smY][smX];
centerMassX = hX;
centerMassY = hY;
}
void fireCensusChanged()
{
for (Listener l : listeners) {
l.censusChanged();
}
}
void fireCityMessage(MicropolisMessage message, CityLocation loc)
{
for (Listener l : listeners) {
l.cityMessage(message, loc);
}
}
void fireCitySound(Sound sound, CityLocation loc)
{
for (Listener l : listeners) {
l.citySound(sound, loc);
}
}
void fireDemandChanged()
{
for (Listener l : listeners) {
l.demandChanged();
}
}
void fireEarthquakeStarted()
{
for (EarthquakeListener l : earthquakeListeners) {
l.earthquakeStarted();
}
}
void fireEvaluationChanged()
{
for (Listener l : listeners) {
l.evaluationChanged();
}
}
void fireFundsChanged()
{
for (Listener l : listeners) {
l.fundsChanged();
}
}
void fireMapOverlayDataChanged(MapState overlayDataType)
{
for (MapListener l : mapListeners) {
l.mapOverlayDataChanged(overlayDataType);
}
}
void fireOptionsChanged()
{
for (Listener l : listeners)
{
l.optionsChanged();
}
}
void fireSpriteMoved(Sprite sprite)
{
for (MapListener l : mapListeners)
{
l.spriteMoved(sprite);
}
}
void fireTileChanged(int xpos, int ypos)
{
for (MapListener l : mapListeners)
{
l.tileChanged(xpos, ypos);
}
}
void fireWholeMapChanged()
{
for (MapListener l : mapListeners)
{
l.wholeMapChanged();
}
}
ArrayList<Listener> listeners = new ArrayList<Listener>();
ArrayList<MapListener> mapListeners = new ArrayList<MapListener>();
ArrayList<EarthquakeListener> earthquakeListeners = new ArrayList<EarthquakeListener>();
public void addListener(Listener l)
{
this.listeners.add(l);
}
public void removeListener(Listener l)
{
this.listeners.remove(l);
}
public void addEarthquakeListener(EarthquakeListener l)
{
this.earthquakeListeners.add(l);
}
public void removeEarthquakeListener(EarthquakeListener l)
{
this.earthquakeListeners.remove(l);
}
public void addMapListener(MapListener l)
{
this.mapListeners.add(l);
}
public void removeMapListener(MapListener l)
{
this.mapListeners.remove(l);
}
/**
* The listener interface for receiving miscellaneous events that occur
* in the Micropolis city.
* Use the Micropolis class's addListener interface to register an object
* that implements this interface.
*/
public interface Listener
{
void cityMessage(MicropolisMessage message, CityLocation loc);
void citySound(Sound sound, CityLocation loc);
/**
* Fired whenever the "census" is taken, and the various historical
* counters have been updated. (Once a month in game.)
*/
void censusChanged();
/**
* Fired whenever resValve, comValve, or indValve changes.
* (Twice a month in game.) */
void demandChanged();
/**
* Fired whenever the city evaluation is recalculated.
* (Once a year.)
*/
void evaluationChanged();
/**
* Fired whenever the mayor's money changes.
*/
void fundsChanged();
/**
* Fired whenever autoBulldoze, autoBudget, noDisasters,
* or simSpeed change.
*/
void optionsChanged();
}
public int getWidth()
{
return map[0].length;
}
public int getHeight()
{
return map.length;
}
public char getTile(int xpos, int ypos)
{
return map[ypos][xpos];
}
public void setTile(int xpos, int ypos, char newTile)
{
if (map[ypos][xpos] != newTile)
{
map[ypos][xpos] = newTile;
fireTileChanged(xpos, ypos);
}
}
final public boolean testBounds(int xpos, int ypos)
{
return xpos >= 0 && xpos < getWidth() &&
ypos >= 0 && ypos < getHeight();
}
final boolean hasPower(int x, int y)
{
return powerMap[y][x];
}
/**
* Checks whether the next call to animate() will collect taxes and
* process the budget.
*/
public boolean isBudgetTime()
{
return (
cityTime != 0 &&
(cityTime % TAXFREQ) == 0 &&
((fcycle + 1) % 16) == 10 &&
((acycle + 1) % 2) == 0
);
}
void step()
{
fcycle = (fcycle + 1) % 1024;
simulate(fcycle % 16);
}
void clearCensus()
{
poweredZoneCount = 0;
unpoweredZoneCount = 0;
firePop = 0;
roadTotal = 0;
railTotal = 0;
resPop = 0;
comPop = 0;
indPop = 0;
resZoneCount = 0;
comZoneCount = 0;
indZoneCount = 0;
hospitalCount = 0;
churchCount = 0;
policeCount = 0;
fireStationCount = 0;
stadiumCount = 0;
coalCount = 0;
nuclearCount = 0;
seaportCount = 0;
airportCount = 0;
powerPlants.clear();
for (int y = 0; y < fireStMap.length; y++) {
for (int x = 0; x < fireStMap[y].length; x++) {
fireStMap[y][x] = 0;
policeMap[y][x] = 0;
}
}
}
void simulate(int mod16)
{
final int band = getWidth() / 8;
switch (mod16)
{
case 0:
scycle = (scycle + 1) % 1024;
cityTime++;
if (scycle % 2 == 0) {
setValves();
}
clearCensus();
break;
case 1:
mapScan(0 * band, 1 * band);
break;
case 2:
mapScan(1 * band, 2 * band);
break;
case 3:
mapScan(2 * band, 3 * band);
break;
case 4:
mapScan(3 * band, 4 * band);
break;
case 5:
mapScan(4 * band, 5 * band);
break;
case 6:
mapScan(5 * band, 6 * band);
break;
case 7:
mapScan(6 * band, 7 * band);
break;
case 8:
mapScan(7 * band, getWidth());
break;
case 9:
if (cityTime % CENSUSRATE == 0) {
takeCensus();
if (cityTime % (CENSUSRATE*12) == 0) {
takeCensus2();
}
fireCensusChanged();
}
collectTaxPartial();
if (cityTime % TAXFREQ == 0) {
collectTax();
evaluation.cityEvaluation();
}
break;
case 10:
if (scycle % 5 == 0) { // every ~10 weeks
decROGMem();
}
decTrafficMem();
fireMapOverlayDataChanged(MapState.TRAFFIC_OVERLAY); //TDMAP
fireMapOverlayDataChanged(MapState.TRANSPORT); //RDMAP
fireMapOverlayDataChanged(MapState.ALL); //ALMAP
fireMapOverlayDataChanged(MapState.RESIDENTIAL); //REMAP
fireMapOverlayDataChanged(MapState.COMMERCIAL); //COMAP
fireMapOverlayDataChanged(MapState.INDUSTRIAL); //INMAP
doMessages();
break;
case 11:
powerScan();
fireMapOverlayDataChanged(MapState.POWER_OVERLAY);
newPower = true;
break;
case 12:
ptlScan();
break;
case 13:
crimeScan();
break;
case 14:
popDenScan();
break;
case 15:
fireAnalysis();
doDisasters();
break;
default:
throw new Error("unreachable");
}
}
private int computePopDen(int x, int y, char tile)
{
if (tile == RESCLR)
return doFreePop(x, y);
if (tile < COMBASE)
return residentialZonePop(tile);
if (tile < INDBASE)
return commercialZonePop(tile) * 8;
if (tile < PORTBASE)
return industrialZonePop(tile) * 8;
return 0;
}
private static int [][] doSmooth(int [][] tem)
{
final int h = tem.length;
final int w = tem[0].length;
int [][] tem2 = new int[h][w];
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
int z = tem[y][x];
if (x > 0)
z += tem[y][x-1];
if (x + 1 < w)
z += tem[y][x+1];
if (y > 0)
z += tem[y-1][x];
if (y + 1 < h)
z += tem[y+1][x];
z /= 4;
if (z > 255)
z = 255;
tem2[y][x] = z;
}
}
return tem2;
}
public void calculateCenterMass()
{
popDenScan();
}
private void popDenScan()
{
int xtot = 0;
int ytot = 0;
int zoneCount = 0;
int width = getWidth();
int height = getHeight();
int [][] tem = new int[(height+1)/2][(width+1)/2];
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
char tile = map[y][x];
if (isZoneCenter(tile))
{
tile &= LOMASK;
int den = computePopDen(x, y, (char)tile) * 8;
if (den > 254)
den = 254;
tem[y/2][x/2] = den;
xtot += x;
ytot += y;
zoneCount++;
}
}
}
tem = doSmooth(tem);
tem = doSmooth(tem);
tem = doSmooth(tem);
for (int x = 0; x < (width+1)/2; x++)
{
for (int y = 0; y < (height+1)/2; y++)
{
popDensity[y][x] = 2 * tem[y][x];
}
}
distIntMarket(); //set ComRate
// find center of mass for city
if (zoneCount != 0)
{
centerMassX = xtot / zoneCount;
centerMassY = ytot / zoneCount;
}
else
{
centerMassX = (width+1)/2;
centerMassY = (height+1)/2;
}
fireMapOverlayDataChanged(MapState.POPDEN_OVERLAY); //PDMAP
fireMapOverlayDataChanged(MapState.GROWTHRATE_OVERLAY); //RGMAP
}
private void distIntMarket()
{
for (int y = 0; y < comRate.length; y++)
{
for (int x = 0; x < comRate[y].length; x++)
{
int z = getDisCC(x*4, y*4);
z /= 4;
z = 64 - z;
comRate[y][x] = z;
}
}
}
//tends to empty RateOGMem[][]
private void decROGMem()
{
for (int y = 0; y < rateOGMem.length; y++)
{
for (int x = 0; x < rateOGMem[y].length; x++)
{
int z = rateOGMem[y][x];
if (z == 0)
continue;
if (z > 0)
{
rateOGMem[y][x]--;
if (z > 200)
{
rateOGMem[y][x] = 200; //prevent overflow?
}
continue;
}
if (z < 0)
{
rateOGMem[y][x]++;
if (z < -200)
{
rateOGMem[y][x] = -200;
}
continue;
}
}
}
}
//tends to empty trfDensity
private void decTrafficMem()
{
for (int y = 0; y < trfDensity.length; y++)
{
for (int x = 0; x < trfDensity[y].length; x++)
{
int z = trfDensity[y][x];
if (z != 0)
{
if (z > 200)
trfDensity[y][x] = z - 34;
else if (z > 24)
trfDensity[y][x] = z - 24;
else
trfDensity[y][x] = 0;
}
}
}
}
void crimeScan()
{
policeMap = smoothFirePoliceMap(policeMap);
policeMap = smoothFirePoliceMap(policeMap);
policeMap = smoothFirePoliceMap(policeMap);
for (int sy = 0; sy < policeMap.length; sy++) {
for (int sx = 0; sx < policeMap[sy].length; sx++) {
policeMapEffect[sy][sx] = policeMap[sy][sx];
}
}
int count = 0;
int sum = 0;
int cmax = 0;
for (int hy = 0; hy < landValueMem.length; hy++) {
for (int hx = 0; hx < landValueMem[hy].length; hx++) {
int val = landValueMem[hy][hx];
if (val != 0) {
count++;
int z = 128 - val + popDensity[hy][hx];
z = Math.min(300, z);
z -= policeMap[hy/4][hx/4];
z = Math.min(250, z);
z = Math.max(0, z);
crimeMem[hy][hx] = z;
sum += z;
if (z > cmax || (z == cmax && PRNG.nextInt(4) == 0)) {
cmax = z;
crimeMaxLocationX = hx*2;
crimeMaxLocationY = hy*2;
}
}
else {
crimeMem[hy][hx] = 0;
}
}
}
if (count != 0)
crimeAverage = sum / count;
else
crimeAverage = 0;
fireMapOverlayDataChanged(MapState.POLICE_OVERLAY);
}
void doDisasters()
{
if (floodCnt > 0) {
floodCnt--;
}
final int [] DisChance = { 480, 240, 60 };
if (noDisasters)
return;
if (PRNG.nextInt(DisChance[gameLevel]+1) != 0)
return;
switch (PRNG.nextInt(9))
{
case 0:
case 1:
setFire();
break;
case 2:
case 3:
makeFlood();
break;
case 4:
break;
case 5:
makeTornado();
break;
case 6:
makeEarthquake();
break;
case 7:
case 8:
if (pollutionAverage > 60) {
makeMonster();
}
break;
}
}
private int[][] smoothFirePoliceMap(int[][] omap)
{
int smX = omap[0].length;
int smY = omap.length;
int[][] nmap = new int[smY][smX];
for (int sy = 0; sy < smY; sy++) {
for (int sx = 0; sx < smX; sx++) {
int edge = 0;
if (sx > 0) { edge += omap[sy][sx-1]; }
if (sx + 1 < smX) { edge += omap[sy][sx+1]; }
if (sy > 0) { edge += omap[sy-1][sx]; }
if (sy + 1 < smY) { edge += omap[sy+1][sx]; }
edge = edge / 4 + omap[sy][sx];
nmap[sy][sx] = edge / 2;
}
}
return nmap;
}
void fireAnalysis()
{
fireStMap = smoothFirePoliceMap(fireStMap);
fireStMap = smoothFirePoliceMap(fireStMap);
fireStMap = smoothFirePoliceMap(fireStMap);
for (int sy = 0; sy < fireStMap.length; sy++) {
for (int sx = 0; sx < fireStMap[sy].length; sx++) {
fireRate[sy][sx] = fireStMap[sy][sx];
}
}
fireMapOverlayDataChanged(MapState.FIRE_OVERLAY);
}
private boolean testForCond(CityLocation loc, int dir)
{
int xsave = loc.x;
int ysave = loc.y;
boolean rv = false;
if (movePowerLocation(loc,dir))
{
rv = (
isConductive(map[loc.y][loc.x]) &&
map[loc.y][loc.x] != NUCLEAR &&
map[loc.y][loc.x] != POWERPLANT &&
!hasPower(loc.x, loc.y)
);
}
loc.x = xsave;
loc.y = ysave;
return rv;
}
private boolean movePowerLocation(CityLocation loc, int dir)
{
switch(dir)
{
case 0:
if (loc.y > 0)
{
loc.y--;
return true;
}
else
return false;
case 1:
if (loc.x + 1 < getWidth())
{
loc.x++;
return true;
}
else
return false;
case 2:
if (loc.y + 1 < getHeight())
{
loc.y++;
return true;
}
else
return false;
case 3:
if (loc.x > 0)
{
loc.x--;
return true;
}
else
return false;
case 4:
return true;
}
return false;
}
void powerScan()
{
// clear powerMap
for (boolean [] bb : powerMap)
{
Arrays.fill(bb, false);
}
//
// Note: brownouts are based on total number of power plants, not the number
// of powerplants connected to your city.
//
int maxPower = coalCount * 700 + nuclearCount * 2000;
int numPower = 0;
// This is kind of odd algorithm, but I haven't the heart to rewrite it at
// this time.
while (!powerPlants.isEmpty())
{
CityLocation loc = powerPlants.pop();
int aDir = 4;
int conNum;
do
{
if (++numPower > maxPower)
{
// trigger notification
sendMessage(MicropolisMessage.BROWNOUTS_REPORT);
return;
}
movePowerLocation(loc, aDir);
powerMap[loc.y][loc.x] = true;
conNum = 0;
int dir = 0;
while (dir < 4 && conNum < 2)
{
if (testForCond(loc, dir))
{
conNum++;
aDir = dir;
}
else
{
}
dir++;
}
if (conNum > 1)
{
powerPlants.add(new CityLocation(loc.x,loc.y));
}
}
while (conNum != 0);
}
}
/**
* Increase the traffic-density measurement at a particular
* spot.
* @param traffic the amount to add to the density
*/
void addTraffic(int mapX, int mapY, int traffic)
{
int z = trfDensity[mapY/2][mapX/2];
z += traffic;
//FIXME- why is this only capped to 240
// by random chance. why is there no cap
// the rest of the time?
if (z > 240 && PRNG.nextInt(6) == 0)
{
z = 240;
trafficMaxLocationX = mapX;
trafficMaxLocationY = mapY;
HelicopterSprite copter = (HelicopterSprite) getSprite(SpriteKind.COP);
if (copter != null) {
copter.destX = mapX;
copter.destY = mapY;
}
}
trfDensity[mapY/2][mapX/2] = z;
}
/** Accessor method for landValueMem overlay. */
public int getLandValue(int xpos, int ypos)
{
if (testBounds(xpos, ypos)) {
return landValueMem[ypos/2][xpos/2];
}
else {
return 0;
}
}
public int getTrafficDensity(int xpos, int ypos)
{
if (testBounds(xpos, ypos)) {
return trfDensity[ypos/2][xpos/2];
} else {
return 0;
}
}
//power, terrain, land value
void ptlScan()
{
final int qX = (getWidth()+3)/4;
final int qY = (getHeight()+3)/4;
int [][] qtem = new int[qY][qX];
int landValueTotal = 0;
int landValueCount = 0;
final int HWLDX = (getWidth()+1)/2;
final int HWLDY = (getHeight()+1)/2;
int [][] tem = new int[HWLDY][HWLDX];
for (int x = 0; x < HWLDX; x++)
{
for (int y = 0; y < HWLDY; y++)
{
int plevel = 0;
int lvflag = 0;
int zx = 2*x;
int zy = 2*y;
for (int mx = zx; mx <= zx+1; mx++)
{
for (int my = zy; my <= zy+1; my++)
{
int tile = (map[my][mx] & LOMASK);
if (tile != DIRT)
{
if (tile < RUBBLE) //natural land features
{
//inc terrainMem
qtem[y/2][x/2] += 15;
continue;
}
plevel += getPollutionValue(tile);
if (isConstructed(tile))
lvflag++;
}
}
}
if (plevel < 0)
plevel = 250; //?
if (plevel > 255)
plevel = 255;
tem[y][x] = plevel;
if (lvflag != 0)
{
//land value equation
int dis = 34 - getDisCC(x, y);
dis *= 4;
dis += terrainMem[y/2][x/2];
dis -= pollutionMem[y][x];
if (crimeMem[y][x] > 190) {
dis -= 20;
}
if (dis > 250)
dis = 250;
if (dis < 1)
dis = 1;
landValueMem[y][x] = dis;
landValueTotal += dis;
landValueCount++;
}
else
{
landValueMem[y][x] = 0;
}
}
}
landValueAverage = landValueCount != 0 ? (landValueTotal/landValueCount) : 0;
tem = doSmooth(tem);
tem = doSmooth(tem);
int pcount = 0;
int ptotal = 0;
int pmax = 0;
for (int x = 0; x < HWLDX; x++)
{
for (int y = 0; y < HWLDY; y++)
{
int z = tem[y][x];
pollutionMem[y][x] = z;
if (z != 0)
{
pcount++;
ptotal += z;
if (z > pmax ||
(z == pmax && PRNG.nextInt(4) == 0))
{
pmax = z;
pollutionMaxLocationX = 2*x;
pollutionMaxLocationY = 2*y;
}
}
}
}
pollutionAverage = pcount != 0 ? (ptotal / pcount) : 0;
terrainMem = smoothTerrain(qtem);
fireMapOverlayDataChanged(MapState.POLLUTE_OVERLAY); //PLMAP
fireMapOverlayDataChanged(MapState.LANDVALUE_OVERLAY); //LVMAP
}
public CityLocation getLocationOfMaxPollution()
{
return new CityLocation(pollutionMaxLocationX, pollutionMaxLocationY);
}
static final int [] TaxTable = {
200, 150, 120, 100, 80, 50, 30, 0, -10, -40, -100,
-150, -200, -250, -300, -350, -400, -450, -500, -550, -600 };
public static class History
{
public int cityTime;
public int [] res = new int[240];
public int [] com = new int[240];
public int [] ind = new int[240];
public int [] money = new int[240];
public int [] pollution = new int[240];
public int [] crime = new int[240];
int resMax;
int comMax;
int indMax;
}
public History history = new History();
void setValves()
{
double normResPop = (double)resPop / 8.0;
totalPop = (int) (normResPop + comPop + indPop);
double employment;
if (normResPop != 0.0)
{
employment = (history.com[1] + history.ind[1]) / normResPop;
}
else
{
employment = 1;
}
double migration = normResPop * (employment - 1);
final double BIRTH_RATE = 0.02;
double births = (double)normResPop * BIRTH_RATE;
double projectedResPop = normResPop + migration + births;
double temp = (history.com[1] + history.ind[1]);
double laborBase;
if (temp != 0.0)
{
laborBase = history.res[1] / temp;
}
else
{
laborBase = 1;
}
// clamp laborBase to between 0.0 and 1.3
laborBase = Math.max(0.0, Math.min(1.3, laborBase));
double internalMarket = (double)(normResPop + comPop + indPop) / 3.7;
double projectedComPop = internalMarket * laborBase;
int z = gameLevel;
temp = 1.0;
switch (z)
{
case 0: temp = 1.2; break;
case 1: temp = 1.1; break;
case 2: temp = 0.98; break;
}
double projectedIndPop = indPop * laborBase * temp;
if (projectedIndPop < 5.0)
projectedIndPop = 5.0;
double resRatio;
if (normResPop != 0)
{
resRatio = (double)projectedResPop / (double)normResPop;
}
else
{
resRatio = 1.3;
}
double comRatio;
if (comPop != 0)
comRatio = (double)projectedComPop / (double)comPop;
else
comRatio = projectedComPop;
double indRatio;
if (indPop != 0)
indRatio = (double)projectedIndPop / (double)indPop;
else
indRatio = projectedIndPop;
if (resRatio > 2.0)
resRatio = 2.0;
if (comRatio > 2.0)
comRatio = 2.0;
if (indRatio > 2.0)
indRatio = 2.0;
int z2 = taxEffect + gameLevel;
if (z2 > 20)
z2 = 20;
resRatio = (resRatio - 1) * 600 + TaxTable[z2];
comRatio = (comRatio - 1) * 600 + TaxTable[z2];
indRatio = (indRatio - 1) * 600 + TaxTable[z2];
// ratios are velocity changes to valves
resValve += (int) resRatio;
comValve += (int) comRatio;
indValve += (int) indRatio;
if (resValve > 2000)
resValve = 2000;
else if (resValve < -2000)
resValve = -2000;
if (comValve > 1500)
comValve = 1500;
else if (comValve < -1500)
comValve = -1500;
if (indValve > 1500)
indValve = 1500;
else if (indValve < -1500)
indValve = -1500;
if (resCap && resValve > 0) {
// residents demand stadium
resValve = 0;
}
if (comCap && comValve > 0) {
// commerce demands airport
comValve = 0;
}
if (indCap && indValve > 0) {
// industry demands sea port
indValve = 0;
}
fireDemandChanged();
}
int [][] smoothTerrain(int [][] qtem)
{
final int QWX = qtem[0].length;
final int QWY = qtem.length;
int [][] mem = new int[QWY][QWX];
for (int y = 0; y < QWY; y++)
{
for (int x = 0; x < QWX; x++)
{
int z = 0;
if (x > 0)
z += qtem[y][x-1];
if (x+1 < QWX)
z += qtem[y][x+1];
if (y > 0)
z += qtem[y-1][x];
if (y+1 < QWY)
z += qtem[y+1][x];
mem[y][x] = z / 4 + qtem[y][x] / 2;
}
}
return mem;
}
// calculate manhatten distance (in 2-units) from center of city
// capped at 32
int getDisCC(int x, int y)
{
assert x >= 0 && x <= getWidth()/2;
assert y >= 0 && y <= getHeight()/2;
int xdis = Math.abs(x - centerMassX/2);
int ydis = Math.abs(y - centerMassY/2);
int z = (xdis + ydis);
if (z > 32)
return 32;
else
return z;
}
Map<String,TileBehavior> tileBehaviors;
void initTileBehaviors()
{
HashMap<String,TileBehavior> bb;
bb = new HashMap<String,TileBehavior>();
bb.put("FIRE", new MapScanner(this, MapScanner.B.FIRE));
bb.put("FLOOD", new MapScanner(this, MapScanner.B.FLOOD));
bb.put("RADIOACTIVE", new MapScanner(this, MapScanner.B.RADIOACTIVE));
bb.put("ROAD", new MapScanner(this, MapScanner.B.ROAD));
bb.put("RAIL", new MapScanner(this, MapScanner.B.RAIL));
bb.put("EXPLOSION", new MapScanner(this, MapScanner.B.EXPLOSION));
bb.put("RESIDENTIAL", new MapScanner(this, MapScanner.B.RESIDENTIAL));
bb.put("HOSPITAL_CHURCH", new MapScanner(this, MapScanner.B.HOSPITAL_CHURCH));
bb.put("COMMERCIAL", new MapScanner(this, MapScanner.B.COMMERCIAL));
bb.put("INDUSTRIAL", new MapScanner(this, MapScanner.B.INDUSTRIAL));
bb.put("COAL", new MapScanner(this, MapScanner.B.COAL));
bb.put("NUCLEAR", new MapScanner(this, MapScanner.B.NUCLEAR));
bb.put("FIRESTATION", new MapScanner(this, MapScanner.B.FIRESTATION));
bb.put("POLICESTATION", new MapScanner(this, MapScanner.B.POLICESTATION));
bb.put("STADIUM_EMPTY", new MapScanner(this, MapScanner.B.STADIUM_EMPTY));
bb.put("STADIUM_FULL", new MapScanner(this, MapScanner.B.STADIUM_FULL));
bb.put("AIRPORT", new MapScanner(this, MapScanner.B.AIRPORT));
bb.put("SEAPORT", new MapScanner(this, MapScanner.B.SEAPORT));
this.tileBehaviors = bb;
}
void mapScan(int x0, int x1)
{
for (int x = x0; x < x1; x++)
{
for (int y = 0; y < getHeight(); y++)
{
mapScanTile(x, y);
}
}
}
void mapScanTile(int xpos, int ypos)
{
int rawTile = getTile(xpos, ypos);
String behaviorStr = getTileBehavior(rawTile & LOMASK);
if (behaviorStr == null) {
return; //nothing to do
}
TileBehavior b = tileBehaviors.get(behaviorStr);
if (b != null) {
b.processTile(xpos, ypos);
}
else {
throw new Error("Unknown behavior: "+behaviorStr);
}
}
void generateShip()
{
int edge = PRNG.nextInt(4);
if (edge == 0) {
for (int x = 4; x < getWidth() - 2; x++) {
if (getTile(x,0) == CHANNEL) {
makeShipAt(x, 0, ShipSprite.NORTH_EDGE);
return;
}
}
}
else if (edge == 1) {
for (int y = 1; y < getHeight() - 2; y++) {
if (getTile(0,y) == CHANNEL) {
makeShipAt(0, y, ShipSprite.EAST_EDGE);
return;
}
}
}
else if (edge == 2) {
for (int x = 4; x < getWidth() - 2; x++) {
if (getTile(x, getHeight()-1) == CHANNEL) {
makeShipAt(x, getHeight()-1, ShipSprite.SOUTH_EDGE);
return;
}
}
}
else {
for (int y = 1; y < getHeight() - 2; y++) {
if (getTile(getWidth()-1, y) == CHANNEL) {
makeShipAt(getWidth()-1, y, ShipSprite.EAST_EDGE);
return;
}
}
}
}
Sprite getSprite(SpriteKind kind)
{
for (Sprite s : sprites) {
if (s.kind == kind)
return s;
}
return null;
}
boolean hasSprite(SpriteKind kind)
{
return getSprite(kind) != null;
}
void makeShipAt(int xpos, int ypos, int edge)
{
assert !hasSprite(SpriteKind.SHI);
sprites.add(new ShipSprite(this, xpos, ypos, edge));
}
void generateCopter(int xpos, int ypos)
{
if (!hasSprite(SpriteKind.COP)) {
sprites.add(new HelicopterSprite(this, xpos, ypos));
}
}
void generatePlane(int xpos, int ypos)
{
if (!hasSprite(SpriteKind.AIR)) {
sprites.add(new AirplaneSprite(this, xpos, ypos));
}
}
void generateTrain(int xpos, int ypos)
{
if (totalPop > 20 &&
!hasSprite(SpriteKind.TRA) &&
PRNG.nextInt(26) == 0)
{
sprites.add(new TrainSprite(this, xpos, ypos));
}
}
Stack<CityLocation> powerPlants = new Stack<CityLocation>();
// counts the population in a certain type of residential zone
int doFreePop(int xpos, int ypos)
{
int count = 0;
for (int x = xpos - 1; x <= xpos + 1; x++)
{
for (int y = ypos - 1; y <= ypos + 1; y++)
{
if (testBounds(x,y))
{
char loc = (char) (map[y][x] & LOMASK);
if (loc >= LHTHR && loc <= HHTHR)
count++;
}
}
}
return count;
}
// called every several cycles; this takes the census data collected in this
// cycle and records it to the history
//
void takeCensus()
{
int resMax = 0;
int comMax = 0;
int indMax = 0;
for (int i = 118; i >= 0; i--)
{
if (history.res[i] > resMax)
resMax = history.res[i];
if (history.com[i] > comMax)
comMax = history.res[i];
if (history.ind[i] > indMax)
indMax = history.ind[i];
history.res[i + 1] = history.res[i];
history.com[i + 1] = history.com[i];
history.ind[i + 1] = history.ind[i];
history.crime[i + 1] = history.crime[i];
history.pollution[i + 1] = history.pollution[i];
history.money[i + 1] = history.money[i];
}
history.resMax = resMax;
history.comMax = comMax;
history.indMax = indMax;
//graph10max = Math.max(resMax, Math.max(comMax, indMax));
history.res[0] = resPop / 8;
history.com[0] = comPop;
history.ind[0] = indPop;
crimeRamp += (crimeAverage - crimeRamp) / 4;
history.crime[0] = Math.min(255, crimeRamp);
polluteRamp += (pollutionAverage - polluteRamp) / 4;
history.pollution[0] = Math.min(255, polluteRamp);
int moneyScaled = cashFlow / 20 + 128;
if (moneyScaled < 0)
moneyScaled = 0;
if (moneyScaled > 255)
moneyScaled = 255;
history.money[0] = moneyScaled;
history.cityTime = cityTime;
if (hospitalCount < resPop / 256)
{
needHospital = 1;
}
else if (hospitalCount > resPop / 256)
{
needHospital = -1;
}
else
{
needHospital = 0;
}
if (churchCount < resPop / 256)
{
needChurch = 1;
}
else if (churchCount > resPop / 256)
{
needChurch = -1;
}
else
{
needChurch = 0;
}
}
void takeCensus2()
{
// update long term graphs
int resMax = 0;
int comMax = 0;
int indMax = 0;
for (int i = 238; i >= 120; i--)
{
if (history.res[i] > resMax)
resMax = history.res[i];
if (history.com[i] > comMax)
comMax = history.res[i];
if (history.ind[i] > indMax)
indMax = history.ind[i];
history.res[i + 1] = history.res[i];
history.com[i + 1] = history.com[i];
history.ind[i + 1] = history.ind[i];
history.crime[i + 1] = history.crime[i];
history.pollution[i + 1] = history.pollution[i];
history.money[i + 1] = history.money[i];
}
history.res[120] = resPop / 8;
history.com[120] = comPop;
history.ind[120] = indPop;
history.crime[120] = history.crime[0];
history.pollution[120] = history.pollution[0];
history.money[120] = history.money[0];
}
/** Road/rail maintenance cost multiplier, for various difficulty settings.
*/
static final double [] RLevels = { 0.7, 0.9, 1.2 };
/** Tax income multiplier, for various difficulty settings.
*/
static final double [] FLevels = { 1.4, 1.2, 0.8 };
void collectTaxPartial()
{
lastRoadTotal = roadTotal;
lastRailTotal = railTotal;
lastTotalPop = totalPop;
lastFireStationCount = fireStationCount;
lastPoliceCount = policeCount;
BudgetNumbers b = generateBudget();
budget.taxFund += b.taxIncome;
budget.roadFundEscrow -= b.roadFunded;
budget.fireFundEscrow -= b.fireFunded;
budget.policeFundEscrow -= b.policeFunded;
taxEffect = b.taxRate;
roadEffect = b.roadRequest != 0 ?
(int)Math.floor(32.0 * (double)b.roadFunded / (double)b.roadRequest) :
32;
policeEffect = b.policeRequest != 0 ?
(int)Math.floor(1000.0 * (double)b.policeFunded / (double)b.policeRequest) :
1000;
fireEffect = b.fireRequest != 0 ?
(int)Math.floor(1000.0 * (double)b.fireFunded / (double)b.fireRequest) :
1000;
}
public static class FinancialHistory
{
public int cityTime;
public int totalFunds;
public int taxIncome;
public int operatingExpenses;
}
public ArrayList<FinancialHistory> financialHistory = new ArrayList<FinancialHistory>();
void collectTax()
{
int revenue = budget.taxFund / TAXFREQ;
int expenses = -(budget.roadFundEscrow + budget.fireFundEscrow + budget.policeFundEscrow) / TAXFREQ;
FinancialHistory hist = new FinancialHistory();
hist.cityTime = cityTime;
hist.taxIncome = revenue;
hist.operatingExpenses = expenses;
cashFlow = revenue - expenses;
spend(-cashFlow);
hist.totalFunds = budget.totalFunds;
financialHistory.add(0,hist);
budget.taxFund = 0;
budget.roadFundEscrow = 0;
budget.fireFundEscrow = 0;
budget.policeFundEscrow = 0;
}
/** Annual maintenance cost of each police station. */
static final int POLICE_STATION_MAINTENANCE = 100;
/** Annual maintenance cost of each fire station. */
static final int FIRE_STATION_MAINTENANCE = 100;
/**
* Calculate the current budget numbers.
*/
public BudgetNumbers generateBudget()
{
BudgetNumbers b = new BudgetNumbers();
b.taxRate = Math.max(0, cityTax);
b.roadPercent = Math.max(0.0, roadPercent);
b.firePercent = Math.max(0.0, firePercent);
b.policePercent = Math.max(0.0, policePercent);
b.previousBalance = budget.totalFunds;
b.taxIncome = (int)Math.round(lastTotalPop * landValueAverage / 120 * b.taxRate * FLevels[gameLevel]);
assert b.taxIncome >= 0;
b.roadRequest = (int)Math.round((lastRoadTotal + lastRailTotal * 2) * RLevels[gameLevel]);
b.fireRequest = FIRE_STATION_MAINTENANCE * lastFireStationCount;
b.policeRequest = POLICE_STATION_MAINTENANCE * lastPoliceCount;
b.roadFunded = (int)Math.round(b.roadRequest * b.roadPercent);
b.fireFunded = (int)Math.round(b.fireRequest * b.firePercent);
b.policeFunded = (int)Math.round(b.policeRequest * b.policePercent);
int yumDuckets = budget.totalFunds + b.taxIncome;
assert yumDuckets >= 0;
if (yumDuckets >= b.roadFunded)
{
yumDuckets -= b.roadFunded;
if (yumDuckets >= b.fireFunded)
{
yumDuckets -= b.fireFunded;
if (yumDuckets >= b.policeFunded)
{
yumDuckets -= b.policeFunded;
}
else
{
assert b.policeRequest != 0;
b.policeFunded = yumDuckets;
b.policePercent = (double)b.policeFunded / (double)b.policeRequest;
yumDuckets = 0;
}
}
else
{
assert b.fireRequest != 0;
b.fireFunded = yumDuckets;
b.firePercent = (double)b.fireFunded / (double)b.fireRequest;
b.policeFunded = 0;
b.policePercent = 0.0;
yumDuckets = 0;
}
}
else
{
assert b.roadRequest != 0;
b.roadFunded = yumDuckets;
b.roadPercent = (double)b.roadFunded / (double)b.roadRequest;
b.fireFunded = 0;
b.firePercent = 0.0;
b.policeFunded = 0;
b.policePercent = 0.0;
}
b.operatingExpenses = b.roadFunded + b.fireFunded + b.policeFunded;
b.newBalance = b.previousBalance + b.taxIncome - b.operatingExpenses;
return b;
}
/**
* The three main types of zones found in Micropolis.
*/
static enum ZoneType
{
RESIDENTIAL, COMMERCIAL, INDUSTRIAL;
}
TrafficGen traffic = new TrafficGen(this);
/**
* @return 1 if traffic "passed", 0 if traffic "failed", -1 if no roads found
*/
int makeTraffic(int xpos, int ypos, ZoneType zoneType)
{
traffic.mapX = xpos;
traffic.mapY = ypos;
traffic.sourceZone = zoneType;
return traffic.makeTraffic();
}
int getPopulationDensity(int xpos, int ypos)
{
return popDensity[ypos/2][xpos/2];
}
void doMeltdown(int xpos, int ypos)
{
meltdownLocation = new CityLocation(xpos, ypos);
makeExplosion(xpos - 1, ypos - 1);
makeExplosion(xpos - 1, ypos + 2);
makeExplosion(xpos + 2, ypos - 1);
makeExplosion(xpos + 2, ypos + 2);
for (int x = xpos - 1; x < xpos + 3; x++) {
for (int y = ypos - 1; y < ypos + 3; y++) {
setTile(x, y, (char)(FIRE + PRNG.nextInt(4)));
}
}
for (int z = 0; z < 200; z++) {
int x = xpos - 20 + PRNG.nextInt(41);
int y = ypos - 15 + PRNG.nextInt(31);
if (!testBounds(x,y))
continue;
int t = map[y][x];
if (isZoneCenter(t)) {
continue;
}
if (isCombustible(t) || t == DIRT) {
setTile(x, y, RADTILE);
}
}
clearMes();
sendMessageAt(MicropolisMessage.MELTDOWN_REPORT, xpos, ypos);
}
static final int [] MltdwnTab = { 30000, 20000, 10000 };
void loadHistoryArray(int [] array, DataInputStream dis)
throws IOException
{
for (int i = 0; i < 240; i++)
{
array[i] = dis.readShort();
}
}
void writeHistoryArray(int [] array, DataOutputStream out)
throws IOException
{
for (int i = 0; i < 240; i++)
{
out.writeShort(array[i]);
}
}
void loadMisc(DataInputStream dis)
throws IOException
{
dis.readShort(); //[0]... ignored?
dis.readShort(); //[1] externalMarket, ignored
resPop = dis.readShort(); //[2-4] populations
comPop = dis.readShort();
indPop = dis.readShort();
resValve = dis.readShort(); //[5-7] valves
comValve = dis.readShort();
indValve = dis.readShort();
cityTime = dis.readInt(); //[8-9] city time
crimeRamp = dis.readShort(); //[10]
polluteRamp = dis.readShort();
landValueAverage = dis.readShort(); //[12]
crimeAverage = dis.readShort();
pollutionAverage = dis.readShort(); //[14]
gameLevel = dis.readShort();
evaluation.cityClass = dis.readShort(); //[16]
evaluation.cityScore = dis.readShort();
for (int i = 18; i < 50; i++)
{
dis.readShort();
}
budget.totalFunds = dis.readInt(); //[50-51] total funds
autoBulldoze = dis.readShort() != 0; //52
autoBudget = dis.readShort() != 0;
autoGo = dis.readShort() != 0; //54
dis.readShort(); // userSoundOn (this setting not saved to game file
// in this edition of the game)
cityTax = dis.readShort(); //56
taxEffect = cityTax;
int simSpeedAsInt = dis.readShort();
if (simSpeedAsInt >= 0 && simSpeedAsInt <= 4)
simSpeed = Speed.values()[simSpeedAsInt];
else
simSpeed = Speed.NORMAL;
// read budget numbers, convert them to percentages
//
long n = dis.readInt(); //58,59... police percent
policePercent = (double)n / 65536.0;
n = dis.readInt(); //60,61... fire percent
firePercent = (double)n / 65536.0;
n = dis.readInt(); //62,63... road percent
roadPercent = (double)n / 65536.0;
for (int i = 64; i < 120; i++)
{
dis.readShort();
}
if (cityTime < 0) { cityTime = 0; }
if (cityTax < 0 || cityTax > 20) { cityTax = 7; }
if (gameLevel < 0 || gameLevel > 2) { gameLevel = 0; }
if (evaluation.cityClass < 0 || evaluation.cityClass > 5) { evaluation.cityClass = 0; }
if (evaluation.cityScore < 1 || evaluation.cityScore > 999) { evaluation.cityScore = 500; }
resCap = false;
comCap = false;
indCap = false;
}
void writeMisc(DataOutputStream out)
throws IOException
{
out.writeShort(0);
out.writeShort(0);
out.writeShort(resPop);
out.writeShort(comPop);
out.writeShort(indPop);
out.writeShort(resValve);
out.writeShort(comValve);
out.writeShort(indValve);
//8
out.writeInt(cityTime);
out.writeShort(crimeRamp);
out.writeShort(polluteRamp);
//12
out.writeShort(landValueAverage);
out.writeShort(crimeAverage);
out.writeShort(pollutionAverage);
out.writeShort(gameLevel);
//16
out.writeShort(evaluation.cityClass);
out.writeShort(evaluation.cityScore);
//18
for (int i = 18; i < 50; i++) {
out.writeShort(0);
}
//50
out.writeInt(budget.totalFunds);
out.writeShort(autoBulldoze ? 1 : 0);
out.writeShort(autoBudget ? 1 : 0);
//54
out.writeShort(autoGo ? 1 : 0);
out.writeShort(1); //userSoundOn
out.writeShort(cityTax);
out.writeShort(simSpeed.ordinal());
//58
out.writeInt((int)(policePercent * 65536));
out.writeInt((int)(firePercent * 65536));
out.writeInt((int)(roadPercent * 65536));
//64
for (int i = 64; i < 120; i++) {
out.writeShort(0);
}
}
void loadMap(DataInputStream dis)
throws IOException
{
for (int x = 0; x < DEFAULT_WIDTH; x++)
{
for (int y = 0; y < DEFAULT_HEIGHT; y++)
{
int z = dis.readShort();
z &= ~(1024 | 2048 | 8192 | 16384); // clear ZONEBIT,ANIMBIT,BURNBIT,CONDBIT on import
map[y][x] = (char) z;
}
}
}
void writeMap(DataOutputStream out)
throws IOException
{
for (int x = 0; x < DEFAULT_WIDTH; x++)
{
for (int y = 0; y < DEFAULT_HEIGHT; y++)
{
int z = map[y][x];
if (isConductive(z)) {
z |= 16384; //synthesize CONDBIT on export
}
if (isCombustible(z)) {
z |= 8192; //synthesize BURNBIT on export
}
if (isAnimated(z)) {
z |= 2048; //synthesize ANIMBIT on export
}
if (isZoneCenter(z)) {
z |= 1024; //synthesize ZONEBIT
}
out.writeShort(z);
}
}
}
public void load(File filename)
throws IOException
{
FileInputStream fis = new FileInputStream(filename);
if (fis.getChannel().size() > 27120) {
// some editions of the classic Simcity game
// start the file off with a 128-byte header,
// but otherwise use the same format as us,
// so read in that 128-byte header and continue
// as before.
byte [] bbHeader = new byte[128];
fis.read(bbHeader);
}
load(fis);
}
void checkPowerMap()
{
coalCount = 0;
nuclearCount = 0;
powerPlants.clear();
for (int y = 0; y < map.length; y++) {
for (int x = 0; x < map[y].length; x++) {
int tile = getTile(x,y);
if ((tile & LOMASK) == NUCLEAR) {
nuclearCount++;
powerPlants.add(new CityLocation(x,y));
}
else if ((tile & LOMASK) == POWERPLANT) {
coalCount++;
powerPlants.add(new CityLocation(x,y));
}
}
}
powerScan();
newPower = true;
}
public void load(InputStream inStream)
throws IOException
{
DataInputStream dis = new DataInputStream(inStream);
loadHistoryArray(history.res, dis);
loadHistoryArray(history.com, dis);
loadHistoryArray(history.ind, dis);
loadHistoryArray(history.crime, dis);
loadHistoryArray(history.pollution, dis);
loadHistoryArray(history.money, dis);
loadMisc(dis);
loadMap(dis);
dis.close();
checkPowerMap();
fireWholeMapChanged();
fireDemandChanged();
fireFundsChanged();
}
public void save(File filename)
throws IOException
{
save(new FileOutputStream(filename));
}
public void save(OutputStream outStream)
throws IOException
{
DataOutputStream out = new DataOutputStream(outStream);
writeHistoryArray(history.res, out);
writeHistoryArray(history.com, out);
writeHistoryArray(history.ind, out);
writeHistoryArray(history.crime, out);
writeHistoryArray(history.pollution, out);
writeHistoryArray(history.money, out);
writeMisc(out);
writeMap(out);
out.close();
}
public void toggleAutoBudget()
{
autoBudget = !autoBudget;
fireOptionsChanged();
}
public void toggleAutoBulldoze()
{
autoBulldoze = !autoBulldoze;
fireOptionsChanged();
}
public void toggleDisasters()
{
noDisasters = !noDisasters;
fireOptionsChanged();
}
public void setSpeed(Speed newSpeed)
{
simSpeed = newSpeed;
fireOptionsChanged();
}
public void animate()
{
this.acycle = (this.acycle+1) % 960;
if (this.acycle % 2 == 0) {
step();
}
moveObjects();
animateTiles();
}
public Sprite [] allSprites()
{
return sprites.toArray(new Sprite[0]);
}
void moveObjects()
{
for (Sprite sprite : allSprites())
{
sprite.move();
if (sprite.frame == 0) {
sprites.remove(sprite);
}
}
}
void animateTiles()
{
for (int y = 0; y < map.length; y++)
{
for (int x = 0; x < map[y].length; x++)
{
char tilevalue = map[y][x];
TileSpec spec = Tiles.get(tilevalue & LOMASK);
if (spec != null && spec.animNext != null) {
int flags = tilevalue & ALLBITS;
setTile(x, y, (char)
(spec.animNext.tileNumber | flags)
);
}
}
}
}
public int getCityPopulation()
{
return lastCityPop;
}
void makeSound(int x, int y, Sound sound)
{
fireCitySound(sound, new CityLocation(x,y));
}
public void makeEarthquake()
{
makeSound(centerMassX, centerMassY, Sound.EXPLOSION_LOW);
fireEarthquakeStarted();
sendMessageAt(MicropolisMessage.EARTHQUAKE_REPORT, centerMassX, centerMassY);
int time = PRNG.nextInt(701) + 300;
for (int z = 0; z < time; z++) {
int x = PRNG.nextInt(getWidth());
int y = PRNG.nextInt(getHeight());
assert testBounds(x, y);
if (TileConstants.isVulnerable(getTile(x, y))) {
if (PRNG.nextInt(4) != 0) {
setTile(x, y, (char)(RUBBLE + BULLBIT + PRNG.nextInt(4)));
} else {
setTile(x, y, (char)(FIRE + PRNG.nextInt(8)));
}
}
}
}
void setFire()
{
int x = PRNG.nextInt(getWidth());
int y = PRNG.nextInt(getHeight());
int t = getTile(x, y);
if (TileConstants.isArsonable(t)) {
setTile(x, y, (char)(FIRE + PRNG.nextInt(8)));
crashLocation = new CityLocation(x, y);
sendMessageAt(MicropolisMessage.FIRE_REPORT, x, y);
}
}
public void makeFire()
{
// forty attempts at finding place to start fire
for (int t = 0; t < 40; t++)
{
int x = PRNG.nextInt(getWidth());
int y = PRNG.nextInt(getHeight());
int tile = map[y][x];
if (!isZoneCenter(tile) && isCombustible(tile))
{
tile &= LOMASK;
if (tile > 21 && tile < LASTZONE) {
setTile(x, y, (char)(FIRE + PRNG.nextInt(8)));
sendMessageAt(MicropolisMessage.FIRE_REPORT, x, y);
return;
}
}
}
}
/**
* Force a meltdown to occur.
* @return true if a metldown was initiated.
*/
public boolean makeMeltdown()
{
ArrayList<CityLocation> candidates = new ArrayList<CityLocation>();
for (int y = 0; y < map.length; y++) {
for (int x = 0; x < map[y].length; x++) {
if ((map[y][x] & LOMASK) == NUCLEAR) {
candidates.add(new CityLocation(x,y));
}
}
}
if (candidates.isEmpty()) {
// tell caller that no nuclear plants were found
return false;
}
int i = PRNG.nextInt(candidates.size());
CityLocation p = candidates.get(i);
doMeltdown(p.x, p.y);
return true;
}
public void makeMonster()
{
MonsterSprite monster = (MonsterSprite) getSprite(SpriteKind.GOD);
if (monster != null) {
// already have a monster in town
monster.soundCount = 1;
monster.count = 1000;
monster.flag = false;
monster.destX = pollutionMaxLocationX;
monster.destY = pollutionMaxLocationY;
return;
}
// try to find a suitable starting spot for monster
for (int i = 0; i < 300; i++) {
int x = PRNG.nextInt(getWidth() - 19) + 10;
int y = PRNG.nextInt(getHeight() - 9) + 5;
int t = getTile(x, y);
if ((t & LOMASK) == RIVER) {
makeMonsterAt(x, y);
return;
}
}
// no "nice" location found, just start in center of map then
makeMonsterAt(getWidth()/2, getHeight()/2);
}
void makeMonsterAt(int xpos, int ypos)
{
assert !hasSprite(SpriteKind.GOD);
sprites.add(new MonsterSprite(this, xpos, ypos));
}
public void makeTornado()
{
TornadoSprite tornado = (TornadoSprite) getSprite(SpriteKind.TOR);
if (tornado != null) {
// already have a tornado, so extend the length of the
// existing tornado
tornado.count = 200;
return;
}
//FIXME- this is not exactly like the original code
int xpos = PRNG.nextInt(getWidth() - 19) + 10;
int ypos = PRNG.nextInt(getHeight() - 19) + 10;
sprites.add(new TornadoSprite(this, xpos, ypos));
sendMessageAt(MicropolisMessage.TORNADO_REPORT, xpos, ypos);
}
public void makeFlood()
{
final int [] DX = { 0, 1, 0, -1 };
final int [] DY = { -1, 0, 1, 0 };
for (int z = 0; z < 300; z++) {
int x = PRNG.nextInt(getWidth());
int y = PRNG.nextInt(getHeight());
int tile = map[y][x] & LOMASK;
if (isRiverEdge(tile))
{
for (int t = 0; t < 4; t++) {
int xx = x + DX[t];
int yy = y + DY[t];
if (testBounds(xx,yy)) {
int c = map[yy][xx];
if (isFloodable(c)) {
setTile(xx, yy, FLOOD);
floodCnt = 30;
sendMessageAt(MicropolisMessage.FLOOD_REPORT, xx, yy);
floodX = xx;
floodY = yy;
return;
}
}
}
}
}
}
/**
* Makes all component tiles of a zone bulldozable.
* Should be called whenever the key zone tile of a zone is destroyed,
* since otherwise the user would no longer have a way of destroying
* the zone.
* @see #shutdownZone
*/
void killZone(int xpos, int ypos, int zoneTile)
{
rateOGMem[ypos/8][xpos/8] -= 20;
assert isZoneCenter(zoneTile);
CityDimension dim = getZoneSizeFor(zoneTile);
assert dim != null;
assert dim.width >= 3;
assert dim.height >= 3;
int zoneBase = (zoneTile&LOMASK) - 1 - dim.width;
// this will take care of stopping smoke animations
shutdownZone(xpos, ypos, dim);
for (int y = 0; y < dim.height; y++) {
for (int x = 0; x < dim.width; x++, zoneBase++) {
int xtem = xpos - 1 + x;
int ytem = ypos - 1 + y;
if (!testBounds(xtem, ytem))
continue;
int t = getTile(xtem, ytem);
if (isConstructed(t)) {
setTile(xtem, ytem, (char)(t | BULLBIT));
}
}
}
}
/**
* If a zone has a different image (animation) for when it is
* powered, switch to that different image here.
* Note: pollution is not accumulated here; see ptlScan()
* instead.
* @see #shutdownZone
*/
void powerZone(int xpos, int ypos, CityDimension zoneSize)
{
assert zoneSize.width >= 3;
assert zoneSize.height >= 3;
for (int dx = 0; dx < zoneSize.width; dx++) {
for (int dy = 0; dy < zoneSize.height; dy++) {
int x = xpos - 1 + dx;
int y = ypos - 1 + dy;
int tile = getTile(x, y);
TileSpec ts = Tiles.get(tile & LOMASK);
if (ts != null && ts.onPower != null) {
setTile(x, y,
(char) (ts.onPower.tileNumber | (tile & ALLBITS))
);
}
}
}
}
/**
* If a zone has a different image (animation) for when it is
* powered, switch back to the original image.
* @see #powerZone
* @see #killZone
*/
void shutdownZone(int xpos, int ypos, CityDimension zoneSize)
{
assert zoneSize.width >= 3;
assert zoneSize.height >= 3;
for (int dx = 0; dx < zoneSize.width; dx++) {
for (int dy = 0; dy < zoneSize.height; dy++) {
int x = xpos - 1 + dx;
int y = ypos - 1 + dy;
int tile = getTile(x, y);
TileSpec ts = Tiles.get(tile & LOMASK);
if (ts != null && ts.onShutdown != null) {
setTile(x, y,
(char) (ts.onShutdown.tileNumber | (tile & ALLBITS))
);
}
}
}
}
void makeExplosion(int xpos, int ypos)
{
makeExplosionAt(xpos*16+8, ypos*16+8);
}
/**
* Uses x,y coordinates as 1/16th-length tiles.
*/
void makeExplosionAt(int x, int y)
{
sprites.add(new ExplosionSprite(this, x, y));
}
void checkGrowth()
{
if (cityTime % 4 == 0) {
int newPop = (resPop + comPop * 8 + indPop * 8) * 20;
if (lastCityPop != 0) {
MicropolisMessage z = null;
if (lastCityPop < 500000 && newPop >= 500000) {
z = MicropolisMessage.POP_500K_REACHED;
} else if (lastCityPop < 100000 && newPop >= 100000) {
z = MicropolisMessage.POP_100K_REACHED;
} else if (lastCityPop < 50000 && newPop >= 50000) {
z = MicropolisMessage.POP_50K_REACHED;
} else if (lastCityPop < 10000 && newPop >= 10000) {
z = MicropolisMessage.POP_10K_REACHED;
} else if (lastCityPop < 2000 && newPop >= 2000) {
z = MicropolisMessage.POP_2K_REACHED;
}
if (z != null) {
sendMessage(z);
}
}
lastCityPop = newPop;
}
}
void doMessages()
{
//MORE (scenario stuff)
checkGrowth();
int totalZoneCount = resZoneCount + comZoneCount + indZoneCount;
int powerCount = nuclearCount + coalCount;
int z = cityTime % 64;
switch (z) {
case 1:
if (totalZoneCount / 4 >= resZoneCount) {
sendMessage(MicropolisMessage.NEED_RES);
}
break;
case 5:
if (totalZoneCount / 8 >= comZoneCount) {
sendMessage(MicropolisMessage.NEED_COM);
}
break;
case 10:
if (totalZoneCount / 8 >= indZoneCount) {
sendMessage(MicropolisMessage.NEED_IND);
}
break;
case 14:
if (totalZoneCount > 10 && totalZoneCount * 2 > roadTotal) {
sendMessage(MicropolisMessage.NEED_ROADS);
}
break;
case 18:
if (totalZoneCount > 50 && totalZoneCount > railTotal) {
sendMessage(MicropolisMessage.NEED_RAILS);
}
break;
case 22:
if (totalZoneCount > 10 && powerCount == 0) {
sendMessage(MicropolisMessage.NEED_POWER);
}
break;
case 26:
resCap = (resPop > 500 && stadiumCount == 0);
if (resCap) {
sendMessage(MicropolisMessage.NEED_STADIUM);
}
break;
case 28:
indCap = (indPop > 70 && seaportCount == 0);
if (indCap) {
sendMessage(MicropolisMessage.NEED_SEAPORT);
}
break;
case 30:
comCap = (comPop > 100 && airportCount == 0);
if (comCap) {
sendMessage(MicropolisMessage.NEED_AIRPORT);
}
break;
case 32:
int TM = unpoweredZoneCount + poweredZoneCount;
if (TM != 0) {
if ((double)poweredZoneCount / (double)TM < 0.7) {
sendMessage(MicropolisMessage.BLACKOUTS);
}
}
break;
case 35:
if (pollutionAverage > 60) { // FIXME, consider changing threshold to 80
sendMessage(MicropolisMessage.HIGH_POLLUTION);
}
break;
case 42:
if (crimeAverage > 100) {
sendMessage(MicropolisMessage.HIGH_CRIME);
}
break;
case 45:
if (totalPop > 60 && fireStationCount == 0) {
sendMessage(MicropolisMessage.NEED_FIRESTATION);
}
break;
case 48:
if (totalPop > 60 && policeCount == 0) {
sendMessage(MicropolisMessage.NEED_POLICE);
}
break;
case 51:
if (cityTax > 12) {
sendMessage(MicropolisMessage.HIGH_TAXES);
}
break;
case 54:
if (roadEffect < 20 && roadTotal > 30) {
sendMessage(MicropolisMessage.ROADS_NEED_FUNDING);
}
break;
case 57:
if (fireEffect < 700 && totalPop > 20) {
sendMessage(MicropolisMessage.FIRE_NEED_FUNDING);
}
break;
case 60:
if (policeEffect < 700 && totalPop > 20) {
sendMessage(MicropolisMessage.POLICE_NEED_FUNDING);
}
break;
case 63:
if (trafficAverage > 60) {
sendMessage(MicropolisMessage.HIGH_TRAFFIC);
}
break;
default:
//nothing
}
}
void clearMes()
{
//TODO.
// What this does in the original code is clears the 'last message'
// properties, ensuring that the next message will be delivered even
// if it is a repeat.
}
void sendMessage(MicropolisMessage message)
{
fireCityMessage(message, null);
}
void sendMessageAt(MicropolisMessage message, int x, int y)
{
fireCityMessage(message, new CityLocation(x,y));
}
public ZoneStatus queryZoneStatus(int xpos, int ypos)
{
ZoneStatus zs = new ZoneStatus();
zs.building = getDescriptionNumber(getTile(xpos, ypos));
int z;
z = (popDensity[ypos/2][xpos/2] / 64) % 4;
zs.popDensity = z + 1;
z = landValueMem[ypos/2][xpos/2];
z = z < 30 ? 4 : z < 80 ? 5 : z < 150 ? 6 : 7;
zs.landValue = z + 1;
z = ((crimeMem[ypos/2][xpos/2] / 64) % 4) + 8;
zs.crimeLevel = z + 1;
z = Math.max(13,((pollutionMem[ypos/2][xpos/2] / 64) % 4) + 12);
zs.pollution = z + 1;
z = rateOGMem[ypos/8][xpos/8];
z = z < 0 ? 16 : z == 0 ? 17 : z <= 100 ? 18 : 19;
zs.growthRate = z + 1;
return zs;
}
public int getResValve()
{
return resValve;
}
public int getComValve()
{
return comValve;
}
public int getIndValve()
{
return indValve;
}
public void setGameLevel(int newLevel)
{
assert GameLevel.isValid(newLevel);
gameLevel = newLevel;
fireOptionsChanged();
}
public void setFunds(int totalFunds)
{
budget.totalFunds = totalFunds;
}
}