/** * @license * Copyright 2010-2022 3d.City.js Authors * SPDX-License-Identifier: MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.city = {})); }(this, (function (exports) { 'use strict'; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const Micro = { haveMapAnimation: true, localStorage: null, GameMapProps: ['cityCentreX', 'cityCentreY', 'pollutionMaxX', 'pollutionMaxY', 'width', 'height'], savePropsVar: ['cityTime'], CensusProps: ['resPop', 'comPop', 'indPop', 'crimeRamp', 'pollutionRamp', 'landValueAverage', 'pollutionAverage', 'crimeAverage', 'totalPop', 'resHist10', 'resHist120', 'comHist10', 'comHist120', 'indHist10', 'indHist120', 'crimeHist10', 'crimeHist120', 'moneyHist10', 'moneyHist120', 'pollutionHist10', 'pollutionHist120'], BudgetProps: ['autoBudget', 'totalFunds', 'policePercent', 'roadPercent', 'firePercent', 'roadSpend', 'policeSpend', 'fireSpend', 'roadMaintenanceBudget', 'policeMaintenanceBudget', 'fireMaintenanceBudget', 'cityTax', 'roadEffect', 'policeEffect', 'fireEffect'], // eval PROBLEMS: ['CVP_CRIME', 'CVP_POLLUTION', 'CVP_HOUSING', 'CVP_TAXES', 'CVP_TRAFFIC', 'CVP_UNEMPLOYMENT', 'CVP_FIRE'], NUMPROBLEMS: 7, //this.PROBLEMS.length, NUM_COMPLAINTS: 4, problemData: [], EvalProps: ['cityClass', 'cityScore'], speedPowerScan: [2, 4, 5, 6], speedPollutionTerrainLandValueScan: [2, 7, 17, 30], speedCrimeScan: [1, 8, 18, 32], speedPopulationDensityScan: [1, 9, 19, 38], speedFireAnalysis: [1, 10, 20, 40], CENSUS_FREQUENCY_10: 4, CENSUS_FREQUENCY_120: 4 * 10, TAX_FREQUENCY: 48, MAP_WIDTH: 128, MAP_HEIGHT: 128, MAP_DEFAULT_WIDTH: 128 * 3, //Micro.MAP_WIDTH*3, MAP_DEFAULT_HEIGHT: 128 * 3, //Micro.MAP_HEIGHT*3, MAP_BIG_DEFAULT_WIDTH: 128 * 16, //Micro.MAP_WIDTH*16, MAP_BIG_DEFAULT_HEIGHT: 128 * 16, //Micro.MAP_HEIGHT*16, MAP_BIG_DEFAULT_ID: "bigMap", MAP_PARENT_ID: "splashContainer", MAP_DEFAULT_ID: "SplashCanvas", //GameCanvas DEFAULT_WIDTH: 400, DEFAULT_HEIGHT: 400, DEFAULT_ID: "MicropolisCanvas", RCI_DEFAULT_ID: "RCICanvas", // Simulation LEVEL_EASY: 0, LEVEL_MED: 1, LEVEL_HARD: 2, SPEED_PAUSED: 0, SPEED_SLOW: 1, SPEED_MED: 2, SPEED_FAST: 3, SPEED_ULTRA: 4, // Traffic ROUTE_FOUND: 1, NO_ROUTE_FOUND: 0, NO_ROAD_FOUND: -1, MAX_TRAFFIC_DISTANCE: 30, perimX: [-1, 0, 1, 2, 2, 2, 1, 0, -1, -2, -2, -2], perimY: [-2, -2, -2, -1, 0, 1, 2, 2, 2, 1, 0, -1], //SpriteConstants SPRITE_TRAIN: 1, SPRITE_HELICOPTER: 2, SPRITE_AIRPLANE: 3, SPRITE_SHIP: 4, SPRITE_MONSTER: 5, SPRITE_TORNADO: 6, SPRITE_EXPLOSION: 7, // Evaluation CC_VILLAGE: 'VILLAGE', CC_TOWN: 'TOWN', CC_CITY: 'CITY', CC_CAPITAL: 'CAPITAL', CC_METROPOLIS: 'METROPOLIS', CC_MEGALOPOLIS: 'MEGALOPOLIS', CRIME: 0, POLLUTION: 1, HOUSING: 2, TAXES: 3, TRAFFIC: 4, UNEMPLOYMENT: 5, FIRE: 6, // Valves RES_VALVE_RANGE: 2000, COM_VALVE_RANGE: 1500, IND_VALVE_RANGE: 1500, taxTable: [200, 150, 120, 100, 80, 50, 30, 0, -10, -40, -100, -150, -200, -250, -300, -350, -400, -450, -500, -550, -600], extMarketParamTable: [1.2, 1.1, 0.98], // Budget RLevels: [0.7, 0.9, 1.2], FLevels: [1.4, 1.2, 0.8], MAX_ROAD_EFFECT: 32, MAX_POLICESTATION_EFFECT: 1000, MAX_FIRESTATION_EFFECT: 1000, policeMaintenanceCost: 100, fireMaintenanceCost: 100, roadMaintenanceCost: 1, railMaintenanceCost: 2, // PowerManager COAL_POWER_STRENGTH: 700, NUCLEAR_POWER_STRENGTH: 2000, //DisasterWindow DISASTER_NONE: 'None', DISASTER_MONSTER: 'Monster', DISASTER_FIRE: 'Fire', DISASTER_FLOOD: 'Flood', DISASTER_CRASH: 'Crash', DISASTER_MELTDOWN: 'Meltdown', DISASTER_TORNADO: 'Tornado', // storage CURRENT_VERSION: 3, KEY: 'micropolisJSGame', // disasters DisChance: [479, 239, 59], // map generator TERRAIN_CREATE_ISLAND: 0, TERRAIN_TREE_LEVEL: -1, //level for tree creation TERRAIN_LAKE_LEVEL: -1, //level for river curviness; -1==auto, 0==none, >0==level TERRAIN_CURVE_LEVEL: -1, //level for lake creation; -1==auto, 0==none, >0==level ISLAND_RADIUS: 18, M_ARRAY_TYPE: typeof Float32Array !== 'undefined' ? Float32Array : Array, // census arrs: ['res', 'com', 'ind', 'crime', 'money', 'pollution'], directionTable: [0, 3, 2, 1, 3, 4, 5, 7, 6, 5, 7, 8, 1], SMOOTH_NEIGHBOURS_THEN_BLOCK: 0, SMOOTH_ALL_THEN_CLAMP: 1, simData: null, messageManager: null }; class EventEmitter { static emitEvent(event, value) { // ??? Micro.messageManager.sendMessage(event, value); } } //var M_ARRAY_TYPE; //if(!M_ARRAY_TYPE) { M_ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array; } class MiscUtils { /**/ //static makeConstantDescriptor = function(value) { static mcd(value) { return { configurable: false, enumerable: false, writeable: false, value: value }; } static rotate10Arrays() { for (var i = 0; i < Micro.arrs.length; i++) { var name10 = Micro.arrs[i] + 'Hist10'; //this[name10] = [0].concat(this[name10].slice(0, -1)); this[name10].pop(); this[name10].unshift(0); } } static rotate120Arrays() { for (var i = 0; i < Micro.arrs.length; i++) { var name120 = Micro.arrs[i] + 'Hist120'; //this[name120] = [0].concat(this[name120].slice(0, -1)); this[name120].pop(); this[name120].unshift(0); } } static isCallable(f) { return typeof f === 'function'; } static copyFrom(data, sourceMap, sourceFn) { var mapFn = function (elem) { return sourceFn(elem); }; var i = sourceMap.data.length; while (i--) data[i] = sourceMap.data[i].map(mapFn); } static makeArrayOf(length, value) { //var result = []; //var result = new M_ARRAY_TYPE(length); var result = new Array(length); //for (var a = 0; a < length; a++) result[a] = value; var i = length; while (i--) result[i] = value; return result; } } class math { static lerp(x, y, t) { return (1 - t) * x + t * y; } static rand(low, high) { return low + Math.random() * (high - low); } static randInt(low, high) { return low + Math.floor(Math.random() * (high - low + 1)); } static clamp(value, min, max) { if (value < min) return min; if (value > max) return max; return value; } static getChance(chance) { return (math.getRandom16() & chance) === 0; } static getERandom(max) { var r1 = math.getRandom(max); var r2 = math.getRandom(max); return Math.min(r1, r2); } static getRandom(max) { return Math.floor(Math.random() * (max + 1)); } static getRandom16() { return math.getRandom(65535); } static getRandom16Signed() { var value = math.getRandom16(); if (value < 32768) return value;else return -(2 ** 16) + value; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class ZoneUtils { // TileUtils static pixToWorld(p) { return p >> 4; } static worldToPix(w) { return w << 4; } static unwrapTile(tile) { if (tile.isTile) return tile = tile.getValue(); return tile; } static canBulldoze(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.FIRSTRIVEDGE && tile <= Tile.LASTRUBBLE || tile >= Tile.POWERBASE + 2 && tile <= Tile.POWERBASE + 12 || tile >= Tile.TINYEXP && tile <= Tile.LASTTINYEXP + 2; } static isCommercial(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.COMBASE && tile < Tile.INDBASE; } static isIndustrial(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.INDBASE && tile < Tile.PORTBASE; } static isResidential(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.RESBASE && tile < Tile.HOSPITALBASE; } static isDriveable(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.ROADBASE && tile <= Tile.LASTRAIL || tile === Tile.RAILHPOWERV || tile === Tile.RAILVPOWERH; } static isFire(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.FIREBASE && tile < Tile.ROADBASE; } static isFlood(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.FLOOD && tile < Tile.LASTFLOOD; } static isManualExplosion(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.TINYEXP && tile <= Tile.LASTTINYEXP; } static isRail(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.RAILBASE && tile < Tile.RESBASE; } static isRoad(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.ROADBASE && tile < Tile.POWERBASE; } static normalizeRoad(tile) { tile = ZoneUtils.unwrapTile(tile); return tile >= Tile.ROADBASE && tile <= Tile.LASTROAD + 1 ? (tile & 15) + 64 : tile; } /// static isCommercialZone(tile) { return tile.isZone() && ZoneUtils.isCommercial(tile); } static isIndustrialZone(tile) { return tile.isZone() && ZoneUtils.isIndustrial(tile); } static isResidentialZone(tile) { return tile.isZone() && ZoneUtils.isResidential(tile); } static randomFire(tile) { return new Tiles(Tile.FIRE + (math.getRandom16() & 3), Tile.ANIMBIT); } static randomRubble(tile) { return new Tiles(Tile.RUBBLE + (math.getRandom16() & 3), Tile.BULLBIT); } static HOSPITAL(tile) {} // ZoneUtils static checkZoneSize(tile) { if (tile >= Tile.RESBASE - 1 && tile <= Tile.PORTBASE - 1 || tile >= Tile.LASTPOWERPLANT + 1 && tile <= Tile.POLICESTATION + 4 || tile >= Tile.CHURCH1BASE && tile <= Tile.CHURCH7LAST) { return 3; } if (tile >= Tile.PORTBASE && tile <= Tile.LASTPORT || tile >= Tile.COALBASE && tile <= Tile.LASTPOWERPLANT || tile >= Tile.STADIUMBASE && tile <= Tile.LASTZONE) { return 4; } return 0; } static fireZone(map, x, y, blockMaps) { let tileValue = map.getTileValue(x, y); let zoneSize = 2; // A zone being on fire naturally hurts growth let value = blockMaps.rateOfGrowthMap.worldGet(x, y); value = math.clamp(value - 20, -200, 200); blockMaps.rateOfGrowthMap.worldSet(x, y, value); if (tileValue === Tile.AIRPORT) zoneSize = 5;else if (tileValue >= Tile.PORTBASE) zoneSize = 3;else if (tileValue < Tile.PORTBASE) zoneSize = 2; // Make remaining tiles of the zone bulldozable let xDelta, yDelta, xTem, yTem; for (xDelta = -1; xDelta < zoneSize; xDelta++) { for (yDelta = -1; yDelta < zoneSize; yDelta++) { xTem = x + xDelta; yTem = y + yDelta; if (!map.testBounds(xTem, yTem)) continue; if (map.getTileValue(xTem, yTem >= Tile.ROADBASE)) map.addTileFlags(xTem, yTem, Tile.BULLBIT); } } } static getLandPollutionValue(blockMaps, x, y) { let landValue = blockMaps.landValueMap.worldGet(x, y); landValue -= blockMaps.pollutionDensityMap.worldGet(x, y); if (landValue < 30) return 0; if (landValue < 80) return 1; if (landValue < 150) return 2; return 3; } static incRateOfGrowth(blockMaps, x, y, growthDelta) { let currentRate = blockMaps.rateOfGrowthMap.worldGet(x, y); // TODO why the scale of 4 here let newValue = math.clamp(currentRate + growthDelta * 4, -200, 200); blockMaps.rateOfGrowthMap.worldSet(x, y, newValue); } // Calls map.putZone after first checking for flood, fire and radiation static putZone(map, x, y, centreTile, isPowered) { let dY, dX, tileValue; for (dY = -1; dY < 2; dY++) { for (dX = -1; dX < 2; dX++) { tileValue = map.getTileValue(x + dX, y + dY); if (tileValue >= Tile.FLOOD && tileValue < Tile.ROADBASE) return; } } map.putZone(x, y, centreTile, 3); map.addTileFlags(x, y, Tile.BULLBIT); if (isPowered) map.addTileFlags(x, y, Tile.POWERBIT); } } //export const ZoneUtils = ZoneUtils ZoneUtils.checkBigZone = function (tile) { let result; switch (tile) { case Tile.POWERPLANT: case Tile.PORT: case Tile.NUCLEAR: case Tile.STADIUM: result = { zoneSize: 4, deltaX: 0, deltaY: 0 }; break; case Tile.POWERPLANT + 1: case Tile.COALSMOKE3: case Tile.COALSMOKE3 + 1: case Tile.COALSMOKE3 + 2: case Tile.PORT + 1: case Tile.NUCLEAR + 1: case Tile.STADIUM + 1: result = { zoneSize: 4, deltaX: -1, deltaY: 0 }; break; case Tile.POWERPLANT + 4: case Tile.PORT + 4: case Tile.NUCLEAR + 4: case Tile.STADIUM + 4: result = { zoneSize: 4, deltaX: 0, deltaY: -1 }; break; case Tile.POWERPLANT + 5: case Tile.PORT + 5: case Tile.NUCLEAR + 5: case Tile.STADIUM + 5: result = { zoneSize: 4, deltaX: -1, deltaY: -1 }; break; case Tile.AIRPORT: result = { zoneSize: 6, deltaX: 0, deltaY: 0 }; break; case Tile.AIRPORT + 1: result = { zoneSize: 6, deltaX: -1, deltaY: 0 }; break; case Tile.AIRPORT + 2: result = { zoneSize: 6, deltaX: -2, deltaY: 0 }; break; case Tile.AIRPORT + 3: result = { zoneSize: 6, deltaX: -3, deltaY: 0 }; break; case Tile.AIRPORT + 6: result = { zoneSize: 6, deltaX: 0, deltaY: -1 }; break; case Tile.AIRPORT + 7: result = { zoneSize: 6, deltaX: -1, deltaY: -1 }; break; case Tile.AIRPORT + 8: result = { zoneSize: 6, deltaX: -2, deltaY: -1 }; break; case Tile.AIRPORT + 9: result = { zoneSize: 6, deltaX: -3, deltaY: -1 }; break; case Tile.AIRPORT + 12: result = { zoneSize: 6, deltaX: 0, deltaY: -2 }; break; case Tile.AIRPORT + 13: result = { zoneSize: 6, deltaX: -1, deltaY: -2 }; break; case Tile.AIRPORT + 14: result = { zoneSize: 6, deltaX: -2, deltaY: -2 }; break; case Tile.AIRPORT + 15: result = { zoneSize: 6, deltaX: -3, deltaY: -2 }; break; case Tile.AIRPORT + 18: result = { zoneSize: 6, deltaX: 0, deltaY: -3 }; break; case Tile.AIRPORT + 19: result = { zoneSize: 6, deltaX: -1, deltaY: -3 }; break; case Tile.AIRPORT + 20: result = { zoneSize: 6, deltaX: -2, deltaY: -3 }; break; case Tile.AIRPORT + 21: result = { zoneSize: 6, deltaX: -3, deltaY: -3 }; break; default: result = { zoneSize: 0, deltaX: 0, deltaY: 0 }; break; } return result; }; class Tiles { constructor(tileValue = Tile.DIRT, bitMask) { this.isTile = true; /* if (!(this.isTile)) return new Tiles(); if (arguments.length > 0 && typeof(tileValue) !== 'number') throw new Error('Tile constructor called with invalid tileValue ' + tileValue); if (arguments.length > 1 && typeof(bitMask) !== 'number') throw new Error('Tile constructor called with invalid bitMask ' + bitMask); if (arguments.length > 1 && (tileValue < Tile.TILE_INVALID || tileValue >= Tile.TILE_COUNT)) throw new Error('Tile constructor called with out-of-range tileValue ' + tileValue); if (arguments.length > 1 && (bitMask < Tile.BIT_START || bitMask >= (Tile.BIT_END << 1))) throw new Error('Tile constructor called with out-of-range bitmask ' + bitMask); */ this._value = tileValue; if (arguments.length > 1) this._value |= bitMask; } getValue() { return this._value & Tile.BIT_MASK; } setValue(tileValue) { if (arguments.length === 0 || typeof tileValue !== 'number' || tileValue < 0) throw new Error('Invalid parameter'); let existingFlags = 0; if (tileValue < Tile.BIT_START) existingFlags = this._value & Tile.ALLBITS; //this.getFlags(); this._value = tileValue | existingFlags; } isBulldozable() { return (this._value & Tile.BULLBIT) > 0; } isAnimated() { return (this._value & Tile.ANIMBIT) > 0; } isConductive() { return (this._value & Tile.CONDBIT) > 0; } isCombustible() { return (this._value & Tile.BURNBIT) > 0; } isPowered() { return (this._value & Tile.POWERBIT) > 0; } isZone() { return (this._value & Tile.ZONEBIT) > 0; } addFlags(bitMask) { if (!arguments.length || typeof bitMask !== 'number' || bitMask < Tile.BIT_START || bitMask >= Tile.BIT_END << 1) throw new Error('Invalid parameter'); this._value |= bitMask; } removeFlags(bitMask) { if (!arguments.length || typeof bitMask !== 'number' || bitMask < Tile.BIT_START || bitMask >= Tile.BIT_END << 1) throw new Error('Invalid parameter'); this._value &= ~bitMask; } setFlags(bitMask) { //if (typeof(bitMask) !== 'number' || bitMask < Tile.BIT_START || bitMask >= (Tile.BIT_END << 1)) throw new Error('Invalid parameter'); //if (arguments.length === 0) throw new Error('Tile setFlags called with no arguments'); //if (typeof(bitMask) !== 'number') throw new Error('Tile setFlags called with invalid bitmask ' + bitMask); // if (bitMask < Tile.BIT_START || bitMask >= (Tile.BIT_END << 1)) throw new Error('Tile setFlags called with out-of-range bitmask ' + bitMask); let existingValue = this._value & ~Tile.ALLBITS; this._value = existingValue | bitMask; } getFlags() { return this._value & Tile.ALLBITS; } getRawValue() { return this._value; } set(tileValue, bitMask) { if (arguments.length < 2 || typeof tileValue !== 'number' || typeof bitMask !== 'number' || tileValue >= Tile.TILE_COUNT) throw new Error('Invalid parameter'); this.setValue(tileValue); this.setFlags(bitMask); } toString() { let value = this.getValue(); let s = 'Tile# ' + value; s += this.isCombustible() ? ' burning' : ''; s += this.isPowered() ? ' powered' : ''; s += this.isAnimated() ? ' animated' : ''; s += this.isConductive() ? ' conductive' : ''; s += this.isZone() ? ' zone' : ''; s += this.isBulldozable() ? ' bulldozeable' : ''; return s; } } const Tile = { // Bit-masks for statusBits POWERBIT: 0x8000, // bit 15, tile has power. CONDBIT: 0x4000, // bit 14. tile can conduct electricity. BURNBIT: 0x2000, // bit 13, tile can be lit. BULLBIT: 0x1000, // bit 12, tile is bulldozable. ANIMBIT: 0x0800, // bit 11, tile is animated. ZONEBIT: 0x0400, // bit 10, tile is the center tile of the zone. BLBNBIT: 0x1000 | 0x2000, //BULLBIT | BURNBIT, BLBNCNBIT: 0x1000 | 0x2000 | 0x4000, //BULLBIT | BURNBIT | CONDBIT, BNCNBIT: 0x2000 | 0x4000, //BURNBIT | CONDBIT, ASCBIT: 0x0800 | 0x4000 | 0x2000, // ANIMBIT | CONDBIT | BURNBIT, ALLBITS: 0x8000 | 0x4000 | 0x2000 | 0x1000 | 0x0800 | 0x0400, // POWERBIT | CONDBIT | BURNBIT | BULLBIT | ANIMBIT | ZONEBIT, BIT_START: 0x400, BIT_END: 0x8000, BIT_MASK: 0x400 - 1, // BIT_START - 1, // TODO Add comment for each tile DIRT: 0, // Clear tile // tile 1 ? /* Water */ RIVER: 2, REDGE: 3, CHANNEL: 4, FIRSTRIVEDGE: 5, // tile 6 -- 19 ? LASTRIVEDGE: 20, WATER_LOW: 2, //RIVER, // First water tile WATER_HIGH: 20, //LASTRIVEDGE, // Last water tile (inclusive) TREEBASE: 21, WOODS_LOW: 21, //TREEBASE, LASTTREE: 36, WOODS: 37, UNUSED_TRASH1: 38, UNUSED_TRASH2: 39, WOODS_HIGH: 39, //UNUSED_TRASH2, // Why is an 'UNUSED' tile used? WOODS2: 40, WOODS3: 41, WOODS4: 42, WOODS5: 43, // Rubble (4 tiles) RUBBLE: 44, LASTRUBBLE: 47, // fLOOD 4 tiles FLOOD: 48, LASTFLOOD: 51, // Radiation RADTILE: 52, // Radio-active contaminated tile UNUSED_TRASH3: 53, UNUSED_TRASH4: 54, UNUSED_TRASH5: 55, /* Fire animation (8 tiles) */ FIRE: 56 - 8, FIREBASE: 56 - 8, //FIRE, LASTFIRE: 63 - 8, HBRIDGE: 64, // Horizontal bridge ROADBASE: 64, // HBRIDGE, VBRIDGE: 65, // Vertical bridge ROADS: 66, ROADS2: 67, ROADS3: 68, ROADS4: 69, ROADS5: 70, ROADS6: 71, ROADS7: 72, ROADS8: 73, ROADS9: 74, ROADS10: 75, INTERSECTION: 76, HROADPOWER: 77, VROADPOWER: 78, BRWH: 79, LTRFBASE: 80, // First tile with low traffic // tile 81 -- 94 ? BRWV: 95, // tile 96 -- 110 ? BRWXXX1: 111, // tile 96 -- 110 ? BRWXXX2: 127, // tile 96 -- 110 ? BRWXXX3: 143, HTRFBASE: 144, // First tile with high traffic // tile 145 -- 158 ? BRWXXX4: 159, // tile 160 -- 174 ? BRWXXX5: 175, // tile 176 -- 190 ? BRWXXX6: 191, // tile 192 -- 205 ? LASTROAD: 206, BRWXXX7: 207, /* Power lines */ HPOWER: 208, VPOWER: 209, LHPOWER: 210, LVPOWER: 211, LVPOWER2: 212, LVPOWER3: 213, LVPOWER4: 214, LVPOWER5: 215, LVPOWER6: 216, LVPOWER7: 217, LVPOWER8: 218, LVPOWER9: 219, LVPOWER10: 220, RAILHPOWERV: 221, // Horizontal rail, vertical power RAILVPOWERH: 222, // Vertical rail, horizontal power POWERBASE: 208, //HPOWER, LASTPOWER: 222, //RAILVPOWERH, UNUSED_TRASH6: 223, /* Rail */ HRAIL: 224, VRAIL: 225, LHRAIL: 226, LVRAIL: 227, LVRAIL2: 228, LVRAIL3: 229, LVRAIL4: 230, LVRAIL5: 231, LVRAIL6: 232, LVRAIL7: 233, LVRAIL8: 234, LVRAIL9: 235, LVRAIL10: 236, HRAILROAD: 237, VRAILROAD: 238, RAILBASE: 224, //HRAIL, LASTRAIL: 238, ROADVPOWERH: 239, /* bogus? */ // Residential zone tiles RESBASE: 240, // Empty residential, tiles 240--248 FREEZ: 244, // center-tile of 3x3 empty residential HOUSE: 249, // Single tile houses until 260 LHTHR: 249, //HOUSE, HHTHR: 260, RZB: 265, // center tile first 3x3 tile residential HOSPITALBASE: 405, // Center of hospital (tiles 405--413) HOSPITAL: 409, // Center of hospital (tiles 405--413) CHURCHBASE: 414, // Center of church (tiles 414--422) CHURCH0BASE: 414, // numbered alias CHURCH: 418, // Center of church (tiles 414--422) CHURCH0: 418, // numbered alias // Commercial zone tiles COMBASE: 423, // Empty commercial, tiles 423--431 // tile 424 -- 426 ? COMCLR: 427, // tile 428 -- 435 ? CZB: 436, // tile 437 -- 608 ? COMLAST: 609, // tile 610, 611 ? // Industrial zone tiles. INDBASE: 612, // Top-left tile of empty industrial zone. INDCLR: 616, // Center tile of empty industrial zone. LASTIND: 620, // Last tile of empty industrial zone. // Industrial zone population 0, value 0: 621 -- 629 IND1: 621, // Top-left tile of first non-empty industry zone. IZB: 625, // Center tile of first non-empty industry zone. // Industrial zone population 1, value 0: 630 -- 638 // Industrial zone population 2, value 0: 639 -- 647 IND2: 641, IND3: 644, // Industrial zone population 3, value 0: 648 -- 656 IND4: 649, IND5: 650, // Industrial zone population 0, value 1: 657 -- 665 // Industrial zone population 1, value 1: 666 -- 674 // Industrial zone population 2, value 1: 675 -- 683 IND6: 676, IND7: 677, // Industrial zone population 3, value 1: 684 -- 692 IND8: 686, IND9: 689, // Seaport PORTBASE: 693, // Top-left tile of the seaport. PORT: 698, // Center tile of the seaport. LASTPORT: 708, // Last tile of the seaport. AIRPORTBASE: 709, // tile 710 ? RADAR: 711, // tile 712 -- 715 ? AIRPORT: 716, // tile 717 -- 744 ? // Coal power plant (4x4). COALBASE: 745, // First tile of coal power plant. POWERPLANT: 750, // 'Center' tile of coal power plant. LASTPOWERPLANT: 760, // Last tile of coal power plant. // Fire station (3x3). FIRESTBASE: 761, // First tile of fire station. FIRESTATION: 765, // 'Center tile' of fire station. // 769 last tile fire station. POLICESTBASE: 770, // tile 771 -- 773 ? POLICESTATION: 774, // tile 775 -- 778 ? // Stadium (4x4). STADIUMBASE: 779, // First tile stadium. STADIUM: 784, // 'Center tile' stadium. // Last tile stadium 794. // tile 785 -- 799 ? FULLSTADIUM: 800, // tile 801 -- 810 ? // Nuclear power plant (4x4). NUCLEARBASE: 811, // First tile nuclear power plant. NUCLEAR: 816, // 'Center' tile nuclear power plant. LASTZONE: 826, // Also last tile nuclear power plant. LIGHTNINGBOLT: 827, // bridge horisontal open close HBRDG0: 828, HBRDG1: 829, HBRDG2: 830, HBRDG3: 831, //HBRDG_END : 832, RADAR0: 832, RADAR1: 833, RADAR2: 834, RADAR3: 835, RADAR4: 836, RADAR5: 837, RADAR6: 838, RADAR7: 839, FOUNTAIN: 840, // tile 841 -- 843: fountain animation. INDBASE2: 844, TELEBASE: 844, // tile 845 -- 850 ? TELELAST: 851, SMOKEBASE: 852, // tile 853 -- 859 ? TINYEXP: 860, // tile 861 -- 863 ? SOMETINYEXP: 864, // tile 865 -- 866 ? LASTTINYEXP: 867, // tile 868 -- 882 ? TINYEXPLAST: 883, // tile 884 -- 915 ? COALSMOKE1: 916, // Chimney animation at coal power plant (2, 0). // 919 last animation tile for chimney at coal power plant (2, 0). COALSMOKE2: 920, // Chimney animation at coal power plant (3, 0). // 923 last animation tile for chimney at coal power plant (3, 0). COALSMOKE3: 924, // Chimney animation at coal power plant (2, 1). // 927 last animation tile for chimney at coal power plant (2, 1). COALSMOKE4: 928, // Chimney animation at coal power plant (3, 1). // 931 last animation tile for chimney at coal power plant (3, 1). FOOTBALLGAME1: 932, // tile 933 -- 939 ? FOOTBALLGAME2: 940, // tile 941 -- 94 bridge open close VBRDG0: 948, VBRDG1: 949, VBRDG2: 950, VBRDG3: 951, // nuclear animation // disable NUKESWIRL1: 952, NUKESWIRL2: 953, NUKESWIRL3: 954, NUKESWIRL4: 955, // Tiles 956-959 unused (originally) // TILE_COUNT : 960, // Extended zones: 956-1019 CHURCH1BASE: 956, CHURCH1: 960, CHURCH2BASE: 965, CHURCH2: 969, CHURCH3BASE: 974, CHURCH3: 978, CHURCH4BASE: 983, CHURCH4: 987, CHURCH5BASE: 992, CHURCH5: 996, CHURCH6BASE: 1001, CHURCH6: 1005, CHURCH7BASE: 1010, CHURCH7: 1014, CHURCH7LAST: 1018, // Tiles 1020-1023 unused TILE_COUNT: 1024, TILE_INVALID: -1, // Invalid tile (not used in the world map). MIN_SIZE: 16 // Minimum size of tile in pixels }; const RoadTable = [Tile.ROADS, Tile.ROADS2, Tile.ROADS, Tile.ROADS3, Tile.ROADS2, Tile.ROADS2, Tile.ROADS4, Tile.ROADS8, Tile.ROADS, Tile.ROADS6, Tile.ROADS, Tile.ROADS7, Tile.ROADS5, Tile.ROADS10, Tile.ROADS9, Tile.INTERSECTION]; const RailTable = [Tile.LHRAIL, Tile.LVRAIL, Tile.LHRAIL, Tile.LVRAIL2, Tile.LVRAIL, Tile.LVRAIL, Tile.LVRAIL3, Tile.LVRAIL7, Tile.LHRAIL, Tile.LVRAIL5, Tile.LHRAIL, Tile.LVRAIL6, Tile.LVRAIL4, Tile.LVRAIL9, Tile.LVRAIL8, Tile.LVRAIL10]; const WireTable = [Tile.LHPOWER, Tile.LVPOWER, Tile.LHPOWER, Tile.LVPOWER2, Tile.LVPOWER, Tile.LVPOWER, Tile.LVPOWER3, Tile.LVPOWER7, Tile.LHPOWER, Tile.LVPOWER5, Tile.LHPOWER, Tile.LVPOWER6, Tile.LVPOWER4, Tile.LVPOWER9, Tile.LVPOWER8, Tile.LVPOWER10]; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class MessageManager { constructor() { this.data = []; } sendMessage(message, data) { this.data.push({ message: message, data: data }); } clear() { this.data = []; } getMessages() { return this.data.slice(); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ var messageData = { AUTOBUDGET_CHANGED: MiscUtils.mcd('Autobudget changed'), BUDGET_NEEDED: MiscUtils.mcd('User needs to budget'), BUDGET_REQUESTED: MiscUtils.mcd('Budget window requested'), BUDGET_WINDOW_CLOSED: MiscUtils.mcd('Budget window closed'), BLACKOUTS_REPORTED: MiscUtils.mcd('Blackouts reported'), CLASSIFICATION_UPDATED: MiscUtils.mcd('Classification updated'), CONGRATS_SHOWING: MiscUtils.mcd('Congratulations showing'), CONGRATS_WINDOW_CLOSED: MiscUtils.mcd('Congratulations window closed'), DATE_UPDATED: MiscUtils.mcd('Date changed'), DEBUG_WINDOW_REQUESTED: MiscUtils.mcd('Debug Window Requested'), DEBUG_WINDOW_CLOSED: MiscUtils.mcd('Debug Window Closed'), DISASTER_REQUESTED: MiscUtils.mcd('Disaster Requested'), DISASTER_WINDOW_CLOSED: MiscUtils.mcd('Disaster window closed'), EARTHQUAKE: MiscUtils.mcd('Earthquake'), EVAL_REQUESTED: MiscUtils.mcd('Evaluation Requested'), EVAL_UPDATED: MiscUtils.mcd('Evaluation Updated'), EVAL_WINDOW_CLOSED: MiscUtils.mcd('Eval window closed'), EXPLOSION_REPORTED: MiscUtils.mcd('Explosion Reported'), FIRE_REPORTED: MiscUtils.mcd('Fire!'), FIRE_STATION_NEEDS_FUNDING: MiscUtils.mcd('Fire station needs funding'), FLOODING_REPORTED: MiscUtils.mcd('Flooding reported'), FRONT_END_MESSAGE: MiscUtils.mcd('Front-end Message'), FUNDS_CHANGED: MiscUtils.mcd('Total funds has changed'), HEAVY_TRAFFIC: MiscUtils.mcd('Heavy traffic in city'), HELICOPTER_CRASHED: MiscUtils.mcd('Helicopter crashed'), HIGH_CRIME: MiscUtils.mcd('High crime'), HIGH_POLLUTION: MiscUtils.mcd('High pollution'), MONSTER_SIGHTED: MiscUtils.mcd('Monster sighted'), NAG_WINDOW_CLOSED: MiscUtils.mcd('Nag window closed'), NEED_AIRPORT: MiscUtils.mcd('Airport needed'), NEED_ELECTRICITY: MiscUtils.mcd('More power needed'), NEED_FIRE_STATION: MiscUtils.mcd('Fire station needed'), NEED_MORE_COMMERCIAL: MiscUtils.mcd('More commercial zones needed'), NEED_MORE_INDUSTRIAL: MiscUtils.mcd('More industrial zones needed'), NEED_MORE_RAILS: MiscUtils.mcd('More railways needed'), NEED_MORE_RESIDENTIAL: MiscUtils.mcd('More residential needed'), NEED_MORE_ROADS: MiscUtils.mcd('More roads needed'), NEED_POLICE_STATION: MiscUtils.mcd('Police station needed'), NEED_SEAPORT: MiscUtils.mcd('Seaport needed'), NEED_STADIUM: MiscUtils.mcd('Stadium needed'), NO_MONEY: MiscUtils.mcd('No money'), NOT_ENOUGH_POWER: MiscUtils.mcd('Not enough power'), NUCLEAR_MELTDOWN: MiscUtils.mcd('Nuclear Meltdown'), PLANE_CRASHED: MiscUtils.mcd('Plane crashed'), POLICE_NEEDS_FUNDING: MiscUtils.mcd('Police need funding'), POPULATION_UPDATED: MiscUtils.mcd('Population updated'), QUERY_WINDOW_CLOSED: MiscUtils.mcd('Query window closed'), QUERY_WINDOW_NEEDED: MiscUtils.mcd('Query window needed'), REACHED_CAPITAL: MiscUtils.mcd('Now a capital'), REACHED_CITY: MiscUtils.mcd('Now a city'), REACHED_METROPOLIS: MiscUtils.mcd('Now a metropolis'), REACHED_MEGALOPOLIS: MiscUtils.mcd('Now a megalopolis'), REACHED_TOWN: MiscUtils.mcd('Now a town'), REACHED_VILLAGE: MiscUtils.mcd('Now a village'), ROAD_NEEDS_FUNDING: MiscUtils.mcd('Roads need funding'), SAVE_REQUESTED: MiscUtils.mcd('Save requested'), SAVE_WINDOW_CLOSED: MiscUtils.mcd('Save window closed'), SCORE_UPDATED: MiscUtils.mcd('Score updated'), SCREENSHOT_LINK_CLOSED: MiscUtils.mcd('Screenshot link closed'), SCREENSHOT_WINDOW_CLOSED: MiscUtils.mcd('Screenshot window closed'), SCREENSHOT_WINDOW_REQUESTED: MiscUtils.mcd('Screenshot window requested'), SETTINGS_WINDOW_CLOSED: MiscUtils.mcd('Settings window closed'), SETTINGS_WINDOW_REQUESTED: MiscUtils.mcd('Settings window requested'), SHIP_CRASHED: MiscUtils.mcd('Shipwrecked'), SOUND_EXPLOSIONHIGH: MiscUtils.mcd('Explosion! Bang!'), SOUND_EXPLOSIONLOW: MiscUtils.mcd('Explosion! Bang!'), SOUND_HEAVY_TRAFFIC: MiscUtils.mcd('Heavy Traffic sound'), SOUND_HONKHONK: MiscUtils.mcd('HonkHonk sound'), SOUND_MONSTER: MiscUtils.mcd('Monster sound'), SPEED_CHANGE: MiscUtils.mcd('Speed change'), SPRITE_DYING: MiscUtils.mcd('Sprite dying'), SPRITE_MOVED: MiscUtils.mcd('Sprite move'), TAX_TOO_HIGH: MiscUtils.mcd('Tax too high'), TOOL_CLICKED: MiscUtils.mcd('Tool clicked'), TORNADO_SIGHTED: MiscUtils.mcd('Tornado sighted'), TOUCH_WINDOW_CLOSED: MiscUtils.mcd('Touch Window closed'), TRAFFIC_JAMS: MiscUtils.mcd('Traffic jams reported'), TRAIN_CRASHED: MiscUtils.mcd('Train crashed'), VALVES_UPDATED: MiscUtils.mcd('Valves updated'), WELCOME: MiscUtils.mcd('Welcome to micropolisJS'), WELCOMEBACK: MiscUtils.mcd('Welcome back to your 3D city') }; const Messages = Object.defineProperties({}, messageData); //var Messages = Object.defineProperties({}, messageData); var disasterMessages = [Messages.EARTHQUAKE, Messages.EXPLOSION_REPORTED, Messages.FIRE_REPORTED, Messages.FLOODING_REPORTED, Messages.MONSTER_SIGHTED, Messages.NUCLEAR_MELTDOWN, Messages.TORNADO_SIGHTED]; Object.defineProperty(Messages, 'disasterMessages', MiscUtils.mcd(disasterMessages)); var crashes = [Messages.HELICOPTER_CRASHED, Messages.PLANE_CRASHED, Messages.SHIP_CRASHED, Messages.TRAIN_CRASHED]; Object.defineProperty(Messages, 'crashes', MiscUtils.mcd(crashes)); //export { Messages }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const Text = function () { // TODO Some kind of rudimentary L20N based on navigator.language? // Query tool strings var densityStrings = ['Low', 'Medium', 'High', 'Very High']; var landValueStrings = ['Slum', 'Lower Class', 'Middle Class', 'High']; var crimeStrings = ['Safe', 'Light', 'Moderate', 'Dangerous']; var pollutionStrings = ['None', 'Moderate', 'Heavy', 'Very Heavy']; var rateStrings = ['Declining', 'Stable', 'Slow Growth', 'Fast Growth']; var zoneTypes = ['Clear', 'Water', 'Trees', 'Rubble', 'Flood', 'Radioactive Waste', 'Fire', 'Road', 'Power', 'Rail', 'Residential', 'Commercial', 'Industrial', 'Seaport', 'Airport', 'Coal Power', 'Fire Department', 'Police Department', 'Stadium', 'Nuclear Power', 'Draw Bridge', 'Radar Dish', 'Fountain', 'Industrial', 'Steelers 38 Bears 3', 'Draw Bridge', 'Ur 238']; // Evaluation window var gameLevel = {}; gameLevel['' + Micro.LEVEL_EASY] = 'Easy'; gameLevel['' + Micro.LEVEL_MED] = 'Medium'; gameLevel['' + Micro.LEVEL_HARD] = 'Hard'; var cityClass = {}; cityClass[Micro.CC_VILLAGE] = 'VILLAGE'; cityClass[Micro.CC_TOWN] = 'TOWN'; cityClass[Micro.CC_CITY] = 'CITY'; cityClass[Micro.CC_CAPITAL] = 'CAPITAL'; cityClass[Micro.CC_METROPOLIS] = 'METROPOLIS'; cityClass[Micro.CC_MEGALOPOLIS] = 'MEGALOPOLIS'; var problems = {}; problems[Micro.CRIME] = 'Crime'; problems[Micro.POLLUTION] = 'Pollution'; problems[Micro.HOUSING] = 'Housing'; problems[Micro.TAXES] = 'Taxes'; problems[Micro.TRAFFIC] = 'Traffic'; problems[Micro.UNEMPLOYMENT] = 'Unemployment'; problems[Micro.FIRE] = 'Fire'; // months var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // Tool strings var toolMessages = { noMoney: 'Insufficient funds to build that', needsDoze: 'Area must be bulldozed first' }; // Message strings var neutralMessages = {}; neutralMessages[Messages.FIRE_STATION_NEEDS_FUNDING] = 'Fire departments need funding'; neutralMessages[Messages.NEED_AIRPORT] = 'Commerce requires an Airport'; neutralMessages[Messages.NEED_FIRE_STATION] = 'Citizens demand a Fire Department'; neutralMessages[Messages.NEED_ELECTRICITY] = 'Build a Power Plant'; neutralMessages[Messages.NEED_MORE_INDUSTRIAL] = 'More industrial zones needed'; neutralMessages[Messages.NEED_MORE_COMMERCIAL] = 'More commercial zones needed'; neutralMessages[Messages.NEED_MORE_RESIDENTIAL] = 'More residential zones needed'; neutralMessages[Messages.NEED_MORE_RAILS] = 'Inadequate rail system'; neutralMessages[Messages.NEED_MORE_ROADS] = 'More roads required'; neutralMessages[Messages.NEED_POLICE_STATION] = 'Citizens demand a Police Department'; neutralMessages[Messages.NEED_SEAPORT] = 'Industry requires a Sea Port'; neutralMessages[Messages.NEED_STADIUM] = 'Residents demand a Stadium'; neutralMessages[Messages.ROAD_NEEDS_FUNDING] = 'Roads deteriorating, due to lack of funds'; neutralMessages[Messages.POLICE_NEEDS_FUNDING] = 'Police departments need funding'; neutralMessages[Messages.WELCOME] = 'Welcome to 3D City'; neutralMessages[Messages.WELCOMEBACK] = 'Welcome to 3D City'; var badMessages = {}; badMessages[Messages.BLACKOUTS_REPORTED] = 'Brownouts, build another Power Plant'; badMessages[Messages.COPTER_CRASHED] = 'A helicopter crashed '; badMessages[Messages.EARTHQUAKE] = 'Major earthquake reported !!'; badMessages[Messages.EXPLOSION_REPORTED] = 'Explosion detected '; badMessages[Messages.FLOODING_REPORTED] = 'Flooding reported !'; badMessages[Messages.FIRE_REPORTED] = 'Fire reported '; badMessages[Messages.HEAVY_TRAFFIC] = 'Heavy Traffic reported'; badMessages[Messages.HIGH_CRIME] = 'Crime very high'; badMessages[Messages.HIGH_POLLUTION] = 'Pollution very high'; badMessages[Messages.MONSTER_SIGHTED] = 'A Monster has been sighted !'; badMessages[Messages.NO_MONEY] = 'YOUR CITY HAS GONE BROKE'; badMessages[Messages.NOT_ENOUGH_POWER] = 'Blackouts reported. insufficient power capacity'; badMessages[Messages.NUCLEAR_MELTDOWN] = 'A Nuclear Meltdown has occurred !!'; badMessages[Messages.PLANE_CRASHED] = 'A plane has crashed '; badMessages[Messages.SHIP_CRASHED] = 'Shipwreck reported '; badMessages[Messages.TAX_TOO_HIGH] = 'Citizens upset. The tax rate is too high'; badMessages[Messages.TORNADO_SIGHTED] = 'Tornado reported !'; badMessages[Messages.TRAFFIC_JAMS] = 'Frequent traffic jams reported'; badMessages[Messages.TRAIN_CRASHED] = 'A train crashed '; var goodMessages = {}; goodMessages[Messages.REACHED_CAPITAL] = 'Population has reached 50,000'; goodMessages[Messages.REACHED_CITY] = 'Population has reached 10,000'; goodMessages[Messages.REACHED_MEGALOPOLIS] = 'Population has reached 500,000'; goodMessages[Messages.REACHED_METROPOLIS] = 'Population has reached 100,000'; goodMessages[Messages.REACHED_TOWN] = 'Population has reached 2,000'; return { badMessages: badMessages, cityClass: cityClass, crimeStrings: crimeStrings, densityStrings: densityStrings, gameLevel: gameLevel, goodMessages: goodMessages, landValueStrings: landValueStrings, months: months, neutralMessages: neutralMessages, problems: problems, pollutionStrings: pollutionStrings, rateStrings: rateStrings, toolMessages: toolMessages, zoneTypes: zoneTypes }; }; const TXT = new Text(); /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class SpriteUtils { static pixToWorld(p) { return ZoneUtils.pixToWorld(p); } static worldToPix(w) { return ZoneUtils.worldToPix(w); } // Attempt to move 45° towards the desired direction, either // clockwise or anticlockwise, whichever gets us there quicker static turnTo(presentDir, desiredDir) { if (presentDir === desiredDir) return presentDir; if (presentDir < desiredDir) { // select clockwise or anticlockwise if (desiredDir - presentDir < 4) presentDir++;else presentDir--; } else { if (presentDir - desiredDir < 4) presentDir--;else presentDir++; } if (presentDir > 8) presentDir = 1; if (presentDir < 1) presentDir = 8; return presentDir; } static absoluteValue(x) { return Math.abs(x); } static getTileValue(map, x, y) { let wX = ZoneUtils.pixToWorld(x); let wY = ZoneUtils.pixToWorld(y); if (wX < 0 || wX >= map.width || wY < 0 || wY >= map.height) return -1; return map.getTileValue(wX, wY); } // Choose the best direction to get from the origin to the destination // If the destination is equidistant in both x and y deltas, a diagonal // will be chosen, otherwise the most 'dominant' difference will be selected // (so if a destination is 4 units north and 2 units east, north will be chosen). // This code seems to always choose south if we're already there which seems like // a bug static getDir(orgX, orgY, destX, destY) { let deltaX = destX - orgX; let deltaY = destY - orgY; let i; if (deltaX < 0) { i = deltaY < 0 ? 11 : 8; } else { i = deltaY < 0 ? 2 : 5; } deltaX = Math.abs(deltaX); deltaY = Math.abs(deltaY); if (deltaX * 2 < deltaY) i++;else if (deltaY * 2 < deltaX) i--; if (i < 0 || i > 12) i = 0; return Micro.directionTable[i]; } static absoluteDistance(orgX, orgY, destX, destY) { let deltaX = destX - orgX; let deltaY = destY - orgY; return Math.abs(deltaX) + Math.abs(deltaY); } static checkWet(tileValue) { if (tileValue === Tile.HPOWER || tileValue === Tile.VPOWER || tileValue === Tile.HRAIL || tileValue === Tile.VRAIL || tileValue === Tile.BRWH || tileValue === Tile.BRWV) return true;else return false; } static destroyMapTile(spriteManager, map, blockMaps, ox, oy) { let x = ZoneUtils.pixToWorld(ox); let y = ZoneUtils.pixToWorld(oy); if (!map.testBounds(x, y)) return; let tile = map.getTile(x, y); let tileValue = tile.getValue(); if (tileValue < Tile.TREEBASE) return; if (!tile.isCombustible()) { if (tileValue >= Tile.ROADBASE && tileValue <= Tile.LASTROAD) map.setTile(x, y, Tile.RIVER, 0); return; } if (tile.isZone()) { ZoneUtils.fireZone(map, x, y, blockMaps); if (tileValue > Tile.RZB) spriteManager.makeExplosionAt(ox, oy); } if (SpriteUtils.checkWet(tileValue)) map.setTile(x, y, Tile.RIVER, 0);else map.setTile(x, y, Tile.TINYEXP, Tile.BULLBIT | Tile.ANIMBIT); } static getDistance(x1, y1, x2, y2) { return Math.abs(x1 - x2) + Math.abs(y1 - y2); } static checkSpriteCollision(s1, s2) { return s1.frame !== 0 && s2.frame !== 0 && SpriteUtils.getDistance(s1.x, s1.y, s2.x, s2.y) < 30; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class BaseSprite { constructor() {} init(type, map, spriteManager, x, y) { this.type = type; this.map = map; this.spriteManager = spriteManager; this.x = x; this.y = y; this.origX = 0; this.origY = 0; this.destX = 0; this.destY = 0; this.count = 0; this.soundCount = 0; this.dir = 0; this.newDir = 0; this.step = 0; this.flag = 0; this.turn = 0; this.accel = 0; this.speed = 100; } getFileName() { return ['obj', this.type, '-', this.frame - 1].join(''); } spriteNotInBounds() { let x = SpriteUtils.pixToWorld(this.x); let y = SpriteUtils.pixToWorld(this.y); return x < 0 || y < 0 || x >= this.map.width || y >= this.map.height; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class TrainSprite extends BaseSprite { constructor(map, spriteManager, x, y) { super(); this.init(Micro.SPRITE_TRAIN, map, spriteManager, x, y); this.width = 32; this.height = 32; this.xOffset = -16; this.yOffset = -16; this.frame = 1; this.dir = 4; this.tileDeltaX = [0, 16, 0, -16]; this.tileDeltaY = [-16, 0, 16, 0]; this.xDelta = [0, 4, 0, -4, 0]; this.yDelta = [-4, 0, 4, 0, 0]; this.TrainPic2 = [1, 2, 1, 2, 5]; // Frame values this.NORTHSOUTH = 1; this.EASTWEST = 2; this.NWSE = 3; this.NESW = 4; this.UNDERWATER = 5; // Direction values this.NORTH = 0; this.EAST = 1; this.SOUTH = 2; this.WEST = 3; this.CANTMOVE = 4; } move(spriteCycle, messageManager, disasterManager, blockMaps) { // Trains can only move in the 4 cardinal directions // Over the course of 4 frames, we move through a tile, so // ever fourth frame, we try to find a direction to move in // (excluding the opposite direction from the current direction // of travel). If there is no possible direction found, our direction // is set to CANTMOVE. (Thus, if we're in a dead end, we can start heading // backwards next time round). If we fail to find a destination after 2 attempts, // we die. if (this.frame === this.NWSE || this.frame === this.NESW) this.frame = this.TrainPic2[this.dir]; this.x += this.xDelta[this.dir]; this.y += this.yDelta[this.dir]; // Find a new direction. if ((spriteCycle & 3) === 0) { // Choose a random starting point for our search let dir = math.getRandom16() & 3; for (let i = dir; i < dir + 4; i++) { let dir2 = i & 3; if (this.dir !== this.CANTMOVE) { // Avoid the opposite direction if (dir2 === (this.dir + 2 & 3)) continue; } let tileValue = SpriteUtils.getTileValue(this.map, this.x + this.tileDeltaX[dir2], this.y + this.tileDeltaY[dir2]); if (tileValue >= Tile.RAILBASE && tileValue <= Tile.LASTRAIL || tileValue === Tile.RAILVPOWERH || tileValue === Tile.RAILHPOWERV) { if (this.dir !== dir2 && this.dir !== this.CANTMOVE) { if (this.dir + dir2 === this.WEST) this.frame = this.NWSE;else this.frame = this.NESW; } else { this.frame = this.TrainPic2[dir2]; } if (tileValue === Tile.HRAIL || tileValue === Tile.VRAIL) this.frame = this.UNDERWATER; this.dir = dir2; return; } } // Nowhere to go. Die. if (this.dir === this.CANTMOVE) { this.frame = 0; return; } // We didn't find a direction this time. We'll try the opposite // next time around this.dir = this.CANTMOVE; } } explodeSprite(messageManager) { this.frame = 0; this.spriteManager.makeExplosionAt(this.x, this.y); messageManager.sendMessage(Messages.TRAIN_CRASHED); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class BoatSprite extends BaseSprite { constructor(map, spriteManager, x, y) { super(); this.init(Micro.SPRITE_SHIP, map, spriteManager, x, y); this.width = 48; this.height = 48; this.xOffset = -24; this.yOffset = -24; if (x < SpriteUtils.worldToPix(4)) this.frame = 3;else if (x >= SpriteUtils.worldToPix(map.width - 4)) this.frame = 7;else if (y < SpriteUtils.worldToPix(4)) this.frame = 5;else if (y >= SpriteUtils.worldToPix(map.height - 4)) this.frame = 1;else this.frame = 3; this.newDir = this.frame; this.dir = 10; this.count = 1; this.tileDeltaX = [0, 0, 1, 1, 1, 0, -1, -1, -1]; this.tileDeltaY = [0, -1, -1, 0, 1, 1, 1, 0, -1]; this.xDelta = [0, 0, 2, 2, 2, 0, -2, -2, -2]; this.yDelta = [0, -2, -2, 0, 2, 2, 2, 0, -2]; this.tileWhiteList = [Tile.RIVER, Tile.CHANNEL, Tile.POWERBASE, Tile.POWERBASE + 1, Tile.RAILBASE, Tile.RAILBASE + 1, Tile.BRWH, Tile.BRWV]; this.CANTMOVE = 10; } move(spriteCycle, messageManager, disasterManager, blockMaps) { let t = Tile.RIVER; let startDir, frame, dir, x, y, tileValue; if (this.soundCount > 0) this.soundCount--; if (this.soundCount === 0) { if ((math.getRandom16() & 3) === 1) { // TODO Scenarios // TODO Sound messageManager.sendMessage(Messages.SOUND_HONKHONK); } this.soundCount = 200; } if (this.count > 0) this.count--; if (this.count === 0) { // Ships turn slowly: only 45° every 9 cycles this.count = 9; // If already executing a turn, continue to do so if (this.frame !== this.newDir) { this.frame = SpriteUtils.turnTo(this.frame, this.newDir); return; } // Otherwise pick a new direction // Choose a random starting direction to search from // 0 = N, 1 = NE, ... 7 = NW startDir = math.getRandom16() & 7; frame = this.frame; for (dir = startDir; dir < startDir + 8; dir++) { frame = (dir & 7) + 1; if (frame === this.dir) continue; x = SpriteUtils.pixToWorld(this.x) + this.tileDeltaX[frame]; y = SpriteUtils.pixToWorld(this.y) + this.tileDeltaY[frame]; if (this.map.testBounds(x, y)) { tileValue = this.map.getTileValue(x, y); // Test for a suitable water tile if (tileValue === Tile.CHANNEL || tileValue === Tile.BRWH || tileValue === Tile.BRWV || this.oppositeAndUnderwater(tileValue, this.dir, frame)) { this.newDir = frame; this.frame = SpriteUtils.turnTo(this.frame, this.newDir); this.dir = frame + 4; if (this.dir > 8) this.dir -= 8; break; } } } if (dir === startDir + 8) { this.dir = this.CANTMOVE; this.newDir = (math.getRandom16() & 7) + 1; } } else { frame = this.frame; if (frame === this.newDir) { this.x += this.xDelta[frame]; this.y += this.yDelta[frame]; } } if (this.spriteNotInBounds()) { this.frame = 0; return; } // If we didn't find a new direction, we might explode // depending on the last tile we looked at. for (let i = 0; i < 8; i++) { if (t === this.tileWhiteList[i]) break; if (i === 7) { this.explodeSprite(messageManager); SpriteUtils.destroyMapTile(this.spriteManager, this.map, blockMaps, this.x, this.y); } } } explodeSprite(messageManager) { this.frame = 0; this.spriteManager.makeExplosionAt(this.x, this.y); messageManager.sendMessage(Messages.SHIP_CRASHED); } // This is an odd little function. It returns true if // oldDir is 180° from newDir and tileValue is underwater // rail or wire, and returns false otherwise oppositeAndUnderwater(tileValue, oldDir, newDir) { let opposite = oldDir + 4; if (opposite > 8) opposite -= 8; if (newDir != opposite) return false; if (tileValue == Tile.POWERBASE || tileValue == Tile.POWERBASE + 1 || tileValue == Tile.RAILBASE || tileValue == Tile.RAILBASE + 1) return true; return false; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class MonsterSprite extends BaseSprite { constructor(map, spriteManager, x, y) { super(); this.init(Micro.SPRITE_MONSTER, map, spriteManager, x, y); this.width = 48; this.height = 48; this.xOffset = -24; this.yOffset = -24; if (x > SpriteUtils.worldToPix(map.width) / 2) { if (y > SpriteUtils.worldToPix(map.height) / 2) this.frame = 10;else this.frame = 7; } else if (y > SpriteUtils.worldToPix(map.height) / 2) { this.frame = 1; } else { this.frame = 4; } this.flag = 0; this.count = 1000; this.destX = SpriteUtils.worldToPix(map.pollutionMaxX); this.destY = SpriteUtils.worldToPix(map.pollutionMaxY); this.origX = this.x; this.origY = this.y; this._seenLand = false; this.xDelta = [2, 2, -2, -2, 0]; this.yDelta = [-2, 2, 2, -2, 0]; this.cardinals1 = [0, 1, 2, 3]; this.cardinals2 = [1, 2, 3, 0]; this.diagonals1 = [2, 5, 8, 11]; this.diagonals2 = [11, 2, 5, 8]; } move(spriteCycle, messageManager, disasterManager, blockMaps) { if (this.soundCount > 0) this.soundCount--; // Frames 1 - 12 are diagonal sprites, 3 for each direction. // 1-3 NE, 2-6 SE, etc. 13-16 represent the cardinal directions. let currentDir = Math.floor((this.frame - 1) / 3); let frame, dir; if (currentDir < 4) { /* turn n s e w */ // Calculate how far in the 3 step animation we were, // move on to the next one frame = (this.frame - 1) % 3; if (frame === 2) this.step = 0; if (frame === 0) this.step = 1; if (this.step) frame++;else frame--; let absDist = SpriteUtils.absoluteDistance(this.x, this.y, this.destX, this.destY); if (absDist < 60) { if (this.flag === 0) { this.flag = 1; this.destX = this.origX; this.destY = this.origY; } else { this.frame = 0; return; } } // Perhaps switch to a cardinal direction dir = SpriteUtils.getDir(this.x, this.y, this.destX, this.destY); dir = Math.floor((dir - 1) / 2); if (dir !== currentDir && math.getChance(10)) { if (math.getRandom16() & 1) frame = this.cardinals1[currentDir];else frame = this.cardinals2[currentDir]; currentDir = 4; if (!this.soundCount) { messageManager.sendMessage(Messages.SOUND_MONSTER); this.soundCount = 50 + math.getRandom(100); } } } else { // Travelling in a cardinal direction. Switch to a diagonal currentDir = 4; dir = this.frame; frame = dir - 13 & 3; if (!(math.getRandom16() & 3)) { if (math.getRandom16() & 1) frame = this.diagonals1[frame];else frame = this.diagonals2[frame]; // We mung currentDir and frame here to // make the assignment below work currentDir = Math.floor((frame - 1) / 3); frame = (frame - 1) % 3; } } frame = currentDir * 3 + frame + 1; if (frame > 16) frame = 16; this.frame = frame; this.x += this.xDelta[currentDir]; this.y += this.yDelta[currentDir]; if (this.count > 0) this.count--; let tileValue = SpriteUtils.getTileValue(this.map, this.x, this.y); if (tileValue === -1 || tileValue === Tile.RIVER && this.count < 500) this.frame = 0; if (tileValue === Tile.DIRT || tileValue > Tile.WATER_HIGH) this._seenLand = true; let spriteList = this.spriteManager.getSpriteList(); for (let i = 0; i < spriteList.length; i++) { let s = spriteList[i]; if (s.frame !== 0 && (s.type === Micro.SPRITE_AIRPLANE || s.type === Micro.SPRITE_HELICOPTER || s.type === Micro.SPRITE_SHIP || s.type === Micro.SPRITE_TRAIN) && SpriteUtils.checkSpriteCollision(this, s)) s.explodeSprite(messageManager); } SpriteUtils.destroyMapTile(this.spriteManager, this.map, blockMaps, this.x, this.y); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class CopterSprite extends BaseSprite { constructor(map, spriteManager, x, y) { super(); this.init(Micro.SPRITE_HELICOPTER, map, spriteManager, x, y); this.width = 32; this.height = 32; this.xOffset = -16; this.yOffset = -16; this.frame = 5; this.count = 1500; this.destX = math.getRandom(SpriteUtils.worldToPix(map.width)) + 8; this.destY = math.getRandom(SpriteUtils.worldToPix(map.height)) + 8; this.origX = x; this.origY = y; this.xDelta = [0, 0, 3, 5, 3, 0, -3, -5, -3]; this.yDelta = [0, -5, -3, 0, 3, 5, 3, 0, -3]; } move(spriteCycle, messageManager, disasterManager, blockMaps) { if (this.soundCount > 0) this.soundCount--; if (this.count > 0) this.count--; if (this.count === 0) { // Head towards a monster, and certain doom let s = this.spriteManager.getSprite(Micro.SPRITE_MONSTER); if (s !== null) { this.destX = s.x; this.destY = s.y; } else { // No monsters. Hm. I bet flying near that tornado is sensible s = this.spriteManager.getSprite(Micro.SPRITE_TORNADO); if (s !== null) { this.destX = s.x; this.destY = s.y; } else { this.destX = this.origX; this.destY = this.origY; } } // If near destination, let's get her on the ground let absDist = SpriteUtils.absoluteDistance(this.x, this.y, this.origX, this.origY); if (absDist < 30) { this.frame = 0; return; } } if (this.soundCount === 0) { let x = SpriteUtils.pixToWorld(this.x); let y = SpriteUtils.pixToWorld(this.y); if (x >= 0 && x < this.map.width && y >= 0 && y < this.map.height) { if (blockMaps.trafficDensityMap.worldGet(x, y) > 170 && (math.getRandom16() & 7) === 0) { messageManager.sendMessage(Messages.HEAVY_TRAFFIC, { x: x, y: y }); messageManager.sendMessage(Messages.SOUND_HEAVY_TRAFFIC); this.soundCount = 200; } } } let frame = this.frame; if ((spriteCycle & 3) === 0) { let dir = SpriteUtils.getDir(this.x, this.y, this.destX, this.destY); frame = SpriteUtils.turnTo(frame, dir); this.frame = frame; } this.x += this.xDelta[frame]; this.y += this.yDelta[frame]; } explodeSprite(messageManager) { this.frame = 0; this.spriteManager.makeExplosionAt(this.x, this.y); messageManager.sendMessage(Messages.HELICOPTER_CRASHED); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class AirplaneSprite extends BaseSprite { constructor(map, spriteManager, x, y) { super(); this.init(Micro.SPRITE_AIRPLANE, map, spriteManager, x, y); this.width = 48; this.height = 48; this.xOffset = -24; this.yOffset = -24; if (x > SpriteUtils.worldToPix(map.width - 20)) { this.destX = this.x - 200; this.frame = 7; } else { this.destX = this.x + 200; this.frame = 11; } this.destY = this.y; this.xDelta = [0, 0, 6, 8, 6, 0, -6, -8, -6, 8, 8, 8]; this.yDelta = [0, -8, -6, 0, 6, 8, 6, 0, -6, 0, 0, 0]; } move(spriteCycle, messageManager, disasterManager, blockMaps) { let frame = this.frame; if (spriteCycle % 5 === 0) { // Frames > 8 mean the plane is taking off if (frame > 8) { frame--; if (frame < 9) { // Planes always take off to the east frame = 3; } this.frame = frame; } else { let d = SpriteUtils.getDir(this.x, this.y, this.destX, this.destY); frame = SpriteUtils.turnTo(frame, d); this.frame = frame; } } let absDist = SpriteUtils.absoluteDistance(this.x, this.y, this.destX, this.destY); if (absDist < 50) { // We're pretty close to the destination this.destX = math.getRandom(SpriteUtils.worldToPix(this.map.width)) + 8; this.destY = math.getRandom(SpriteUtils.worldToPix(this.map.height)) + 8; } if (disasterManager.enableDisasters) { let explode = false; let spriteList = this.spriteManager.getSpriteList(); for (let i = 0; i < spriteList.length; i++) { let s = spriteList[i]; //if (s.frame === 0 || s === sprite) continue; if (s.frame === 0) continue; if ((s.type === Micro.SPRITE_HELICOPTER || s.type === Micro.SPRITE_AIRPLANE) && SpriteUtils.checkSpriteCollision(this, s)) { s.explodeSprite(messageManager); explode = true; } } if (explode) this.explodeSprite(messageManager); } this.x += this.xDelta[frame]; this.y += this.yDelta[frame]; if (this.spriteNotInBounds()) this.frame = 0; } explodeSprite(messageManager) { this.frame = 0; this.spriteManager.makeExplosionAt(this.x, this.y); messageManager.sendMessage(Messages.PLANE_CRASHED); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class TornadoSprite extends BaseSprite { constructor(map, spriteManager, x, y) { super(); this.init(Micro.SPRITE_TORNADO, map, spriteManager, x, y); this.width = 48; this.height = 48; this.xOffset = -24; this.yOffset = -40; this.frame = 1; this.count = 200; this.xDelta = [2, 3, 2, 0, -2, -3]; this.yDelta = [-2, 0, 2, 3, 2, 0]; } move(spriteCycle, messageManager, disasterManager, blockMaps) { let frame; frame = this.frame; // If middle frame, move right or left // depending on the flag value // If frame = 1, perhaps die based on flag // value if (frame === 2) { if (this.flag) frame = 3;else frame = 1; } else { if (frame === 1) this.flag = 1;else this.flag = 0; frame = 2; } if (this.count > 0) this.count--; this.frame = frame; let spriteList = this.spriteManager.getSpriteList(); for (let i = 0; i < spriteList.length; i++) { let s = spriteList[i]; // Explode vulnerable sprites if (s.frame !== 0 && (s.type === Micro.SPRITE_AIRPLANE || s.type === Micro.SPRITE_HELICOPTER || s.type === Micro.SPRITE_SHIP || s.type === Micro.SPRITE_TRAIN) && SpriteUtils.checkSpriteCollision(this, s)) { s.explodeSprite(messageManager); } } frame = Random.getRandom(5); this.x += this.xDelta[frame]; this.y += this.yDelta[frame]; if (this.spriteNotInBounds()) this.frame = 0; if (this.count !== 0 && Random.getRandom(500) === 0) this.frame = 0; SpriteUtils.destroyMapTile(this.spriteManager, this.map, blockMaps, this.x, this.y); } } /* // Metadata for image loading Object.defineProperties(TornadoSprite, {ID: Micro.makeConstantDescriptor(6), width: Micro.makeConstantDescriptor(48), frames: Micro.makeConstantDescriptor(3)}); return TornadoSprite; }); */ /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class ExplosionSprite extends BaseSprite { constructor(map, spriteManager, x, y) { super(); this.init(Micro.SPRITE_EXPLOSION, map, spriteManager, x, y); this.width = 48; this.height = 48; this.xOffset = -24; this.yOffset = -24; this.frame = 1; } startFire(x, y) { x = ZoneUtils.pixToWorld(x); y = ZoneUtils.pixToWorld(y); if (!this.map.testBounds(x, y)) return; let tile = this.map.getTile(x, y); let tileValue = tile.getValue(); if (!tile.isCombustible() && tileValue !== Tile.DIRT) return; if (tile.isZone()) return; this.map.setTo(x, y, ZoneUtils.randomFire()); } move(spriteCycle, messageManager, disasterManager, blockMaps) { if ((spriteCycle & 1) === 0) { if (this.frame === 1) { // Convert sprite coordinates to tile coordinates. let explosionX = ZoneUtils.pixToWorld(this.x); let explosionY = ZoneUtils.pixToWorld(this.y); messageManager.sendMessage(Messages.SOUND_EXPLOSIONHIGH); messageManager.sendMessage(Messages.EXPLOSION_REPORTED, { x: explosionX, y: explosionY }); } this.frame++; } if (this.frame > 6) { this.frame = 0; this.startFire(this.x, this.y); this.startFire(this.x - 16, this.y - 16); this.startFire(this.x + 16, this.y + 16); this.startFire(this.x - 16, this.y + 16); this.startFire(this.x + 16, this.y + 16); } } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const constructors = {}; constructors[Micro.SPRITE_TRAIN] = TrainSprite; constructors[Micro.SPRITE_SHIP] = BoatSprite; constructors[Micro.SPRITE_MONSTER] = MonsterSprite; constructors[Micro.SPRITE_HELICOPTER] = CopterSprite; constructors[Micro.SPRITE_AIRPLANE] = AirplaneSprite; constructors[Micro.SPRITE_TORNADO] = TornadoSprite; constructors[Micro.SPRITE_EXPLOSION] = ExplosionSprite; class SpriteManager { constructor(map) { this.spriteList = []; this.map = map; this.spriteCycle = 0; } getSprite(type) { let filteredList = this.spriteList.filter(function (s) { return s.frame !== 0 && s.type === type; }); if (filteredList.length === 0) return null; return filteredList[0]; } getSpriteList() { return this.spriteList.slice(); } getSpritesInView(startX, startY, lastX, lastY) { startX = SpriteUtils.worldToPix(startX); startY = SpriteUtils.worldToPix(startY); lastX = SpriteUtils.worldToPix(lastX); lastY = SpriteUtils.worldToPix(lastY); return this.spriteList.filter(function (s) { return s.x + s.xOffset >= startX && s.y + s.yOffset >= startY && !(s.x + s.xOffset >= lastX && s.y + s.yOffset >= lastY); }); } moveObjects(simData) { if (!simData) simData = Micro.simData; let messageManager = simData.messageManager; let disasterManager = simData.disasterManager; let blockMaps = simData.blockMaps; this.spriteCycle += 1; let list = this.spriteList.slice(); let i = list.length; while (i--) { //for (let i = 0, l = list.length; i < l; i++) { let sprite = list[i]; if (sprite.frame === 0) continue; sprite.move(this.spriteCycle, messageManager, disasterManager, blockMaps); } this.pruneDeadSprites(); } makeSprite(type, x, y) { this.spriteList.push(new constructors[type](this.map, this, x, y)); } makeTornado(messageManager) { let sprite = this.getSprite(Micro.SPRITE_TORNADO); if (sprite !== null) { sprite.count = 200; return; } let x = math.getRandom(SpriteUtils.worldToPix(this.map.width) - 800) + 400; let y = math.getRandom(SpriteUtils.worldToPix(this.map.height) - 200) + 100; this.makeSprite(Micro.SPRITE_TORNADO, x, y); messageManager.sendMessage(Messages.TORNADO_SIGHTED, { x: SpriteUtils.pixToWorld(x), y: SpriteUtils.pixToWorld(y) }); } makeExplosion(x, y) { if (this.map.testBounds(x, y)) this.makeExplosionAt(SpriteUtils.worldToPix(x), SpriteUtils.worldToPix(y)); } makeExplosionAt(x, y) { this.makeSprite(Micro.SPRITE_EXPLOSION, x, y); } generatePlane(x, y) { if (this.getSprite(Micro.SPRITE_AIRPLANE) !== null) return; this.makeSprite(Micro.SPRITE_AIRPLANE, SpriteUtils.worldToPix(x), SpriteUtils.worldToPix(y)); } generateTrain(census, x, y) { if (census.totalPop > 20 && this.getSprite(Micro.SPRITE_TRAIN) === null && math.getRandom(25) === 0) this.makeSprite(Micro.SPRITE_TRAIN, SpriteUtils.worldToPix(x) + 8, SpriteUtils.worldToPix(y) + 8); } generateShip() { // XXX This code is borked. The map generator will never // place a channel tile on the edges of the map let x, y; if (math.getChance(3)) { for (x = 4; x < this.map.width - 2; x++) { if (this.map.getTileValue(x, 0) === Tile.CHANNEL) { this.makeShipHere(x, 0); return; } } } if (math.getChance(3)) { for (y = 1; y < this.map.height - 2; y++) { if (this.map.getTileValue(0, y) === Tile.CHANNEL) { this.makeShipHere(0, y); return; } } } if (math.getChance(3)) { for (x = 4; x < this.map.width - 2; x++) { if (this.map.getTileValue(x, this.map.height - 1) === Tile.CHANNEL) { this.makeShipHere(x, this.map.height - 1); return; } } } if (math.getChance(3)) { for (y = 1; y < this.map.height - 2; y++) { if (this.map.getTileValue(this.map.width - 1, y) === Tile.CHANNEL) { this.makeShipHere(this.map.width - 1, y); return; } } } } getBoatDistance(x, y) { let dist = 99999; let pixelX = SpriteUtils.worldToPix(x) + 8; let pixelY = SpriteUtils.worldToPix(y) + 8; let sprite; for (let i = 0, l = this.spriteList.length; i < l; i++) { sprite = this.spriteList[i]; if (sprite.type === Micro.SPRITE_SHIP && sprite.frame !== 0) { //let sprDist = Micro.absoluteValue(sprite.x - pixelX) + Micro.absoluteValue(sprite.y - pixelY); let sprDist = Math.abs(sprite.x - pixelX) + Math.abs(sprite.y - pixelY); dist = Math.min(dist, sprDist); } } return dist; } makeShipHere(x, y) { this.makeSprite(Micro.SPRITE_SHIP, SpriteUtils.worldToPix(x), SpriteUtils.worldToPix(y)); } generateCopter(x, y) { if (this.getSprite(Micro.SPRITE_HELICOPTER) !== null) return; this.makeSprite(Micro.SPRITE_HELICOPTER, SpriteUtils.worldToPix(x), SpriteUtils.worldToPix(y)); } makeMonsterAt(messageManager, x, y) { this.makeSprite(Micro.SPRITE_MONSTER, SpriteUtils.worldToPix(x), SpriteUtils.worldToPix(y)); messageManager.sendMessage(Messages.MONSTER_SIGHTED, { x: x, y: y }); } makeMonster(messageManager) { let sprite = this.getSprite(Micro.SPRITE_MONSTER); if (sprite !== null) { sprite.soundCount = 1; sprite.count = 1000; sprite.destX = SpriteUtils.worldToPix(this.map.pollutionMaxX); sprite.destY = SpriteUtils.worldToPix(this.map.pollutionMaxY); } let done = 0; for (let i = 0; i < 300; i++) { let x = math.getRandom(this.map.width - 20) + 10; let y = math.getRandom(this.map.height - 10) + 5; let tile = this.map.getTile(x, y); if (tile.getValue() === Tile.RIVER) { this.makeMonsterAt(messageManager, x, y); done = 1; break; } } if (done === 0) this.makeMonsterAt(messageManager, 60, 50); } pruneDeadSprites(type) { this.spriteList = this.spriteList.filter(function (s) { return s.frame !== 0; }); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class EvaluationUtils { static getTrafficAverage(blockMaps, census) { var trafficDensityMap = blockMaps.trafficDensityMap; var landValueMap = blockMaps.landValueMap; var trafficTotal = 0; var count = 1; for (var x = 0; x < landValueMap.gameMapWidth; x += landValueMap.blockSize) { for (var y = 0; y < landValueMap.gameMapHeight; y += landValueMap.blockSize) { if (landValueMap.worldGet(x, y) > 0) { trafficTotal += trafficDensityMap.worldGet(x, y); count++; } } } var trafficAverage = Math.floor(trafficTotal / count) * 2.4; //census.trafficAverage = trafficAverage //?? return trafficAverage; } static getUnemployment(census) { var b = (census.comPop + census.indPop) * 8; if (b === 0) return 0; // Ratio total people / working. At least 1. var r = census.resPop / b; b = Math.round((r - 1) * 255); return Math.min(b, 255); } static getFireSeverity(census) { return Math.min(census.firePop * 5, 255); } } class Evaluation { constructor(gameLevel) { this.problemVotes = []; this.problemOrder = []; this.evalInit(); this.gameLevel = '' + gameLevel; this.changed = false; } save(saveData) { for (var i = 0, l = Micro.EvalProps.length; i < l; i++) saveData[Micro.EvalProps[i]] = this[Micro.EvalProps[i]]; } load(saveData) { for (var i = 0, l = Micro.EvalProps.length; i < l; i++) this[Micro.EvalProps[i]] = saveData[Micro.EvalProps[i]]; } cityEvaluation(simData) { if (!simData) simData = Micro.simData; var census = simData.census; if (census.totalPop > 0) { Micro.problemData = []; //var problemTable = []; for (var i = 0; i < Micro.NUMPROBLEMS; i++) Micro.problemData.push(0); this.getAssessedValue(census); this.getPopulation(census); this.doProblems(simData.census, simData.budget, simData.blockMaps); this.getScore(simData); this.doVotes(); this.changeEval(); } else { this.evalInit(); this.cityYes = 50; this.changeEval(); } } evalInit() { let i; this.cityYes = 0; this.cityPop = 0; this.cityPopDelta = 0; this.cityAssessedValue = 0; this.cityClass = Micro.CC_VILLAGE; this.cityClassLast = Micro.CC_VILLAGE; this.cityScore = 500; this.cityScoreDelta = 0; for (i = 0; i < Micro.NUMPROBLEMS; i++) this.problemVotes[i] = { index: i, voteCount: 0 }; for (i = 0; i < Micro.NUM_COMPLAINTS; i++) this.problemOrder[i] = Micro.NUMPROBLEMS; } getAssessedValue(census) { var value; value = census.roadTotal * 5; value += census.railTotal * 10; value += census.policeStationPop * 1000; value += census.fireStationPop * 1000; value += census.hospitalPop * 400; value += census.stadiumPop * 3000; value += census.seaportPop * 5000; value += census.airportPop * 10000; value += census.coalPowerPop * 3000; value += census.nuclearPowerPop * 6000; this.cityAssessedValue = value * 1000; } getPopulation(census) { let oldPopulation = this.cityPop; this.cityPop = (census.resPop + (census.comPop + census.indPop) * 8) * 20; this.cityPopDelta = this.cityPop - oldPopulation; if (this.cityPopDelta !== 0) EventEmitter.emitEvent(Messages.POPULATION_UPDATED, this.cityPop); return this.cityPop; } getCityClass(cityPopulation) { this.cityClass = Micro.CC_VILLAGE; if (cityPopulation > 2000) this.cityClass = Micro.CC_TOWN; if (cityPopulation > 10000) this.cityClass = Micro.CC_CITY; if (cityPopulation > 50000) this.cityClass = Micro.CC_CAPITAL; if (cityPopulation > 100000) this.cityClass = Micro.CC_METROPOLIS; if (cityPopulation > 500000) this.cityClass = Micro.CC_MEGALOPOLIS; if (this.cityClass !== this.cityClassLast) { this.cityClassLast = this.cityClass; EventEmitter.emitEvent(Messages.CLASSIFICATION_UPDATED, this.cityClass); } return this.cityClass; } voteProblems() { for (var i = 0; i < Micro.NUMPROBLEMS; i++) { this.problemVotes[i].index = i; this.problemVotes[i].voteCount = 0; } var problem = 0; var voteCount = 0; var loopCount = 0; while (voteCount < 100 && loopCount < 600) { var voterProblemTolerance = math.getRandom(300); if (Micro.problemData[problem] > voterProblemTolerance) { this.problemVotes[problem].voteCount += 1; voteCount++; } problem = (problem + 1) % Micro.NUMPROBLEMS; loopCount++; } } doProblems(census, budget, blockMaps) { //var problemTaken = []; /*for (var i = 0; i < Micro.NUMPROBLEMS; i++) { problemTaken[i] = false; problemTable[i] = 0; }*/ Micro.problemData[Micro.CRIME] = census.crimeAverage; Micro.problemData[Micro.POLLUTION] = census.pollutionAverage; Micro.problemData[Micro.HOUSING] = census.landValueAverage * 7 / 10; Micro.problemData[Micro.TAXES] = budget.cityTax * 10; Micro.problemData[Micro.TRAFFIC] = EvaluationUtils.getTrafficAverage(blockMaps, census); Micro.problemData[Micro.UNEMPLOYMENT] = EvaluationUtils.getUnemployment(census); Micro.problemData[Micro.FIRE] = EvaluationUtils.getFireSeverity(census); this.voteProblems(); // Rank the problems this.problemVotes.sort(function (a, b) { return b.voteCount - a.voteCount; }); this.problemOrder = this.problemVotes.map(function (pv, i) { if (i >= Micro.NUM_COMPLAINTS || pv.voteCount === 0) return null; return pv.index; }); /*for (i = 0; i < Micro.NUM_COMPLAINTS; i++) { // Find biggest problem not taken yet var maxVotes = 0; var bestProblem = Micro.NUMPROBLEMS; for (var j = 0; j < Micro.NUMPROBLEMS; j++) { if ((this.problemVotes[j] > maxVotes) && (!problemTaken[j])) { bestProblem = j; maxVotes = this.problemVotes[j]; } } // bestProblem == NUMPROBLEMS means no problem found this.problemOrder[i] = bestProblem; if (bestProblem < Micro.NUMPROBLEMS) { problemTaken[bestProblem] = true; } }*/ } getScore(simData) { var census = simData.census; var budget = simData.budget; var valves = simData.valves; var cityScoreLast; cityScoreLast = this.cityScore; var score = 0; for (var i = 0; i < Micro.NUMPROBLEMS; i++) score += Micro.problemData[i]; score = Math.floor(score / 3); score = (250 - Math.min(score, 250)) * 4; //score = Math.min(score, 256); //score = math.clamp((256 - score) * 4, 0, 1000); // Penalise the player by 15% if demand for any type of zone is capped due // to lack of suitable buildings let demandPenalty = 0.85; if (valves.resCap) score = Math.round(score * demandPenalty); if (valves.comCap) score = Math.round(score * demandPenalty); if (valves.indCap) score = Math.round(score * demandPenalty); // Penalize if roads/rail underfunded if (budget.roadEffect < budget.MAX_ROAD_EFFECT) score -= budget.MAX_ROAD_EFFECT - budget.roadEffect; // Penalize player by up to 10% for underfunded police and fire services if (budget.policeEffect < budget.MAX_POLICE_STATION_EFFECT) { score = Math.round(score * (0.9 + budget.policeEffect / (10 * budget.MAX_POLICE_STATION_EFFECT))); } if (budget.fireEffect < budget.MAX_FIRE_STATION_EFFECT) { score = Math.round(score * (0.9 + budget.fireEffect / (10 * budget.MAX_FIRE_STATION_EFFECT))); } // Penalise the player by 15% if demand for any type of zone has collapsed due to overprovision if (valves.resValve < -1000) score = Math.round(score * 0.85); if (valves.comValve < -1000) score = Math.round(score * 0.85); if (valves.indValve < -1000) score = Math.round(score * 0.85); var scale = 1.0; if (this.cityPop === 0 || this.cityPopDelta === 0 || this.cityPopDelta === this.cityPop) { // Leave score unchanged if city is empty, if there hasn't been any migration, if the // initial settlers have just arrived, or if the city has doubled in size scale = 1.0; } else if (this.cityPopDelta > 0) { // If the city is growing, scale score by percentage growth in population scale = this.cityPopDelta / this.cityPop + 1.0; } else if (this.cityPopDelta < 0) { // If the city is shrinking, scale down by up to 5% based on level of outward migration scale = 0.95 + Math.floor(this.cityPopDelta / (this.cityPop - this.cityPopDelta)); } score = Math.round(score * scale); // Penalize player for having fires and a burdensome tax rate score = score - EvaluationUtils.getFireSeverity(census) - budget.cityTax; // dec score for fires and tax // Penalize player based on ratio of unpowered zones to total zones scale = census.unpoweredZoneCount + census.poweredZoneCount; // dec score for unpowered zones if (scale > 0.0) score = Math.round(score * (census.poweredZoneCount / scale)); // Force in to range 0-1000. New score is average of last score and new computed value score = math.clamp(score, 0, 1000); this.cityScore = Math.round((this.cityScore + score) / 2); this.cityScoreDelta = this.cityScore - cityScoreLast; if (this.cityScoreDelta !== 0) EventEmitter.emitEvent(Messages.SCORE_UPDATED, this.cityScore); } doVotes() { this.cityYes = 0; for (let i = 0; i < 100; i++) { let voterExpectation = math.getRandom(1000); if (this.cityScore > voterExpectation) this.cityYes++; } } changeEval() { this.changed = true; } countProblems() { var i; for (i = 0; i < Micro.NUM_COMPLAINTS; i++) { if (this.problemOrder[i] === Micro.NUMPROBLEMS) break; } return i; } getProblemNumber(i) { if (i < 0 || i >= Micro.NUM_COMPLAINTS || this.problemOrder[i] === Micro.NUMPROBLEMS) return -1;else return this.problemOrder[i]; } getProblemVotes(i) { if (i < 0 || i >= Micro.NUM_COMPLAINTS || this.problemOrder[i] == Micro.NUMPROBLEMS) return -1;else return this.problemVotes[this.problemOrder[i]].voteCount; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class Valves { constructor() { this.resValve = 0; this.comValve = 0; this.indValve = 0; this.resCap = false; this.comCap = false; this.indCap = false; } save(saveData) { saveData.resValve = this.resValve; saveData.comValve = this.comValve; saveData.indValve = this.indValve; } load(saveData) { this.resValve = saveData.resValve; this.comValve = saveData.comValve; this.indValve = saveData.indValve; EventEmitter.emitEvent(Messages.VALVES_UPDATED, { residential: this.resValve, commercial: this.comValve, industrial: this.indValve }); } setValves(gameLevel, census, budget) { var resPopDenom = 8; var birthRate = 0.02; var labourBaseMax = 1.3; var internalMarketDenom = 3.7; var projectedIndPopMin = 5.0; var resRatioDefault = 1.3; var resRatioMax = 2; var comRatioMax = 2; var indRatioMax = 2; var taxMax = 20; var taxTableScale = 600; var employment, labourBase; // Residential zones scale their population index when reporting it to the census var normalizedResPop = census.resPop / resPopDenom; census.totalPop = Math.round(normalizedResPop + census.comPop + census.indPop); // A lack of developed commercial and industrial zones means there are no employment opportunities, which constrain // growth. (This might hurt initially if, for example, the player lays out an initial grid, as the residential zones // will likely develop first, so the residential valve will immediately crater). if (census.resPop > 0) employment = (census.comHist10[1] + census.indHist10[1]) / normalizedResPop;else employment = 1; // Given the employment rate, calculate expected migration, add in births, and project the new population. var migration = normalizedResPop * (employment - 1); var births = normalizedResPop * birthRate; var projectedResPop = normalizedResPop + migration + births; // Examine how many zones require workers labourBase = census.comHist10[1] + census.indHist10[1]; if (labourBase > 0.0) labourBase = census.resHist10[1] / labourBase;else labourBase = 1; labourBase = math.clamp(labourBase, 0.0, labourBaseMax); // Project future industry and commercial needs, taking into account available labour, and competition from // other global cities var internalMarket = (normalizedResPop + census.comPop + census.indPop) / internalMarketDenom; var projectedComPop = internalMarket * labourBase; var projectedIndPop = census.indPop * labourBase * Micro.extMarketParamTable[gameLevel]; projectedIndPop = Math.max(projectedIndPop, projectedIndPopMin); // Calculate the expected percentage changes in each population type var resRatio; if (normalizedResPop > 0) resRatio = projectedResPop / normalizedResPop;else resRatio = resRatioDefault; var comRatio; if (census.comPop > 0) comRatio = projectedComPop / census.comPop;else comRatio = projectedComPop; var indRatio; if (census.indPop > 0) indRatio = projectedIndPop / census.indPop;else indRatio = projectedIndPop; resRatio = Math.min(resRatio, resRatioMax); comRatio = Math.min(comRatio, comRatioMax); indRatio = Math.min(indRatio, indRatioMax); // Global tax and game level effects. var z = Math.min(budget.cityTax + gameLevel, taxMax); resRatio = (resRatio - 1) * taxTableScale + Micro.taxTable[z]; comRatio = (comRatio - 1) * taxTableScale + Micro.taxTable[z]; indRatio = (indRatio - 1) * taxTableScale + Micro.taxTable[z]; // Ratios are velocity changes to valves. this.resValve = math.clamp(this.resValve + Math.round(resRatio), -Micro.RES_VALVE_RANGE, Micro.RES_VALVE_RANGE); this.comValve = math.clamp(this.comValve + Math.round(comRatio), -Micro.COM_VALVE_RANGE, Micro.COM_VALVE_RANGE); this.indValve = math.clamp(this.indValve + Math.round(indRatio), -Micro.IND_VALVE_RANGE, Micro.IND_VALVE_RANGE); if (this.resCap && this.resValve > 0) this.resValve = 0; if (this.comCap && this.comValve > 0) this.comValve = 0; if (this.indCap && this.indValve > 0) this.indValve = 0; EventEmitter.emitEvent(Messages.VALVES_UPDATED, { residential: this.resValve, commercial: this.comValve, industrial: this.indValve }); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class Budget { constructor() { this.roadEffect = Micro.MAX_ROAD_EFFECT; this.policeEffect = Micro.MAX_POLICESTATION_EFFECT; this.fireEffect = Micro.MAX_FIRESTATION_EFFECT; this.totalFunds = 0; this.cityTax = 7; this.cashFlow = 0; this.taxFund = 0; // These values denote how much money is required to fully maintain the relevant services this.roadMaintenanceBudget = 0; this.fireMaintenanceBudget = 0; this.policeMaintenanceBudget = 0; // Percentage of budget used this.roadPercent = 1; this.firePercent = 1; this.policePercent = 1; // Cash value of spending. Should equal Math.round(_Fund * _Percent) this.roadSpend = 0; this.fireSpend = 0; this.policeSpend = 0; this.awaitingValues = false; this.autoBudget = true; } save(saveData) { for (var i = 0, l = Micro.BudgetProps.length; i < l; i++) saveData[Micro.BudgetProps[i]] = this[Micro.BudgetProps[i]]; } load(saveData) { for (var i = 0, l = Micro.BudgetProps.length; i < l; i++) this[Micro.BudgetProps[i]] = saveData[Micro.BudgetProps[i]]; EventEmitter.emitEvent(Messages.AUTOBUDGET_CHANGED, this.autoBudget); EventEmitter.emitEvent(Messages.FUNDS_CHANGED, this.totalFunds); } setAutoBudget(value) { this.autoBudget = value; EventEmitter.emitEvent(Messages.AUTOBUDGET_CHANGED, this.autoBudget); } // Calculates the best possible outcome in terms of funding the various services // given the player's current funds and tax yield. On entry, roadPercent etc. are // assumed to contain the desired percentage level, and taxFunds should contain the // most recent tax collected. On exit, the *Percent members will be updated with what // we can actually afford to spend. Returns an object containing the amount of cash // that would be spent on each service. _calculateBestPercentages() { // How much would we be spending based on current percentages? // Note: the *Budget items are updated every January by collectTax this.roadSpend = Math.round(this.roadMaintenanceBudget * this.roadPercent); this.fireSpend = Math.round(this.fireMaintenanceBudget * this.firePercent); this.policeSpend = Math.round(this.policeMaintenanceBudget * this.policePercent); var total = this.roadSpend + this.fireSpend + this.policeSpend; // If we don't have any services on the map, we can bail early if (total === 0) { this.roadPercent = 1; this.firePercent = 1; this.policePercent = 1; return { road: 1, fire: 1, police: 1 }; } // How much are we actually going to spend? var roadCost = 0; var fireCost = 0; var policeCost = 0; var cashRemaining = this.totalFunds + this.taxFund; // Spending priorities: road, fire, police if (cashRemaining >= this.roadSpend) roadCost = this.roadSpend;else roadCost = cashRemaining; cashRemaining -= roadCost; if (cashRemaining >= this.fireSpend) fireCost = this.fireSpend;else fireCost = cashRemaining; cashRemaining -= fireCost; if (cashRemaining >= this.policeSpend) policeCost = this.policeSpend;else policeCost = cashRemaining; cashRemaining -= policeCost; if (this.roadMaintenanceBudget > 0) this.roadPercent = (roadCost / this.roadMaintenanceBudget).toPrecision(2) - 0;else this.roadPercent = 1; if (this.fireMaintenanceBudget > 0) this.firePercent = (fireCost / this.fireMaintenanceBudget).toPrecision(2) - 0;else this.firePercent = 1; if (this.policeMaintenanceBudget > 0) this.policePercent = (policeCost / this.policeMaintenanceBudget).toPrecision(2) - 0;else this.policePercent = 1; return { road: roadCost, police: policeCost, fire: fireCost }; } // User initiated budget doBudgetWindow() { //doBudgetMenu return this.doBudgetNow(true); } doBudgetNow(fromWindow) { var costs = this._calculateBestPercentages(); if (!this.autoBudget && !fromWindow) { this.autoBudget = false; this.awaitingValues = true; EventEmitter.emitEvent(Messages.BUDGET_NEEDED); return; } var roadCost = costs.road; var policeCost = costs.police; var fireCost = costs.fire; var totalCost = roadCost + policeCost + fireCost; var cashRemaining = this.totalFunds + this.taxFund - totalCost; // Autobudget if (cashRemaining > 0 && this.autoBudget || fromWindow) { // Either we were able to fully fund services, or we have just normalised user input. Go ahead and spend. this.awaitingValues = false; this.doBudgetSpend(roadCost, fireCost, policeCost); return; } // Uh-oh. Not enough money. Make this the user's problem. // They don't know it yet, but they're about to get a budget window. this.setAutoBudget(false); this.awaitingValues = true; EventEmitter.emitEvent(Messages.BUDGET_NEEDED); EventEmitter.emitEvent(Messages.NO_MONEY); } doBudgetSpend(roadValue, fireValue, policeValue) { this.roadSpend = roadValue; this.fireSpend = fireValue; this.policeSpend = policeValue; var total = this.roadSpend + this.fireSpend + this.policeSpend; this.spend(-(this.taxFund - total)); this.updateFundEffects(); } updateFundEffects() { // The caller is assumed to have correctly set the percentage spend this.roadSpend = Math.round(this.roadMaintenanceBudget * this.roadPercent); this.fireSpend = Math.round(this.fireMaintenanceBudget * this.firePercent); this.policeSpend = Math.round(this.policeMaintenanceBudget * this.policePercent); // Update the effect this level of spending will have on infrastructure deterioration this.roadEffect = Micro.MAX_ROAD_EFFECT; this.policeEffect = Micro.MAX_POLICESTATION_EFFECT; this.fireEffect = Micro.MAX_FIRESTATION_EFFECT; if (this.roadMaintenanceBudget > 0) this.roadEffect = Math.floor(this.roadEffect * this.roadSpend / this.roadMaintenanceBudget); if (this.fireMaintenanceBudget > 0) this.fireEffect = Math.floor(this.fireEffect * this.fireSpend / this.fireMaintenanceBudget); if (this.policeMaintenanceBudget > 0) this.policeEffect = Math.floor(this.policeEffect * this.policeSpend / this.policeMaintenanceBudget); } collectTax(gameLevel, census) { this.cashFlow = 0; // How much would it cost to fully fund every service? this.policeMaintenanceBudget = census.policeStationPop * Micro.policeMaintenanceCost; this.fireMaintenanceBudget = census.fireStationPop * Micro.fireMaintenanceCost; var roadCost = census.roadTotal * Micro.roadMaintenanceCost; var railCost = census.railTotal * Micro.railMaintenanceCost; this.roadMaintenanceBudget = Math.floor((roadCost + railCost) * Micro.RLevels[gameLevel]); this.taxFund = Math.floor(Math.floor(census.totalPop * census.landValueAverage / 120) * this.cityTax * Micro.FLevels[gameLevel]); if (census.totalPop > 0) { this.cashFlow = this.taxFund - (this.policeMaintenanceBudget + this.fireMaintenanceBudget + this.roadMaintenanceBudget); this.doBudgetNow(false); } else { // We don't want roads etc deteriorating when population hasn't yet been established // (particularly early game) this.roadEffect = Micro.MAX_ROAD_EFFECT; this.policeEffect = Micro.MAX_POLICESTATION_EFFECT; this.fireEffect = Micro.MAX_FIRESTATION_EFFECT; } } setTax(amount) { if (amount === this.cityTax) return; this.cityTax = amount; } setFunds(amount) { if (amount === this.totalFunds) return; this.totalFunds = Math.max(0, amount); EventEmitter.emitEvent(Messages.FUNDS_CHANGED, this.totalFunds); if (this.totalFunds === 0) EventEmitter.emitEvent(Messages.NO_MONEY); } spend(amount) { this.setFunds(this.totalFunds - amount); } shouldDegradeRoad() { return this.roadEffect < Math.floor(15 * this.MAX_ROAD_EFFECT / 16); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class Census { constructor() { this.clearCensus(); this.changed = false; this.crimeRamp = 0; this.pollutionRamp = 0; // Set externally this.landValueAverage = 0; this.pollutionAverage = 0; this.crimeAverage = 0; this.totalPop = 0; var createArray = function (arrName) { //this[arrName] = new M_ARRAY_TYPE(120); this[arrName] = []; //for (var a = 0; a < 120; a++) this[arrName][a] = 0; var a = 120; while (a--) this[arrName][a] = 0; }; //for (var i = 0; i < Micro.arrs.length; i++) { let i = Micro.arrs.length; while (i--) { var name10 = Micro.arrs[i] + 'Hist10'; var name120 = Micro.arrs[i] + 'Hist120'; createArray.call(this, name10); createArray.call(this, name120); } } save(saveData) { for (var i = 0, l = Micro.CensusProps.length; i < l; i++) saveData[Micro.CensusProps[i]] = this[Micro.CensusProps[i]]; } load(saveData) { for (var i = 0, l = Micro.CensusProps.length; i < l; i++) this[Micro.CensusProps[i]] = saveData[Micro.CensusProps[i]]; } clearCensus() { this.poweredZoneCount = 0; this.unpoweredZoneCount = 0; this.firePop = 0; this.roadTotal = 0; this.railTotal = 0; this.resPop = 0; this.comPop = 0; this.indPop = 0; this.resZonePop = 0; this.comZonePop = 0; this.indZonePop = 0; this.hospitalPop = 0; this.churchPop = 0; this.policeStationPop = 0; this.fireStationPop = 0; this.stadiumPop = 0; this.coalPowerPop = 0; this.nuclearPowerPop = 0; this.seaportPop = 0; this.airportPop = 0; } take10Census(budget) { var resPopDenom = 8; MiscUtils.rotate10Arrays.call(this); this.resHist10[0] = Math.floor(this.resPop / resPopDenom); this.comHist10[0] = this.comPop; this.indHist10[0] = this.indPop; this.crimeRamp += Math.floor((this.crimeAverage - this.crimeRamp) / 4); this.crimeHist10[0] = Math.min(this.crimeRamp, 255); this.pollutionRamp += Math.floor((this.pollutionAverage - this.pollutionRamp) / 4); this.pollutionHist10[0] = Math.min(this.pollutionRamp, 255); var x = Math.floor(budget.cashFlow / 20) + 128; this.moneyHist10[0] = math.clamp(x, 0, 255); this.resPop >> 8; if (this.hospitalPop < this.resPopScaled) this.needHospital = 1;else if (this.hospitalPop > this.resPopScaled) this.needHospital = -1;else if (this.hospitalPop === this.resPopScaled) this.needHospital = 0; this.changed = true; } take120Census() { MiscUtils.rotate120Arrays.call(this); var resPopDenom = 8; this.resHist120[0] = Math.floor(this.resPop / resPopDenom); this.comHist120[0] = this.comPop; this.indHist120[0] = this.indPop; this.crimeHist120[0] = this.crimeHist10[0]; this.pollutionHist120[0] = this.pollutionHist10[0]; this.moneyHist120[0] = this.moneyHist10[0]; this.changed = true; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class BlockMap { constructor(gameMapWidth, gameMapHeight, blockSize) { if (gameMapWidth === undefined || gameMapHeight === undefined || blockSize === undefined) throw new Error('Invalid dimensions for block map'); this.isBlockMap = true; this.blockSize = blockSize; this.gameMapWidth = gameMapWidth; this.gameMapHeight = gameMapHeight; this.width = Math.floor((gameMapWidth + blockSize - 1) / blockSize); this.height = Math.floor((gameMapHeight + blockSize - 1) / blockSize); this.data = []; this.clear(); } clear() { let x, y = this.height; while (y--) { x = this.width; while (x--) { this.data[this.width * y + x] = 0; } } } copyFrom(sourceMap, sourceFn) { if (sourceMap.width !== this.width || sourceMap.height !== this.height || sourceMap.blockSize !== this.blockSize) console.warn('Copying from incompatible blockMap!'); let x, y, height = sourceMap.height, width = sourceMap.width; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { this.data[width * y + x] = sourceFn(sourceMap.data[width * y + x]); } } } get(x, y) { return this.data[this.width * y + x]; } set(x, y, value) { this.data[this.width * y + x] = value; } toBlock(num) { return Math.floor(num / this.blockSize); } worldGet(x, y) { return this.get(this.toBlock(x), this.toBlock(y)); } worldSet(x, y, value) { this.set(this.toBlock(x), this.toBlock(y), value); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const Direction = { INVALID: -1, NORTH: 0, NORTHEAST: 1, EAST: 2, SOUTHEAST: 3, SOUTH: 4, SOUTHWEST: 5, WEST: 6, NORTHWEST: 7, BEGIN: 0, END: 8, // Move direction clockwise by 45 degrees. No bounds checking // i.e. result could be >= END. Has no effect on INVALID. Undefined // when dir >= END increment45: function (dir, count) { if (arguments.length < 1) throw new TypeError(); if (dir == Direction.INVALID) return dir; if (!count && count !== 0) count = 1; return dir + count; }, // Move direction clockwise by 90 degrees. No bounds checking // i.e. result could be >= END. Has no effect on INVALID. Undefined // when dir >= END increment90: function (dir) { if (arguments.length < 1) throw new TypeError(); return Direction.increment45(dir, 2); }, // Move direction clockwise by 45 degrees, taking the direction modulo 8 // if necessary to force it into valid bounds. Has no effect on INVALID. rotate45: function (dir, count) { if (arguments.length < 1) throw new TypeError(); if (dir == Direction.INVALID) return dir; if (!count && count !== 0) count = 1; return (dir - Direction.NORTH + count & 7) + Direction.NORTH; }, // Move direction clockwise by 90 degrees, taking the direction modulo 8 // if necessary to force it into valid bounds. Has no effect on INVALID. rotate90: function (dir) { if (arguments.length < 1) throw new TypeError(); return Direction.rotate45(dir, 2); }, // Move direction clockwise by 180 degrees, taking the direction modulo 8 // if necessary to force it into valid bounds. Has no effect on INVALID. rotate180: function (dir) { if (arguments.length < 1) throw new TypeError(); return Direction.rotate45(dir, 4); } }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class Position { constructor(pos, deltaX, deltaY) { this.isPosition = true; this.width = Micro.MAP_WIDTH; this.height = Micro.MAP_HEIGHT; this.x = 0; this.y = 0; this.validDirs = [Direction.NORTH, Direction.NORTHEAST, Direction.EAST, Direction.SOUTHEAST, Direction.SOUTH, Direction.SOUTHWEST, Direction.WEST, Direction.NORTHWEST, Direction.INVALID]; if (arguments.length === 0) return this; if (arguments.length === 1 && !pos.isPosition) throw new Error('Position constructor called with invalid pos ' + pos); if (arguments.length === 3 && !pos.isPosition) throw new Error('Position constructor called with invalid pos ' + pos); if (arguments.length === 3 && !(this.isNumber(deltaX) && this.isNumber(deltaY))) throw new Error('Position constructor called with invalid deltas ' + deltaX + ' ' + deltaY); if (arguments.length === 2 && this.isNumber(pos) && !this.isNumber(deltaX)) throw new Error('Position constructor called with invalid y coordinate ' + pos + ' ' + deltaX); if (arguments.length === 2 && pos.isPosition && !(this.isNumber(deltaX) && this.isDirection(deltaX))) throw new Error('Position constructor called with invalid direction ' + pos + ' ' + deltaX); if (arguments.length === 2 && !this.isNumber(pos) && !pos.isPosition) throw new Error('Position constructor called with bad existing position ' + pos + ' ' + deltaX); // This overloaded constructor accepts the following parameters // Position(x, y) - positive integral coordinates // Position(Position p) - assign from existing position // Position(Position p, Direction d) - assign from existing position and move in Direction d // Position(Position p, deltaX, deltaY) - assign from p and then adjust x/y coordinates // Check for the possible combinations of arguments, and error out for invalid arguments //if ((arguments.length === 1 || arguments.length === 3) && !(pos instanceof Position)) throw new Error('Invalid parameter'); //if (arguments.length === 3 && (!isNumber(deltaX) || !isNumber(deltaY))) throw new Error('Invalid parameter'); //if (arguments.length === 2 && ((isNumber(pos) && !isNumber(deltaX)) || (pos instanceof Position && !isNumber(deltaX)) || (pos instanceof Position && isNumber(deltaX) && !isDirection(deltaX)) || (!isNumber(pos) && !(pos instanceof Position)))) throw new Error('Invalid parameter'); let moveOK = true; if (this.isNumber(pos)) { // Coordinates this.x = pos; this.y = deltaX; } else { this.set(pos); if (arguments.length === 2) moveOK = this.move(deltaX);else if (arguments.length === 3) { this.x += deltaX; this.y += deltaY; } } if (this.x < 0 || this.x >= this.width || this.y < 0 || this.y >= this.height || !moveOK) throw new Error('Invalid parameter'); } isNumber(v) { return !isNaN(v); //return typeof(v) === 'number'; } isDirection(param) { return this.isNumber(param) && this.validDirs.indexOf(param) !== -1; } set(from) { this.x = from.x; this.y = from.y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } toInt() { return this.y * this.width + this.x; } move(dir) { let up = false; switch (dir) { case Direction.INVALID: return true; //up = true; break; case Direction.NORTH: if (this.y > 0) { this.y--; up = true; } break; case Direction.NORTHEAST: if (this.y > 0 && this.x < this.width - 1) { this.y--; this.x++; up = true; } break; case Direction.EAST: if (this.x < this.width - 1) { this.x++; up = true; } break; case Direction.SOUTHEAST: if (this.y < this.height - 1 && this.x < this.width - 1) { this.x++; this.y++; up = true; } break; case Direction.SOUTH: if (this.y < this.height - 1) { this.y++; up = true; } break; case Direction.SOUTHWEST: if (this.y < this.height - 1 && this.x > 0) { this.y++; this.x--; up = true; } break; case Direction.WEST: if (this.x > 0) { this.x--; up = true; } break; case Direction.NORTHWEST: if (this.y > 0 && this.x > 0) { this.y--; this.x--; up = true; } break; } return up; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class PowerManager { constructor(map) { this._map = map; this._powerStack = []; this.powerGridMap = new BlockMap(this._map.width, this._map.height, 1, 0); } setTilePower(x, y) { var tile = this._map.getTile(x, y); var tileValue = tile.getValue(); if (tileValue === Tile.NUCLEAR || tileValue === Tile.POWERPLANT || this.powerGridMap.worldGet(x, y) > 0) { tile.addFlags(Tile.POWERBIT); return; } tile.removeFlags(Tile.POWERBIT); } clearPowerStack() { this._powerStackPointer = 0; this._powerStack = []; } testForConductive(pos, testDir) { var movedPos = new Position(pos); if (movedPos.move(testDir)) { if (this._map.getTile(movedPos.x, movedPos.y).isConductive()) { if (this.powerGridMap.worldGet(movedPos.x, movedPos.y) === 0) return true; } } return false; } // Note: the algorithm is buggy: if you have two adjacent power // plants, the second will be regarded as drawing power from the first // rather than as a power source itself doPowerScan(census) { // Clear power this._map. this.powerGridMap.clear(); // Power that the combined coal and nuclear power plants can deliver. let maxPower = census.coalPowerPop * Micro.COAL_POWER_STRENGTH + census.nuclearPowerPop * Micro.NUCLEAR_POWER_STRENGTH; let powerConsumption = 0; // Amount of power used. while (this._powerStack.length > 0) { var pos = this._powerStack.pop(); var anyDir = Direction.INVALID; var conNum; do { powerConsumption++; if (powerConsumption > maxPower) { EventEmitter.emitEvent(Messages.NOT_ENOUGH_POWER); return; } if (anyDir !== Direction.INVALID) pos.move(anyDir); this.powerGridMap.worldSet(pos.x, pos.y, 1); conNum = 0; var dir = Direction.BEGIN; while (dir < Direction.END && conNum < 2) { if (this.testForConductive(pos, dir)) { conNum++; anyDir = dir; } dir = Direction.increment90(dir); } if (conNum > 1) this._powerStack.push(new Position(pos)); } while (conNum); } } coalPowerFound(map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.coalPowerPop += 1; this._powerStack.push(new Position(x, y)); // Ensure animation runs var dX = [-1, 2, 1, 2]; var dY = [-1, -1, 0, 0]; // Ensure animation bits set no animation for 3d if (!simData.is3D) for (var i = 0; i < 4; i++) map.addTileFlags(x + dX[i], y + dY[i], Tile.ANIMBIT); } nuclearPowerFound(map, x, y, simData) { if (!simData) simData = Micro.simData; var meltdownTable = [30000, 20000, 10000]; // TODO With the auto repair system, zone gets repaired before meltdown // In original Micropolis code, we bail and don't repair if melting down if (simData.disasterManager.disastersEnabled && math.getRandom(meltdownTable[simData.gameLevel]) === 0) { // simData.disasterManager.doMeltdown(messageManager, x, y); return; } simData.census.nuclearPowerPop += 1; this._powerStack.push(new Position(x, y)); //console.log(x, y, new map.Position(x, y)) // Ensure animation bits set no animation for 3d if (!simData.is3D) for (var i = 0; i < 4; i++) map.addTileFlags(x, y, Tile.ANIMBIT | Tile.CONDBIT | Tile.POWERBIT | Tile.BURNBIT); } registerHandlers(mapScanner, repairManager) { mapScanner.addAction(Tile.POWERPLANT, this.coalPowerFound.bind(this)); mapScanner.addAction(Tile.NUCLEAR, this.nuclearPowerFound.bind(this)); repairManager.addAction(Tile.POWERPLANT, 7, 4); repairManager.addAction(Tile.NUCLEAR, 7, 4); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * * switch to ES6 by lo-th * */ class MapScanner { constructor(map) { this._map = map; this._actions = []; } addAction(criterion, action) { this._actions.push({ criterion: criterion, action: action }); } mapScan(startX, maxX, simData) { if (!simData) simData = Micro.simData; let y = this._map.height, x, i, id, tile, tileValue, current, callable; while (y--) { for (x = startX; x < maxX; x++) { id = this._map.getId(x, y); //x + y * this.mapWidth; tile = this._map.data[id] || new Tiles(); tileValue = tile.getValue(); if (tileValue < Tile.FLOOD) continue; if (tile.isConductive()) simData.powerManager.setTilePower(x, y); if (tile.isZone()) { simData.repairManager.checkTile(x, y, simData.cityTime); if (tile.isPowered()) { simData.census.poweredZoneCount += 1; this._map.powered({ v: 1, id: id }); /*this._map.powerData[id] = 1;*/ } else { simData.census.unpoweredZoneCount += 1; this._map.powered({ v: 2, id: id }); /*this._map.powerData[id] = 2;*/ } } i = this._actions.length; while (i--) { current = this._actions[i]; callable = MiscUtils.isCallable(current.criterion); if (callable && current.criterion.call(null, tile)) { current.action.call(null, this._map, x, y, simData); break; } else if (!callable && current.criterion === tileValue) { current.action.call(null, this._map, x, y, simData); break; } } } } } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class RepairManager { constructor(map) { this._map = map; this._actions = []; } addAction(criterion, period, zoneSize) { this._actions.push({ criterion: criterion, period: period, zoneSize: zoneSize }); } repairZone(x, y, zoneSize) { let xx, yy, current, currentValue; let centre = this._map.getTileValue(x, y); let tileValue = centre - zoneSize - 2; for (yy = -1; yy < zoneSize - 1; yy++) { for (xx = -1; xx < zoneSize - 1; xx++) { tileValue++; current = this._map.getTile(x + xx, y + yy); if (current.isZone() || current.isAnimated()) continue; currentValue = current.getValue(); if (currentValue < Tile.RUBBLE || currentValue >= Tile.ROADBASE) this._map.setTile(x + xx, y + yy, tileValue, Tile.CONDBIT | Tile.BURNBIT); } } } checkTile(x, y, cityTime) { let i = this._actions.length, current, period, tile, tileValue, callable; while (i--) { current = this._actions[i]; period = current.period; if ((cityTime & period) !== 0) continue; tile = this._map.getTile(x, y); tileValue = tile.getValue(); callable = MiscUtils.isCallable(current.criterion); if (callable && current.criterion.call(null, tile)) this.repairZone(x, y, current.zoneSize);else if (!callable && current.criterion === tileValue) this.repairZone(x, y, current.zoneSize); } } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class Traffic { constructor(map, spriteManager) { this._map = map; this._stack = []; this._spriteManager = spriteManager; } makeTraffic(x, y, blockMaps, destFn) { this._stack = []; let pos = new Position(x, y); if (this.findPerimeterRoad(pos)) { if (this.tryDrive(pos, destFn)) { this.addToTrafficDensityMap(blockMaps); return Micro.ROUTE_FOUND; } return Micro.NO_ROUTE_FOUND; } else { return Micro.NO_ROAD_FOUND; } } addToTrafficDensityMap(blockMaps) { let trafficDensityMap = blockMaps.trafficDensityMap; while (this._stack.length > 0) { let pos = this._stack.pop(); // Could this happen?!? if (!this._map.testBounds(pos.x, pos.y)) continue; let tileValue = this._map.getTileValue(pos.x, pos.y); if (tileValue >= Tile.ROADBASE && tileValue < Tile.POWERBASE) { // Update traffic density. let traffic = trafficDensityMap.worldGet(pos.x, pos.y); traffic += 50; traffic = Math.min(traffic, 240); trafficDensityMap.worldSet(pos.x, pos.y, traffic); // Attract traffic copter to the traffic if (traffic >= 240 && math.getRandom(5) === 0) { let sprite = this._spriteManager.getSprite(Micro.SPRITE_HELICOPTER); if (sprite !== null) { sprite.destX = ZoneUtils.worldToPix(pos.x); sprite.destY = ZoneUtils.worldToPix(pos.y); } } } } } findPerimeterRoad(pos) { for (let i = 0; i < 12; i++) { let xx = pos.x + Micro.perimX[i]; let yy = pos.y + Micro.perimY[i]; if (this._map.testBounds(xx, yy)) { if (ZoneUtils.isDriveable(this._map.getTileValue(xx, yy))) { pos.x = xx; pos.y = yy; return true; } } } return false; } tryDrive(startPos, destFn) { let dirLast = Direction.INVALID; let drivePos = new Position(startPos); /* Maximum distance to try */ for (let dist = 0; dist < Micro.MAX_TRAFFIC_DISTANCE; dist++) { let dir = this.tryGo(drivePos, dirLast); if (dir != Direction.INVALID) { drivePos.move(dir); dirLast = Direction.rotate180(dir); if (dist & 1) this._stack.push(new Position(drivePos)); if (this.driveDone(drivePos, destFn)) return true; } else { if (this._stack.length > 0) { this._stack.pop(); dist += 3; } else { return false; } } } return false; } tryGo(pos, dirLast) { let directions = []; // Find connections from current position. let dir = Direction.NORTH; let i, count = 0; for (i = 0; i < 4; i++) { if (dir != dirLast && ZoneUtils.isDriveable(this._map.getTileFromMapOrDefault(pos, dir, Tile.DIRT))) { // found a road in an allowed direction directions[i] = dir; count++; } else { directions[i] = Direction.INVALID; } dir = Direction.rotate90(dir); } if (count === 0) return Direction.INVALID; if (count === 1) { for (i = 0; i < 4; i++) { if (directions[i] != Direction.INVALID) return directions[i]; } } i = math.getRandom16() & 3; while (directions[i] === Direction.INVALID) i = i + 1 & 3; return directions[i]; } driveDone(pos, destFn) { if (pos.y > 0) { if (destFn(this._map.getTileValue(pos.x, pos.y - 1))) return true; } if (pos.x < this._map.width - 1) { if (destFn(this._map.getTileValue(pos.x + 1, pos.y))) return true; } if (pos.y < this._map.height - 1) { if (destFn(this._map.getTileValue(pos.x, pos.y + 1))) return true; } if (pos.x > 0) { if (destFn(this._map.getTileValue(pos.x - 1, pos.y))) return true; } return false; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const vulnerable = function (tile) { let tileValue = tile.getValue(); if (tileValue < Tile.RESBASE || tileValue > Tile.LASTZONE || tile.isZone()) return false; return true; }; class DisasterManager { constructor(map, spriteManager, gameLevel) { this._map = map; this._spriteManager = spriteManager; this._gameLevel = gameLevel; this._floodCount = 0; this.disastersEnabled = false; this.Dx = [0, 1, 0, -1]; this.Dy = [-1, 0, 1, 0]; // TODO enable disasters //Object.defineProperty(this, 'disastersEnabled', MiscUtils.mcd(false)); } doDisasters(census) { if (this._floodCount) this._floodCount--; // TODO Scenarios if (!this.disastersEnabled) return; if (math.getRandom(Micro.DisChance[this._gameLevel])) { switch (math.getRandom(8)) { case 0: case 1: this.setFire(); break; case 2: case 3: this.makeFlood(); break; case 4: break; case 5: this._spriteManager.makeTornado(); break; case 6: // TODO Earthquakes //this.makeEarthquake(); break; case 7: case 8: if (census.pollutionAverage > 60) this._spriteManager.makeMonster(); break; } } } setDifficulty(gameLevel) { this._gameLevel = gameLevel; } scenarioDisaster() {// TODO Scenarios } // User initiated meltdown: need to find the plant first makeMeltdown() { for (let x = 0; x < this._map.width - 1; x++) { for (let y = 0; y < this._map.height - 1; y++) { if (this._map.getTileValue(x, y) === Tile.NUCLEAR) { this.doMeltdown(x, y); return; } } } } makeEarthquake() { let strength = math.getRandom(700) + 300; this.doEarthquake(strength); EventEmitter.emitEvent(Messages.EARTHQUAKE, { x: this._map.cityCenterX, y: this._map.cityCenterY }); let i, x, y; for (i = 0; i < strength; i++) { x = math.getRandom(this._map.width - 1); y = math.getRandom(this._map.height - 1); if (vulnerable(this._map.getTile(x, y))) { if ((i & 0x3) !== 0) this._map.setTo(x, y, ZoneUtils.randomRubble());else this._map.setTo(x, y, ZoneUtils.randomFire()); } } } setFire(times = 1, zonesOnly = false) { let i, x, y, tile, lowerLimit; for (i = 0; i < times; i++) { x = math.getRandom(this._map.width - 1); y = math.getRandom(this._map.height - 1); if (!this._map.testBounds(x, y)) continue; tile = this._map.getTile(x, y); if (!tile.isZone()) { tile = tile.getValue(); lowerLimit = zonesOnly ? Tile.LHTHR : Tile.TREEBASE; if (tile > lowerLimit && tile < Tile.LASTZONE) { this._map.setTo(x, y, ZoneUtils.randomFire()); EventEmitter.emitEvent(Messages.FIRE_REPORTED, { showable: true, x: x, y: y }); return; } } } } makeCrash() { let s = this._spriteManager.getSprite(Micro.SPRITE_AIRPLANE); if (s !== null) { s.explodeSprite(); return; } let x = math.getRandom(this._map.width - 1); let y = math.getRandom(this._map.height - 1); this._spriteManager.generatePlane(x, y); s = this._spriteManager.getSprite(Micro.SPRITE_AIRPLANE); s.explodeSprite(); } makeFire() { this.setFire(40, false); } makeFlood() { let i, x, y, tileValue, j, xx, yy, tile; for (i = 0; i < 300; i++) { x = math.getRandom(this._map.width - 1); y = math.getRandom(this._map.height - 1); if (!this._map.testBounds(x, y)) continue; tileValue = this._map.getTileValue(x, y); if (tileValue > Tile.CHANNEL && tileValue <= Tile.WATER_HIGH) { for (j = 0; j < 4; j++) { xx = x + this.Dx[j]; yy = y + this.Dy[j]; if (!this._map.testBounds(xx, yy)) continue; tile = this._map.getTile(xx, yy); tileValue = tile.getValue(); if (tile === Tile.DIRT || tile.isBulldozable() && tile.isCombustible) { this._map.setTo(xx, yy, new Tiles(Tile.FLOOD)); this._floodCount = 30; EventEmitter.emitEvent(Messages.FLOODING_REPORTED, { showable: true, x: xx, y: yy }); return; } } } } } doFlood(x, y, blockMaps) { let i, xx, yy, tile, tileValue; if (this._floodCount > 0) { // Flood is not over yet for (i = 0; i < 4; i++) { if (math.getChance(7)) { xx = x + this.Dx[i]; yy = y + this.Dy[i]; if (this._map.testBounds(xx, yy)) { tile = this._map.getTile(xx, yy); tileValue = tile.getValue(); if (tile.isCombustible() || tileValue === Tile.DIRT || tileValue >= Tile.WOODS5 && tileValue < Tile.FLOOD) { if (tile.isZone()) ZoneUtils.fireZone(this.map, xx, yy, blockMaps); this._map.setTile(xx, yy, Tile.FLOOD + math.getRandom(2), 0); //this._map.setTo(xx, yy, new Tiles(Tile.FLOOD + math.getRandom(2))); } } } } } else { if (math.getChance(15)) this._map.setTile(x, y, Tile.DIRT, 0); } } doMeltdown(x, y) { this._spriteManager.makeExplosion(x - 1, y - 1); this._spriteManager.makeExplosion(x - 1, y + 2); this._spriteManager.makeExplosion(x + 2, y - 1); this._spriteManager.makeExplosion(x + 2, y + 2); let i, dY, dX, tile; // Whole power plant is at fire for (dX = x - 1; dX < x + 3; dX++) { for (dY = y - 1; dY < y + 3; dY++) { this._map.setTo(dX, dY, ZoneUtils.randomFire()); } } // Add lots of radiation tiles around the plant for (i = 0; i < 200; i++) { dX = x - 20 + math.getRandom(40); dY = y - 15 + math.getRandom(30); if (!this._map.testBounds(dX, dY)) continue; tile = this._map.getTile(dX, dY); if (tile.isZone()) continue; if (tile.isCombustible() || tile.getValue() === Tile.DIRT) this._map.setTile(dX, dY, Tile.RADTILE, 0); //this._map.setTo(dX, dY, new Tiles(Tile.RADTILE)); } // Report disaster to the user EventEmitter.emitEvent(Messages.NUCLEAR_MELTDOWN, { showable: true, x: x, y: y }); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const freeZone = [0, 3, 6, 1, 4, 7, 2, 5, 8]; const Residential = { registerHandlers: function (mapScanner, repairManager) { mapScanner.addAction(ZoneUtils.isResidentialZone, Residential.residentialFound); mapScanner.addAction(ZoneUtils.HOSPITAL, Residential.hospitalFound); repairManager.addAction(Tile.HOSPITAL, 15, 3); }, // Residential tiles have 'populations' of 16, 24, 32 or 40 // and value from 0 to 3. The tiles are laid out in // increasing order of land value, cycling through // each population value placeResidential: function (map, x, y, population, lpValue, zonePower) { let centreTile = (lpValue * 4 + population) * 9 + Tile.RZB; ZoneUtils.putZone(map, x, y, centreTile, zonePower); }, // Look for housing in the adjacent 8 tiles getFreeZonePopulation: function (map, x, y, tileValue) { let count = 0, xx, yy; for (xx = x - 1; xx <= x + 1; xx++) { for (yy = y - 1; yy <= y + 1; yy++) { if (xx === x && yy === y) continue; tileValue = map.getTileValue(xx, yy); if (tileValue >= Tile.LHTHR && tileValue <= Tile.HHTHR) count += 1; } } return count; }, getZonePopulation: function (map, x, y, tileValue) { //if ( tileValue.isTile ) tileValue = new Tiles().getValue(); if (tileValue === Tile.FREEZ) return Residential.getFreeZonePopulation(map, x, y, tileValue); let populationIndex = Math.floor((tileValue - Tile.RZB) / 9) % 4 + 1; return populationIndex * 8 + 16; }, // Assess a tile for suitability for a house. Prefer tiles near roads evalLot: function (map, x, y) { let xDelta = [0, 1, 0, -1]; let yDelta = [-1, 0, 1, 0]; if (!map.testBounds(x, y)) return -1; let tileValue = map.getTileValue(x, y); if (tileValue < Tile.RESBASE || tileValue > Tile.RESBASE + 8) return -1; let score = 1, i, edgeX, edgeY; for (i = 0; i < 4; i++) { edgeX = x + xDelta[i]; edgeY = y + yDelta[i]; if (edgeX < 0 || edgeX >= map.width || edgeY < 0 || edgeY >= map.height) continue; tileValue = map.getTileValue(edgeX, edgeY); if (tileValue !== Tile.DIRT && tileValue <= Tile.LASTROAD) score += 1; } return score; }, buildHouse: function (map, x, y, lpValue) { let best = 0; let bestScore = 0; // Deliberately ordered so that the centre tile is at index 0 let xDelta = [0, -1, 0, 1, -1, 1, -1, 0, 1]; let yDelta = [0, -1, -1, -1, 0, 0, 1, 1, 1]; let i, xx, yy, score; for (i = 0; i < 9; i++) { xx = x + xDelta[i]; yy = y + yDelta[i]; score = Residential.evalLot(map, xx, yy); if (score > bestScore) { bestScore = score; best = i; } else if (score === bestScore && math.getChance(7)) { // Ensures we don't always select the same position when we // have a choice best = i; } } if (best > 0 && map.testBounds(x + xDelta[best], y + yDelta[best])) map.setTile(x + xDelta[best], y + yDelta[best], Tile.HOUSE + math.getRandom(2) + lpValue * 3, Tile.BLBNCNBIT); //map.setTo(x + xDelta[best], y + yDelta[best], new Tiles(Tile.HOUSE + math.getRandom(2) + lpValue * 3, Tile.BLBNCNBIT)); //map.setTile(x + xDelta[best], y + yDelta[best], new Tiles(Tile.HOUSE + math.getRandom(2) + lpValue * 3, Tile.BLBNCNBIT)); }, growZone: function (map, x, y, blockMaps, population, lpValue, zonePower) { let pollution = blockMaps.pollutionDensityMap.worldGet(x, y); // Cough! Too polluted noone wants to move here! if (pollution > 128) return; let tileValue = map.getTileValue(x, y); if (tileValue === Tile.FREEZ) { if (population < 8) { // Zone capacity not yet reached: build another house Residential.buildHouse(map, x, y, lpValue); ZoneUtils.incRateOfGrowth(blockMaps, x, y, 1); } else if (blockMaps.populationDensityMap.worldGet(x, y) > 64) { // There is local demand for higher density housing Residential.placeResidential(map, x, y, 0, lpValue, zonePower); ZoneUtils.incRateOfGrowth(blockMaps, x, y, 8); } return; } if (population < 40) { // Zone population not yet maxed out Residential.placeResidential(map, x, y, Math.floor(population / 8) - 1, lpValue, zonePower); ZoneUtils.incRateOfGrowth(blockMaps, x, y, 8); } }, degradeZone: function (map, x, y, blockMaps, population, lpValue, zonePower) { let xx, yy; if (population === 0) return; if (population > 16) { // Degrade to a lower density block Residential.placeResidential(map, x, y, Math.floor((population - 24) / 8), lpValue, zonePower); ZoneUtils.incRateOfGrowth(blockMaps, x, y, -8); return; } if (population === 16) { // Already at lowest density: degrade to 8 individual houses map.setTo(x, y, new Tiles(Tile.FREEZ, Tile.BLBNCNBIT | Tile.ZONEBIT)); for (yy = y - 1; yy <= y + 1; yy++) { for (xx = x - 1; xx <= x + 1; xx++) { if (xx === x && yy === y) continue; map.setTile(x, y, Tile.LHTHR + lpValue + math.getRandom(2), Tile.BLBNCNBIT); //map.setTo(x, y, new Tiles(Tile.LHTHR + lpValue + math.getRandom(2), Tile.BLBNCNBIT)); } } ZoneUtils.incRateOfGrowth(blockMaps, x, y, -8); return; } // Already down to individual houses. Remove one let i = 0; ZoneUtils.incRateOfGrowth(blockMaps, x, y, -1); for (xx = x - 1; xx <= x + 1; xx++) { for (yy = y - 1; yy <= y + 1; yy++) { let currentValue = map.getTileValue(xx, yy); if (currentValue >= Tile.LHTHR && currentValue <= Tile.HHTHR) { // We've found a house. Replace it with the normal free zone tile map.setTile(xx, yy, freeZone[i] + Tile.RESBASE, Tile.BLBNCNBIT); //map.setTo(xx, yy, new Tiles(freeZone[i] + Tile.RESBASE, Tile.BLBNCNBIT)); return; } i += 1; } } }, // Returns a score for the zone in the range -3000 - 3000 evalResidential: function (blockMaps, x, y, traffic) { if (traffic === Micro.NO_ROAD_FOUND) return -3000; let landValue = blockMaps.landValueMap.worldGet(x, y); landValue -= blockMaps.pollutionDensityMap.worldGet(x, y); if (landValue < 0) landValue = 0;else landValue = Math.min(landValue * 32, 6000); return landValue - 3000; }, residentialFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; // If we choose to grow this zone, we will fill it with an index in the range 0-3 reflecting the land value and // pollution scores (higher is better). This is then used to select the variant to build let lpValue; // Notify the census simData.census.resZonePop += 1; // Also, notify the census of our population let tileValue = map.getTileValue(x, y); let population = Residential.getZonePopulation(map, x, y, tileValue); simData.census.resPop += population; let zonePower = map.getTile(x, y).isPowered(); let trafficOK = Micro.ROUTE_FOUND; // Occasionally check to see if the zone is connected to the road network. The chance of this happening increases // as the zone's population increases. Note: we will never execute this conditional if the zone is empty, as zero // will never be be bigger than any of the values Random will generate if (population > math.getRandom(35)) { // Is there a route from this zone to a commercial zone? trafficOK = simData.traffic.makeTraffic(x, y, simData.blockMaps, ZoneUtils.isCommercial); // If we're not connected to the road network, then going shopping will be a pain. Move out. if (trafficOK === Micro.NO_ROAD_FOUND) { lpValue = ZoneUtils.getLandPollutionValue(simData.blockMaps, x, y); Residential.degradeZone(map, x, y, simData.blockMaps, population, lpValue, zonePower); return; } } // Sometimes we will randomly choose to assess this block. However, always assess it if it's empty or contains only single houses. if (tileValue === Tile.FREEZ || math.getChance(7)) { // First, score the individual zone. This is a value in the range -3000 to 3000 // Then take into account global demand for housing. let locationScore = Residential.evalResidential(simData.blockMaps, x, y, trafficOK); let zoneScore = simData.valves.resValve + locationScore; // Naturally unpowered zones should be penalized if (!zonePower) zoneScore = -500; // The residential demand valve has range -2000 to 2000, so taking into account the "no traffic" and // "no power" modifiers above, zoneScore must lie in the range -5500 - 5000. // Now, observe that if there are no roads we will never take this branch, as zoneScore will equal -3000. // Given the comment above about ranges for zoneScore, zoneScore - 26380, will be in the range -26729 to -20880. // getRandom16() has a range of 65536 possible numbers, in the range -32768 to 32767. // Of those, 9.2% will always be below zoneScore and hence will always take this branch and trigger zone growth. // 81.8% of them are above -20880, so nearly 82% of the time, we will never take this branch. // Thus, there's approximately a 9% chance that the value will be in the range, and we *might* grow. //if (trafficOK && (zoneScore > -350) && ((zoneScore - 26380) > math.getRandom16Signed())) { if (zoneScore > -350 && zoneScore - 26380 > math.getRandom16Signed()) { // If this zone is empty, and residential demand is strong, we might make a hospital //if (population === 0 && ((math.getRandom16() & 3) === 0)) { if (population === 0 && math.getChance(3)) { Residential.makeHospital(map, x, y, simData, zonePower); return; } // Get an index in the range 0-3 scoring the land desirability and pollution, and grow the zone to the next // population rank lpValue = ZoneUtils.getLandPollutionValue(simData.blockMaps, x, y); Residential.growZone(map, x, y, simData.blockMaps, population, lpValue, zonePower); return; } // Again, given the above, zoneScore + 26380 must lie in the range 20880 - 26030. // There is a 10.2% chance of getRandom16() always yielding a number > 27994 which would take this branch. // There is a 89.7% chance of the number being below 20880 thus never triggering this branch, which leaves a // 0.1% chance of this branch being conditional on zoneScore. if (zoneScore < 350 && zoneScore + 26380 < math.getRandom16Signed()) { // Get an index in the range 0-3 scoring the land desirability and pollution, and degrade to the next // lower ranked zone lpValue = ZoneUtils.getLandPollutionValue(simData.blockMaps, x, y); Residential.degradeZone(map, x, y, simData.blockMaps, population, lpValue, zonePower); } } }, makeHospital: function (map, x, y, simData, zonePower) { if (!simData) simData = Micro.simData; // We only build a hospital if the population requires it if (simData.census.needHospital > 0) { ZoneUtils.putZone(map, x, y, Tile.HOSPITAL, zonePower); simData.census.needHospital = 0; return; } }, hospitalFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.hospitalPop += 1; // Degrade to an empty zone if a hospital is no longer sustainable if (simData.census.needHospital === -1) { if (math.getRandom(20) === 0) ZoneUtils.putZone(map, x, y, Tile.FREEZ, map.getTile(x, y).isPowered()); } } }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const Commercial = { registerHandlers: function (mapScanner, repairManager) { mapScanner.addAction(ZoneUtils.isCommercialZone, Commercial.commercialFound); }, // Commercial tiles have 'populations' from 1 to 5, // and value from 0 to 3. The tiles are laid out in // increasing order of land value, cycling through // each population value getZonePopulation: function (map, x, y, tileValue) { //if (tileValue.isTile ) tileValue = new Tiles().getValue(); //COMCLEAR) if (tileValue === Tile.COMCLR) return 0; return Math.floor((tileValue - Tile.CZB) / 9) % 5 + 1; }, // Takes a map and coordinates, a population category in the range 1-5, a value category in the range 0-3, and places // the appropriate industrial zone on the map placeCommercial: function (map, x, y, population, lpValue, zonePower) { var centreTile = (lpValue * 5 + population) * 9 + Tile.CZB; ZoneUtils.putZone(map, x, y, centreTile, zonePower); }, growZone: function (map, x, y, blockMaps, population, lpValue, zonePower) { // landValueMap contains values in the range 0-250, representing the desirability of the land. // Thus, after shifting, landValue will be in the range 0-7. var landValue = blockMaps.landValueMap.worldGet(x, y); landValue = landValue >> 5; if (population > landValue) return; // This zone is desirable, and seemingly not to crowded. Switch to the next category of zone. if (population < 5) { Commercial.placeCommercial(map, x, y, population, lpValue, zonePower); ZoneUtils.incRateOfGrowth(blockMaps, x, y, 8); } }, degradeZone: function (map, x, y, blockMaps, populationCategory, lpCategory, zonePower) { // Note that we special case empty zones here, rather than having to check population value on every // call to placeIndustrial (which we anticipate will be called more often) if (populationCategory > 1) { Commercial.placeCommercial(map, x, y, populationCategory - 2, lpCategory, zonePower); } else { ZoneUtils.putZone(map, x, y, Tile.COMCLR, zonePower); } ZoneUtils.incRateOfGrowth(blockMaps, x, y, -8); }, // Called by the map scanner when it finds the centre of an commercial zone commercialFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; // lpValue will be filled if we actually decide to trigger growth/decay. It will be an index of the land/pollution // value in the range 0-3 var lpValue; // Notify the census simData.census.comZonePop += 1; // Calculate the population level for this tile, and add to census var tileValue = map.getTileValue(x, y); var population = Commercial.getZonePopulation(map, x, y, tileValue); simData.census.comPop += population; var zonePower = map.getTile(x, y).isPowered(); // Occasionally check to see if the zone is connected to the transport network (the chance of this happening // increases as the population increases). Growth naturally stalls if consumers cannot reach the shops. // Note in particular, we will never take this branch if the zone is empty. var trafficOK = Micro.ROUTE_FOUND; if (population > math.getRandom(5)) { // Try to find a route from here to an industrial zone trafficOK = simData.traffic.makeTraffic(x, y, simData.blockMaps, ZoneUtils.isIndustrial); // Trigger outward migration if not connected to road network if (trafficOK === Micro.NO_ROAD_FOUND) { lpValue = ZoneUtils.getLandPollutionValue(simData.blockMaps, x, y); Commercial.degradeZone(map, x, y, simData.blockMaps, population, lpValue, zonePower); return; } } // Occasionally assess and perhaps modify the tile if (math.getChance(7)) { var locationScore = trafficOK === Micro.NO_ROAD_FOUND ? -3000 : simData.blockMaps.cityCentreDistScoreMap.worldGet(x, y); var zoneScore = simData.valves.comValve + locationScore; // Unpowered zones should of course be penalized if (!zonePower) zoneScore = -500; // The commercial demand valve has range -1500 to 1500, so taking into account the "no traffic" and // "no power" modifiers above, zoneScore must lie in the range -5064 - 1564. (The comRateMap, which scores // commercial neighbourhoods based on their distance from the city centre, has range -64 to 64). // First: observe that if there are no roads we will never take this branch, as zoneScore will be <= -3000. // Given the comment above about ranges for zoneScore, zoneScore - 26380, will be in the range -26729 to -24816. // getRandom16() has a range of 65536 possible numbers, in the range -32768 to 32767. // Of those, 9.2% will always be below zoneScore and hence will always take this branch and trigger zone growth. // 87.8% of them are above -24816, so nearly 88% of the time, we will never take this branch. // Thus, there's approximately a 3% chance that the value will be in the range, and we *might* grow. // This has the nice effect of not preventing an individual unit from growing even if overall demand has collapsed // (the business itself might still be growing. if (zonePower && zoneScore > -350 && zoneScore - 26380 > math.getRandom16Signed()) { lpValue = ZoneUtils.getLandPollutionValue(simData.blockMaps, x, y); Commercial.growZone(map, x, y, simData.blockMaps, population, lpValue, zonePower); return; } // Again, given the above, zoneScore + 26380 must lie in the range 21316 - 27944. // There is a 7.3% chance of getRandom16() always yielding a number > 27994 which would take this branch. // There is a 82.5% chance of the number being below 21316 thus never triggering this branch, which leaves a // 10.1% chance of this branch being conditional on zoneScore. if (zoneScore < 350 && zoneScore + 26380 < math.getRandom16Signed()) { lpValue = ZoneUtils.getLandPollutionValue(simData.blockMaps, x, y); Commercial.degradeZone(map, x, y, simData.blockMaps, population, lpValue, zonePower); } } } }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const animated = [true, false, true, true, false, false, true, true]; const xDelta = [-1, 0, 1, 0, 0, 0, 0, 1]; const yDelta = [-1, 0, -1, -1, 0, 0, -1, -1]; const Industrial = { registerHandlers: function (mapScanner, repairManager) { mapScanner.addAction(ZoneUtils.isIndustrialZone, Industrial.industrialFound); }, // Industrial tiles have 'populations' from 1 to 4, // and value from 0 to 3. The tiles are laid out in // increasing order of land value, cycling through // each population value getZonePopulation: function (map, x, y, tileValue) { if (tileValue === Tile.INDCLR) return 0; return Math.floor((tileValue - Tile.IZB) / 9) % 4 + 1; }, placeIndustrial: function (map, x, y, populationCategory, valueCategory, zonePower) { var centreTile = (valueCategory * 4 + populationCategory) * 9 + Tile.IZB; ZoneUtils.putZone(map, x, y, centreTile, zonePower); }, growZone: function (map, x, y, blockMaps, population, valueCategory, zonePower) { // Switch to the next category of zone if (population < 4) { Industrial.placeIndustrial(map, x, y, population, valueCategory, zonePower); ZoneUtils.incRateOfGrowth(blockMaps, x, y, 8); } }, degradeZone: function (map, x, y, blockMaps, populationCategory, valueCategory, zonePower) { // Note that we special case empty zones here, rather than having to check population value on every // call to placeIndustrial (which we anticipate will be called more often) if (populationCategory > 1) Industrial.placeIndustrial(map, x, y, populationCategory - 2, valueCategory, zonePower);else ZoneUtils.putZone(map, x, y, Tile.INDCLR, zonePower); ZoneUtils.incRateOfGrowth(blockMaps, x, y, -8); }, // Takes a map and coordinates, the tile value of the centre of the zone, and a boolean indicating whether // the zone has power, and sets or unsets the animation bit in the appropriate part of the zone setAnimation: function (map, x, y, tileValue, isPowered) { if (tileValue < Tile.IZB) return; // There are only 7 different types of populated industrial zones. // As tileValue - IZB will never be 8x9 or more away from IZB, we // can shift right by 3, and get the same effect as dividing by 9 var i = tileValue - Tile.IZB >> 3; if (animated[i] && isPowered) { map.addTileFlags(x + xDelta[i], y + yDelta[i], Tile.ASCBIT); } else { map.addTileFlags(x + xDelta[i], y + yDelta[i], Tile.BNCNBIT); map.removeTileFlags(x + xDelta[i], y + yDelta[i], Tile.ANIMBIT); } }, industrialFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.indZonePop += 1; // Calculate the population level for this tile, and add to census var tileValue = map.getTileValue(x, y); var population = Industrial.getZonePopulation(map, x, y, tileValue); simData.census.indPop += population; // Set animation bit if appropriate var zonePower = map.getTile(x, y).isPowered(); if (!simData.is3D) Industrial.setAnimation(map, x, y, tileValue, zonePower); // Occasionally check to see if the zone is connected to the transport network (the chance of this happening // increases as the population increases). Growth naturally stalls if workers cannot reach the factories. // Note in particular, we will never take this branch if the zone is empty. var trafficOK = Micro.ROUTE_FOUND; if (population > math.getRandom(5)) { // Try to find a route from here to a residential zone trafficOK = simData.traffic.makeTraffic(x, y, simData.blockMaps, ZoneUtils.isResidential); // Trigger outward migration if not connected to road network (unless the zone is already empty) if (trafficOK === Micro.NO_ROAD_FOUND) { var newValue = math.getRandom16() & 1; Industrial.degradeZone(map, x, y, simData.blockMaps, population, newValue, zonePower); return; } } // Occasionally assess and perhaps modify the tile if (math.getChance(7)) { var zoneScore = simData.valves.indValve + (trafficOK === Micro.NO_ROAD_FOUND ? -1000 : 0); // Unpowered zones should of course be penalized if (!zonePower) zoneScore = -500; // The industrial demand valve has range -1500 to 1500, so taking into account the "no traffic" and // "no power" modifiers above, zoneScore must lie in the range -3000 - 1500 // First: observe that if there are no roads we will never take this branch, as zoneScore will be <= -1000. // Given the comment above about ranges for zoneScore, zoneScore - 26380, will be in the range -26729 to -24880. // getRandom16() has a range of 65536 possible numbers, in the range -32768 to 32767. // Of those, 9.2% will always be below zoneScore and hence will always take this branch and trigger zone growth. // 87.9% of them are above -24880, so nearly 88% of the time, we will never take this branch. // Thus, there's approximately a 2.9% chance that the value will be in the range, and we *might* grow. // This has the nice effect of not preventing an individual unit from growing even if overall demand has collapsed // (the business itself might still be growing. if (zoneScore > -350 && zoneScore - 26380 > math.getRandom16Signed()) { Industrial.growZone(map, x, y, simData.blockMaps, population, math.getRandom16() & 1, zonePower); return; } // Again, given the above, zoneScore + 26380 must lie in the range 23380 - 27880. // There is a 7.4% chance of getRandom16() always yielding a number > 27880 which would take this branch. // There is a 85.6% chance of the number being below 23380 thus never triggering this branch, which leaves a // 9% chance of this branch being conditional on zoneScore. if (zoneScore < 350 && zoneScore + 26380 < math.getRandom16Signed()) Industrial.degradeZone(map, x, y, simData.blockMaps, population, math.getRandom16() & 1, zonePower); } } }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const verticalDeltaX = [0, 1, 0, 0, 0, 0, 1]; const verticalDeltaY = [-2, -2, -1, 0, 1, 2, 2]; const horizontalDeltaX = [-2, 2, -2, -1, 0, 1, 2]; const horizontalDeltaY = [-1, -1, 0, 0, 0, 0, 0]; const openVertical = [Tile.VBRDG0, Tile.VBRDG1, Tile.RIVER, Tile.BRWV, Tile.RIVER, Tile.VBRDG2, Tile.VBRDG3]; const closeVertical = [Tile.VBRIDGE, Tile.RIVER, Tile.VBRIDGE, Tile.VBRIDGE, Tile.VBRIDGE, Tile.VBRIDGE, Tile.RIVER]; const openHorizontal = [Tile.HBRDG1, Tile.HBRDG3, Tile.HBRDG0, Tile.RIVER, Tile.BRWH, Tile.RIVER, Tile.HBRDG2]; const closeHorizontal = [Tile.RIVER, Tile.RIVER, Tile.HBRIDGE, Tile.HBRIDGE, Tile.HBRIDGE, Tile.HBRIDGE, Tile.HBRIDGE]; /* const openVertical = [ Tile.VBRDG0 | Tile.BULLBIT, Tile.VBRDG1 | Tile.BULLBIT, Tile.RIVER, Tile.BRWV | Tile.BULLBIT, Tile.RIVER, Tile.VBRDG2 | Tile.BULLBIT, Tile.VBRDG3 | Tile.BULLBIT ]; const closeVertical = [ Tile.VBRIDGE | Tile.BULLBIT, Tile.RIVER, Tile.VBRIDGE | Tile.BULLBIT, Tile.VBRIDGE | Tile.BULLBIT, Tile.VBRIDGE | Tile.BULLBIT, Tile.VBRIDGE | Tile.BULLBIT, Tile.RIVER ]; const openHorizontal = [ Tile.HBRDG1 | Tile.BULLBIT, Tile.HBRDG3 | Tile.BULLBIT, Tile.HBRDG0 | Tile.BULLBIT, Tile.RIVER, Tile.BRWH | Tile.BULLBIT, Tile.RIVER, Tile.HBRDG2 | Tile.BULLBIT ]; const closeHorizontal = [ Tile.RIVER, Tile.RIVER, Tile.HBRIDGE | Tile.BULLBIT, Tile.HBRIDGE | Tile.BULLBIT, Tile.HBRIDGE | Tile.BULLBIT, Tile.HBRIDGE | Tile.BULLBIT, Tile.HBRIDGE | Tile.BULLBIT ];*/ const densityTable = [Tile.ROADBASE, Tile.LTRFBASE, Tile.HTRFBASE]; const Road = { registerHandlers: function (mapScanner, repairManager) { mapScanner.addAction(ZoneUtils.isRoad, Road.roadFound); }, openBridge: function (map, origX, origY, xDelta, yDelta, oldTiles, newTiles) { let i, x, y; for (i = 0; i < 7; i++) { x = origX + xDelta[i]; y = origY + yDelta[i]; if (map.testBounds(x, y)) { if (map.getTileValue(x, y) === (oldTiles[i] & Tile.BIT_MASK)) map.setTileValue(x, y, newTiles[i]); } } }, closeBridge: function (map, origX, origY, xDelta, yDelta, oldTiles, newTiles) { let i, x, y, tileValue; for (i = 0; i < 7; i++) { x = origX + xDelta[i]; y = origY + yDelta[i]; if (map.testBounds(x, y)) { tileValue = map.getTileValue(x, y); if (tileValue === Tile.CHANNEL || (tileValue & 15) === (oldTiles[i] & 15)) map.setTileValue(x, y, newTiles[i]); } } }, doBridge: function (map, x, y, currentTile, simData) { //console.log( 'make bridge !!' ) if (!simData) simData = Micro.simData; if (currentTile === Tile.BRWV) { // We have an open vertical bridge. Possibly close it. if (math.getChance(3) && simData.spriteManager.getBoatDistance(x, y) > 340) Road.closeBridge(map, x, y, verticalDeltaX, verticalDeltaY, openVertical, closeVertical); return true; } if (currentTile == Tile.BRWH) { // We have an open horizontal bridge. Possibly close it. if (math.getChance(3) && simData.spriteManager.getBoatDistance(x, y) > 340) Road.closeBridge(map, x, y, horizontalDeltaX, horizontalDeltaY, openHorizontal, closeHorizontal); return true; } if (simData.spriteManager.getBoatDistance(x, y) < 300 || math.getChance(7)) { if (currentTile & 1) { if (x < map.width - 1) { if (map.getTileValue(x + 1, y) === Tile.CHANNEL) { // We have a closed vertical bridge. Open it. Road.openBridge(map, x, y, verticalDeltaX, verticalDeltaY, closeVertical, openVertical); return true; } } return false; } else { if (y > 0) { if (map.getTileValue(x, y - 1) === Tile.CHANNEL) { // We have a closed horizontal bridge. Open it. Road.openBridge(map, x, y, horizontalDeltaX, horizontalDeltaY, closeHorizontal, openHorizontal); return true; } } } } return false; }, roadFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.roadTotal += 1; let currentTile = map.getTile(x, y); let tileValue = currentTile.getValue(); if (simData.budget.shouldDegradeRoad()) { if (math.getChance(511)) { //currentTile = map.getTile(x, y); // Don't degrade tiles with power lines if (!currentTile.isConductive()) { if (simData.budget.roadEffect < (math.getRandom16() & 31)) { //let mapValue = currentTile.getValue(); // Replace bridge tiles with water, otherwise rubble if ((tileValue & 15) < 2 || (tileValue & 15) === 15) map.setTile(x, y, Tile.RIVER);else map.setTo(x, y, ZoneUtils.randomRubble()); return; } } } } // Bridges are not combustible if (!currentTile.isCombustible()) { // The comment in the original Micropolis code states bridges count for 4 // However, with the increment above, it's actually 5. Bug? simData.census.roadTotal += 4; //if ( Road.doBridge(map, x, y, tileValue, simData)) return; } // Examine traffic density, and modify tile to represent last scanned traffic // density let density = 0; if (tileValue < Tile.LTRFBASE) { density = 0; } else if (tileValue < Tile.HTRFBASE) { density = 1; } else { // Heavy traffic counts as two tiles with regards to upkeep cost // Note, if this is heavy traffic on a bridge, and it wasn't handled above, // it actually counts for 7 road tiles simData.census.roadTotal += 1; density = 2; } // Force currentDensity in range 0-3 (trafficDensityMap values are capped at 240) let currentDensity = simData.blockMaps.trafficDensityMap.worldGet(x, y) >> 6; // Force currentDensity in range 0-3 (trafficDensityMap values are capped at 240) if (currentDensity > 1) currentDensity -= 1; if (currentDensity === density) return; let newValue = (tileValue - Tile.ROADBASE & 15) + densityTable[currentDensity]; // Preserve all bits except animation let newFlags = currentTile.getFlags() & ~Tile.ANIMBIT; if (currentDensity > 0) newFlags |= Tile.ANIMBIT; map.setTo(x, y, new Tiles(newValue, newFlags)); } }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const Transport = { registerHandlers: function (mapScanner, repairManager) { mapScanner.addAction(ZoneUtils.isRail, Transport.railFound); mapScanner.addAction(Tile.PORT, Transport.portFound); mapScanner.addAction(Tile.AIRPORT, Transport.airportFound); repairManager.addAction(Tile.PORT, 15, 4); repairManager.addAction(Tile.AIRPORT, 7, 6); }, railFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.railTotal += 1; simData.spriteManager.generateTrain(simData.census, x, y); if (simData.budget.shouldDegradeRoad()) { if (math.getChance(511)) { let currentTile = map.getTile(x, y); // Don't degrade tiles with power lines if (currentTile.isConductive()) return; if (simData.budget.roadEffect < (math.getRandom16() & 31)) { let mapValue = currentTile.getValue(); // Replace bridge tiles with water, otherwise rubble if (mapValue < Tile.RAILBASE + 2) map.setTile(x, y, Tile.RIVER, 0);else map.setTo(x, y, ZoneUtils.randomRubble()); } } } }, airportFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.airportPop += 1; let tile = map.getTile(x, y); if (tile.isPowered()) { if (map.getTileValue(x + 1, y - 1) === Tile.RADAR) map.setTile(x + 1, y - 1, Tile.RADAR0, Tile.CONDBIT | Tile.ANIMBIT | Tile.BURNBIT); if (math.getRandom(5) === 0) { simData.spriteManager.generatePlane(x, y); return; } if (math.getRandom(12) === 0) simData.spriteManager.generateCopter(x, y); } else { map.setTile(x + 1, y - 1, Tile.RADAR, Tile.CONDBIT | Tile.BURNBIT); } }, portFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.seaportPop += 1; let tile = map.getTile(x, y); if (tile.isPowered() && simData.spriteManager.getSprite(Micro.SPRITE_SHIP) === null) simData.spriteManager.generateShip(); } }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const handleService = function (censusStat, budgetEffect, blockMap) { return function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census[censusStat] += 1; var effect = simData.budget[budgetEffect]; var isPowered = map.getTile(x, y).isPowered(); // Unpowered buildings are half as effective if (!isPowered) effect = Math.floor(effect / 2); var pos = new Position(x, y); var connectedToRoads = simData.traffic.findPerimeterRoad(pos); if (!connectedToRoads) effect = Math.floor(effect / 2); var currentEffect = simData.blockMaps[blockMap].worldGet(x, y); currentEffect += effect; simData.blockMaps[blockMap].worldSet(x, y, currentEffect); }; }; const EmergencyServices = { registerHandlers: function (mapScanner, repairManager) { mapScanner.addAction(Tile.POLICESTATION, EmergencyServices.policeStationFound); mapScanner.addAction(Tile.FIRESTATION, EmergencyServices.fireStationFound); }, policeStationFound: handleService('policeStationPop', 'policeEffect', 'policeStationMap'), fireStationFound: handleService('fireStationPop', 'fireEffect', 'fireStationMap') }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const xDelta$1 = [-1, 0, 1, 0]; const yDelta$1 = [0, -1, 0, 1]; const MiscTiles = { registerHandlers: function (mapScanner, repairManager) { mapScanner.addAction(ZoneUtils.isFire, MiscTiles.fireFound, true); mapScanner.addAction(Tile.RADTILE, MiscTiles.radiationFound, true); mapScanner.addAction(ZoneUtils.isFlood, MiscTiles.floodFound, true); //mapScanner.addAction(ZoneUtils.isManualExplosion, MiscTiles.explosionFound, true); }, fireFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.firePop += 1; if ((math.getRandom16() & 3) !== 0) return; let i, xTem, yTem, tile; // Try to set neighbouring tiles on fire as well for (i = 0; i < 4; i++) { if (math.getChance(7)) { xTem = x + xDelta$1[i]; yTem = y + yDelta$1[i]; if (map.testBounds(xTem, yTem)) { tile = map.getTile(x, y); if (!tile.isCombustible()) continue; if (tile.isZone()) { // Neighbour is a ione and burnable ZoneUtils.fireZone(map, x, y, simData.blockMaps); // Industrial zones etc really go boom if (tile.getValue() > Tile.IZB) simData.spriteManager.makeExplosionAt(x, y); } map.setTo(ZoneUtils.randomFire()); } } } // Compute likelyhood of fire running out of fuel let rate = 10; // Likelyhood of extinguishing (bigger means less chance) i = simData.blockMaps.fireStationEffectMap.worldGet(x, y); if (i > 100) rate = 1;else if (i > 20) rate = 2;else if (i > 0) rate = 3; // Decide whether to put out the fire. if (math.getRandom(rate) === 0) map.setTo(x, y, ZoneUtils.randomRubble()); }, radiationFound: function (map, x, y, simData) { if (math.getChance(4095)) map.setTile(x, y, Tile.DIRT, 0); }, floodFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.disasterManager.doFlood(x, y, simData.blockMaps); } /*explosionFound: function ( map, x, y, simData ) { if(!simData) simData = Micro.simData let tileValue = map.getTileValue(x, y); map.setTo(x, y, ZoneUtils.randomRubble()); return; }*/ }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ const Stadia = { registerHandlers: function (mapScanner, repairManager) { mapScanner.addAction(Tile.STADIUM, Stadia.emptyStadiumFound); mapScanner.addAction(Tile.FULLSTADIUM, Stadia.fullStadiumFound); repairManager.addAction(Tile.STADIUM, 15, 4); }, emptyStadiumFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.stadiumPop += 1; if (map.getTile(x, y).isPowered()) { // Occasionally start the big game if ((simData.cityTime + x + y & 31) === 0) { map.putZone(x, y, Tile.FULLSTADIUM, 4); map.addTileFlags(x, y, Tile.POWERBIT); map.setTo(x + 1, y, new Tiles(Tile.FOOTBALLGAME1, Tile.ANIMBIT)); map.setTo(x + 1, y + 1, new Tiles(Tile.FOOTBALLGAME2, Tile.ANIMBIT)); } } }, fullStadiumFound: function (map, x, y, simData) { if (!simData) simData = Micro.simData; simData.census.stadiumPop += 1; let isPowered = map.getTile(x, y).isPowered(); if ((simData.cityTime + x + y & 7) === 0) { map.putZone(x, y, Tile.STADIUM, 4); if (isPowered) map.addTileFlags(x, y, Tile.POWERBIT); } } }; /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * * switch to ES6 by lo-th * */ class MapUtils { // Smooth the map src into dest. The way in which the map is smoothed depends on the value of smoothStyle. // The meanings are as follows: // // SMOOTH_NEIGHBOURS_THEN_BLOCK // ============================ // For each square in src, sum the values of its immediate neighbours, and take the average, then take the average of // that result and the square's value. This result is the new value of the square in dest. // // SMOOTH_ALL_THEN_CLAMP // ===================== // For each square in src, sum the values of that square and it's four immediate neighbours, and take an average // rounding down. Clamp the resulting value in the range 0-255. This clamped value is the square's new value in dest. static smoothMap(src, dest, smoothStyle) { let x = src.width, y, edges; while (x--) { y = src.height; while (y--) { edges = 0; if (x > 0) edges += src.get(x - 1, y); if (x < src.width - 1) edges += src.get(x + 1, y); if (y > 0) edges += src.get(x, y - 1); if (y < src.height - 1) edges += src.get(x, y + 1); if (smoothStyle === Micro.SMOOTH_NEIGHBOURS_THEN_BLOCK) { edges = src.get(x, y) + Math.floor(edges / 4); dest.set(x, y, Math.floor(edges / 2)); } else { edges = edges + src.get(x, y) >> 2; if (edges > 255) edges = 255; dest.set(x, y, edges); } } } } // Over time, the rate of growth of a neighbourhood should trend towards zero (stable) static neutraliseRateOfGrowthMap(blockMaps) { let bm = blockMaps.rateOfGrowthMap; let x = bm.width, y, value; while (x--) { y = bm.height; while (y--) { value = bm.get(x, y); if (value !== 0) { if (value > 0) value--;else value++; value = math.clamp(value, -200, 200); bm.set(x, y, value); } } } } // Over time, traffic density should ease. static neutraliseTrafficMap(blockMaps) { let bm = blockMaps.trafficDensityMap; let x = bm.width, y, value; while (x--) { y = bm.height; while (y--) { value = bm.get(x, y); if (value !== 0) { if (value <= 24) value = 0;else if (value > 200) value = value - 34;else value = value - 24; bm.set(x, y, value); } } } } // Given a tileValue, score it on the pollution it generates, in the range 0-255 static getPollutionValue(tileValue) { if (tileValue < Tile.POWERBASE) { // Roads, fires and radiation lie below POWERBASE // Heavy traffic is bad if (tileValue >= Tile.HTRFBASE) return 75; // Low traffic not so much if (tileValue >= Tile.LTRFBASE) return 50; if (tileValue < Tile.ROADBASE) { // Fire = carbon monoxide = a bad score for you if (tileValue > Tile.FIREBASE) return 90; // Radiation. Top of the charts. if (tileValue >= Tile.RADTILE) return 255; } // All other types of ground are pure. return 0; } // If we've reached this point, we're classifying some form of zone tile // Residential and commercial zones don't pollute if (tileValue <= Tile.LASTIND) return 0; // Industrial zones, however... if (tileValue < Tile.PORTBASE) return 50; // Coal power plants are bad if (tileValue <= Tile.LASTPOWERPLANT) return 100; return 0; } // Compute the Manhattan distance of the given point from the city centre, and force into the range 0-64 static getCityCentreDistance(map, x, y) { let xDis, yDis; if (x > map.cityCentreX) xDis = x - map.cityCentreX;else xDis = map.cityCentreX - x; if (y > map.cityCentreY) yDis = y - map.cityCentreY;else yDis = map.cityCentreY - y; return Math.min(xDis + yDis, 64); } // This monster function fills up the landValueMap, the terrainDensityMap and the pollutionDensityMap based // on values found by iterating over the map. // // Factors that affect land value: // * Distance from the city centre // * High crime // * High pollution // * Proximity to undeveloped terrain (who doesn't love a good view?) // // Pollution is completely determined by the tile types in the block static pollutionTerrainLandValueScan(map, census, blockMaps) { // We record raw pollution readings for each tile into tempMap1, and then use tempMap2 and tempMap1 to smooth // out the pollution in order to construct the new values for the populationDensityMap let tempMap1 = blockMaps.tempMap1; let tempMap2 = blockMaps.tempMap2; // tempMap3 will be used to record raw terrain information, i.e. if the the land is developed. This will be // smoothed in to terrainDensityMap later let tempMap3 = blockMaps.tempMap3; tempMap3.clear(); let landValueMap = blockMaps.landValueMap; let terrainDensityMap = blockMaps.terrainDensityMap; let pollutionDensityMap = blockMaps.pollutionDensityMap; let crimeRateMap = blockMaps.crimeRateMap; let totalLandValue = 0; let developedTileCount = 0; let x = landValueMap.width, y, pollutionLevel, developed, worldX, worldY, mapX, mapY, tileValue, terrainValue, landValue; while (x--) { y = landValueMap.height; while (y--) { pollutionLevel = 0; developed = false; // The land value map has a chunk size of 2 worldX = x * 2; worldY = y * 2; for (mapX = worldX; mapX <= worldX + 1; mapX++) { for (mapY = worldY; mapY <= worldY + 1; mapY++) { tileValue = map.getTileValue(mapX, mapY); if (tileValue === Tile.DIRT) continue; if (tileValue < Tile.RUBBLE) { // Undeveloped land: record in tempMap3. Each undeveloped piece of land scores 15. // tempMap3 has a chunk size of 4, so each square in tempMap3 will ultimately contain a // maximum value of 240 terrainValue = tempMap3.worldGet(mapX, mapY); tempMap3.worldSet(mapX, mapY, terrainValue + 15); continue; } pollutionLevel += MapUtils.getPollutionValue(tileValue); if (tileValue >= Tile.ROADBASE) { developed = true; } } } pollutionLevel = Math.min(pollutionLevel, 255); tempMap1.set(x, y, pollutionLevel); if (developed) { landValue = 34 - Math.floor(MapUtils.getCityCentreDistance(map, worldX, worldY) / 2); landValue = landValue << 2; // Land in the same neighbourhood as unspoiled land is more valuable... landValue += terrainDensityMap.get(x >> 1, y >> 1); // ... and polluted land obviously is less valuable landValue -= pollutionDensityMap.get(x, y); // ... getting mugged won't help either if (crimeRateMap.get(x, y) > 190) { landValue -= 20; } // Clamp in range 1-250 (0 represents undeveloped land) landValue = math.clamp(landValue, 1, 250); landValueMap.set(x, y, landValue); totalLandValue += landValue; developedTileCount++; } else { landValueMap.set(x, y, 0); } } } if (developedTileCount > 0) census.landValueAverage = Math.floor(totalLandValue / developedTileCount);else census.landValueAverage = 0; // Smooth the pollution map twice MapUtils.smoothMap(tempMap1, tempMap2, Micro.SMOOTH_ALL_THEN_CLAMP); MapUtils.smoothMap(tempMap2, tempMap1, Micro.SMOOTH_ALL_THEN_CLAMP); let maxPollution = 0; let pollutedTileCount = 0; let totalPollution = 0; let pollution; // We iterate over the now-smoothed pollution map rather than using the block map's copy routines // so that we can compute the average and total pollution en-route for (x = 0; x < map.width; x += pollutionDensityMap.blockSize) { for (y = 0; y < map.height; y += pollutionDensityMap.blockSize) { // Copy the values into pollutionDensityMap pollution = tempMap1.worldGet(x, y); pollutionDensityMap.worldSet(x, y, pollution); if (pollution !== 0) { pollutedTileCount++; totalPollution += pollution; // Note the most polluted location: any monsters will be drawn there (randomly choosing one // if we have multiple competitors for most polluted) if (pollution > maxPollution || pollution === maxPollution && math.getChance(3)) { maxPollution = pollution; map.pollutionMaxX = x; map.pollutionMaxY = y; } } } } if (pollutedTileCount) census.pollutionAverage = Math.floor(totalPollution / pollutedTileCount);else census.pollutionAverage = 0; MapUtils.smoothMap(tempMap3, terrainDensityMap, Micro.SMOOTH_NEIGHBOURS_THEN_BLOCK); } // Computes the coverage radius of police stations, and scores each neighbourhood in the map on its crime rate. // Factors that attract crime are: // * The zone has a low value // * The zone is a slum // * The zone is far away from those pesky police static crimeScan(census, blockMaps) { let policeStationMap = blockMaps.policeStationMap; let policeStationEffectMap = blockMaps.policeStationEffectMap; let crimeRateMap = blockMaps.crimeRateMap; let landValueMap = blockMaps.landValueMap; let populationDensityMap = blockMaps.populationDensityMap; MapUtils.smoothMap(policeStationMap, policeStationEffectMap, Micro.SMOOTH_NEIGHBOURS_THEN_BLOCK); MapUtils.smoothMap(policeStationEffectMap, policeStationMap, Micro.SMOOTH_NEIGHBOURS_THEN_BLOCK); MapUtils.smoothMap(policeStationMap, policeStationEffectMap, Micro.SMOOTH_NEIGHBOURS_THEN_BLOCK); let totalCrime = 0; let crimeZoneCount = 0; let x, y, width = crimeRateMap.mapWidth, height = crimeRateMap.mapHeight, value; // Scan the map, looking for developed land, as it can attract crime. for (x = 0; x < width; x += blockSize) { for (y = 0; y < height; y += blockSize) { // Remember: landValueMap values are in the range 0-250 value = landValueMap.worldGet(x, y); if (value > 0) { crimeZoneCount += 1; // Force value in the range -122 to 128. Lower valued pieces of land attract more crime. value = 128 - value; // Add population density (a value between 0 and 510). value now lies in range -260 - 382. // Denser areas attract more crime. value += populationDensityMap.worldGet(x, y); // Clamp in range -260 to 300 value = Math.min(value, 300); // If the police are nearby, there's no point committing the crime of the century value -= policeStationMap.worldGet(x, y); // Force in to range 0-250 value = math.clamp(value, 0, 250); crimeRateMap.worldSet(x, y, value); totalCrime += value; } else { crimeRateMap.worldSet(x, y, 0); } } } if (crimeZoneCount > 0) census.crimeAverage = Math.floor(totalCrime / crimeZoneCount);else census.crimeAverage = 0; } // Iterate over the map, and score each neighbourhood on its distance from the city centre. Scores are in the range // -64 to 64. This affects the growth of commercial zones within that neighbourhood. static fillCityCentreDistScoreMap(map, blockMaps) { let bm = blockMaps.cityCentreDistScoreMap; let x = bm.width, y, value; while (x--) { y = bm.height; while (y--) { // First, we compute the Manhattan distance of the top-left hand corner of the neighbourhood to the city centre // and half that value. This leaves us a value in the range 0 - 32 value = Math.floor(MapUtils.getCityCentreDistance(map, x * 8, y * 8) / 2); // Now, we scale up by a factor of 4. We're in the range 0 - 128 value = value * 4; // And finally, subtract from 64, leaving us a score in the range -64 to 64 value = 64 - value; bm.set(x, y, value); } } } // Dispatch to the correct zone type to get the population value for that zone static getPopulationDensity(map, x, y, tile) { if (tile < Tile.COMBASE) return Residential.getZonePopulation(map, x, y, tile); if (tile < Tile.INDBASE) return Commercial.getZonePopulation(map, x, y, tile) * 8; if (tile < Tile.PORTBASE) return Industrial.getZonePopulation(map, x, y, tile) * 8; return 0; } // Iterate over the map, examining each zone for population. We then smooth the results into a population density // map, which is used when deciding to grow residential zones. At the same time, we also note the most populous area // (in terms of zones) to calculate our city centre. Finally, we score each area of the map on its distance from the // city centre. static populationDensityScan(map, blockMaps) { // We will build the initial unsmoothed map in tempMap1, and smooth it in to tempMap2 let tempMap1 = blockMaps.tempMap1; let tempMap2 = blockMaps.tempMap2; blockMaps.populationDensityMap; // We will sum all the coordinates that contain zones into xTot and yTot. They are used in our city centre heuristic. let Xtot = 0; let Ytot = 0; let zoneTotal = 0; tempMap1.clear(); let x = map.width, y, tile, tileValue, population; while (x--) { y = map.height; while (y--) { tile = map.getTile(x, y); if (tile.isZone()) { tileValue = tile.getValue(); // Ask the zone to calculate its population, scale it up, then clamp in the range 0-254 population = MapUtils.getPopulationDensity(map, x, y, tileValue) * 8; population = Math.min(population, 254); // The block size of population density is 2x2, so there can only be 1 zone per block tempMap1.worldSet(x, y, population); Xtot += x; Ytot += y; zoneTotal++; } } } MapUtils.smoothMap(tempMap1, tempMap2, Micro.SMOOTH_ALL_THEN_CLAMP); MapUtils.smoothMap(tempMap2, tempMap1, Micro.SMOOTH_ALL_THEN_CLAMP); MapUtils.smoothMap(tempMap1, tempMap2, Micro.SMOOTH_ALL_THEN_CLAMP); blockMaps.populationDensityMap.copyFrom(tempMap2, function (x) { return x * 2; }); // XXX This follows the original Micropolis source, but it feels weird to me that we score the entire map // based on city centre proximity, and then potentially move the city centre. I think these should be // swapped. MapUtils.fillCityCentreDistScoreMap(map, blockMaps); // Compute new city center if (zoneTotal > 0) { map.cityCentreX = Math.floor(Xtot / zoneTotal); map.cityCentreY = Math.floor(Ytot / zoneTotal); } else { map.cityCentreX = Math.floor(map.width * 0.5); map.cityCentreY = Math.floor(map.height * 0.5); } } // Compute the radius of coverage for the firestations found during the map scan static fireAnalysis(blockMaps) { let fireStationMap = blockMaps.fireStationMap; let fireStationEffectMap = blockMaps.fireStationEffectMap; MapUtils.smoothMap(fireStationMap, fireStationEffectMap, Micro.SMOOTH_NEIGHBOURS_THEN_BLOCK); MapUtils.smoothMap(fireStationEffectMap, fireStationMap, Micro.SMOOTH_NEIGHBOURS_THEN_BLOCK); MapUtils.smoothMap(fireStationMap, fireStationEffectMap, Micro.SMOOTH_NEIGHBOURS_THEN_BLOCK); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * * switch to ES6 & 3D by lo-th * */ class Simulation { constructor(gameMap, gameLevel, speed, is3D, savedGame) { if (gameLevel !== Micro.LEVEL_EASY && gameLevel !== Micro.LEVEL_MED && gameLevel !== Micro.LEVEL_HARD) throw new Error('Invalid level!'); // if (speed !== Micro.SPEED_PAUSED && speed !== Micro.SPEED_SLOW && speed !== Micro.SPEED_MED && speed !== Micro.SPEED_FAST) throw new Error('Invalid speed!'); this.map = gameMap; this.gameLevel = gameLevel; this.div = this.map.width / 8; this.is3D = is3D || false; this.time = typeof performance === 'undefined' ? Date : performance; this.speed = speed; this.speedCycle = 0; this.phaseCycle = 0; this.simCycle = 0; this.doInitialEval = true; this.cityTime = 50; this.cityPopLast = 0; this.messageLast = Messages.VILLAGE_REACHED; this.startingYear = 1900; // Last valves updated to the user this.resValveLast = 0; this.comValveLast = 0; this.indValveLast = 0; // Last date sent to front end this._cityYearLast = -1; this._cityMonthLast = -1; // Last time we relayed a message from PowerManager to the front-end this._lastPowerMessage = null; this.infos = []; // And now, the main cast of characters this.evaluation = new Evaluation(this.gameLevel); this.valves = new Valves(); this.budget = new Budget(); this.census = new Census(); this.powerManager = new PowerManager(this.map); this.spriteManager = new SpriteManager(this.map); this.mapScanner = new MapScanner(this.map); this.repairManager = new RepairManager(this.map); this.traffic = new Traffic(this.map, this.spriteManager); this.disasterManager = new DisasterManager(this.map, this.spriteManager, this.gameLevel); this.messageManager = new MessageManager(); Micro.messageManager = this.messageManager; let w = this.map.width, h = this.map.height; this.blockMaps = { // Holds a "distance score" for the block from the city centre, range -64 to 64 cityCentreDistScoreMap: new BlockMap(w, h, 8), // Holds a score representing how dangerous an area is, in range 0-250 (larger is worse) crimeRateMap: new BlockMap(w, h, 2), // A map used to note positions of fire stations during the map scan, range 0-1000 fireStationMap: new BlockMap(w, h, 8), // Holds a value containing a score representing the effect of fire cover in this neighborhood, range 0-1000 fireStationEffectMap: new BlockMap(w, h, 8), // Holds scores representing the land value in the range 0-250 landValueMap: new BlockMap(w, h, 2), // A map used to note positions of police stations during the map scan, range 0-1000 policeStationMap: new BlockMap(w, h, 8), // Holds a value containing a score representing how much crime is dampened in this block, range 0-1000 policeStationEffectMap: new BlockMap(w, h, 8), // Holds a value representing the amount of pollution in a neighbourhood, in the range 0-255 pollutionDensityMap: new BlockMap(w, h, 2), // Holds a value representing population density of a block, in the range 0-510 populationDensityMap: new BlockMap(w, h, 2), // Holds a value representing the rate of growth of a neighbourhood in the range -200 to +200 rateOfGrowthMap: new BlockMap(w, h, 8), // Scores a block on how undeveloped/unspoilt it is, range 0-240 terrainDensityMap: new BlockMap(w, h, 4), // Scores the volume of traffic in this cluster, range 0-240 trafficDensityMap: new BlockMap(w, h, 2), // Temporary maps tempMap1: new BlockMap(w, h, 2), tempMap2: new BlockMap(w, h, 2), tempMap3: new BlockMap(w, h, 4) }; this.clearCensus(); if (savedGame) { this.load(savedGame); //this.cityPopLast = savedGame.totalPop; } else { this.budget.setFunds(20000); this.census.totalPop = 1; } Micro.simData = this; this.init(); } save(saveData) { for (let i = 0, l = Micro.savePropsVar.length; i < l; i++) saveData[Micro.savePropsVar[i]] = this[Micro.savePropsVar[i]]; this.map.save(saveData); this.evaluation.save(saveData); this.valves.save(saveData); this.budget.save(saveData); this.census.save(saveData); } load(saveData) { //console.log(saveData) this.messageManager.clear(); for (let i = 0, l = Micro.savePropsVar.length; i < l; i++) this[Micro.savePropsVar[i]] = saveData[Micro.savePropsVar[i]]; //this.map.load(saveData); this.evaluation.load(saveData); this.valves.load(saveData); this.budget.load(saveData); this.census.load(saveData); } setSpeed(s) { this.speed = s; } setDifficulty(s) { if (s !== Micro.LEVEL_EASY && s !== Micro.LEVEL_MED && s !== Micro.LEVEL_HARD) throw new Error('Invalid level!'); this.gameLevel = s; this.disasterManager.setDifficulty(this.gameLevel); } isPaused() { return this.speed === Micro.SPEED_PAUSED; } simTick() { let up = this.simFrame(); if (up) { this.updateTime(); this.updateInfo(); } return up; } updateInfo() { this.infos[0] = [TXT.months[this._cityMonthLast], this._cityYearLast].join(' '); this.infos[1] = TXT.cityClass[this.evaluation.cityScore]; this.infos[2] = this.evaluation.cityScore; this.infos[3] = this.evaluation.cityPop; this.infos[4] = this.budget.totalFunds; this.infos[5] = this.valves.resValve; this.infos[6] = this.valves.comValve; this.infos[7] = this.valves.indValve; //this.infos[8] = '' // message this.infos[9] = this.map.powerChange; this.map.powerChange = false; return this.infos; } simFrame() { if (this.budget.awaitingValues) return false; if (this.speed === Micro.SPEED_PAUSED) return false; // Default to slow speed let threshold = 100; if (this.speed === Micro.SPEED_MED) threshold = 50; if (this.speed === Micro.SPEED_FAST) threshold = 10; if (this.speed === Micro.SPEED_ULTRA) threshold = 5; let now = this.time.now(); //new Date() if (now - this.prevTime < threshold) return false; this.messageManager.clear(); this.simulate(); this.prevTime = now; return true; } clearCensus() { this.census.clearCensus(); this.powerManager.clearPowerStack(); this.blockMaps.fireStationMap.clear(); this.blockMaps.policeStationMap.clear(); } init() { this.prevTime = -1; // Register actions this.powerManager.registerHandlers(this.mapScanner, this.repairManager); Commercial.registerHandlers(this.mapScanner, this.repairManager); EmergencyServices.registerHandlers(this.mapScanner, this.repairManager); Industrial.registerHandlers(this.mapScanner, this.repairManager); MiscTiles.registerHandlers(this.mapScanner, this.repairManager); Road.registerHandlers(this.mapScanner, this.repairManager); Residential.registerHandlers(this.mapScanner, this.repairManager); Stadia.registerHandlers(this.mapScanner, this.repairManager); Transport.registerHandlers(this.mapScanner, this.repairManager); this.evaluation.evalInit(); this.valves.setValves(this.gameLevel, this.census, this.budget); this.clearCensus(); //this.mapScanner.mapScan(0, this.map.width, simData); this.mapScanner.mapScan(0, this.map.width, null); this.powerManager.doPowerScan(this.census); MapUtils.pollutionTerrainLandValueScan(this.map, this.census, this.blockMaps); MapUtils.crimeScan(this.census, this.blockMaps); MapUtils.populationDensityScan(this.map, this.blockMaps); MapUtils.fireAnalysis(this.blockMaps); //this.census.totalPop = 1; // if (savedGame) this.load(savedGame); } simulate() { this.phaseCycle &= 15; let speedIndex = this.speed - 1; switch (this.phaseCycle) { case 0: if (++this.simCycle > 1023) this.simCycle = 0; if (this.doInitialEval) { this.doInitialEval = false; this.evaluation.cityEvaluation(); } this.cityTime++; if ((this.simCycle & 1) === 0) this.valves.setValves(this.gameLevel, this.census, this.budget); this.clearCensus(); break; case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: this.mapScanner.mapScan((this.phaseCycle - 1) * this.div, this.phaseCycle * this.div, null); break; case 9: if (this.cityTime % Micro.CENSUS_FREQUENCY_10 === 0) this.census.take10Census(this.budget); if (this.cityTime % Micro.CENSUS_FREQUENCY_120 === 0) this.census.take120Census(this.budget); if (this.cityTime % Micro.TAX_FREQUENCY === 0) { this.budget.collectTax(this.gameLevel, this.census); this.evaluation.cityEvaluation(); } break; case 10: if (this.simCycle % 5 === 0) { MapUtils.neutraliseRateOfGrowthMap(this.blockMaps); } MapUtils.neutraliseTrafficMap(this.blockMaps); this.sendMessages(); break; case 11: if (this.simCycle % Micro.speedPowerScan[speedIndex] === 0) this.powerManager.doPowerScan(this.census); break; case 12: if (this.simCycle % Micro.speedPollutionTerrainLandValueScan[speedIndex] === 0) MapUtils.pollutionTerrainLandValueScan(this.map, this.census, this.blockMaps); break; case 13: if (this.simCycle % Micro.speedCrimeScan[speedIndex] === 0) MapUtils.crimeScan(this.census, this.blockMaps); break; case 14: if (this.simCycle % Micro.speedPopulationDensityScan[speedIndex] === 0) MapUtils.populationDensityScan(this.map, this.blockMaps); break; case 15: if (this.simCycle % Micro.speedFireAnalysis[speedIndex] === 0) MapUtils.fireAnalysis(this.blockMaps); this.disasterManager.doDisasters(this.census); break; } // Go on the the next phase. this.phaseCycle = this.phaseCycle + 1 & 15; } sendMessages() { this.checkGrowth(); let totalZonePop = this.census.resZonePop + this.census.comZonePop + this.census.indZonePop; let powerPop = this.census.nuclearPowerPop + this.census.coalPowerPop; switch (this.cityTime & 63) { case 1: if (Math.floor(totalZonePop / 4) >= this.census.resZonePop) this.messageManager.sendMessage(Messages.NEED_MORE_RESIDENTIAL); break; case 5: if (Math.floor(totalZonePop / 8) >= this.census.comZonePop) this.messageManager.sendMessage(Messages.NEED_MORE_COMMERCIAL); break; case 10: if (Math.floor(totalZonePop / 8) >= this.census.indZonePop) this.messageManager.sendMessage(Messages.NEED_MORE_INDUSTRIAL); break; case 14: if (totalZonePop > 10 && totalZonePop * 2 > this.census.roadTotal) this.messageManager.sendMessage(Messages.NEED_MORE_ROADS); break; case 18: if (totalZonePop > 50 && totalZonePop > this.census.railTotal) this.messageManager.sendMessage(Messages.NEED_MORE_RAILS); break; case 22: if (totalZonePop > 10 && powerPop == 0) this.messageManager.sendMessage(Messages.NEED_ELECTRICITY); break; case 26: if (this.census.resPop > 500 && this.census.stadiumPop === 0) { this.messageManager.sendMessage(Messages.NEED_STADIUM); this.valves.resCap = true; } else { this.valves.resCap = false; } break; case 28: if (this.census.indPop > 70 && this.census.seaportPop === 0) { this.messageManager.sendMessage(Messages.NEED_SEAPORT); this.valves.indCap = true; } else { this.valves.indCap = false; } break; case 30: if (this.census.comPop > 100 && this.census.airportPop === 0) { this.messageManager.sendMessage(Messages._NEED_AIRPORT); this.valves.comCap = true; } else { this.valves.comCap = false; } break; case 32: let zoneCount = this.census.unpoweredZoneCount + this.census.poweredZoneCount; if (zoneCount > 0) { if (this.census.poweredZoneCount / zoneCount < 0.7) this.messageManager.sendMessage(Messages.BLACKOUTS_REPORTED); } break; case 35: if (this.census.pollutionAverage > 60) this.messageManager.sendMessage(Messages.HIGH_POLLUTION); break; case 42: if (this.census.crimeAverage > 100) this.messageManager.sendMessage(Messages.HIGH_CRIME); break; case 45: if (this.census.totalPop > 60 && this.census.fireStationPop === 0) this.messageManager.sendMessage(Messages.NEED_FIRE_STATION); break; case 48: if (this.census.totalPop > 60 && this.census.policeStationPop === 0) this.messageManager.sendMessage(Messages.NEED_POLICE_STATION); break; case 51: if (this.budget.cityTax > 12) this.messageManager.sendMessage(Messages.TAX_TOO_HIGH); break; case 54: if (this.budget.roadEffect < Math.floor(5 * this.budget.MAX_ROAD_EFFECT / 8) && this.census.roadTotal > 30) this.messageManager.sendMessage(Messages.ROAD_NEEDS_FUNDING); break; case 57: if (this.budget.fireEffect < Math.floor(7 * this.budget.MAX_FIRE_STATION_EFFECT / 10) && this.census.totalPop > 20) this.messageManager.sendMessage(Messages.FIRE_STATION_NEEDS_FUNDING); break; case 60: if (this.budget.policeEffect < Math.floor(7 * this.budget.MAX_POLICE_STATION_EFFECT / 10) && this.census.totalPop > 20) this.messageManager.sendMessage(Messages.POLICE_NEEDS_FUNDING); break; case 63: if (this.census.trafficAverage > 60) this.messageManager.sendMessage(Messages.TRAFFIC_JAMS, -1, -1, true); break; } } checkGrowth() { if ((this.cityTime & 3) !== 0) return; let message = ''; let cityPop = this.evaluation.getPopulation(this.census); if (cityPop !== this.cityPopLast) { let lastClass = this.evaluation.getCityClass(this.cityPopLast); let newClass = this.evaluation.getCityClass(cityPop); if (lastClass !== newClass) { switch (newClass) { case Micro.CC_VILLAGE: // Don't mention it. break; case Micro.CC_TOWN: message = Messages.REACHED_TOWN; break; case Micro.CC_CITY: message = Messages.REACHED_CITY; break; case Micro.CC_CAPITAL: message = Messages.REACHED_CAPITAL; break; case Micro.CC_METROPOLIS: message = Messages.REACHED_METROPOLIS; break; case Micro.CC_MEGALOPOLIS: message = Messages.REACHED_MEGALOPOLIS; break; } } } if (message !== '' && message !== this.messageLast) { this.messageManager.sendMessage(message); this.messageLast = message; } this.cityPopLast = cityPop; } // update date setYear(year) { if (year < this.startingYear) year = this.startingYear; year = year - this.startingYear - this.cityTime / 48; this.cityTime += year * 48; this.updateTime(); } updateTime() { let megalinium = 1000000; let cityYear = Math.floor(this.cityTime / 48) + this.startingYear; let cityMonth = Math.floor(this.cityTime % 48) >> 2; if (cityYear >= megalinium) { this.setYear(this.startingYear); return; } if (this._cityYearLast !== cityYear || this._cityMonthLast !== cityMonth) { this._cityYearLast = cityYear; this._cityMonthLast = cityMonth; this.messageManager.sendMessage(Messages.DATE_UPDATED, { month: cityMonth, year: cityYear }); } } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class TileHistory { constructor() { this.clear(); } clear() { this.data = {}; } toKey(x, y) { return [x, y].join(','); } getTile(x, y) { let key = this.toKey(x, y); return this.data[key]; } setTile(x, y, value) { let key = this.toKey(x, y); this.data[key] = value; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class AnimationManager { constructor(map, animationPeriod, blinkPeriod) { animationPeriod = animationPeriod || 5; blinkPeriod = blinkPeriod || 30; this._map = map; this.animationPeriod = animationPeriod; this.blinkPeriod = blinkPeriod; this.shouldBlink = false; this.count = 1; // When painting we keep track of what frames // have been painted at which map coordinates so we can // consistently display the correct frame even as the // canvas moves this._lastPainted = null; this._data = []; this.initArray(); this.registerAnimations(); } initArray() { // Map all tiles to their own value in case we ever // look up a tile that is not animated for (let i = 0; i < Tile.TILE_COUNT; i++) this._data[i] = i; } inSequence(tileValue, lastValue) { // It is important that we use the base value as the starting point // rather than the last painted value: base values often don't recur // in their sequences let seen = [tileValue]; let current = this._data[tileValue]; while (seen.indexOf(current) === -1) { if (current === lastValue) return true; seen.push(current); current = this._data[current]; } return false; } getTiles(startX, startY, boundX, boundY, isPaused = false) { let shouldChangeAnimation = false; if (!isPaused) this.count += 1; if (this.count % this.blinkPeriod === 0) this.shouldBlink = !this.shouldBlink; if (this.count % this.animationPeriod === 0 && !isPaused) shouldChangeAnimation = true; let newPainted = new TileHistory(); let tilesToPaint = []; for (let x = startX; x < boundX; x++) { for (let y = startY; y < boundY; y++) { if (x < 0 || x >= this._map.width || y < 0 || y >= this._map.height) continue; let tile = this._map.getTile(x, y); /*if (tile.isZone() && !tile.isPowered() && this.shouldBlink) { tilesToPaint.push({x: x, y: y, tileValue: Tile.LIGHTNINGBOLT}); continue; }*/ if (!tile.isAnimated()) continue; let tileValue = tile.getValue(); let newTile = Tile.TILE_INVALID; let last; if (this._lastPainted) last = this._lastPainted.getTile(x, y); if (shouldChangeAnimation) { // Have we painted any of this sequence before? If so, paint the next tile if (last && this.inSequence(tileValue, last)) { newTile = this._data[last]; if (last === Tile.LASTTINYEXP) { this._map.setTo(x, y, ZoneUtils.randomRubble()); newTile = this._map.getTileValue(x, y); } else { newTile = this._data[last]; } } else { // Either we haven't painted anything here before, or the last tile painted // there belongs to a different tile's animation sequence newTile = this._data[tileValue]; } } else { // Have we painted any of this sequence before? If so, paint the same tile if (last && this.inSequence(tileValue, last)) newTile = last; } if (newTile === Tile.TILE_INVALID) continue; tilesToPaint.push({ x: x, y: y, tileValue: newTile }); newPainted.setTile(x, y, newTile); this._map.setPaintValue(x, y, newTile); /// DIRECT SET TEXTURES } } this._lastPainted = newPainted; return tilesToPaint; } registerSingleAnimation(arr) { for (let i = 1; i < arr.length; i++) this._data[arr[i - 1]] = arr[i]; } registerAnimations() { this.registerSingleAnimation([56, 57, 58, 59, 60, 61, 62, 63, 56]); // fire this.registerSingleAnimation([860, 861, 862, 863, 864, 865, 866, 867]); // explosion // traffic this.registerSingleAnimation([80, 128, 112, 96, 80]); this.registerSingleAnimation([81, 129, 113, 97, 81]); this.registerSingleAnimation([82, 130, 114, 98, 82]); this.registerSingleAnimation([83, 131, 115, 99, 83]); this.registerSingleAnimation([84, 132, 116, 100, 84]); this.registerSingleAnimation([85, 133, 117, 101, 85]); this.registerSingleAnimation([86, 134, 118, 102, 86]); this.registerSingleAnimation([87, 135, 119, 103, 87]); this.registerSingleAnimation([88, 136, 120, 104, 88]); this.registerSingleAnimation([89, 137, 121, 105, 89]); this.registerSingleAnimation([90, 138, 122, 106, 90]); this.registerSingleAnimation([91, 139, 123, 107, 91]); this.registerSingleAnimation([92, 140, 124, 108, 92]); this.registerSingleAnimation([93, 141, 125, 109, 93]); this.registerSingleAnimation([94, 142, 126, 110, 94]); this.registerSingleAnimation([95, 143, 127, 111, 95]); this.registerSingleAnimation([144, 192, 176, 160, 144]); this.registerSingleAnimation([145, 193, 177, 161, 145]); this.registerSingleAnimation([146, 194, 178, 162, 146]); this.registerSingleAnimation([147, 195, 179, 163, 147]); this.registerSingleAnimation([148, 196, 180, 164, 148]); this.registerSingleAnimation([149, 197, 181, 165, 149]); this.registerSingleAnimation([150, 198, 182, 166, 150]); this.registerSingleAnimation([151, 199, 183, 167, 151]); this.registerSingleAnimation([152, 200, 184, 168, 152]); this.registerSingleAnimation([153, 201, 185, 169, 153]); this.registerSingleAnimation([154, 202, 186, 170, 154]); this.registerSingleAnimation([155, 203, 187, 171, 155]); this.registerSingleAnimation([156, 204, 188, 172, 156]); this.registerSingleAnimation([157, 205, 189, 173, 157]); this.registerSingleAnimation([158, 206, 190, 174, 158]); this.registerSingleAnimation([159, 207, 191, 175, 159]); /* // NOT NEED this.registerSingleAnimation([621, 852, 853, 854, 855, 856, 857, 858, 859, 852]);// industrial polution this.registerSingleAnimation([641, 884, 885, 886, 887, 884]); this.registerSingleAnimation([644, 888, 889, 890, 891, 888]); this.registerSingleAnimation([649, 892, 893, 894, 895, 892]); this.registerSingleAnimation([650, 896, 897, 898, 899, 896]); this.registerSingleAnimation([676, 900, 901, 902, 903, 900]); this.registerSingleAnimation([677, 904, 905, 906, 907, 904]); this.registerSingleAnimation([686, 908, 909, 910, 911, 908]); this.registerSingleAnimation([689, 912, 913, 914, 915, 912]); this.registerSingleAnimation([747, 916, 917, 918, 919, 916]); this.registerSingleAnimation([748, 920, 921, 922, 923, 920]); this.registerSingleAnimation([751, 924, 925, 926, 927, 924]); this.registerSingleAnimation([752, 928, 929, 930, 931, 928]); this.registerSingleAnimation([820, 952, 953, 954, 955, 952]); this.registerSingleAnimation([832, 833, 834, 835, 836, 837, 838, 839, 832]); this.registerSingleAnimation([840, 841, 842, 843, 840]); this.registerSingleAnimation([844, 845, 846, 847, 848, 849, 850, 851, 844]); this.registerSingleAnimation([932, 933, 934, 935, 936, 937, 938, 939, 932]);// football this.registerSingleAnimation([940, 941, 942, 943, 944, 945, 946, 947, 940]);// football */ } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * * switch to ES6 & 3D by lo-th * */ class GameMap { constructor(width = 128, height = 128, defaultValue) { this.isIsland = false; this.pp = []; //this.Position = new PositionMaker( width, height ); // !! find better way this.width = width; this.height = height; this.fsize = this.width * this.height; this.defaultValue = new Tiles().getValue(); this.data = []; //new Array(this.fsize); this.tilesData = new Micro.M_ARRAY_TYPE(this.fsize); this.powerData = new Micro.M_ARRAY_TYPE(this.fsize); let i = this.fsize; while (i--) { this.data[i] = new Tiles(this.defaultValue); this.tilesData[i] = this.defaultValue; } /*console.log(this.data.length)*/ // Generally set externally this.cityCentreX = Math.floor(this.width * 0.5); this.cityCentreY = Math.floor(this.height * 0.5); this.pollutionMaxX = this.cityCentreX; this.pollutionMaxY = this.cityCentreY; this.powerChange = false; this.layer = []; this.resetLayer(); this.makePP(); } // 3D LAYER upLayer(id, value, x, y) { if (value >= Tile.TINYEXP && value <= Tile.TINYEXPLAST) value -= 860 - 35; // explosion decal //if(value>=Tile.HBRDG0 && value<=Tile.HBRDG3) value -= 832+32 //if(value>=Tile.VBRDG0 && value<=Tile.VBRDG3) value -= 832+32 this.tilesData[id] = value; //if( this.goodValue(value) ){ this.layer[this.findLayer(x, y)] = 1; } resetLayer() { let i = 64; while (i--) this.layer[i] = 0; } findLayer(x, y) { let cx = Math.floor(x / 16); let cy = Math.floor(y / 16); return cx + cy * 8; } goodValue(v) { if (v === 0) return true; // dirt else if (v > 1 && v < 240) return true; // water edge tree //else if( v > 1 && v < 44 ) return true // water edge tree //if( v > 43 && v < 48 ) return true // rubble // else if( v > 43 && v < 240 ) return true // road wire rail return false; } // makePP() { let x = this.width, y, n = 0; let pp = []; while (x--) { y = this.height; while (y--) { pp[n] = [x, y]; n++; } } this.pp = pp; } // change power statue for 3d powered(o) { let id = o.id || this.getId(o.x, o.y); this.powerData[id] = o.v; this.powerChange = true; } /*makePos (){ return new PositionMaker( this.width, this.height ); }*/ save(saveData) { let i = 0, lng; // GAME PROPS lng = Micro.GameMapProps.length; while (i < lng) { saveData[Micro.GameMapProps[i]] = this[Micro.GameMapProps[i]]; i++; } // MAP DATA //saveData.map = this.data.map(function(t) { return {value: t.getRawValue()}; }); saveData.map = []; i = 0; lng = this.fsize; while (i < lng) { saveData.map[i] = this.data[i].getRawValue(); i++; } // TILES VALUES saveData.tileValue = []; i = 0; lng = this.fsize; while (i < lng) { saveData.tileValue[i] = this.tilesData[i]; i++; } } load(saveData) { let x, y, lng, i = 0, map = saveData.map, tiles = saveData.tileValue; // GAME PROPS lng = Micro.GameMapProps.length; while (i < lng) { this[Micro.GameMapProps[i]] = saveData[Micro.GameMapProps[i]]; i++; } // MAP DATA let isOld = map[0].value !== undefined ? true : false; i = 0; lng = this.fsize; while (i < lng) { x = i % this.width; y = Math.floor(i / this.width); if (isOld) this.setTileValue(x, y, map[i].value);else this.setTileValue(x, y, map[i]); i++; } // TILES VALUES i = 0; lng = this.fsize; while (i < lng) { this.tilesData[i] = tiles[i]; i++; } } testBounds(x, y) { return x >= 0 && y >= 0 && x < this.width && y < this.height; } getId(x, y) { return x + y * this.width; } getTile(x, y, newTile) { //var e = new Error('Invalid parameter'); //if (arguments.length < 1) throw e; // Argument-shuffling if (typeof x === 'object') { y = x.y; x = x.x; } //if (!this.testBounds(x, y)) throw e; let width = this.width; let height = this.height; if (x < 0 || y < 0 || x >= width || y >= height) { console.warn('getTile called with bad bounds', x, y); return new Tiles(Tile.TILE_INVALID); } let tileIndex = this.getId(x, y); let tile = this.data[tileIndex]; //var tileIndex = this._calculateIndex(x, y); // Return the original tile if we're not given a tile to fill if (!newTile) return tile; newTile.set(tile); return tile; //if (!(tileIndex in this.data)) this.data[tileIndex] = new Tiles(this.defaultValue); //return this.data[tileIndex]; } getTileValue(x, y) { let e = new Error('Invalid parameter'); if (arguments.length < 1) throw e; // Argument-shuffling if (typeof x === 'object') { y = x.y; x = x.x; } if (!this.testBounds(x, y)) throw e; let tileIndex = this.getId(x, y); if (!(tileIndex in this.data)) this.data[tileIndex] = new Tiles(this.defaultValue); return this.data[tileIndex].getValue(); } getTileFlags(x, y) { let e = new Error('Invalid parameter'); if (arguments.length < 1) throw e; // Argument-shuffling if (typeof x === 'object') { y = x.y; x = x.x; } if (!this.testBounds(x, y)) throw e; let tileIndex = this.getId(x, y); if (!(tileIndex in this.data)) this.data[tileIndex] = new Tiles(this.defaultValue); return this.data[tileIndex].getFlags(); } getTiles(x, y, w, h) { let e = new Error('Invalid parameter'); if (arguments.length < 3) throw e; // Argument-shuffling if (arguments.length === 3) { h = w; w = y; y = x.y; x = x.x; } if (!this.testBounds(x, y)) throw e; let res = []; for (let a = y, ylim = y + h; a < ylim; a++) { res[a - y] = []; for (let b = x, xlim = x + w; b < xlim; b++) { let tileIndex = this.getId(b, a); //if (!(tileIndex in this.data)) this.data[tileIndex] = new Tiles(this.defaultValue); res[a - y].push(this.data[tileIndex]); } } return res; } getTileValues(x, y, w, h, result) { result = result || []; let e = new Error('Invalid parameter'); if (arguments.length < 3) throw e; // Argument-shuffling if (arguments.length === 3) { h = w; w = y; y = x.y; x = x.x; } //if (!this.testBounds(x, y)) throw e; let width = this.width; let height = this.height; // Result is stored in row-major order for (let a = y, ylim = y + h; a < ylim; a++) { for (let b = x, xlim = x + w; b < xlim; b++) { if (a < 0 || b < 0 || a >= height || b >= width) { result[(a - y) * w + (b - x)] = Tile.TILE_INVALID; continue; } let tileIndex = b + a * width; //result[(a - y) * w + (b - x)] = this._data[tileIndex].getRawValue(); result[(a - y) * w + (b - x)] = this.data[tileIndex].getRawValue(); } } return result; } getTileFromMapOrDefault(pos, dir, defaultTile) { switch (dir) { case Direction.NORTH: if (pos.y > 0) return this.getTileValue(pos.x, pos.y - 1); return defaultTile; case Direction.EAST: if (pos.x < this.width - 1) return this.getTileValue(pos.x + 1, pos.y); return defaultTile; case Direction.SOUTH: if (pos.y < this.height - 1) return this.getTileValue(pos.x, pos.y + 1); return defaultTile; case Direction.WEST: if (pos.x > 0) return this.getTileValue(pos.x - 1, pos.y); return defaultTile; default: return defaultTile; } } //---------------------- testOld(id, value) { if (this.data[id].getValue() !== value) return true; return false; } setTile(x, y, value, flags) { //var e = new Error('Invalid parameter'); //if (arguments.length < 3) throw e; // Argument-shuffling if (arguments.length === 3) { flags = value; value = y; y = x.y; x = x.x; } //if (!this.testBounds(x, y)) throw e; let id = this.getId(x, y); let isNew = this.testOld(id, value); this.data[id].set(value, flags); if (isNew) this.upLayer(id, value, x, y); } setTo(x, y, tile) { //var e = new Error('Invalid parameter'); //if (arguments.length < 2) throw e; // Argument-shuffling if (tile === undefined) { tile = y; y = x.y; x = x.x; } //if (!this.testBounds(x, y)) throw e; let id = this.getId(x, y); let value = tile.getValue(); let isNew = this.testOld(id, value); this.data[id] = tile; // this.data[ id ].setValue(value); if (isNew) this.upLayer(id, value, x, y); } setTileValue(x, y, value) { //var e = new Error('Invalid parameter'); //if (arguments.length < 2) throw e; // Argument-shuffling if (arguments.length === 2) { value = y; y = x.y; x = x.x; } //if (!this.testBounds(x, y)) throw e; let id = this.getId(x, y); let isNew = this.testOld(id, value); //if (!(tileIndex in this.data)) this.data[tileIndex] = new Tiles(this.defaultValue); this.data[id].setValue(value); if (isNew) this.upLayer(id, value, x, y); } setPaintValue(x, y, value) { //var e = new Error('Invalid parameter'); //if (arguments.length < 2) throw e; // Argument-shuffling if (arguments.length === 2) { value = y; y = x.y; x = x.x; } //if (!this.testBounds(x, y)) throw e; let id = this.getId(x, y); this.upLayer(id, value, x, y); } setTileFlags(x, y, flags) { let e = new Error('Invalid flag parameter'); if (arguments.length < 2) throw e; // Argument-shuffling if (arguments.length === 2) { flags = y; y = x.y; x = x.x; } if (!this.testBounds(x, y)) throw e; let id = this.getId(x, y); this.data[id].setFlags(flags); } addTileFlags(x, y, flags) { let e = new Error('Invalid flag parameter'); if (arguments.length < 2) throw e; // Argument-shuffling if (arguments.length === 2) { flags = y; y = x.y; x = x.x; } if (!this.testBounds(x, y)) throw e; let id = this.getId(x, y); this.data[id].addFlags(flags); } removeTileFlags(x, y, flags) { if (arguments.length < 2) throw new Error('GameMap removeTileFlags called with too few arguments'); if (arguments.length === 2) { flags = y; y = x.y; x = x.x; } if (!this.testBounds(x, y)) throw new Error('GameMap removeTileFlags called with invalid bounds' + x + ', ' + y); let id = this.getId(x, y); this.data[id].removeFlags(flags); ///this.upLayer( id, this.data[id].getValue(), x, y ) } putZone(centreX, centreY, centreTile, size) { if (!this.testBounds(centreX, centreY) || !this.testBounds(centreX - 1 + size, centreY - 1 + size)) throw new Error('GameMap putZone called with invalid bounds'); let tile = centreTile - 1 - size; let startX = centreX - 1; let startY = centreY - 1; let x, y; for (y = startY; y < startY + size; y++) { for (x = startX; x < startX + size; x++) { if (x === centreX && y === centreY) this.setTo(x, y, new Tiles(tile, Tile.BNCNBIT | Tile.ZONEBIT));else this.setTo(x, y, new Tiles(tile, Tile.BNCNBIT)); tile += 1; } } } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * * switch to ES6 by lo-th * */ class MapGenerator { constructor() { this.SRMatrix = [[0, 0, 3, 3, 0, 0], [0, 3, 2, 2, 3, 0], [3, 2, 2, 2, 2, 3], [3, 2, 2, 2, 2, 3], [0, 3, 2, 2, 3, 0], [0, 0, 3, 3, 0, 0]]; this.BRMatrix = [[0, 0, 0, 3, 3, 3, 0, 0, 0], [0, 0, 3, 2, 2, 2, 3, 0, 0], [0, 3, 2, 2, 2, 2, 2, 3, 0], [3, 2, 2, 2, 2, 2, 2, 2, 3], [3, 2, 2, 2, 4, 2, 2, 2, 3], [3, 2, 2, 2, 2, 2, 2, 2, 3], [0, 3, 2, 2, 2, 2, 2, 3, 0], [0, 0, 3, 2, 2, 2, 3, 0, 0], [0, 0, 0, 3, 3, 3, 0, 0, 0]]; this.riverEdge = [13, 13, 17, 15, 5, 2, 19, 17, 9, 11, 2, 13, 7, 9, 5, 2]; this.treeTable = [0, 0, 0, 34, 0, 0, 36, 35, 0, 32, 0, 33, 30, 31, 29, 37]; } construct(w, h, debug = false) { Micro.TERRAIN_TREE_LEVEL = -1; Micro.TERRAIN_LAKE_LEVEL = -1; Micro.TERRAIN_CURVE_LEVEL = -1; Micro.ISLAND_RADIUS = 18; if (debug) console.time("start newmap"); this.map = new GameMap(w || Micro.MAP_WIDTH, h || Micro.MAP_HEIGHT); //this.map.makePP() Micro.TERRAIN_CREATE_ISLAND = math.getRandom(2) - 1; if (Micro.TERRAIN_CREATE_ISLAND < 0) { if (math.getRandom(100) < 10) { this.makeIsland(); return this.map; } } if (Micro.TERRAIN_CREATE_ISLAND === 1) this.makeNakedIsland();else this.clearMap(); // Lay a river. if (Micro.TERRAIN_CURVE_LEVEL !== 0) { let terrainXStart = 40 + math.getRandom(this.map.width - 79); let terrainYStart = 33 + math.getRandom(this.map.height - 66); let terrainPos = new Position(terrainXStart, terrainYStart); this.doRivers(terrainPos); } // Lay a few lakes. if (Micro.TERRAIN_LAKE_LEVEL !== 0) this.makeLakes(); this.smoothRiver(); this.cleanBorder(); // And add trees. if (Micro.TERRAIN_TREE_LEVEL !== 0) this.doTrees(); if (debug) console.timeEnd("start newmap"); return this.map; } cleanBorder() { let map = this.map; if (map.isIsland) return; let x, y, l, r, s; for (x = 0; x < map.width; x++) { l = x + 1; if (l > map.width - 1) l = map.width - 1; r = x - 1; if (r < 0) r = 0; s = 1; if (map.getTileValue(x, s) + map.getTileValue(l, s) + map.getTileValue(r, s) === 6) map.setTile(x, 0, Tile.RIVER, 0); s = map.height - 2; if (map.getTileValue(x, s) + map.getTileValue(l, s) + map.getTileValue(r, s) === 6) map.setTile(x, map.height - 1, Tile.RIVER, 0); } for (y = 0; y < map.height; y++) { l = y + 1; if (l > map.height - 1) l = map.height - 1; r = y - 1; if (r < 0) r = 0; s = 1; if (map.getTileValue(s, y) + map.getTileValue(s, l) + map.getTileValue(s, r) === 6) map.setTile(0, y, Tile.RIVER, 0); s = map.width - 2; if (map.getTileValue(s, y) + map.getTileValue(s, l) + map.getTileValue(s, r) === 6) map.setTile(map.width - 1, y, Tile.RIVER, 0); } } clearMap() { let map = this.map; map.pp.forEach(v => { //function ( v ) { map.setTile(v[0], v[1], Tile.DIRT, 0); }); } clearUnnatural() { let map = this.map, value; map.pp.forEach(v => { value = map.getTileValue(v[0], v[1]); if (value > Tile.WOODS) map.setTile(v[0], v[1], Tile.DIRT, 0); }); } makeNakedIsland() { let map = this.map; let terrainIslandRadius = Micro.ISLAND_RADIUS; let x, y, mapX, mapY; map.isIsland = true; map.pp.forEach(v => { x = v[0]; y = v[1]; if (x < 5 || x >= map.width - 5 || y < 5 || y >= map.height - 5) map.setTile(x, y, Tile.RIVER, 0);else map.setTile(x, y, Tile.DIRT, 0); }); for (x = 0; x < map.width - 5; x += 2) { mapY = math.getERandom(terrainIslandRadius); this.plopBRiver({ x: x, y: mapY }); mapY = map.height - 10 - math.getERandom(terrainIslandRadius); this.plopBRiver({ x: x, y: mapY }); this.plopSRiver({ x: x, y: 0 }); this.plopSRiver({ x: x, y: map.height - 6 }); } for (y = 0; y < map.height - 5; y += 2) { mapX = math.getERandom(terrainIslandRadius); this.plopBRiver({ x: mapX, y: y }); mapX = map.width - 10 - math.getERandom(terrainIslandRadius); this.plopBRiver({ x: mapX, y: y }); this.plopSRiver({ x: 0, y: y }); this.plopSRiver({ x: map.width - 6, y: y }); } } makeIsland() { this.makeNakedIsland(); this.smoothRiver(); this.doTrees(); } makeLakes() { let x, y; let numLakes = Micro.TERRAIN_LAKE_LEVEL < 0 ? math.getRandom(10) : Micro.TERRAIN_LAKE_LEVEL * 0.5; while (numLakes > 0) { x = math.getRandom(this.map.width - 21) + 10; y = math.getRandom(this.map.height - 20) + 10; this.makeSingleLake(new Position(x, y)); numLakes--; } } makeSingleLake(pos) { let numPlops = math.getRandom(12) + 2, plopPos; while (numPlops > 0) { plopPos = new Position(pos, math.getRandom(12) - 6, math.getRandom(12) - 6); if (math.getRandom(4)) this.plopSRiver(plopPos);else this.plopBRiver(plopPos); numPlops--; } } treeSplash(x, y) { let numTrees = Micro.TERRAIN_TREE_LEVEL < 0 ? math.getRandom(150) + 50 : math.getRandom(100 + Micro.TERRAIN_TREE_LEVEL * 2) + 50; let treePos = new Position(x, y), dir; while (numTrees > 0) { dir = Direction.NORTH + math.getRandom(7); treePos.move(dir); // XXX Should use the fact that positions return success/failure for moves if (!this.map.testBounds(treePos.x, treePos.y)) return; if (this.map.getTileValue(treePos) === Tile.DIRT) this.map.setTile(treePos, Tile.WOODS, Tile.BLBNBIT); numTrees--; } } doTrees() { let i = Micro.TERRAIN_TREE_LEVEL < 0 ? math.getRandom(100) + 50 : Micro.TERRAIN_TREE_LEVEL + 3; while (i--) { this.treeSplash(math.getRandom(this.map.width - 1), math.getRandom(this.map.height - 1)); } /*for (var x = 0; x < i; x++) { var xloc = math.getRandom(this.map.width - 1); var yloc = math.getRandom(this.map.height - 1); this.treeSplash( xloc, yloc ); }*/ this.smoothTrees(); this.smoothTrees(); } smoothRiver() { let map = this.map; let riverEdge = this.riverEdge; let x, y, z, xt, yt, tt, bitIndex, temp; let dx = [-1, 0, 1, 0]; let dy = [0, 1, 0, -1]; map.pp.forEach(v => { x = v[0]; y = v[1]; if (map.getTileValue(x, y) === Tile.REDGE) { bitIndex = 0; for (z = 0; z < 4; z++) { bitIndex = bitIndex << 1; xt = x + dx[z]; yt = y + dy[z]; if (map.testBounds(xt, yt)) { tt = map.getTileValue(xt, yt); if (tt !== Tile.DIRT && (tt < Tile.WOODS_LOW || tt > Tile.WOODS_HIGH)) bitIndex++; } } temp = riverEdge[bitIndex & 15]; if (temp !== Tile.RIVER && math.getRandom(1)) temp++; //map.setTileValue(x, y, temp, 0); map.setTile(x, y, temp, Tile.BULLBIT); // or we can't make bridge !!! } }); } isTree(value) { return value >= Tile.WOODS_LOW && value <= Tile.WOODS_HIGH; } smoothTrees() { let map = this.map; let x, y; map.pp.forEach(v => { x = v[0]; y = v[1]; if (this.isTree(map.getTileValue(x, y))) this.smoothTreesAt(x, y, false); }); } smoothTreesAt(x, y, preserve) { let map = this.map; let dx = [-1, 0, 1, 0]; let dy = [0, 1, 0, -1]; if (!this.isTree(this.map.getTileValue(x, y))) return; let i, xTemp, yTemp, temp, bitIndex = 0; for (i = 0; i < 4; i++) { bitIndex = bitIndex << 1; xTemp = x + dx[i]; yTemp = y + dy[i]; if (map.testBounds(xTemp, yTemp) && this.isTree(map.getTileValue(xTemp, yTemp))) bitIndex++; } temp = this.treeTable[bitIndex & 15]; if (temp) { if (temp !== Tile.WOODS) { if (x + y & 1) temp -= 8; } if (temp > 28 && temp < 38) temp -= 8; map.setTile(x, y, temp, Tile.BLBNBIT); } else { if (!preserve) { if (temp > 28 && temp < 38) temp -= 8; map.setTileValue(x, y, temp, 0); } } } doRivers(terrainPos) { let riverDir = Direction.NORTH + math.getRandom(3) * 2; this.doBRiver(terrainPos, riverDir, riverDir); riverDir = Direction.rotate180(riverDir); let terrainDir = this.doBRiver(terrainPos, riverDir, riverDir); riverDir = Direction.NORTH + math.getRandom(3) * 2; this.doSRiver(terrainPos, riverDir, terrainDir); } doBRiver(riverPos, riverDir, terrainDir) { let rate1, rate2; if (Micro.TERRAIN_CURVE_LEVEL < 0) { rate1 = 100; rate2 = 200; } else { rate1 = Micro.TERRAIN_CURVE_LEVEL + 10; rate2 = Micro.TERRAIN_CURVE_LEVEL + 100; } let pos = new Position(riverPos); while (this.map.testBounds(pos.x + 4, pos.y + 4)) { this.plopBRiver(pos); if (math.getRandom(rate1 + 1) < 10) { terrainDir = riverDir; } else { if (math.getRandom(rate2 + 1) > 90) terrainDir = Direction.rotate45(terrainDir); if (math.getRandom(rate2 + 1) > 90) terrainDir = Direction.rotate45(terrainDir, 7); } pos.move(terrainDir); } return terrainDir; } doSRiver(riverPos, riverDir, terrainDir) { let rate1, rate2; if (Micro.TERRAIN_CURVE_LEVEL < 0) { rate1 = 100; rate2 = 200; } else { rate1 = Micro.TERRAIN_CURVE_LEVEL + 10; rate2 = Micro.TERRAIN_CURVE_LEVEL + 100; } let pos = new Position(riverPos); while (this.map.testBounds(pos.x + 3, pos.y + 3)) { this.plopSRiver(pos); if (math.getRandom(rate1 + 1) < 10) { terrainDir = riverDir; } else { if (math.getRandom(rate2 + 1) > 90) terrainDir = Direction.rotate45(terrainDir); if (math.getRandom(rate2 + 1) > 90) terrainDir = Direction.rotate45(terrainDir, 7); } pos.move(terrainDir); } return terrainDir; } putOnMap(newVal, x, y) { if (newVal === 0) return; if (!this.map.testBounds(x, y)) return; let tmp = this.map.getTileValue(x, y); if (tmp !== Tile.DIRT) { if (tmp === Tile.RIVER && newVal !== Tile.CHANNEL) return; if (tmp === Tile.CHANNEL) return; } this.map.setTile(x, y, newVal, 0); } plopBRiver(pos) { let x = 9, y; while (x--) { y = 9; while (y--) { this.putOnMap(this.BRMatrix[y][x], pos.x + x, pos.y + y); } } } plopSRiver(pos) { let x = 6, y; while (x--) { y = 6; while (y--) { this.putOnMap(this.SRMatrix[y][x], pos.x + x, pos.y + y); } } } smoothWater() { let map = this.map, tile, pos, dir, x, y, makeRiver; map.pp.forEach(v => { x = v[0]; y = v[1]; tile = map.getTileValue(x, y); if (tile >= Tile.WATER_LOW && tile <= Tile.WATER_HIGH) { pos = new Position(x, y); for (dir = Direction.BEGIN; dir < Direction.END; dir = Direction.increment90(dir)) { tile = map.getTileFromMap(pos, dir, Tile.WATER_LOW); // If nearest object is not water: if (tile < Tile.WATER_LOW || tile > Tile.WATER_HIGH) { // set river edge map.setTileValue(x, y, Tile.REDGE, 0); break; // Continue with next tile } } } }); map.pp.forEach(v => { x = v[0]; y = v[1]; tile = map.getTileValue(x, y); if (tile !== Tile.CHANNEL && tile >= Tile.WATER_LOW && tile <= Tile.WATER_HIGH) { makeRiver = true; pos = new Position(x, y); for (dir = Direction.BEGIN; dir < Direction.END; dir = Direction.increment90(dir)) { tile = map.getTileFromMap(pos, dir, Tile.WATER_LOW); if (tile < Tile.WATER_LOW || tile > Tile.WATER_HIGH) { makeRiver = false; break; } } if (makeRiver) map.setTileValue(x, y, Tile.RIVER, 0); } }); map.pp.forEach(v => { x = v[0]; y = v[1]; tile = map.getTileValue(x, y); if (tile >= Tile.WOODS_LOW && tile <= Tile.WOODS_HIGH) { pos = new Position(x, y); for (dir = Direction.BEGIN; dir < Direction.END; dir = Direction.increment90(dir)) { tile = map.getTileFromMap(pos, dir, TILE_INVALID); if (tile === Tile.RIVER || tile === Tile.CHANNEL) { map.setTileValue(x, y, Tile.REDGE, 0); break; } } } }); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class WorldEffects { constructor(map) { this._map = map; this._data = {}; } toKey(x, y) { return [x, y].join(','); } fromKey(k) { k = k.split(','); return { x: k[0] - 0, y: k[1] - 0 }; } clear() { this._data = []; } getTile(x, y) { let key = this.toKey(x, y); let tile = this._data[key]; if (tile === undefined) tile = this._map.getTile(x, y); return tile; } getTileValue(x, y) { return this.getTile(x, y).getValue(); } setTile(x, y, value, flags) { if (flags !== undefined && value.isTile) throw new Error('Flags supplied with already defined tile'); if (flags === undefined && !value.isTile) value = new Tiles(value);else if (flags !== undefined) value = new Tiles(value, flags); let key = this.toKey(x, y); this._data[key] = value; } apply() { let keys = Object.keys(this._data); for (let i = 0, l = keys.length; i < l; i++) { let coords = this.fromKey(keys[i]); this._map.setTo(coords, this._data[keys[i]]); } } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class BaseTool { constructor() { this.TOOLRESULT_OK = 0; this.TOOLRESULT_FAILED = 1; this.TOOLRESULT_NO_MONEY = 2; this.TOOLRESULT_NEEDS_BULLDOZE = 3; this.autoBulldoze = true; this.bulldozerCost = 1; } init(cost, map, shouldAutoBulldoze, IsDraggable = false) { //Object.defineProperty(this, 'toolCost', MiscUtils.mcd(cost)); this.toolCost = cost; this.result = null; this.isDraggable = IsDraggable; this._shouldAutoBulldoze = shouldAutoBulldoze; this._map = map; this._worldEffects = new WorldEffects(map); this._applicationCost = 0; } clear() { this._applicationCost = 0; this._worldEffects.clear(); } addCost(cost) { this._applicationCost += cost; } doAutoBulldoze(x, y) { //if ( !this._shouldAutoBulldoze ) return; let tile = this._worldEffects.getTile(x, y); if (tile.isBulldozable()) { tile = ZoneUtils.normalizeRoad(tile); if (tile >= Tile.TINYEXP && tile <= Tile.LASTTINYEXP || tile < Tile.HBRIDGE && tile !== Tile.DIRT) { this.addCost(1); this._worldEffects.setTile(x, y, Tile.DIRT); } } } apply(budget) { this._worldEffects.apply(); budget.spend(this._applicationCost); this.clear(); } modifyIfEnoughFunding(budget) { if (this.result !== this.TOOLRESULT_OK) { this.clear(); return false; } if (budget.totalFunds < this._applicationCost) { this.result = this.TOOLRESULT_NO_MONEY; this.clear(); return false; } this.apply.call(this, budget); this.clear(); return true; } setAutoBulldoze(value) { this.autoBulldoze = value; } getAutoBulldoze() { return this.autoBulldoze; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class BaseToolConnector extends BaseTool { constructor() { super(); } fixSingle(x, y) { let adjTile = 0; let tile = this._worldEffects.getTile(x, y); tile = ZoneUtils.normalizeRoad(tile); if (tile >= Tile.ROADS && tile <= Tile.INTERSECTION) { if (y > 0) { tile = this._worldEffects.getTile(x, y - 1); tile = ZoneUtils.normalizeRoad(tile); if ((tile === Tile.HRAILROAD || tile >= Tile.ROADBASE && tile <= Tile.VROADPOWER) && tile !== Tile.HROADPOWER && tile !== Tile.VRAILROAD && tile !== Tile.ROADBASE) adjTile |= 1; } if (x < this._map.width - 1) { tile = this._worldEffects.getTile(x + 1, y); tile = ZoneUtils.normalizeRoad(tile); if ((tile === Tile.VRAILROAD || tile >= Tile.ROADBASE && tile <= Tile.VROADPOWER) && tile !== Tile.VROADPOWER && tile !== Tile.HRAILROAD && tile !== Tile.VBRIDGE) adjTile |= 2; } if (y < this._map.height - 1) { tile = this._worldEffects.getTile(x, y + 1); tile = ZoneUtils.normalizeRoad(tile); if ((tile === Tile.HRAILROAD || tile >= Tile.ROADBASE && tile <= Tile.VROADPOWER) && tile !== Tile.HROADPOWER && tile !== Tile.VRAILROAD && tile !== Tile.ROADBASE) adjTile |= 4; } if (x > 0) { tile = this._worldEffects.getTile(x - 1, y); tile = ZoneUtils.normalizeRoad(tile); if ((tile === Tile.VRAILROAD || tile >= Tile.ROADBASE && tile <= Tile.VROADPOWER) && tile !== Tile.VROADPOWER && tile !== Tile.HRAILROAD && tile !== Tile.VBRIDGE) adjTile |= 8; } this._worldEffects.setTile(x, y, RoadTable[adjTile] | Tile.BULLBIT | Tile.BURNBIT); return; } if (tile >= Tile.LHRAIL && tile <= Tile.LVRAIL10) { if (y > 0) { tile = this._worldEffects.getTile(x, y - 1); tile = ZoneUtils.normalizeRoad(tile); if (tile >= Tile.RAILHPOWERV && tile <= Tile.VRAILROAD && tile !== Tile.RAILHPOWERV && tile !== Tile.HRAILROAD && tile !== Tile.HRAIL) adjTile |= 1; } if (x < this._map.width - 1) { tile = this._worldEffects.getTile(x + 1, y); tile = ZoneUtils.normalizeRoad(tile); if (tile >= Tile.RAILHPOWERV && tile <= Tile.VRAILROAD && tile !== Tile.RAILVPOWERH && tile !== Tile.VRAILROAD && tile !== Tile.VRAIL) adjTile |= 2; } if (y < this._map.height - 1) { tile = this._worldEffects.getTile(x, y + 1); tile = ZoneUtils.normalizeRoad(tile); if (tile >= Tile.RAILHPOWERV && tile <= Tile.VRAILROAD && tile !== Tile.RAILHPOWERV && tile !== Tile.HRAILROAD && tile !== Tile.HRAIL) adjTile |= 4; } if (x > 0) { tile = this._worldEffects.getTile(x - 1, y); tile = ZoneUtils.normalizeRoad(tile); if (tile >= Tile.RAILHPOWERV && tile <= Tile.VRAILROAD && tile !== Tile.RAILVPOWERH && tile !== Tile.VRAILROAD && tile !== Tile.VRAIL) adjTile |= 8; } this._worldEffects.setTile(x, y, RailTable[adjTile] | Tile.BULLBIT | Tile.BURNBIT); return; } if (tile >= Tile.LHPOWER && tile <= Tile.LVPOWER10) { if (y > 0) { tile = this._worldEffects.getTile(x, y - 1); if (tile.isConductive()) { tile = tile.getValue(); tile = ZoneUtils.normalizeRoad(tile); if (tile !== Tile.VPOWER && tile !== Tile.VROADPOWER && tile !== Tile.RAILVPOWERH) adjTile |= 1; } } if (x < this._map.width - 1) { tile = this._worldEffects.getTile(x + 1, y); if (tile.isConductive()) { tile = tile.getValue(); tile = ZoneUtils.normalizeRoad(tile); if (tile !== Tile.HPOWER && tile !== Tile.HROADPOWER && tile !== Tile.RAILHPOWERV) adjTile |= 2; } } if (y < this._map.height - 1) { tile = this._worldEffects.getTile(x, y + 1); if (tile.isConductive()) { tile = tile.getValue(); tile = ZoneUtils.normalizeRoad(tile); if (tile !== Tile.VPOWER && tile !== Tile.VROADPOWER && tile !== Tile.RAILVPOWERH) adjTile |= 4; } } if (x > 0) { tile = this._worldEffects.getTile(x - 1, y); if (tile.isConductive()) { tile = tile.getValue(); tile = ZoneUtils.normalizeRoad(tile); if (tile !== Tile.HPOWER && tile !== Tile.HROADPOWER && tile !== Tile.RAILHPOWERV) adjTile |= 8; } } this._worldEffects.setTile(x, y, WireTable[adjTile] | Tile.BLBNCNBIT); return; } } checkZoneConnections(x, y) { this.fixSingle(x, y); if (y > 0) this.fixSingle(x, y - 1); if (x < this._map.width - 1) this.fixSingle(x + 1, y); if (y < this._map.height - 1) this.fixSingle(x, y + 1); if (x > 0) this.fixSingle(x - 1, y); } checkBorder(x, y, size) { // Adjust to top left tile x = x - 1; y = y - 1; let i; for (i = 0; i < size; i++) this.fixZone(x + i, y - 1); for (i = 0; i < size; i++) this.fixZone(x - 1, y + i); for (i = 0; i < size; i++) this.fixZone(x + i, y + size); for (i = 0; i < size; i++) this.fixZone(x + size, y + i); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class BuildingTool extends BaseToolConnector { constructor(cost, centreTile, map, size, animated) { super(); this.init(cost, map, false); this.centreTile = centreTile; this.size = size; this.animated = animated; } putBuilding(leftX, topY) { let posX, posY, tileValue, tileFlags; let baseTile = this.centreTile - this.size - 1; for (let dy = 0; dy < this.size; dy++) { posY = topY + dy; for (let dx = 0; dx < this.size; dx++) { posX = leftX + dx; tileValue = baseTile; tileFlags = Tile.BNCNBIT; if (dx === 1) { if (dy === 1) tileFlags |= Tile.ZONEBIT;else if (dy === 2 && this.animated) tileFlags |= Tile.ANIMBIT; } this._worldEffects.setTile(posX, posY, tileValue, tileFlags); baseTile++; } } } prepareBuildingSite(leftX, topY) { // Check that the entire site is on the map if (leftX < 0 || leftX + this.size > this._map.width) return this.TOOLRESULT_FAILED; if (topY < 0 || topY + this.size > this._map.height) return this.TOOLRESULT_FAILED; let posX, posY, tileValue; // Check whether the tiles are clear for (let dy = 0; dy < this.size; dy++) { posY = topY + dy; for (let dx = 0; dx < this.size; dx++) { posX = leftX + dx; tileValue = this._worldEffects.getTileValue(posX, posY); if (tileValue === Tile.DIRT) continue; if (!this.autoBulldoze) { // No Tile.DIRT and no bull-dozer => not buildable return this.TOOLRESULT_NEEDS_BULLDOZE; } if (!ZoneUtils.canBulldoze(tileValue)) { // tilevalue cannot be auto-bulldozed return this.TOOLRESULT_NEEDS_BULLDOZE; } this._worldEffects.setTile(posX, posY, Tile.DIRT); this.addCost(this.bulldozerCost); } } return this.TOOLRESULT_OK; } buildBuilding(x, y) { // Correct to top left x--; y--; let prepareResult = this.prepareBuildingSite(x, y); if (prepareResult !== this.TOOLRESULT_OK) return prepareResult; this.addCost(this.toolCost); this.putBuilding(x, y); this.checkBorder(x, y); return this.TOOLRESULT_OK; } doTool(x, y, blockMaps) { this.result = this.buildBuilding(x, y); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class BulldozerTool extends BaseToolConnector { constructor(map) { super(); this.init(10, map, true); } putRubble(x, y, size) { for (let xx = x; xx < x + size; xx++) { for (let yy = y; yy < y + size; yy++) { if (this._map.testBounds(xx, yy)) { let tile = this._worldEffects.getTile(xx, yy); if (tile !== Tile.RADTILE && tile !== Tile.DIRT) { this._worldEffects.setTile(xx, yy, Tile.TINYEXP + math.getRandom(2), Tile.ANIMBIT | Tile.BULLBIT); } } } } } layDoze(x, y) { let tile = this._worldEffects.getTile(x, y); if (!tile.isBulldozable()) return this.TOOLRESULT_FAILED; tile = tile.getValue(); tile = ZoneUtils.normalizeRoad(tile); switch (tile) { case Tile.HBRIDGE: case Tile.VBRIDGE: case Tile.BRWV: case Tile.BRWH: case Tile.HBRDG0: case Tile.HBRDG1: case Tile.HBRDG2: case Tile.HBRDG3: case Tile.VBRDG0: case Tile.VBRDG1: case Tile.VBRDG2: case Tile.VBRDG3: case Tile.HPOWER: case Tile.VPOWER: case Tile.HRAIL: case Tile.VRAIL: this._worldEffects.setTile(x, y, Tile.RIVER); break; default: this._worldEffects.setTile(x, y, Tile.DIRT); break; } this.addCost(1); return this.TOOLRESULT_OK; } doTool(x, y, blockMaps, messageManager) { if (!this._map.testBounds(x, y)) this.result = this.TOOLRESULT_FAILED; let tile = this._worldEffects.getTile(x, y); let tileValue = tile.getValue(); let zoneSize = 0; let deltaX; let deltaY; if (tile.isZone()) { zoneSize = ZoneUtils.checkZoneSize(tileValue); deltaX = 0; deltaY = 0; } else { let result = ZoneUtils.checkBigZone(tileValue); zoneSize = result.zoneSize; deltaX = result.deltaX; deltaY = result.deltaY; } //console.log( zoneSize, deltaX, deltaY ) if (zoneSize > 0) { this.addCost(this.bulldozerCost); //this._map.powerData[ this._map.getId( x, y ) ] = 0; this._map.powered({ v: 1, x: x, y: y }); let centerX = x + deltaX; let centerY = y + deltaY; switch (zoneSize) { case 3: //messageManager.sendMessage(Messages.SOUND_EXPLOSIONHIGH); this.putRubble(centerX - 1, centerY - 1, 3); break; case 4: //messageManager.sendMessage(Messages.SOUND_EXPLOSIONLOW); this.putRubble(centerX - 1, centerY - 1, 4); break; case 6: //messageManager.sendMessage(Messages.SOUND_EXPLOSIONHIGH); //messageManager.sendMessage(Messages.SOUND_EXPLOSIONLOW); this.putRubble(centerX - 1, centerY - 1, 6); break; } this.result = this.TOOLRESULT_OK; } let toolResult; if (tileValue === Tile.RIVER || tileValue === Tile.REDGE || tileValue === Tile.CHANNEL) { toolResult = this.layDoze(x, y); if (tileValue !== this._worldEffects.getTileValue(x, y)) this.addCost(5); } else { toolResult = this.layDoze(x, y); this.checkZoneConnections(x, y); } this.result = toolResult; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class ParkTool extends BaseTool { constructor(map) { super(); this.init(10, map, true); } doTool(x, y, blockMaps) { if (this._worldEffects.getTileValue(x, y) !== Tile.DIRT) { this.result = this.TOOLRESULT_NEEDS_BULLDOZE; return; } let value = math.getRandom(4); let tileFlags = Tile.BURNBIT | Tile.BULLBIT; let tileValue; if (value === 4) { tileValue = Tile.FOUNTAIN; tileFlags |= Tile.ANIMBIT; } else { tileValue = value + Tile.WOODS2; } this._worldEffects.setTile(x, y, tileValue, tileFlags); this.addCost(10); this.result = this.TOOLRESULT_OK; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class RailTool extends BaseToolConnector { constructor(map) { super(); this.init(20, map, true, true); } layRail(x, y) { this.doAutoBulldoze(x, y); let cost = this.toolCost; let tile = this._worldEffects.getTileValue(x, y); tile = ZoneUtils.normalizeRoad(tile); switch (tile) { case Tile.DIRT: this._worldEffects.setTile(x, y, Tile.LHRAIL | Tile.BULLBIT | Tile.BURNBIT); break; case Tile.RIVER: case Tile.REDGE: case Tile.CHANNEL: cost = 100; if (x < this._map.width - 1) { tile = this._worldEffects.getTileValue(x + 1, y); tile = ZoneUtils.normalizeRoad(tile); if (tile == Tile.RAILHPOWERV || tile == Tile.HRAIL || tile >= Tile.LHRAIL && tile <= Tile.HRAILROAD) { this._worldEffects.setTile(x, y, Tile.HRAIL, Tile.BULLBIT); break; } } if (x > 0) { tile = this._worldEffects.getTileValue(x - 1, y); tile = ZoneUtils.normalizeRoad(tile); if (tile == Tile.RAILHPOWERV || tile == Tile.HRAIL || tile > Tile.VRAIL && tile < Tile.VRAILROAD) { this._worldEffects.setTile(x, y, Tile.HRAIL, Tile.BULLBIT); break; } } if (y < this._map.height - 1) { tile = this._worldEffects.getTileValue(x, y + 1); tile = ZoneUtils.normalizeRoad(tile); if (tile == Tile.RAILVPOWERH || tile == Tile.VRAILROAD || tile > Tile.HRAIL && tile < Tile.HRAILROAD) { this._worldEffects.setTile(x, y, Tile.VRAIL, Tile.BULLBIT); break; } } if (y > 0) { tile = this._worldEffects.getTileValue(x, y - 1); tile = ZoneUtils.normalizeRoad(tile); if (tile == Tile.RAILVPOWERH || tile == Tile.VRAILROAD || tile > Tile.HRAIL && tile < Tile.HRAILROAD) { this._worldEffects.setTile(x, y, Tile.VRAIL, Tile.BULLBIT); break; } } return this.TOOLRESULT_FAILED; case Tile.LHPOWER: this._worldEffects.setTile(x, y, Tile.RAILVPOWERH, Tile.CONDBIT | Tile.BURNBIT | Tile.BULLBIT); break; case Tile.LVPOWER: this._worldEffects.setTile(x, y, Tile.RAILHPOWERV, Tile.CONDBIT | Tile.BURNBIT | Tile.BULLBIT); break; case Tile.ROADS: this._worldEffects.setTile(x, y, Tile.VRAILROAD, Tile.BURNBIT | Tile.BULLBIT); break; case Tile.ROADS2: this._worldEffects.setTile(x, y, Tile.HRAILROAD, Tile.BURNBIT | Tile.BULLBIT); break; default: return this.TOOLRESULT_FAILED; } this.addCost(cost); this.checkZoneConnections(x, y); return this.TOOLRESULT_OK; } doTool(x, y, blockMaps) { this.result = this.layRail(x, y); } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class RoadTool extends BaseToolConnector { constructor(map) { super(); this.doTool = function (x, y, blockMaps) { this.result = this.layRoad(x, y); }; this.init(10, map, true, true); } layRoad(x, y) { this.doAutoBulldoze(x, y); let tile = this._worldEffects.getTileValue(x, y); let cost = this.toolCost; switch (tile) { case Tile.DIRT: this._worldEffects.setTile(x, y, Tile.ROADS, Tile.BULLBIT | Tile.BURNBIT); break; case Tile.RIVER: case Tile.REDGE: case Tile.CHANNEL: //console.log('is water') cost = 50; if (x < this._map.width - 1) { tile = this._worldEffects.getTileValue(x + 1, y); tile = ZoneUtils.normalizeRoad(tile); if (tile === Tile.VRAILROAD || tile === Tile.HBRIDGE || tile >= Tile.ROADS && tile <= Tile.HROADPOWER) { this._worldEffects.setTile(x, y, Tile.HBRIDGE, Tile.BULLBIT); break; } } if (x > 0) { tile = this._worldEffects.getTileValue(x - 1, y); tile = ZoneUtils.normalizeRoad(tile); if (tile === Tile.VRAILROAD || tile === Tile.HBRIDGE || tile >= Tile.ROADS && tile <= Tile.INTERSECTION) { this._worldEffects.setTile(x, y, Tile.HBRIDGE, Tile.BULLBIT); break; } } if (y < this._map.height - 1) { tile = this._worldEffects.getTileValue(x, y + 1); tile = ZoneUtils.normalizeRoad(tile); if (tile === Tile.HRAILROAD || tile === Tile.VROADPOWER || tile >= Tile.VBRIDGE && tile <= Tile.INTERSECTION) { this._worldEffects.setTile(x, y, Tile.VBRIDGE, Tile.BULLBIT); break; } } if (y > 0) { tile = this._worldEffects.getTileValue(x, y - 1); tile = ZoneUtils.normalizeRoad(tile); if (tile === Tile.HRAILROAD || tile === Tile.VROADPOWER || tile >= Tile.VBRIDGE && tile <= Tile.INTERSECTION) { this._worldEffects.setTile(x, y, Tile.VBRIDGE, Tile.BULLBIT); break; } } return this.TOOLRESULT_FAILED; case Tile.LHPOWER: this._worldEffects.setTile(x, y, Tile.VROADPOWER | Tile.CONDBIT | Tile.BURNBIT | Tile.BULLBIT); break; case Tile.LVPOWER: this._worldEffects.setTile(x, y, Tile.HROADPOWER | Tile.CONDBIT | Tile.BURNBIT | Tile.BULLBIT); break; case Tile.LHRAIL: this._worldEffects.setTile(x, y, Tile.HRAILROAD | Tile.BURNBIT | Tile.BULLBIT); break; case Tile.LVRAIL: this._worldEffects.setTile(x, y, Tile.VRAILROAD | Tile.BURNBIT | Tile.BULLBIT); break; default: return this.TOOLRESULT_FAILED; } this.addCost(cost); this.checkZoneConnections(x, y); return this.TOOLRESULT_OK; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class QueryTool extends BaseTool { constructor(map) { super(); this.init(0, map, false, false); this.txt = ""; } classifyPopulationDensity(x, y, blockMaps) { var density = blockMaps.populationDensityMap.worldGet(x, y); //if (debug) document.getElementById("queryDensityRaw").innerHTML=density; density = density >> 6; density = density & 3; this.txt += 'Density: ' + TXT.densityStrings[density] + '
'; //document.getElementById("queryDensity").innerHTML=TXT.densityStrings[density]; } classifyLandValue(x, y, blockMaps) { var landValue = blockMaps.landValueMap.worldGet(x, y); //if (debug) document.getElementById("queryLandValueRaw").innerHTML=landValue; var i = 0; if (landValue >= 150) i = 3;else if (landValue >= 80) i = 2;else if (landValue >= 30) i = 1; //var text = TXT.landValueStrings[i]; this.txt += 'Value: ' + TXT.landValueStrings[i] + '
'; //document.getElementById("queryLandValue").innerHTML=text; } classifyCrime(x, y, blockMaps) { var crime = blockMaps.crimeRateMap.worldGet(x, y); //if (debug) document.getElementById("queryCrimeRaw").innerHTML=crime; crime = crime >> 6; crime = crime & 3; this.txt += 'Crime: ' + TXT.crimeStrings[crime] + '
'; //document.getElementById("queryCrime").innerHTML=TXT.crimeStrings[crime]; } classifyPollution(x, y, blockMaps) { var pollution = blockMaps.pollutionDensityMap.worldGet(x, y); //if (debug) document.getElementById("queryPollutionRaw").innerHTML=pollution; pollution = pollution >> 6; pollution = pollution & 3; this.txt += 'Pollution: ' + TXT.pollutionStrings[pollution] + '
'; //document.getElementById("queryPollution").innerHTML=TXT.pollutionStrings[pollution]; } classifyRateOfGrowth(x, y, blockMaps) { var rate = blockMaps.rateOfGrowthMap.worldGet(x, y); //if (debug) document.getElementById("queryRateRaw").innerHTML=rate; rate = rate >> 6; rate = rate & 3; this.txt += 'Growth: ' + TXT.rateStrings[rate]; //document.getElementById("queryRate").innerHTML=TXT.rateStrings[rate]; } classifyDebug(x, y, blockMaps) { /*document.getElementById("queryFireStationRaw").innerHTML=blockMaps.fireStationMap.worldGet(x, y); document.getElementById("queryFireStationEffectRaw").innerHTML=blockMaps.fireStationEffectMap.worldGet(x, y); document.getElementById("queryPoliceStationRaw").innerHTML=blockMaps.policeStationMap.worldGet(x, y); document.getElementById("queryPoliceStationEffectRaw").innerHTML=blockMaps.policeStationEffectMap.worldGet(x, y); document.getElementById("queryTerrainDensityRaw").innerHTML=blockMaps.terrainDensityMap.worldGet(x, y); document.getElementById("queryTrafficDensityRaw").innerHTML=blockMaps.trafficDensityMap.worldGet(x, y); document.getElementById("queryComRateRaw").innerHTML=blockMaps.comRateMap.worldGet(x, y);*/ } classifyZone(x, y) { var baseTiles = [Tile.DIRT, Tile.RIVER, Tile.TREEBASE, Tile.RUBBLE, Tile.FLOOD, Tile.RADTILE, Tile.FIRE, Tile.ROADBASE, Tile.POWERBASE, Tile.RAILBASE, Tile.RESBASE, Tile.COMBASE, Tile.INDBASE, Tile.PORTBASE, Tile.AIRPORTBASE, Tile.COALBASE, Tile.FIRESTBASE, Tile.POLICESTBASE, Tile.STADIUMBASE, Tile.NUCLEARBASE, Tile.HBRDG0, Tile.RADAR0, Tile.FOUNTAIN, Tile.INDBASE2, Tile.FOOTBALLGAME1, Tile.VBRDG0, 952]; var tileValue = this._map.getTileValue(x, y); if (tileValue >= Tile.COALSMOKE1 && tileValue < Tile.FOOTBALLGAME1) tileValue = Tile.COALBASE; var index = 0, l; for (index = 0, l = baseTiles.length - 1; index < l; index++) { if (tileValue < baseTiles[index + 1]) break; } this.txt = 'Zone: ' + TXT.zoneTypes[index] + '
'; //document.getElementById("queryZoneType").innerHTML=TXT.zoneTypes[index]; } getInfo() { return this.txt; } doTool(x, y, blockMaps, messageManager) { var text = 'Position (' + x + ', ' + y + ')'; text += ' TileValue: ' + this._map.getTileValue(x, y); { this._map.getTile(x, y); /*document.getElementById("queryTile").innerHTML=[x,y].join(', '); document.getElementById("queryTileValue").innerHTML=tile.getValue(); document.getElementById("queryTileBurnable").innerHTML=tile.isCombustible(); document.getElementById("queryTileBulldozable").innerHTML=tile.isBulldozable(); document.getElementById("queryTileCond").innerHTML=tile.isConductive(); document.getElementById("queryTileAnim").innerHTML=tile.isAnimated(); document.getElementById("queryTilePowered").innerHTML=tile.isPowered();*/ } this.classifyZone(x, y); this.classifyPopulationDensity(x, y, blockMaps); this.classifyLandValue(x, y, blockMaps); this.classifyCrime(x, y, blockMaps); this.classifyPollution(x, y, blockMaps); this.classifyRateOfGrowth(x, y, blockMaps); this.classifyDebug(x, y, blockMaps); messageManager.sendMessage(Messages.QUERY_WINDOW_NEEDED); this.result = this.TOOLRESULT_OK; } } /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. * * This code is released under the GNU GPL v3, with some additional terms. * Please see the files LICENSE and COPYING for details. Alternatively, * consult http://micropolisjs.graememcc.co.uk/LICENSE and * http://micropolisjs.graememcc.co.uk/COPYING * */ class WireTool extends BaseToolConnector { constructor(map) { super(); this.init(5, map, true, true); } layWire(x, y) { this.doAutoBulldoze(x, y); let cost = 5; let tile = this._worldEffects.getTileValue(x, y); tile = ZoneUtils.normalizeRoad(tile); switch (tile) { case Tile.DIRT: this._worldEffects.setTile(x, y, Tile.LHPOWER, Tile.CONDBIT | Tile.BURNBIT | Tile.BULLBIT); break; case Tile.RIVER: case Tile.REDGE: case Tile.CHANNEL: cost = 25; if (x < this._map.width - 1) { tile = this._worldEffects.getTile(x + 1, y); if (tile.isConductive()) { tile = tile.getValue(); tile = ZoneUtils.normalizeRoad(tile); if (tile != Tile.HROADPOWER && tile != Tile.RAILHPOWERV && tile != Tile.HPOWER) { this._worldEffects.setTile(x, y, Tile.VPOWER, Tile.CONDBIT | Tile.BULLBIT); break; } } } if (x > 0) { tile = this._worldEffects.getTile(x - 1, y); if (tile.isConductive()) { tile = tile.getValue(); tile = ZoneUtils.normalizeRoad(tile); if (tile != Tile.HROADPOWER && tile != Tile.RAILHPOWERV && tile != Tile.HPOWER) { this._worldEffects.setTile(x, y, Tile.VPOWER, Tile.CONDBIT | Tile.BULLBIT); break; } } } if (y < this._map.height - 1) { tile = this._worldEffects.getTile(x, y + 1); if (tile.isConductive()) { tile = tile.getValue(); tile = ZoneUtils.normalizeRoad(tile); if (tile != Tile.VROADPOWER && tile != Tile.RAILVPOWERH && tile != Tile.VPOWER) { this._worldEffects.setTile(x, y, Tile.HPOWER, Tile.CONDBIT | Tile.BULLBIT); break; } } } if (y > 0) { tile = this._worldEffects.getTile(x, y - 1); if (tile.isConductive()) { tile = tile.getValue(); tile = ZoneUtils.normalizeRoad(tile); if (tile != Tile.VROADPOWER && tile != Tile.RAILVPOWERH && tile != Tile.VPOWER) { this._worldEffects.setTile(x, y, Tile.HPOWER, Tile.CONDBIT | Tile.BULLBIT); break; } } } return this.TOOLRESULT_FAILED; case Tile.ROADS: this._worldEffects.setTile(x, y, Tile.HROADPOWER, Tile.CONDBIT | Tile.BURNBIT | Tile.BULLBIT); break; case Tile.ROADS2: this._worldEffects.setTile(x, y, Tile.VROADPOWER, Tile.CONDBIT | Tile.BURNBIT | Tile.BULLBIT); break; case Tile.LHRAIL: this._worldEffects.setTile(x, y, Tile.RAILHPOWERV, Tile.CONDBIT | Tile.BURNBIT | Tile.BULLBIT); break; case Tile.LVRAIL: this._worldEffects.setTile(x, y, Tile.RAILVPOWERH, Tile.CONDBIT | Tile.BURNBIT | Tile.BULLBIT); break; default: return this.TOOLRESULT_FAILED; } this.addCost(cost); this.checkZoneConnections(x, y); return this.TOOLRESULT_OK; } doTool(x, y, blockMaps) { this.result = this.layWire(x, y); } } const postMessage = self.webkitPostMessage || self.postMessage; var Game; var isWorker = true; var returnMessage; //var ab = new ArrayBuffer( 1 ); //CityGame.post( ab, [ab] ); var trans = false; // ( ab.byteLength === 0 ); self.onmessage = function (e) { CityGame.message(e); }; class CityGame { static message(e) { var p = e.data.tell; if (p == "INIT") { if (e.data.returnMessage) { returnMessage = e.data.returnMessage; isWorker = false; } Game = new MainGame(e.data.timestep); } if (p == "NEWMAP") Game.newMap(); if (p == "PLAYMAP") Game.playMap(); if (p == "TOOL") Game.tool(e.data.name); if (p == "MAPCLICK") Game.mapClick(e.data.x, e.data.y, e.data.single || false); //if( p == "DESTROY" ) Game.destroy(e.data.x, e.data.y); //if( p == "RUN" && trans) updateTrans(e.data); if (p == "DIFFICULTY") Game.changeDifficulty(e.data.n); if (p == "SPEED") Game.changeSpeed(e.data.n); if (p == "BUDGET") Game.handleBudgetRequest(); if (p == "NEWBUDGET") Game.setBudget(e.data.budgetData); if (p == "DISASTER") Game.setDisaster(e.data.disaster); if (p == "EVAL") Game.getEvaluation(); if (p == "SAVEGAME") Game.saveGame(e.data.saveCity); if (p == "LOADGAME") Game.loadGame(e.data.isStart); if (p == "MAKELOADGAME") Game.makeLoadGame(e.data.savegame, e.data.isStart); } static post(e, buffer) { if (isWorker) postMessage(e, buffer);else returnMessage({ data: e }); } } var update = function () { Game.tick(); }; class MainGame { constructor(timestep) { this.timestep = timestep; this.mapSize = [128, 128]; this.difficulty = 1; this.speed = 2; this.oldSpeed = 0; this.mapGen = new MapGenerator(); this.simulation = null; this.gameTools = null; this.animationManager = null; this.map = null; this.isPaused = false; this.simNeededBudget = false; this.currentTool = null; this.timer = null; this.infos = []; this.sprites = []; this.spritesData = null; this.animsData = null; //this.tilesData = null; this.spritesData = []; this.power = null; CityGame.post({ tell: "READY" }); } next(delay = 0) { this.timer = setTimeout(update, delay); } stop() { if (this.timer === null) return; clearInterval(this.timer); this.timer = null; } tick() { //if ( this.isPaused ) return let up = this.simulation.simTick(); if (up) { this.infos = this.simulation.infos; this.processMessages(Game.simulation.messageManager.getMessages()); this.animatedTiles(); this.simulation.spriteManager.moveObjects(); this.calculateSprites(); CityGame.post({ tell: "RUN", infos: this.infos, tilesData: this.map.tilesData, powerData: this.map.powerData, sprites: this.spritesData, layer: this.map.layer }); this.map.resetLayer(); } this.next(); } newMap() { this.map = this.mapGen.construct(this.mapSize[0], this.mapSize[1]); CityGame.post({ tell: "NEWMAP", tilesData: this.map.tilesData, mapSize: this.mapSize, island: this.map.isIsland, trans: trans }); } playMap(loading) { var messageMgr = new MessageManager(); var money = 20000; if (this.difficulty == 1) money = 10000; if (this.difficulty == 2) money = 5000; this.gameTools = { airport: new BuildingTool(10000, Tile.AIRPORT, this.map, 6, false), bulldozer: new BulldozerTool(this.map), coal: new BuildingTool(3000, Tile.POWERPLANT, this.map, 4, false), commercial: new BuildingTool(100, Tile.COMCLR, this.map, 3, false), fire: new BuildingTool(500, Tile.FIRESTATION, this.map, 3, false), industrial: new BuildingTool(100, Tile.INDCLR, this.map, 3, false), nuclear: new BuildingTool(5000, Tile.NUCLEAR, this.map, 4, true), park: new ParkTool(this.map), police: new BuildingTool(500, Tile.POLICESTATION, this.map, 3, false), port: new BuildingTool(3000, Tile.PORT, this.map, 4, false), rail: new RailTool(this.map), residential: new BuildingTool(100, Tile.FREEZ, this.map, 3, false), road: new RoadTool(this.map), query: new QueryTool(this.map), stadium: new BuildingTool(5000, Tile.STADIUM, this.map, 4, false), wire: new WireTool(this.map) }; this.animationManager = new AnimationManager(this.map); if (loading) { money = this.savedGame.totalFunds; //this.infos[3] = this.savedGame.totalPop; this.speed = this.savedGame.speed; this.difficulty = this.savedGame.difficulty; this.simulation = new Simulation(this.map, this.difficulty, this.speed, true, this.savedGame); //this.processMessages(Messages.EVAL_UPDATED); messageMgr.sendMessage(Messages.WELCOMEBACK); } else { this.simulation = new Simulation(this.map, this.difficulty, this.speed, true); messageMgr.sendMessage(Messages.WELCOME); } this.simulation.budget.setFunds(money); //messageMgr.sendMessage(Messages.FUNDS_CHANGED, money); this.processMessages(messageMgr.getMessages()); // update simulation time this.isPaused = false; //if(!trans) //this.timer = setInterval(update, 1000/this.timestep); //this.timer = setInterval(update, 0); //else update(); this.tick(); //this.next() } /*changeTimeStep (n){ clearInterval(this.timer); this.next() //this.timestep = n; //this.timer = setInterval(update, 1000/this.timestep) //this.timer = setInterval(update, 0); }*/ changeSpeed(n) { // 0:pause 1:slow 2:medium 3:fast this.speed = n; this.simulation.setSpeed(this.speed); if (this.speed === 0) { this.isPaused = true; this.stop(); } else { if (this.isPaused) { this.isPaused = false; this.stop(); this.tick(); } } /*if(this.speed === 4){ this.simulation.setSpeed(this.speed-1); } else { this.simulation.setSpeed(this.speed); }*/ } changeDifficulty(n) { // 0: easy 1: medium 2: hard this.difficulty = n; if (this.simulation) this.simulation.setDifficulty(this.difficulty); } animatedTiles() { var animTiles = this.animationManager.getTiles(0, 0, this.mapSize[0] + 1, this.mapSize[1] + 1, this.isPaused); var i = animTiles.length; this.animsData = new Micro.M_ARRAY_TYPE(i); while (i--) { var tile = animTiles[i]; this.animsData[i] = [tile.tileValue, tile.x, tile.y]; } } calculateSprites() { this.sprites = this.simulation.spriteManager.getSpritesInView(0, 0, this.mapSize[0] + 1, this.mapSize[1] + 1); var i = this.sprites.length; //this.spritesData = new M_ARRAY_TYPE(i); while (i--) { var sprite = this.sprites[i]; this.spritesData[i] = [sprite.type, sprite.frame, sprite.x || 0, sprite.y || 0]; } } processMessages(messages) { var messageOutput = false; for (var i = 0, l = messages.length; i < l; i++) { var m = messages[i]; switch (m.message) { case Messages.BUDGET_NEEDED: this.simNeededBudget = true; this.handleBudgetRequest(); break; case Messages.QUERY_WINDOW_NEEDED: CityGame.post({ tell: "QUERY", queryTxt: this.currentTool.getInfo() }); break; //case Messages.DATE_UPDATED: this.infos[0] = [TXT.months[ m.data.month ], m.data.year].join(' '); break; // case Messages.EVAL_UPDATED: this.infos[1] = TXT.cityClass[m.data.classification]; this.infos[2] = m.data.score; this.infos[3] = m.data.population; break; //case Messages.FUNDS_CHANGED: this.infos[4] = m.data; break; //case Messages.VALVES_UPDATED: this.infos[5] = m.data.residential; this.infos[6] = m.data.commercial; this.infos[7] = m.data.industrial; break; default: if (!messageOutput && TXT.goodMessages[m.message] !== undefined) { this.infos[8] = TXT.goodMessages[m.message]; break; } if (!messageOutput && TXT.badMessages[m.message] !== undefined) { messageOutput = true; this.infos[8] = TXT.badMessages[m.message]; break; } if (!messageOutput && TXT.neutralMessages[m.message] !== undefined) { messageOutput = true; this.infos[8] = TXT.neutralMessages[m.message]; break; } } } } tool(name) { if (this.currentTool !== null) this.currentTool.clear(); if (name !== "none") this.currentTool = this.gameTools[name];else this.currentTool = null; } destroy(x, y) { console.log('isDestroy'); //console.log( 'destuct ', x, y ) //this.mapClick(x,y); //this.map.powerData[this.findId(x,y)] = 1; // this.simulation.powerManager.setTilePower(x,y); // var messageMgr = new Micro.MessageManager(); // this.gameTools["bulldozer"].doTool(x, y, messageMgr, this.simulation.blockMaps ); } findId(x, y) { var id = x + y * this.mapSize[1]; return id; } mapClick(x, y, single) { if (this.currentTool !== null) { //console.log(this.currentTool[0]) var budget = this.simulation.budget; this.simulation.evaluation; var messageMgr = new MessageManager(); this.currentTool.doTool(x, y, this.simulation.blockMaps, messageMgr); this.currentTool.modifyIfEnoughFunding(budget, messageMgr); switch (this.currentTool.result) { case this.currentTool.TOOLRESULT_NEEDS_BULLDOZE: TXT.toolMessages.needsDoze; break; case this.currentTool.TOOLRESULT_NO_MONEY: TXT.toolMessages.noMoney; break; default: if (!single) CityGame.post({ tell: "BUILD", x: x, y: y }); break; } this.processMessages(messageMgr.getMessages()); } } setDisaster(disaster) { if (disaster === Micro.DISASTER_NONE) return; var m = new MessageManager(); switch (disaster) { case Micro.DISASTER_MONSTER: this.simulation.spriteManager.makeMonster(m); break; case Micro.DISASTER_FIRE: this.simulation.disasterManager.makeFire(m); break; case Micro.DISASTER_FLOOD: this.simulation.disasterManager.makeFlood(m); break; case Micro.DISASTER_CRASH: this.simulation.disasterManager.makeCrash(m); break; case Micro.DISASTER_MELTDOWN: this.simulation.disasterManager.makeMeltdown(m); break; case Micro.DISASTER_TORNADO: this.simulation.spriteManager.makeTornado(m); break; } this.processMessages(m.getMessages()); } setBudget(budgetData) { this.simulation.budget.cityTax = budgetData[0]; this.simulation.budget.roadPercent = budgetData[1] / 100; this.simulation.budget.firePercent = budgetData[2] / 100; this.simulation.budget.policePercent = budgetData[3] / 100; } handleBudgetRequest() { this.budgetShowing = true; let budgetData = { roadFund: this.simulation.budget.roadFund, roadRate: Math.floor(this.simulation.budget.roadPercent * 100), fireFund: this.simulation.budget.fireFund, fireRate: Math.floor(this.simulation.budget.firePercent * 100), policeFund: this.simulation.budget.policeFund, policeRate: Math.floor(this.simulation.budget.policePercent * 100), taxRate: this.simulation.budget.cityTax, totalFunds: this.simulation.budget.totalFunds, taxesCollected: this.simulation.budget.taxFund }; CityGame.post({ tell: "BUDGET", budgetData: budgetData }); if (this.simNeededBudget) { this.simulation.budget.doBudgetWindow(); this.simNeededBudget = false; } else { this.simulation.budget.updateFundEffects(); } //this.budgetWindow.open(this.handleBudgetClosed.bind(this), budgetData); // Let the input know we handled this request //this.inputStatus.budgetHandled(); } getEvaluation() { let evaluation = this.simulation.evaluation; let problemes = ""; for (var i = 0; i < 4; i++) { let problemNo = evaluation.getProblemNumber(i); let text = ''; if (problemNo !== -1) text = TXT.problems[problemNo]; problemes += text + "
"; } let evalData = [evaluation.cityYes, problemes]; CityGame.post({ tell: "EVAL", evalData: evalData }); } //______________________________________ SAVE saveGame(cityData) { //this.oldSpeed = this.speed; //this.changeSpeed(0); let gameData = { name: "Yoooooo", everClicked: true }; gameData.speed = this.speed; gameData.difficulty = this.difficulty; gameData.version = Micro.CURRENT_VERSION; gameData.city = cityData; this.simulation.save(gameData); gameData = JSON.stringify(gameData); CityGame.post({ tell: "SAVEGAME", gameData: gameData, key: Micro.KEY }); //this.changeSpeed(this.oldSpeed); } /*makeSaveGame : function(gameData){ gameData.version = Micro.CURRENT_VERSION; gameData = JSON.stringify(gameData); }*/ //______________________________________ LOAD loadGame(atStart) { var isStart = atStart || false; CityGame.post({ tell: "LOADGAME", key: Micro.KEY, isStart: isStart }); } makeLoadGame(gameData, atStart) { let isStart = atStart || false; clearInterval(this.timer); this.savedGame = JSON.parse(gameData); //this.simulation.load(this.savedGame); //this.map = this.simulation.map; // this.everClicked = savedGame.everClicked; //if (savedGame.version !== Micro.CURRENT_VERSION) this.transitionOldSave(savedGame); //savedGame.isSavedGame = true; /*if(this.map){ this.map.load(this.savedGame); }else{*/ this.map = new GameMap(Micro.MAP_WIDTH, Micro.MAP_HEIGHT); this.map.load(this.savedGame); //} // //this.playMap(true); //this.simulation.map = this.map;//return // //this.map = this.simulation.map; CityGame.post({ tell: "FULLREBUILD", tilesData: this.map.tilesData, mapSize: this.mapSize, island: this.map.isIsland, cityData: this.savedGame.city, isStart: isStart }); } transitionOldSave(savedGame) { switch (savedGame.version) { case 1: savedGame.everClicked = false; /* falls through */ case 2: savedGame.pollutionMaxX = Math.floor(savedGame.width / 2); savedGame.pollutionMaxY = Math.floor(savedGame.height / 2); savedGame.cityCentreX = Math.floor(savedGame.width / 2); savedGame.cityCentreY = Math.floor(savedGame.height / 2); break; //default: throw new Error('Unknown save version!'); } } } exports.CityGame = CityGame; exports.MainGame = MainGame; Object.defineProperty(exports, '__esModule', { value: true }); })));