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

2576 lines
53 KiB
Java
Raw Normal View History

// 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.*;
public class Micropolis
{
static final Random DEFAULT_PRNG = new Random();
Random PRNG;
// full size arrays
char [][] map;
boolean [][] powerMap;
// half-size arrays
public int [][] landValueMem;
public int [][] pollutionMem;
public int [][] crimeMem; //updated each cycle by crimeScan(); affects land value
public int [][] popDensity;
public int [][] trfDensity;
// quarter-size arrays
int [][] terrainMem;
// eighth-size arrays
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 int totalFunds;
public boolean autoBulldoze = true;
public boolean autoBudget = false;
public Speed simSpeed = Speed.NORMAL;
public boolean noDisasters = false;
public static final int MIN_LEVEL = 0;
public static final int MAX_LEVEL = 2;
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;
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 taxFund;
int roadFundEscrow;
int fireFundEscrow;
int policeFundEscrow;
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)
public int acycle; //animation cycle (mod 960)
public CityEval evaluation;
ArrayList<Sprite> sprites = new ArrayList<>();
static final int VALVERATE = 2;
public static final int CENSUSRATE = 4;
static final int TAXFREQ = 48;
public void spend(int amount)
{
totalFunds -= amount;
fireFundsChanged();
}
public Micropolis()
{
PRNG = DEFAULT_PRNG;
evaluation = new CityEval(this);
init();
}
protected void init()
{
int width = DEFAULT_WIDTH;
int height = DEFAULT_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, boolean isPic)
{
for (Listener l : listeners) {
l.cityMessage(message, loc, isPic);
}
}
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<>();
ArrayList<MapListener> mapListeners = new ArrayList<>();
ArrayList<EarthquakeListener> earthquakeListeners = new ArrayList<>();
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, boolean isPic);
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 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 step() will collect taxes and
* process the budget.
*/
public boolean isBudgetTime()
{
return (
cityTime != 0 &&
(cityTime % TAXFREQ) == 0 &&
((fcycle + 1) % 16) == 10
);
}
public 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;
}
}
}
// period of powerScan()
static final int [] spdPwr = { 1, 1, 1, 2, 5 };
// period of ptlScan()
static final int [] spdPtl = { 1, 1, 1, 2, 5 };
// period of crimeScan()
static final int [] spdCri = { 1, 1, 1, 2, 5 };
// period of popDenScan()
static final int [] spdPop = { 1, 1, 1, 2, 5 };
// period of fireAnalysis()
static final int [] spdFir = { 1, 1, 1, 2, 5 };
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:
if (scycle % spdPwr[simSpeed.ordinal()] == 0) {
powerScan();
fireMapOverlayDataChanged(MapState.POWER_OVERLAY);
newPower = true;
}
break;
case 12:
if (scycle % spdPtl[simSpeed.ordinal()] == 0) {
ptlScan();
}
break;
case 13:
if (scycle % spdCri[simSpeed.ordinal()] == 0) {
crimeScan();
}
break;
case 14:
if (scycle % spdPop[simSpeed.ordinal()] == 0) {
popDenScan();
}
break;
case 15:
if (scycle % spdFir[simSpeed.ordinal()] == 0) {
fireAnalysis();
}
doDisasters();
break;
default:
throw new Error("unreachable");
}
}
private int computePopDen(int x, int y, char tile)
{
if (tile == FREEZ)
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 ((tile & ZONEBIT) != 0)
{
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 isConductive(int x, int y)
{
return (map[y][x] & CONDBIT) != 0;
}
private boolean testForCond(CityLocation loc, int dir)
{
int xsave = loc.x;
int ysave = loc.y;
boolean rv = false;
if (movePowerLocation(loc,dir))
{
rv = (
isConductive(loc.x, loc.y) &&
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);
}
}
static int getPollutionValue(int tile)
{
if (tile < POWERBASE)
{
if (tile >= HTRFBASE)
return 75; //heavy traffic
if (tile >= LTRFBASE)
return 50; //light traffic
if (tile < ROADBASE)
{
if (tile > FIREBASE)
return 90;
if (tile >= RADTILE)
return 255; //radioactivity
}
return 0;
}
if (tile <= LASTIND)
return 0;
if (tile < PORTBASE)
return 50;
if (tile <= LASTPOWERPLANT)
return 100;
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 (tile >= ROADBASE)
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();
static class MiscHistory
{
int resPop;
int comPop;
int indPop;
int resValve;
int comValve;
int indValve;
int crimeRamp;
int polluteRamp;
int landValueAverage;
int crimeAverage;
int pollutionAverage;
int gameLevel;
int cityClass;
int cityScore;
}
void setValves()
{
MiscHistory hist = new MiscHistory();
hist.resPop = resPop;
hist.comPop = comPop;
hist.indPop = indPop;
hist.resValve = resValve;
hist.comValve = comValve;
hist.indValve = indValve;
hist.crimeRamp = crimeRamp;
hist.polluteRamp = polluteRamp;
hist.landValueAverage = landValueAverage;
hist.crimeAverage = crimeAverage;
hist.pollutionAverage = pollutionAverage;
hist.gameLevel = gameLevel;
hist.cityClass = evaluation.cityClass;
hist.cityScore = evaluation.cityScore;
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[z];
comRatio = (comRatio - 1) * 600 + TaxTable[z];
indRatio = (indRatio - 1) * 600 + TaxTable[z];
// 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;
}
void mapScan(int x0, int x1)
{
MapScanner scanner = new MapScanner(this);
for (int x = x0; x < x1; x++)
{
scanner.xpos = x;
for (int y = 0; y < getHeight(); y++)
{
scanner.ypos = y;
scanner.cchr = map[y][x];
scanner.scanTile();
}
}
}
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;
}
// counts the population in a certain type of residential zone
// a.k.a. RZPop
int residentialZonePop(char tile)
{
int czDen = ((tile - RZB) / 9) % 4;
return czDen * 8 + 16;
}
int commercialZonePop(int tile)
{
if (tile == COMCLR)
return 0;
int czDen = ((tile - CZB) / 9) % 5 + 1;
return czDen;
}
int industrialZonePop(int tile)
{
if (tile == INDCLR)
return 0;
int czDen = ((tile - IZB) / 9) % 4 + 1;
return czDen;
}
// 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()
{
BudgetNumbers b = generateBudget();
taxFund += b.taxIncome;
roadFundEscrow -= b.roadFunded;
fireFundEscrow -= b.fireFunded;
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<>();
void collectTax()
{
int revenue = taxFund / TAXFREQ;
int expenses = -(roadFundEscrow + fireFundEscrow + policeFundEscrow) / TAXFREQ;
FinancialHistory hist = new FinancialHistory();
hist.cityTime = cityTime;
hist.taxIncome = revenue;
hist.operatingExpenses = expenses;
cashFlow = revenue - expenses;
spend(-cashFlow);
hist.totalFunds = totalFunds;
financialHistory.add(0,hist);
taxFund = 0;
roadFundEscrow = 0;
fireFundEscrow = 0;
policeFundEscrow = 0;
}
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 = totalFunds;
b.taxIncome = (int)Math.round(totalPop * landValueAverage / 120 * b.taxRate * FLevels[gameLevel]);
assert b.taxIncome >= 0;
b.roadRequest = (int)Math.round((roadTotal + railTotal * 2) * RLevels[gameLevel]);
b.fireRequest = 100 * fireStationCount;
b.policeRequest = 100 * policeCount;
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 = 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) + ANIMBIT));
}
}
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 ((t & ZONEBIT) != 0)
continue;
if ((t & BURNBIT) != 0 || t == DIRT) {
setTile(x, y, RADTILE);
}
}
clearMes();
sendMessageAtPic(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();
}
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(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++)
{
map[y][x] = (char) dis.readShort();
}
}
}
void writeMap(DataOutputStream out)
throws IOException
{
for (int x = 0; x < DEFAULT_WIDTH; x++)
{
for (int y = 0; y < DEFAULT_HEIGHT; y++)
{
out.writeShort(map[y][x]);
}
}
}
public void load(File filename)
throws IOException
{
load(new FileInputStream(filename));
}
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;
assert powerPlants.isEmpty();
}
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()
{
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];
if ((tilevalue & ANIMBIT) != 0)
{
int flags = tilevalue & ALLBITS;
setTile(x, y, (char)
(Animate.aniTile[tilevalue & LOMASK] | 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();
sendMessageAtPic(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 + ANIMBIT + 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 + ANIMBIT + PRNG.nextInt(8)));
crashLocation = new CityLocation(x, y);
sendMessageAtPic(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 ((tile & ZONEBIT) == 0 && (tile & BURNBIT) != 0)
{
tile &= LOMASK;
if (tile > 21 && tile < LASTZONE) {
setTile(x, y, (char)(FIRE + ANIMBIT + 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<>();
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));
sendMessageAtPic(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;
sendMessageAtPic(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.
*/
void fireZone(int xpos, int ypos, int zoneTile)
{
rateOGMem[ypos/8][xpos/8] -= 20;
int sz = TileConstants.getZoneSizeFor(zoneTile);
for (int x = 0; x < sz; x++) {
for (int y = 0; y < sz; y++) {
int xtem = xpos - 1 + x;
int ytem = ypos - 1 + y;
if (!testBounds(xtem, ytem))
continue;
int t = getTile(xtem, ytem);
if ((t & LOMASK) >= ROADBASE) {
setTile(xtem, ytem, (char)(t | BULLBIT));
}
}
}
}
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, true);
}
}
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, true);
}
break;
case 42:
if (crimeAverage > 100) {
sendMessage(MicropolisMessage.HIGH_CRIME, true);
}
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, false);
}
void sendMessage(MicropolisMessage message, boolean isPic)
{
fireCityMessage(message, null, true);
}
void sendMessageAt(MicropolisMessage message, int x, int y)
{
fireCityMessage(message, new CityLocation(x,y), false);
}
void sendMessageAtPic(MicropolisMessage message, int x, int y)
{
fireCityMessage(message, new CityLocation(x,y), true);
}
public ZoneStatus queryZoneStatus(int xpos, int ypos)
{
ZoneStatus zs = new ZoneStatus();
zs.building = getBuildingId(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);
int origFunds = GameLevel.getStartingFunds(gameLevel);
int newFunds = GameLevel.getStartingFunds(newLevel);
int delta = origFunds - newFunds;
gameLevel = newLevel;
fireOptionsChanged();
if (totalFunds > delta) {
spend(delta);
}
}
}