├── .gitignore ├── .vscode └── tasks.json ├── COPYING ├── LICENSE ├── README.md ├── about.html ├── css ├── FONT_LICENSE.markdown ├── chunk.woff └── style.css ├── images ├── dirtbg.png ├── sprites.png ├── tiles.png └── tilessnow.png ├── index.html ├── jest.config.js ├── package.json ├── sprites ├── obj1-0.png ├── obj1-1.png ├── obj1-2.png ├── obj1-3.png ├── obj1-4.png ├── obj2-0.png ├── obj2-1.png ├── obj2-2.png ├── obj2-3.png ├── obj2-4.png ├── obj2-5.png ├── obj2-6.png ├── obj2-7.png ├── obj3-0.png ├── obj3-1.png ├── obj3-10.png ├── obj3-2.png ├── obj3-3.png ├── obj3-4.png ├── obj3-5.png ├── obj3-6.png ├── obj3-7.png ├── obj3-8.png ├── obj3-9.png ├── obj4-0.png ├── obj4-1.png ├── obj4-2.png ├── obj4-3.png ├── obj4-4.png ├── obj4-5.png ├── obj4-6.png ├── obj4-7.png ├── obj5-0.png ├── obj5-1.png ├── obj5-10.png ├── obj5-11.png ├── obj5-12.png ├── obj5-13.png ├── obj5-14.png ├── obj5-15.png ├── obj5-2.png ├── obj5-3.png ├── obj5-4.png ├── obj5-5.png ├── obj5-6.png ├── obj5-7.png ├── obj5-8.png ├── obj5-9.png ├── obj6-0.png ├── obj6-1.png ├── obj6-2.png ├── obj7-0.png ├── obj7-1.png ├── obj7-2.png ├── obj7-3.png ├── obj7-4.png ├── obj7-5.png ├── obj8-0.png ├── obj8-1.png ├── obj8-2.png └── obj8-3.png ├── src ├── airplaneSprite.js ├── animationManager.js ├── baseSprite.js ├── baseTool.js ├── blockMap.ts ├── blockMapUtils.js ├── boatSprite.js ├── bounds.ts ├── budget.js ├── budgetWindow.js ├── buildingTool.js ├── bulldozerTool.js ├── census.js ├── commercial.js ├── config.js ├── congratsWindow.js ├── connectingTool.js ├── connector.js ├── copterSprite.js ├── debugAssert.ts ├── debugWindow.js ├── direction.ts ├── disasterManager.js ├── disasterWindow.js ├── emergencyServices.js ├── evaluation.js ├── evaluationWindow.js ├── eventEmitter.js ├── explosionSprite.js ├── game.js ├── gameCanvas.js ├── gameMap.js ├── gameTools.js ├── industrial.js ├── infoBar.js ├── inputStatus.js ├── mapGenerator.js ├── mapScanner.js ├── messages.ts ├── micropolis.js ├── miscTiles.js ├── miscUtils.js ├── modalWindow.js ├── monsterSprite.js ├── monsterTV.js ├── mouseBox.js ├── nagWindow.js ├── notification.js ├── parkTool.js ├── position.ts ├── powerManager.js ├── queryTool.js ├── queryWindow.js ├── railTool.js ├── random.ts ├── rci.js ├── repairManager.js ├── residential.js ├── road.js ├── roadTool.js ├── saveWindow.js ├── screenshotLinkWindow.js ├── screenshotWindow.js ├── settingsWindow.js ├── simulation.js ├── splashCanvas.js ├── splashScreen.js ├── spriteConstants.ts ├── spriteManager.js ├── spriteUtils.js ├── stadia.js ├── storage.js ├── text.js ├── tile.ts ├── tileFlags.ts ├── tileHistory.js ├── tileSet.js ├── tileSetSnowURI.ts ├── tileSetURI.ts ├── tileUtils.js ├── tileValues.ts ├── tornadoSprite.js ├── touchWarnWindow.js ├── traffic.js ├── trainSprite.js ├── transport.js ├── valves.js ├── wireTool.js ├── worldEffects.js └── zoneUtils.js ├── test ├── blockMap.ts ├── bounds.ts ├── debugAssert.ts ├── direction.ts ├── position.ts ├── random.ts └── tile.ts ├── thirdparty └── jquery │ ├── MIT-LICENSE.txt │ ├── jquery-1.7.2.min.js │ ├── jquery-2.0.3.min.js │ └── jquery-2.1.1.min.js ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | coverage/ 4 | dist/ 5 | node_modules/ 6 | TODO 7 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | } 11 | }, 12 | { 13 | "type": "npm", 14 | "script": "start", 15 | "problemMatcher": [] 16 | }, 17 | { 18 | "type": "npm", 19 | "script": "test", 20 | "group": { 21 | "kind": "test", 22 | "isDefault": true 23 | } 24 | }, 25 | { 26 | "type": "npm", 27 | "script": "test:watch", 28 | "problemMatcher": [] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | micropolisJS. 2 | 3 | Copyright (C) Graeme McCutcheon, 2013. 4 | 5 | Adapted from the original Micropolis source code release, and so released 6 | under the GNU General Public License V3. 7 | 8 | ADDITIONAL TERMS per GNU GPL Section 7 9 | 10 | No trademark or publicity rights are granted. This license does NOT 11 | give you any right, title or interest in the trademark SimCity or any 12 | other Electronic Arts trademark. You may not distribute any 13 | modification of this program using the trademark SimCity or claim any 14 | affliation or association with Electronic Arts Inc. or its employees. 15 | 16 | Any propagation or conveyance of this program must include this 17 | copyright notice and these terms. 18 | 19 | If you convey this program (or any modifications of it) and assume 20 | contractual liability for the program to recipients of it, you agree 21 | to indemnify Electronic Arts for any liability that those contractual 22 | assumptions impose on Electronic Arts. 23 | 24 | You may not misrepresent the origins of this program; modified 25 | versions of the program must be marked as such and not identified as 26 | the original program. 27 | 28 | This disclaimer supplements the one included in the General Public 29 | License. TO THE FULLEST EXTENT PERMISSIBLE UNDER APPLICABLE LAW, THIS 30 | PROGRAM IS PROVIDED TO YOU "AS IS," WITH ALL FAULTS, WITHOUT WARRANTY 31 | OF ANY KIND, AND YOUR USE IS AT YOUR SOLE RISK. THE ENTIRE RISK OF 32 | SATISFACTORY QUALITY AND PERFORMANCE RESIDES WITH YOU. ELECTRONIC ARTS 33 | DISCLAIMS ANY AND ALL EXPRESS, IMPLIED OR STATUTORY WARRANTIES, 34 | INCLUDING IMPLIED WARRANTIES OF MERCHANTABILITY, SATISFACTORY QUALITY, 35 | FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT OF THIRD PARTY 36 | RIGHTS, AND WARRANTIES (IF ANY) ARISING FROM A COURSE OF DEALING, 37 | USAGE, OR TRADE PRACTICE. ELECTRONIC ARTS DOES NOT WARRANT AGAINST 38 | INTERFERENCE WITH YOUR ENJOYMENT OF THE PROGRAM; THAT THE PROGRAM WILL 39 | MEET YOUR REQUIREMENTS; THAT OPERATION OF THE PROGRAM WILL BE 40 | UNINTERRUPTED OR ERROR-FREE, OR THAT THE PROGRAM WILL BE COMPATIBLE 41 | WITH THIRD PARTY SOFTWARE OR THAT ANY ERRORS IN THE PROGRAM WILL BE 42 | CORRECTED. NO ORAL OR WRITTEN ADVICE PROVIDED BY ELECTRONIC ARTS OR 43 | ANY AUTHORIZED REPRESENTATIVE SHALL CREATE A WARRANTY. SOME 44 | JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF OR LIMITATIONS ON IMPLIED 45 | WARRANTIES OR THE LIMITATIONS ON THE APPLICABLE STATUTORY RIGHTS OF A 46 | CONSUMER, SO SOME OR ALL OF THE ABOVE EXCLUSIONS AND LIMITATIONS MAY 47 | NOT APPLY TO YOU. 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | micropolisJS 2 | ============ 3 | 4 | https://www.graememcc.co.uk/micropolisJS 5 | 6 | A port of Micropolis to JS/HTML5. Licensed under the GPLv3, with some additional terms - please be mindful of these. 7 | -------------------------------------------------------------------------------- /css/FONT_LICENSE.markdown: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Meredith Mandel , with Reserved Font Name: "Chunk". 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | Version 1.1 - 26 February 2007 8 | 9 | 10 | SIL Open Font License 11 | ==================================================== 12 | 13 | 14 | Preamble 15 | ---------- 16 | 17 | The goals of the Open Font License (OFL) are to stimulate worldwide 18 | development of collaborative font projects, to support the font creation 19 | efforts of academic and linguistic communities, and to provide a free and 20 | open framework in which fonts may be shared and improved in partnership 21 | with others. 22 | 23 | The OFL allows the licensed fonts to be used, studied, modified and 24 | redistributed freely as long as they are not sold by themselves. The 25 | fonts, including any derivative works, can be bundled, embedded, 26 | redistributed and/or sold with any software provided that any reserved 27 | names are not used by derivative works. The fonts and derivatives, 28 | however, cannot be released under any other type of license. The 29 | requirement for fonts to remain under this license does not apply 30 | to any document created using the fonts or their derivatives. 31 | 32 | Definitions 33 | ------------- 34 | 35 | `"Font Software"` refers to the set of files released by the Copyright 36 | Holder(s) under this license and clearly marked as such. This may 37 | include source files, build scripts and documentation. 38 | 39 | `"Reserved Font Name"` refers to any names specified as such after the 40 | copyright statement(s). 41 | 42 | `"Original Version"` refers to the collection of Font Software components as 43 | distributed by the Copyright Holder(s). 44 | 45 | `"Modified Version"` refers to any derivative made by adding to, deleting, 46 | or substituting -- in part or in whole -- any of the components of the 47 | Original Version, by changing formats or by porting the Font Software to a 48 | new environment. 49 | 50 | `"Author"` refers to any designer, engineer, programmer, technical 51 | writer or other person who contributed to the Font Software. 52 | 53 | Permission & Conditions 54 | ------------------------ 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining 57 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 58 | redistribute, and sell modified and unmodified copies of the Font 59 | Software, subject to the following conditions: 60 | 61 | 1. Neither the Font Software nor any of its individual components, 62 | in Original or Modified Versions, may be sold by itself. 63 | 64 | 2. Original or Modified Versions of the Font Software may be bundled, 65 | redistributed and/or sold with any software, provided that each copy 66 | contains the above copyright notice and this license. These can be 67 | included either as stand-alone text files, human-readable headers or 68 | in the appropriate machine-readable metadata fields within text or 69 | binary files as long as those fields can be easily viewed by the user. 70 | 71 | 3. No Modified Version of the Font Software may use the Reserved Font 72 | Name(s) unless explicit written permission is granted by the corresponding 73 | Copyright Holder. This restriction only applies to the primary font name as 74 | presented to the users. 75 | 76 | 4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font 77 | Software shall not be used to promote, endorse or advertise any 78 | Modified Version, except to acknowledge the contribution(s) of the 79 | Copyright Holder(s) and the Author(s) or with their explicit written 80 | permission. 81 | 82 | 5. The Font Software, modified or unmodified, in part or in whole, 83 | must be distributed entirely under this license, and must not be 84 | distributed under any other license. The requirement for fonts to 85 | remain under this license does not apply to any document created 86 | using the Font Software. 87 | 88 | Termination 89 | ----------- 90 | 91 | This license becomes null and void if any of the above conditions are 92 | not met. 93 | 94 | 95 | DISCLAIMER 96 | 97 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 98 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 99 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 100 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 101 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 102 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 103 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 104 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 105 | OTHER DEALINGS IN THE FONT SOFTWARE. 106 | -------------------------------------------------------------------------------- /css/chunk.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/css/chunk.woff -------------------------------------------------------------------------------- /images/dirtbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/images/dirtbg.png -------------------------------------------------------------------------------- /images/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/images/sprites.png -------------------------------------------------------------------------------- /images/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/images/tiles.png -------------------------------------------------------------------------------- /images/tilessnow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/images/tilessnow.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: [ 4 | "src/**/*.ts", 5 | "test/**/*.ts", 6 | ], 7 | coverageReporters: ["json", "lcov", "text", "html"], 8 | moduleFileExtensions: [ 9 | "ts", 10 | "js", 11 | ], 12 | roots: [ 13 | "/src", 14 | "/test", 15 | ], 16 | testEnvironment: "node", 17 | testMatch: ["**/test/*.ts", "**/test/**/*.ts"], 18 | transform: { 19 | "^.+\\.ts$": "ts-jest" 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micropolisJS", 3 | "version": "0.0.1", 4 | "description": "A port of the open-source simulator \"Micropolis\" to Javascript/HTML5", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/graememcc/micropolisJS.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/graememcc/micropolisJS/issues" 11 | }, 12 | "scripts": { 13 | "build": "webpack --env.production", 14 | "watch": "webpack --watch --env.development", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "lint": "tslint src/*.ts test/*.ts", 18 | "dev": "webpack-dev-server --env.development", 19 | "production": "webpack-dev-server --env.production" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "23.1.0", 23 | "awesome-typescript-loader": "5.0.0", 24 | "clean-webpack-plugin": "0.1.19", 25 | "copy-webpack-plugin": "^4.5.1", 26 | "git-revision-webpack-plugin": "3.0.1", 27 | "html-webpack-plugin": "3.2.0", 28 | "jest": "23.1.0", 29 | "script-ext-html-webpack-plugin": "2.0.1", 30 | "ts-jest": "22.4.4", 31 | "ts-transformer-unassert": "2.0.1", 32 | "tslint": "5.10.0", 33 | "typescript": "2.9.2", 34 | "webpack": "4.6.0", 35 | "webpack-cli": "2.0.15", 36 | "webpack-dev-server": "3.1.14" 37 | }, 38 | "author": "Graeme McCutcheon (http://www.graememcc.co.uk)", 39 | "license": "GPL-3.0", 40 | "readmeFilename": "README.md" 41 | } 42 | -------------------------------------------------------------------------------- /sprites/obj1-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj1-0.png -------------------------------------------------------------------------------- /sprites/obj1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj1-1.png -------------------------------------------------------------------------------- /sprites/obj1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj1-2.png -------------------------------------------------------------------------------- /sprites/obj1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj1-3.png -------------------------------------------------------------------------------- /sprites/obj1-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj1-4.png -------------------------------------------------------------------------------- /sprites/obj2-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj2-0.png -------------------------------------------------------------------------------- /sprites/obj2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj2-1.png -------------------------------------------------------------------------------- /sprites/obj2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj2-2.png -------------------------------------------------------------------------------- /sprites/obj2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj2-3.png -------------------------------------------------------------------------------- /sprites/obj2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj2-4.png -------------------------------------------------------------------------------- /sprites/obj2-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj2-5.png -------------------------------------------------------------------------------- /sprites/obj2-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj2-6.png -------------------------------------------------------------------------------- /sprites/obj2-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj2-7.png -------------------------------------------------------------------------------- /sprites/obj3-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-0.png -------------------------------------------------------------------------------- /sprites/obj3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-1.png -------------------------------------------------------------------------------- /sprites/obj3-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-10.png -------------------------------------------------------------------------------- /sprites/obj3-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-2.png -------------------------------------------------------------------------------- /sprites/obj3-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-3.png -------------------------------------------------------------------------------- /sprites/obj3-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-4.png -------------------------------------------------------------------------------- /sprites/obj3-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-5.png -------------------------------------------------------------------------------- /sprites/obj3-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-6.png -------------------------------------------------------------------------------- /sprites/obj3-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-7.png -------------------------------------------------------------------------------- /sprites/obj3-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-8.png -------------------------------------------------------------------------------- /sprites/obj3-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj3-9.png -------------------------------------------------------------------------------- /sprites/obj4-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj4-0.png -------------------------------------------------------------------------------- /sprites/obj4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj4-1.png -------------------------------------------------------------------------------- /sprites/obj4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj4-2.png -------------------------------------------------------------------------------- /sprites/obj4-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj4-3.png -------------------------------------------------------------------------------- /sprites/obj4-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj4-4.png -------------------------------------------------------------------------------- /sprites/obj4-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj4-5.png -------------------------------------------------------------------------------- /sprites/obj4-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj4-6.png -------------------------------------------------------------------------------- /sprites/obj4-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj4-7.png -------------------------------------------------------------------------------- /sprites/obj5-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-0.png -------------------------------------------------------------------------------- /sprites/obj5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-1.png -------------------------------------------------------------------------------- /sprites/obj5-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-10.png -------------------------------------------------------------------------------- /sprites/obj5-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-11.png -------------------------------------------------------------------------------- /sprites/obj5-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-12.png -------------------------------------------------------------------------------- /sprites/obj5-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-13.png -------------------------------------------------------------------------------- /sprites/obj5-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-14.png -------------------------------------------------------------------------------- /sprites/obj5-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-15.png -------------------------------------------------------------------------------- /sprites/obj5-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-2.png -------------------------------------------------------------------------------- /sprites/obj5-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-3.png -------------------------------------------------------------------------------- /sprites/obj5-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-4.png -------------------------------------------------------------------------------- /sprites/obj5-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-5.png -------------------------------------------------------------------------------- /sprites/obj5-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-6.png -------------------------------------------------------------------------------- /sprites/obj5-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-7.png -------------------------------------------------------------------------------- /sprites/obj5-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-8.png -------------------------------------------------------------------------------- /sprites/obj5-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj5-9.png -------------------------------------------------------------------------------- /sprites/obj6-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj6-0.png -------------------------------------------------------------------------------- /sprites/obj6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj6-1.png -------------------------------------------------------------------------------- /sprites/obj6-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj6-2.png -------------------------------------------------------------------------------- /sprites/obj7-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj7-0.png -------------------------------------------------------------------------------- /sprites/obj7-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj7-1.png -------------------------------------------------------------------------------- /sprites/obj7-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj7-2.png -------------------------------------------------------------------------------- /sprites/obj7-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj7-3.png -------------------------------------------------------------------------------- /sprites/obj7-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj7-4.png -------------------------------------------------------------------------------- /sprites/obj7-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj7-5.png -------------------------------------------------------------------------------- /sprites/obj8-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj8-0.png -------------------------------------------------------------------------------- /sprites/obj8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj8-1.png -------------------------------------------------------------------------------- /sprites/obj8-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj8-2.png -------------------------------------------------------------------------------- /sprites/obj8-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graememcc/micropolisJS/e9d224f515924ad0ebf5fddb59697372397b371b/sprites/obj8-3.png -------------------------------------------------------------------------------- /src/airplaneSprite.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BaseSprite } from './baseSprite'; 11 | import { PLANE_CRASHED } from './messages'; 12 | import { MiscUtils } from './miscUtils'; 13 | import { Random } from './random'; 14 | import { SPRITE_AIRPLANE, SPRITE_HELICOPTER } from './spriteConstants'; 15 | import { SpriteUtils } from './spriteUtils'; 16 | 17 | function AirplaneSprite(map, spriteManager, x, y) { 18 | this.init(SPRITE_AIRPLANE, map, spriteManager, x, y); 19 | this.width = 48; 20 | this.height = 48; 21 | this.xOffset = -24; 22 | this.yOffset = -24; 23 | if (x > SpriteUtils.worldToPix(map.width - 20)) { 24 | this.destX = this.x - 200; 25 | this.frame = 7; 26 | } else { 27 | this.destX = this.x + 200; 28 | this.frame = 11; 29 | } 30 | this.destY = this.y; 31 | } 32 | 33 | 34 | BaseSprite(AirplaneSprite); 35 | 36 | 37 | var xDelta = [0, 0, 6, 8, 6, 0, -6, -8, -6, 8, 8, 8]; 38 | var yDelta = [0, -8, -6, 0, 6, 8, 6, 0, -6, 0, 0, 0]; 39 | 40 | AirplaneSprite.prototype.move = function(spriteCycle, disasterManager, blockMaps) { 41 | var frame = this.frame; 42 | 43 | if ((spriteCycle % 5) === 0) { 44 | // Frames > 8 mean the plane is taking off 45 | if (frame > 8) { 46 | frame--; 47 | if (frame < 9) { 48 | // Planes always take off to the east 49 | frame = 3; 50 | } 51 | this.frame = frame; 52 | } else { 53 | var d = SpriteUtils.getDir(this.x, this.y, this.destX, this.destY); 54 | frame = SpriteUtils.turnTo(frame, d); 55 | this.frame = frame; 56 | } 57 | } 58 | 59 | var absDist = SpriteUtils.absoluteDistance(this.x, this.y, this.destX, this.destY); 60 | if (absDist < 50) { 61 | // We're pretty close to the destination 62 | this.destX = Random.getRandom(SpriteUtils.worldToPix(this.map.width)) + 8; 63 | this.destY = Random.getRandom(SpriteUtils.worldToPix(this.map.height)) + 8; 64 | } 65 | 66 | if (disasterManager.enableDisasters) { 67 | var explode = false; 68 | 69 | var spriteList = this.spriteManager.getSpriteList(); 70 | for (var i = 0; i < spriteList.length; i++) { 71 | var s = spriteList[i]; 72 | 73 | if (s.frame === 0 || s === this) 74 | continue; 75 | 76 | if ((s.type === SPRITE_HELICOPTER || 77 | s.type === SPRITE_AIRPLANE) && 78 | SpriteUtils.checkSpriteCollision(this, s)) { 79 | s.explodeSprite(); 80 | explode = true; 81 | } 82 | } 83 | 84 | if (explode) 85 | this.explodeSprite(); 86 | } 87 | 88 | this.x += xDelta[frame]; 89 | this.y += yDelta[frame]; 90 | 91 | if (this.spriteNotInBounds()) 92 | this.frame = 0; 93 | }; 94 | 95 | 96 | AirplaneSprite.prototype.explodeSprite = function() { 97 | this.frame = 0; 98 | this.spriteManager.makeExplosionAt(this.x, this.y); 99 | this._emitEvent(PLANE_CRASHED, {showable: true, x: this.worldX, y: this.worldY}); 100 | }; 101 | 102 | 103 | // Metadata for image loading 104 | Object.defineProperties(AirplaneSprite, 105 | {ID: MiscUtils.makeConstantDescriptor(3), 106 | width: MiscUtils.makeConstantDescriptor(48), 107 | frames: MiscUtils.makeConstantDescriptor(11)}); 108 | 109 | 110 | export { AirplaneSprite }; 111 | -------------------------------------------------------------------------------- /src/baseSprite.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { EventEmitter } from './eventEmitter'; 11 | import { SpriteUtils } from './spriteUtils'; 12 | 13 | var init = function(type, map, spriteManager, x, y) { 14 | this.type = type; 15 | this.map = map; 16 | this.spriteManager = spriteManager; 17 | 18 | var pixX = x; 19 | var pixY = y; 20 | var worldX = x >> 4; 21 | var worldY = y >> 4; 22 | 23 | Object.defineProperty(this, 'x', 24 | {configurable: false, 25 | enumerable: true, 26 | set: function(val) { 27 | // XXX These getters have implicit knowledge of tileWidth: need to decide whether to disallow non 16px tiles 28 | pixX = val; 29 | worldX = val >> 4; 30 | }, 31 | get: function() { 32 | return pixX; 33 | } 34 | }); 35 | 36 | Object.defineProperty(this, 'y', 37 | {configurable: false, 38 | enumerable: true, 39 | set: function(val) { 40 | pixY = val; 41 | worldY = val >> 4; 42 | }, 43 | get: function() { 44 | return pixY; 45 | } 46 | }); 47 | 48 | Object.defineProperty(this, 'worldX', 49 | {configurable: false, 50 | enumerable: true, 51 | set: function(val) { 52 | worldX = val; 53 | pixX = val << 4; 54 | }, 55 | get: function() { 56 | return worldX; 57 | } 58 | }); 59 | 60 | Object.defineProperty(this, 'worldY', 61 | {configurable: false, 62 | enumerable: true, 63 | set: function(val) { 64 | worldY = val; 65 | pixY = val << 4; 66 | }, 67 | get: function() { 68 | return worldY; 69 | } 70 | }); 71 | 72 | this.origX = 0; 73 | this.origY = 0; 74 | this.destX = 0; 75 | this.destY = 0; 76 | this.count = 0; 77 | this.soundCount = 0; 78 | this.dir = 0; 79 | this.newDir = 0; 80 | this.step = 0; 81 | this.flag = 0; 82 | this.turn = 0; 83 | this.accel = 0; 84 | this.speed = 100; 85 | }; 86 | 87 | 88 | var getFileName = function() { 89 | return ['obj', this.type, '-', this.frame - 1].join(''); 90 | }; 91 | 92 | 93 | var spriteNotInBounds = function() { 94 | var x = this.worldX; 95 | var y = this.worldY; 96 | 97 | return x < 0 || y < 0 || x >= this.map.width || y >= this.map.height; 98 | }; 99 | 100 | 101 | var base = { 102 | init: init, 103 | getFileName: getFileName, 104 | spriteNotInBounds: spriteNotInBounds 105 | }; 106 | 107 | 108 | var BaseSprite = function(spriteConstructor) { 109 | spriteConstructor.prototype = Object.create(base); 110 | EventEmitter(spriteConstructor); 111 | }; 112 | 113 | 114 | export { BaseSprite }; 115 | -------------------------------------------------------------------------------- /src/baseTool.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { MiscUtils } from './miscUtils'; 11 | import { TileUtils } from './tileUtils'; 12 | import { DIRT, HBRIDGE, LASTTINYEXP, TINYEXP } from "./tileValues"; 13 | import { WorldEffects } from './worldEffects'; 14 | 15 | var init = function(cost, map, shouldAutoBulldoze, isDraggable) { 16 | isDraggable = isDraggable || false; 17 | Object.defineProperty(this, 'toolCost', MiscUtils.makeConstantDescriptor(cost)); 18 | this.result = null; 19 | this.isDraggable = isDraggable; 20 | this._shouldAutoBulldoze = shouldAutoBulldoze; 21 | this._map = map; 22 | this._worldEffects = new WorldEffects(map); 23 | this._applicationCost = 0; 24 | }; 25 | 26 | 27 | var clear = function() { 28 | this._applicationCost = 0; 29 | this._worldEffects.clear(); 30 | }; 31 | 32 | 33 | var addCost = function(cost) { 34 | this._applicationCost += cost; 35 | }; 36 | 37 | 38 | var doAutoBulldoze = function(x, y) { 39 | var tile = this._worldEffects.getTile(x, y); 40 | if (tile.isBulldozable()) { 41 | tile = TileUtils.normalizeRoad(tile.getValue()); 42 | if ((tile >= TINYEXP && tile <= LASTTINYEXP) || 43 | (tile < HBRIDGE && tile !== DIRT)) { 44 | this.addCost(1); 45 | this._worldEffects.setTile(x, y, DIRT); 46 | } 47 | } 48 | }; 49 | 50 | 51 | var apply = function(budget) { 52 | this._worldEffects.apply(); 53 | budget.spend(this._applicationCost); 54 | this.clear(); 55 | }; 56 | 57 | 58 | var modifyIfEnoughFunding = function(budget) { 59 | if (this.result !== this.TOOLRESULT_OK) { 60 | this.clear(); 61 | return false; 62 | } 63 | 64 | if (budget.totalFunds < this._applicationCost) { 65 | this.result = this.TOOLRESULT_NO_MONEY; 66 | this.clear(); 67 | return false; 68 | } 69 | 70 | apply.call(this, budget); 71 | this.clear(); 72 | return true; 73 | }; 74 | 75 | 76 | var TOOLRESULT_OK = 0; 77 | var TOOLRESULT_FAILED = 1; 78 | var TOOLRESULT_NO_MONEY = 2; 79 | var TOOLRESULT_NEEDS_BULLDOZE = 3; 80 | 81 | var BaseToolConstructor = { 82 | addCost: addCost, 83 | autoBulldoze: true, 84 | bulldozerCost: 1, 85 | clear: clear, 86 | doAutoBulldoze: doAutoBulldoze, 87 | init: init, 88 | modifyIfEnoughFunding: modifyIfEnoughFunding, 89 | TOOLRESULT_OK: TOOLRESULT_OK, 90 | TOOLRESULT_FAILED: TOOLRESULT_FAILED, 91 | TOOLRESULT_NO_MONEY: TOOLRESULT_NO_MONEY, 92 | TOOLRESULT_NEEDS_BULLDOZE: TOOLRESULT_NEEDS_BULLDOZE 93 | }; 94 | 95 | 96 | var save = function(saveData) { 97 | saveData.autoBulldoze = BaseToolConstructor.autoBulldoze; 98 | }; 99 | 100 | 101 | var load = function(saveData) { 102 | BaseTool.autoBulldoze = saveData.autoBulldoze; 103 | }; 104 | 105 | 106 | var makeTool = function(toolConstructor) { 107 | toolConstructor.prototype = Object.create(BaseToolConstructor); 108 | return toolConstructor; 109 | }; 110 | 111 | 112 | var BaseTool = { 113 | makeTool: makeTool, 114 | setAutoBulldoze: function(value) { 115 | BaseToolConstructor.autoBulldoze = value; 116 | }, 117 | getAutoBulldoze: function() { 118 | return BaseToolConstructor.autoBulldoze; 119 | }, 120 | save: save, 121 | load: load 122 | }; 123 | 124 | export { BaseTool }; 125 | -------------------------------------------------------------------------------- /src/blockMap.ts: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | interface BlockCoordinate { 11 | x: number; 12 | y: number; 13 | } 14 | 15 | type ForEachFunction = (x: number, y: number) => any; 16 | 17 | type TransformationFunction = (n: number) => number; 18 | 19 | const ID: TransformationFunction = (n) => n; 20 | 21 | /* 22 | * 23 | * BlockMaps are data maps where each entry corresponds to data representing a block of tiles in the original 24 | * game map. 25 | * 26 | */ 27 | 28 | export class BlockMap { 29 | 30 | // tslint:disable-next-line:variable-name 31 | private _width: number; 32 | // tslint:disable-next-line:variable-name 33 | private _height: number; 34 | private data: number[] = []; 35 | 36 | // Construct a block map. Takes three integers: the game map's width and height, and the block size (i.e. how many 37 | // tiles in each direction should map to the same block). The BlockMap entries will be initialised to zero. 38 | constructor(readonly gameMapWidth: number, readonly gameMapHeight: number, readonly blockSize: number) { 39 | this._width = this.convertToBlockCount(this.gameMapWidth); 40 | this._height = this.convertToBlockCount(this.gameMapHeight); 41 | this.clear(); 42 | } 43 | 44 | get width(): number { 45 | return this._width; 46 | } 47 | 48 | get height(): number { 49 | return this._height; 50 | } 51 | 52 | public get(blockX: number, blockY: number): number { 53 | const index = this.toIndex(blockX, blockY); 54 | return this.data[index]; 55 | } 56 | 57 | public set(blockX: number, blockY: number, value: number) { 58 | const index = this.toIndex(blockX, blockY); 59 | this.data[index] = value; 60 | } 61 | 62 | public worldGet(worldX: number, worldY: number): number { 63 | const {x, y} = this.toBlockCoordinate(worldX, worldY); 64 | return this.get(x, y); 65 | } 66 | 67 | public worldSet(worldX: number, worldY: number, value: number) { 68 | const {x, y} = this.toBlockCoordinate(worldX, worldY); 69 | this.set(x, y, value); 70 | } 71 | 72 | public clear() { 73 | this.forEach((x, y) => this.set(x, y, 0)); 74 | } 75 | 76 | public copyFrom(source: BlockMap, transform: TransformationFunction = ID) { 77 | if (this.hasIncompatibleDimensions(source)) { 78 | console.warn("Copying from incompatible blockMap!"); 79 | } 80 | 81 | this.forEach((x, y) => this.set(x, y, transform(source.get(x, y)))); 82 | } 83 | 84 | private forEach(fn: ForEachFunction) { 85 | const maxWidth = this.width; 86 | const maxHeight = this.height; 87 | 88 | for (let x = 0; x < maxWidth; x++) { 89 | for (let y = 0; y < maxHeight; y++) { 90 | fn(x, y); 91 | } 92 | } 93 | } 94 | 95 | private convertToBlockCount(value: number) { 96 | return Math.floor((value + this.blockSize - 1) / this.blockSize); 97 | } 98 | 99 | private hasIncompatibleDimensions(map: BlockMap): boolean { 100 | return map.gameMapHeight !== this.gameMapHeight || 101 | map.gameMapWidth !== this.gameMapWidth || 102 | map.blockSize !== this.blockSize; 103 | } 104 | 105 | private toBlockCoordinate(worldX: number, worldY: number): BlockCoordinate { 106 | const x = this.toBlockIndex(worldX); 107 | const y = this.toBlockIndex(worldY); 108 | return {x, y}; 109 | } 110 | 111 | private toBlockIndex(worldIndex: number): number { 112 | return Math.floor(worldIndex / this.blockSize); 113 | } 114 | 115 | private toIndex(blockX: number, blockY: number) { 116 | return this.width * blockY + blockX; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/bounds.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "./debugAssert"; 2 | import { Position } from "./position"; 3 | 4 | export class Bounds { 5 | 6 | static fromOrigin(width: number, height: number): Bounds { 7 | return new Bounds(0, 0, width, height); 8 | } 9 | 10 | private readonly exclusiveEndX: number; 11 | private readonly exclusiveEndY: number; 12 | 13 | constructor(private readonly inclusiveStartX: number, 14 | private readonly inclusiveStartY: number, 15 | widthCount: number, 16 | heightCount: number) { 17 | assert(widthCount > 0, "bounded region must have a width"); 18 | assert(heightCount > 0, "bounded region must have a width"); 19 | 20 | this.exclusiveEndX = inclusiveStartX + widthCount; 21 | this.exclusiveEndY = inclusiveStartY + heightCount; 22 | } 23 | 24 | contains(position: Position): boolean { 25 | const {x, y} = position; 26 | return this.xInBounds(x) && this.yInBounds(y); 27 | } 28 | 29 | toString(): string { 30 | const upperCorner = new Position(this.inclusiveStartX, this.inclusiveStartY); 31 | const lowerCorner = new Position(this.exclusiveEndX - 1, this.exclusiveEndY - 1); 32 | return `Bounds Rectangle: ${upperCorner} - ${lowerCorner}`; 33 | } 34 | 35 | private xInBounds(x: number): boolean { 36 | return x >= this.inclusiveStartX && x < this.exclusiveEndX; 37 | } 38 | 39 | private yInBounds(y: number): boolean { 40 | return y >= this.inclusiveStartY && y < this.exclusiveEndY; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/budgetWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BUDGET_WINDOW_CLOSED } from './messages'; 11 | import { MiscUtils } from './miscUtils'; 12 | import { ModalWindow } from './modalWindow'; 13 | 14 | var BudgetWindow = ModalWindow(function() { 15 | $(budgetCancelID).on('click', cancel.bind(this)); 16 | $(budgetResetID).on('click', resetItems.bind(this)); 17 | $(budgetFormID).on('submit', submit.bind(this)); 18 | }); 19 | 20 | 21 | var dataKeys = ['roadMaintenanceBudget', 'fireMaintenanceBudget', 'policeMaintenanceBudget']; 22 | var spendKeys = ['roadRate', 'fireRate', 'policeRate']; 23 | 24 | var budgetResetID = '#budgetReset'; 25 | var budgetCancelID = '#budgetCancel'; 26 | var budgetFormID = '#budgetForm'; 27 | var budgetOKID = '#budgetOK'; 28 | 29 | 30 | var setSpendRangeText = function(element, percentage, totalSpend) { 31 | var labelID = element + 'Label'; 32 | var cash = Math.floor(totalSpend * (percentage / 100)); 33 | var text = [percentage, '% of $', totalSpend, ' = $', cash].join(''); 34 | $(MiscUtils.normaliseDOMid(labelID)).text(text); 35 | }; 36 | 37 | 38 | var onFundingUpdate = function(elementID, e) { 39 | var element = $(MiscUtils.normaliseDOMid(elementID))[0]; 40 | var percentage = element.value - 0; 41 | var dataSource = element.getAttribute('data-source'); 42 | setSpendRangeText(elementID, percentage, this[dataSource]); 43 | }; 44 | 45 | 46 | var onTaxUpdate = function(e) { 47 | var elem = $('#taxRateLabel')[0]; 48 | var sourceElem = $('#taxRate')[0]; 49 | $(elem).text(['Tax rate: ', sourceElem.value, '%'].join('')); 50 | }; 51 | 52 | 53 | var resetItems = function(e) { 54 | for (var i = 0; i < spendKeys.length; i++) { 55 | var original = this['original' + spendKeys[i]]; 56 | $(MiscUtils.normaliseDOMid(spendKeys[i]))[0].value = original; 57 | setSpendRangeText(spendKeys[i], original, this[dataKeys[i]]); 58 | } 59 | $('#taxRate')[0].value = this.originaltaxRate; 60 | onTaxUpdate(); 61 | 62 | e.preventDefault(); 63 | }; 64 | 65 | 66 | BudgetWindow.prototype.close = function(data) { 67 | data = data || {cancelled: true}; 68 | this._emitEvent(BUDGET_WINDOW_CLOSED, data); 69 | this._toggleDisplay(); 70 | }; 71 | 72 | 73 | var cancel = function(e) { 74 | e.preventDefault(); 75 | this.close({cancelled: true}); 76 | }; 77 | 78 | 79 | var submit = function(e) { 80 | e.preventDefault(); 81 | 82 | // Get element values 83 | var roadPercent = $('#roadRate')[0].value; 84 | var firePercent = $('#fireRate')[0].value; 85 | var policePercent = $('#policeRate')[0].value; 86 | var taxPercent = $('#taxRate')[0].value; 87 | 88 | var data = {cancelled: false, roadPercent: roadPercent, firePercent: firePercent, 89 | policePercent: policePercent, taxPercent: taxPercent, e: e, original: e.type}; 90 | this.close(data); 91 | }; 92 | 93 | 94 | BudgetWindow.prototype.open = function(budgetData) { 95 | var i, elem; 96 | 97 | // Store max funding levels 98 | for (i = 0; i < dataKeys.length; i++) { 99 | if (budgetData[dataKeys[i]] === undefined) 100 | throw new Error('Missing budget data (' + dataKeys[i] + ')'); 101 | this[dataKeys[i]] = budgetData[dataKeys[i]]; 102 | } 103 | 104 | // Update form elements with percentages, and set up listeners 105 | for (i = 0; i < spendKeys.length; i++) { 106 | if (budgetData[spendKeys[i]] === undefined) 107 | throw new Error('Missing budget data (' + spendKeys[i] + ')'); 108 | 109 | elem = spendKeys[i]; 110 | this['original' + elem] = budgetData[elem]; 111 | setSpendRangeText(elem, budgetData[spendKeys[i]], this[dataKeys[i]]); 112 | elem = $(MiscUtils.normaliseDOMid(elem)); 113 | elem.on('change', onFundingUpdate.bind(this, spendKeys[i])); 114 | elem = elem[0]; 115 | elem.value = budgetData[spendKeys[i]]; 116 | } 117 | 118 | if (budgetData.taxRate === undefined) 119 | throw new Error('Missing budget data (taxRate)'); 120 | 121 | this.originalTaxRate = budgetData.taxRate; 122 | elem = $('#taxRate'); 123 | elem.on('change', onTaxUpdate); 124 | elem = elem[0]; 125 | elem.value = budgetData.taxRate; 126 | onTaxUpdate(); 127 | 128 | // Update static parts 129 | var previousFunds = budgetData.totalFunds; 130 | if (previousFunds === undefined) 131 | throw new Error('Missing budget data (previousFunds)'); 132 | 133 | var taxesCollected = budgetData.taxesCollected; 134 | if (taxesCollected === undefined) 135 | throw new Error('Missing budget data (taxesCollected)'); 136 | 137 | var cashFlow = taxesCollected - this.roadMaintenanceBudget - this.fireMaintenanceBudget - this.policeMaintenanceBudget; 138 | var currentFunds = previousFunds + cashFlow; 139 | $('#taxesCollected').text('$' + taxesCollected); 140 | $('#cashFlow').text((cashFlow < 0 ? '-$' : '$') + cashFlow); 141 | $('#previousFunds').text((previousFunds < 0 ? '-$' : '$') + previousFunds); 142 | $('#currentFunds').text('$' + currentFunds); 143 | 144 | this._toggleDisplay(); 145 | }; 146 | 147 | 148 | export { BudgetWindow }; 149 | -------------------------------------------------------------------------------- /src/buildingTool.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { ConnectingTool } from './connectingTool'; 11 | import { ANIMBIT, BNCNBIT, ZONEBIT } from "./tileFlags"; 12 | import { DIRT } from "./tileValues"; 13 | import { TileUtils } from './tileUtils'; 14 | 15 | var BuildingTool = ConnectingTool(function(cost, centreTile, map, size, animated) { 16 | this.init(cost, map, false); 17 | this.centreTile = centreTile; 18 | this.size = size; 19 | this.animated = animated; 20 | }); 21 | 22 | 23 | BuildingTool.prototype.putBuilding = function(leftX, topY) { 24 | var posX, posY, tileValue, tileFlags; 25 | var baseTile = this.centreTile - this.size - 1; 26 | 27 | for (var dy = 0; dy < this.size; dy++) { 28 | posY = topY + dy; 29 | 30 | for (var dx = 0; dx < this.size; dx++) { 31 | posX = leftX + dx; 32 | tileValue = baseTile; 33 | tileFlags = BNCNBIT; 34 | 35 | if (dx === 1) { 36 | if (dy === 1) 37 | tileFlags |= ZONEBIT; 38 | else if (dy === 2 && this.animated) 39 | tileFlags |= ANIMBIT; 40 | } 41 | 42 | this._worldEffects.setTile(posX, posY, tileValue, tileFlags); 43 | 44 | baseTile++; 45 | } 46 | } 47 | }; 48 | 49 | 50 | BuildingTool.prototype.prepareBuildingSite = function(leftX, topY) { 51 | // Check that the entire site is on the map 52 | if (leftX < 0 || leftX + this.size > this._map.width) 53 | return this.TOOLRESULT_FAILED; 54 | 55 | if (topY < 0 || topY + this.size > this._map.height) 56 | return this.TOOLRESULT_FAILED; 57 | 58 | var posX, posY, tileValue; 59 | 60 | // Check whether the tiles are clear 61 | for (var dy = 0; dy < this.size; dy++) { 62 | posY = topY + dy; 63 | 64 | for (var dx = 0; dx < this.size; dx++) { 65 | posX = leftX + dx; 66 | 67 | tileValue = this._worldEffects.getTileValue(posX, posY); 68 | 69 | if (tileValue === DIRT) 70 | continue; 71 | 72 | if (!this.autoBulldoze) { 73 | // No TileValues.DIRT and no bull-dozer => not buildable 74 | return this.TOOLRESULT_NEEDS_BULLDOZE; 75 | } 76 | 77 | if (!TileUtils.canBulldoze(tileValue)) { 78 | // tilevalue cannot be auto-bulldozed 79 | return this.TOOLRESULT_NEEDS_BULLDOZE; 80 | } 81 | 82 | this._worldEffects.setTile(posX, posY, DIRT); 83 | this.addCost(this.bulldozerCost); 84 | } 85 | } 86 | 87 | return this.TOOLRESULT_OK; 88 | }; 89 | 90 | 91 | BuildingTool.prototype.buildBuilding = function(x, y) { 92 | // Correct to top left 93 | x--; 94 | y--; 95 | 96 | var prepareResult = this.prepareBuildingSite(x, y); 97 | if (prepareResult !== this.TOOLRESULT_OK) 98 | return prepareResult; 99 | 100 | this.addCost(this.toolCost); 101 | 102 | this.putBuilding(x, y); 103 | 104 | this.checkBorder(x, y); 105 | 106 | return this.TOOLRESULT_OK; 107 | }; 108 | 109 | 110 | BuildingTool.prototype.doTool = function(x, y, blockMaps) { 111 | this.result = this.buildBuilding(x, y); 112 | }; 113 | 114 | 115 | export { BuildingTool }; 116 | -------------------------------------------------------------------------------- /src/bulldozerTool.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { ConnectingTool } from './connectingTool'; 11 | import { EventEmitter } from './eventEmitter'; 12 | import { SOUND_EXPLOSIONLOW, SOUND_EXPLOSIONHIGH } from './messages'; 13 | import { Random } from './random'; 14 | import { ANIMBIT, BULLBIT } from "./tileFlags"; 15 | import { TileUtils } from './tileUtils'; 16 | import * as TileValues from "./tileValues"; 17 | import { ZoneUtils } from './zoneUtils'; 18 | 19 | var BulldozerTool = EventEmitter(ConnectingTool(function(map) { 20 | this.init(10, map, true); 21 | })); 22 | 23 | 24 | BulldozerTool.prototype.putRubble = function(x, y, size) { 25 | for (var xx = x; xx < x + size; xx++) { 26 | for (var yy = y; yy < y + size; yy++) { 27 | if (this._map.testBounds(xx, yy)) { 28 | var tile = this._worldEffects.getTileValue(xx, yy); 29 | if (tile != TileValues.RADTILE && tile != TileValues.DIRT) 30 | this._worldEffects.setTile(xx, yy, TileValues.TINYEXP + Random.getRandom(2), ANIMBIT | BULLBIT); 31 | } 32 | } 33 | } 34 | }; 35 | 36 | 37 | BulldozerTool.prototype.layDoze = function(x, y) { 38 | var tile = this._worldEffects.getTile(x, y); 39 | 40 | if (!tile.isBulldozable()) 41 | return this.TOOLRESULT_FAILED; 42 | 43 | tile = tile.getValue(); 44 | tile = TileUtils.normalizeRoad(tile); 45 | 46 | switch (tile) { 47 | case TileValues.HBRIDGE: 48 | case TileValues.VBRIDGE: 49 | case TileValues.BRWV: 50 | case TileValues.BRWH: 51 | case TileValues.HBRDG0: 52 | case TileValues.HBRDG1: 53 | case TileValues.HBRDG2: 54 | case TileValues.HBRDG3: 55 | case TileValues.VBRDG0: 56 | case TileValues.VBRDG1: 57 | case TileValues.VBRDG2: 58 | case TileValues.VBRDG3: 59 | case TileValues.HPOWER: 60 | case TileValues.VPOWER: 61 | case TileValues.HRAIL: 62 | case TileValues.VRAIL: 63 | this._worldEffects.setTile(x, y, TileValues.RIVER); 64 | break; 65 | 66 | default: 67 | this._worldEffects.setTile(x, y, TileValues.DIRT); 68 | break; 69 | } 70 | 71 | this.addCost(1); 72 | 73 | return this.TOOLRESULT_OK; 74 | }; 75 | 76 | 77 | BulldozerTool.prototype.doTool = function(x, y, blockMaps) { 78 | if (!this._map.testBounds(x, y)) 79 | this.result = this.TOOLRESULT_FAILED; 80 | 81 | var tile = this._worldEffects.getTile(x, y); 82 | var tileValue = tile.getValue(); 83 | 84 | var zoneSize = 0; 85 | var deltaX; 86 | var deltaY; 87 | 88 | if (tile.isZone()) { 89 | zoneSize = ZoneUtils.checkZoneSize(tileValue); 90 | deltaX = 0; 91 | deltaY = 0; 92 | } else { 93 | var result = ZoneUtils.checkBigZone(tileValue); 94 | zoneSize = result.zoneSize; 95 | deltaX = result.deltaX; 96 | deltaY = result.deltaY; 97 | } 98 | 99 | if (zoneSize > 0) { 100 | this.addCost(this.bulldozerCost); 101 | 102 | var dozeX = x; 103 | var dozeY = y; 104 | var centerX = x + deltaX; 105 | var centerY = y + deltaY; 106 | 107 | switch (zoneSize) { 108 | case 3: 109 | this._emitEvent(SOUND_EXPLOSIONHIGH); 110 | this.putRubble(centerX - 1, centerY - 1, 3); 111 | break; 112 | 113 | case 4: 114 | this._emitEvent(SOUND_EXPLOSIONLOW); 115 | this.putRubble(centerX - 1, centerY - 1, 4); 116 | break; 117 | 118 | case 6: 119 | this._emitEvent(SOUND_EXPLOSIONHIGH); 120 | this._emitEvent(SOUND_EXPLOSIONLOW); 121 | this.putRubble(centerX - 1, centerY - 1, 6); 122 | break; 123 | } 124 | 125 | this.result = this.TOOLRESULT_OK; 126 | } else { 127 | var toolResult; 128 | if (tileValue === TileValues.RIVER || tileValue === TileValues.REDGE || tileValue === TileValues.CHANNEL) { 129 | toolResult = this.layDoze(x, y); 130 | 131 | if (tileValue !== this._worldEffects.getTileValue(x, y)) 132 | this.addCost(5); 133 | } else { 134 | toolResult = this.layDoze(x, y); 135 | this.checkZoneConnections(x, y); 136 | } 137 | 138 | this.result = toolResult; 139 | } 140 | }; 141 | 142 | 143 | export { BulldozerTool }; 144 | -------------------------------------------------------------------------------- /src/census.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { MiscUtils } from './miscUtils'; 11 | 12 | var arrs = ['res', 'com', 'ind', 'crime', 13 | 'money', 'pollution']; 14 | function Census() { 15 | this.clearCensus(); 16 | this.changed = false; 17 | this.crimeRamp = 0; 18 | this.pollutionRamp = 0; 19 | 20 | // Set externally 21 | this.landValueAverage = 0; 22 | this.pollutionAverage = 0; 23 | this.crimeAverage = 0; 24 | this.totalPop = 0; 25 | 26 | var createArray = function(arrName) { 27 | this[arrName] = []; 28 | for (var a = 0; a < 120; a++) 29 | this[arrName][a] = 0; 30 | }; 31 | 32 | for (var i = 0; i < arrs.length; i++) { 33 | var name10 = arrs[i] + 'Hist10'; 34 | var name120 = arrs[i] + 'Hist120'; 35 | createArray.call(this, name10); 36 | createArray.call(this, name120); 37 | } 38 | } 39 | 40 | 41 | var rotate10Arrays = function() { 42 | for (var i = 0; i < arrs.length; i++) { 43 | var name10 = arrs[i] + 'Hist10'; 44 | this[name10].pop(); 45 | this[name10].unshift(0); 46 | } 47 | }; 48 | 49 | 50 | var rotate120Arrays = function() { 51 | for (var i = 0; i < arrs.length; i++) { 52 | var name120 = arrs[i] + 'Hist120'; 53 | this[name120].pop(); 54 | this[name120].unshift(0); 55 | } 56 | }; 57 | 58 | 59 | Census.prototype.clearCensus = function() { 60 | this.poweredZoneCount = 0; 61 | this.unpoweredZoneCount = 0; 62 | this.firePop = 0; 63 | this.roadTotal = 0; 64 | this.railTotal = 0; 65 | this.resPop = 0; 66 | this.comPop = 0; 67 | this.indPop = 0; 68 | this.resZonePop = 0; 69 | this.comZonePop = 0; 70 | this.indZonePop = 0; 71 | this.hospitalPop = 0; 72 | this.churchPop = 0; 73 | this.policeStationPop = 0; 74 | this.fireStationPop = 0; 75 | this.stadiumPop = 0; 76 | this.coalPowerPop = 0; 77 | this.nuclearPowerPop = 0; 78 | this.seaportPop = 0; 79 | this.airportPop = 0; 80 | }; 81 | 82 | 83 | var saveProps = ['resPop', 'comPop', 'indPop', 'crimeRamp', 'pollutionRamp', 'landValueAverage', 'pollutionAverage', 84 | 'crimeAverage', 'totalPop', 'resHist10', 'resHist120', 'comHist10', 'comHist120', 'indHist10', 85 | 'indHist120', 'crimeHist10', 'crimeHist120', 'moneyHist10', 'moneyHist120', 'pollutionHist10', 86 | 'pollutionHist120']; 87 | 88 | Census.prototype.save = function(saveData) { 89 | for (var i = 0, l = saveProps.length; i < l; i++) 90 | saveData[saveProps[i]] = this[saveProps[i]]; 91 | }; 92 | 93 | 94 | Census.prototype.load = function(saveData) { 95 | for (var i = 0, l = saveProps.length; i < l; i++) 96 | this[saveProps[i]] = saveData[saveProps[i]]; 97 | }; 98 | 99 | 100 | Census.prototype.take10Census = function(budget) { 101 | var resPopDenom = 8; 102 | 103 | rotate10Arrays.call(this); 104 | 105 | this.resHist10[0] = Math.floor(this.resPop / resPopDenom); 106 | this.comHist10[0] = this.comPop; 107 | this.indHist10[0] = this.indPop; 108 | 109 | this.crimeRamp += Math.floor((this.crimeAverage - this.crimeRamp) / 4); 110 | this.crimeHist10[0] = Math.min(this.crimeRamp, 255); 111 | 112 | this.pollutionRamp += Math.floor((this.pollutionAverage - this.pollutionRamp) / 4); 113 | this.pollutionHist10[0] = Math.min(this.pollutionRamp, 255); 114 | 115 | var x = Math.floor(budget.cashFlow / 20) + 128; 116 | this.moneyHist10[0] = MiscUtils.clamp(x, 0, 255); 117 | 118 | var resPopScaled = this.resPop >> 8; 119 | 120 | if (this.hospitalPop < this.resPopScaled) 121 | this.needHospital = 1; 122 | else if (this.hospitalPop > this.resPopScaled) 123 | this.needHospital = -1; 124 | else if (this.hospitalPop === this.resPopScaled) 125 | this.needHospital = 0; 126 | 127 | this.changed = true; 128 | }; 129 | 130 | 131 | Census.prototype.take120Census = function() { 132 | rotate120Arrays.call(this); 133 | var resPopDenom = 8; 134 | 135 | this.resHist120[0] = Math.floor(this.resPop / resPopDenom); 136 | this.comHist120[0] = this.comPop; 137 | this.indHist120[0] = this.indPop; 138 | this.crimeHist120[0] = this.crimeHist10[0]; 139 | this.pollutionHist120[0] = this.pollutionHist10[0]; 140 | this.moneyHist120[0] = this.moneyHist10[0]; 141 | this.changed = true; 142 | }; 143 | 144 | 145 | export { Census }; 146 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | // All areas of micropolisJS that provide debug options should also check the debug 11 | // value as well as their individual value. The debug flag is intended to switch on all 12 | // debug options. 13 | 14 | var Config = { 15 | debug: false, 16 | gameDebug: false, 17 | queryDebug: false 18 | }; 19 | 20 | 21 | export { Config }; 22 | -------------------------------------------------------------------------------- /src/congratsWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { CONGRATS_WINDOW_CLOSED } from './messages'; 11 | import { ModalWindow } from './modalWindow'; 12 | 13 | var CongratsWindow = ModalWindow(function() { 14 | $(congratsFormID).on('submit', submit.bind(this)); 15 | }); 16 | 17 | 18 | var congratsFormID = '#congratsForm'; 19 | var congratsMessageID = '#congratsMessage'; 20 | var congratsOKID = '#congratsOK'; 21 | 22 | 23 | var submit = function(e) { 24 | e.preventDefault(); 25 | this.close(); 26 | }; 27 | 28 | 29 | CongratsWindow.prototype.close = function() { 30 | this._toggleDisplay(); 31 | this._emitEvent(CONGRATS_WINDOW_CLOSED); 32 | }; 33 | 34 | 35 | CongratsWindow.prototype.open = function(message) { 36 | this._toggleDisplay(); 37 | $(congratsMessageID).text(message); 38 | $(congratsOKID).focus(); 39 | }; 40 | 41 | 42 | export { CongratsWindow }; 43 | -------------------------------------------------------------------------------- /src/connectingTool.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BaseTool } from './baseTool'; 11 | import { Connector } from './connector'; 12 | 13 | // Take a tool constructor, make it inherit from BaseTool, and add 14 | // the various connection related functions 15 | var makeTool = BaseTool.makeTool; 16 | var ConnectingTool = function(toolConstructor) { 17 | return Connector(makeTool(toolConstructor)); 18 | }; 19 | 20 | 21 | export { ConnectingTool }; 22 | -------------------------------------------------------------------------------- /src/copterSprite.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BaseSprite } from './baseSprite'; 11 | import { HEAVY_TRAFFIC, HELICOPTER_CRASHED, SOUND_HEAVY_TRAFFIC } from './messages'; 12 | import { MiscUtils } from './miscUtils'; 13 | import { Random } from './random'; 14 | import { SPRITE_HELICOPTER, SPRITE_MONSTER, SPRITE_TORNADO } from './spriteConstants'; 15 | import { SpriteUtils } from './spriteUtils'; 16 | 17 | function CopterSprite(map, spriteManager, x, y) { 18 | this.init(SPRITE_HELICOPTER, map, spriteManager, x, y); 19 | this.width = 32; 20 | this.height = 32; 21 | this.xOffset = -16; 22 | this.yOffset = -16; 23 | this.frame = 5; 24 | this.count = 1500; 25 | this.destX = Random.getRandom(SpriteUtils.worldToPix(map.width)) + 8; 26 | this.destY = Random.getRandom(SpriteUtils.worldToPix(map.height)) + 8; 27 | this.origX = x; 28 | this.origY = y; 29 | } 30 | 31 | 32 | BaseSprite(CopterSprite); 33 | 34 | 35 | var xDelta = [0, 0, 3, 5, 3, 0, -3, -5, -3]; 36 | var yDelta = [0, -5, -3, 0, 3, 5, 3, 0, -3]; 37 | 38 | CopterSprite.prototype.move = function(spriteCycle, disasterManager, blockMaps) { 39 | if (this.soundCount > 0) 40 | this.soundCount--; 41 | 42 | if (this.count > 0) 43 | this.count--; 44 | 45 | if (this.count === 0) { 46 | // Head towards a monster, and certain doom 47 | var s = this.spriteManager.getSprite(SPRITE_MONSTER); 48 | 49 | if (s !== null) { 50 | this.destX = s.x; 51 | this.destY = s.y; 52 | } else { 53 | // No monsters. Hm. I bet flying near that tornado is sensible 54 | s = this.spriteManager.getSprite(SPRITE_TORNADO); 55 | 56 | if (s !== null) { 57 | this.destX = s.x; 58 | this.destY = s.y; 59 | } else { 60 | this.destX = this.origX; 61 | this.destY = this.origY; 62 | } 63 | } 64 | 65 | // If near destination, let's get her on the ground 66 | var absDist = SpriteUtils.absoluteDistance(this.x, this.y, this.origX, this.origY); 67 | if (absDist < 30) { 68 | this.frame = 0; 69 | return; 70 | } 71 | } 72 | 73 | if (this.soundCount === 0) { 74 | var x = this.worldX; 75 | var y = this.worldY; 76 | 77 | if (x >= 0 && x < this.map.width && y >= 0 && y < this.map.height) { 78 | if (blockMaps.trafficDensityMap.worldGet(x, y) > 170 && (Random.getRandom16() & 7) === 0) { 79 | this._emitEvent(HEAVY_TRAFFIC, {x: x, y: y}); 80 | this._emitEvent(SOUND_HEAVY_TRAFFIC); 81 | this.soundCount = 200; 82 | } 83 | } 84 | } 85 | 86 | var frame = this.frame; 87 | 88 | if ((spriteCycle & 3) === 0) { 89 | var dir = SpriteUtils.getDir(this.x, this.y, this.destX, this.destY); 90 | frame = SpriteUtils.turnTo(frame, dir); 91 | this.frame = frame; 92 | } 93 | 94 | this.x += xDelta[frame]; 95 | this.y += yDelta[frame]; 96 | }; 97 | 98 | 99 | CopterSprite.prototype.explodeSprite = function() { 100 | this.frame = 0; 101 | this.spriteManager.makeExplosionAt(this.x, this.y); 102 | this._emitEvent(HELICOPTER_CRASHED, {x: this.worldX, y: this.worldY}); 103 | }; 104 | 105 | 106 | // Metadata for image loading 107 | Object.defineProperties(CopterSprite, 108 | {ID: MiscUtils.makeConstantDescriptor(2), 109 | width: MiscUtils.makeConstantDescriptor(32), 110 | frames: MiscUtils.makeConstantDescriptor(8)}); 111 | 112 | 113 | export { CopterSprite }; 114 | -------------------------------------------------------------------------------- /src/debugAssert.ts: -------------------------------------------------------------------------------- 1 | // TODO: More descriptive asserting functions: will require better 2 | // assert-stripping than is provided by ts-transform-unassert at time of writing 3 | 4 | export function assert(assertionPassed: boolean, message: string) { 5 | // TODO: Less invasive reporting than an alert 6 | if (!assertionPassed) { 7 | alert(`Assertion failed: ${message}`); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/debugWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { DEBUG_WINDOW_CLOSED } from './messages'; 11 | import { ModalWindow } from './modalWindow'; 12 | import { MiscUtils } from './miscUtils'; 13 | 14 | var DebugWindow = ModalWindow(function() { 15 | $(debugCancelID).on('click', cancel.bind(this)); 16 | $(debugFormID).on('submit', submit.bind(this)); 17 | }); 18 | 19 | 20 | var debugCancelID = '#debugCancel'; 21 | var debugFormID = '#debugForm'; 22 | var debugOKID = '#debugOK'; 23 | 24 | 25 | DebugWindow.prototype.close = function(actions) { 26 | actions = actions || []; 27 | this._emitEvent(DEBUG_WINDOW_CLOSED, actions); 28 | this._toggleDisplay(); 29 | }; 30 | 31 | 32 | var cancel = function(e) { 33 | e.preventDefault(); 34 | this.close([]); 35 | }; 36 | 37 | 38 | var submit = function(e) { 39 | e.preventDefault(); 40 | 41 | var actions = []; 42 | 43 | // Get element values 44 | var shouldAdd = $('.debugAdd:checked').val(); 45 | if (shouldAdd === 'true') 46 | actions.push({action: DebugWindow.ADD_FUNDS, data: {}}); 47 | 48 | this.close(actions); 49 | }; 50 | 51 | 52 | DebugWindow.prototype.open = function() { 53 | this._toggleDisplay(); 54 | }; 55 | 56 | 57 | var defineAction = (function() { 58 | var uid = 0; 59 | 60 | return function(name) { 61 | Object.defineProperty(DebugWindow, name, MiscUtils.makeConstantDescriptor(uid)); 62 | uid += 1; 63 | }; 64 | })(); 65 | 66 | 67 | defineAction('ADD_FUNDS'); 68 | 69 | 70 | export { DebugWindow }; 71 | -------------------------------------------------------------------------------- /src/direction.ts: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Random } from "./random"; 11 | 12 | export type DirectionFn = (direction: Direction) => void; 13 | 14 | export interface Direction { 15 | oppositeDirection(): Direction; 16 | rotateClockwise(): Direction; 17 | rotateCounterClockwise(): Direction; 18 | } 19 | 20 | class DirectionValue implements Direction { 21 | 22 | constructor(private readonly name: string) {} 23 | 24 | oppositeDirection(): Direction { 25 | return this.transform(4); 26 | } 27 | 28 | rotateClockwise(): Direction { 29 | return this.transform(1); 30 | } 31 | 32 | rotateCounterClockwise(): Direction { 33 | return this.transform(allDirections.length - 1); 34 | } 35 | 36 | toString(): string { 37 | return this.name; 38 | } 39 | 40 | private transform(delta: number): Direction { 41 | const ourIndex = directionIndex(this); 42 | const desired = ourIndex + delta; 43 | return allDirections[desired % allDirections.length]; 44 | } 45 | } 46 | 47 | export const NORTH = Object.freeze(new DirectionValue("NORTH")); 48 | export const NORTHEAST = Object.freeze(new DirectionValue("NORTHEAST")); 49 | export const EAST = Object.freeze(new DirectionValue("EAST")); 50 | export const SOUTHEAST = Object.freeze(new DirectionValue("SOUTHEAST")); 51 | export const SOUTH = Object.freeze(new DirectionValue("SOUTH")); 52 | export const SOUTHWEST = Object.freeze(new DirectionValue("SOUTHWEST")); 53 | export const WEST = Object.freeze(new DirectionValue("WEST")); 54 | export const NORTHWEST = Object.freeze(new DirectionValue("NORTHWEST")); 55 | 56 | const allDirections = [ 57 | NORTH, 58 | NORTHEAST, 59 | EAST, 60 | SOUTHEAST, 61 | SOUTH, 62 | SOUTHWEST, 63 | WEST, 64 | NORTHWEST, 65 | ]; 66 | 67 | function directionIndex(direction: DirectionValue): number { 68 | return allDirections.indexOf(direction); 69 | } 70 | 71 | const cardinalDirections = [ 72 | NORTH, 73 | EAST, 74 | SOUTH, 75 | WEST, 76 | ]; 77 | 78 | export function forEachCardinalDirection(callback: DirectionFn) { 79 | cardinalDirections.forEach((dir) => callback(dir)); 80 | } 81 | 82 | export function getRandomCardinalDirection(): Direction { 83 | return getRandomDirectionFrom(cardinalDirections); 84 | } 85 | 86 | export function getRandomDirection(): Direction { 87 | return getRandomDirectionFrom(allDirections); 88 | } 89 | 90 | function getRandomDirectionFrom(directionArray: Direction[]): Direction { 91 | const maxIndex = directionArray.length - 1; 92 | const index = Random.getRandom(maxIndex); 93 | return directionArray[index]; 94 | } 95 | -------------------------------------------------------------------------------- /src/disasterWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { DISASTER_WINDOW_CLOSED } from './messages'; 11 | import { MiscUtils } from './miscUtils'; 12 | import { ModalWindow } from './modalWindow'; 13 | 14 | var disasterSelectID = '#disasterSelect'; 15 | var disasterCancelID = '#disasterCancel'; 16 | var disasterOKID = '#disasterOK'; 17 | var disasterFormID = '#disasterForm'; 18 | 19 | 20 | var DisasterWindow = ModalWindow(function() { 21 | $(disasterFormID).on('submit', submit.bind(this)); 22 | $(disasterCancelID).on('click', cancel.bind(this)); 23 | }, disasterSelectID); 24 | 25 | 26 | DisasterWindow.prototype.close = function(disaster) { 27 | disaster = disaster || DisasterWindow.DISASTER_NONE; 28 | this._toggleDisplay(); 29 | this._emitEvent(DISASTER_WINDOW_CLOSED, disaster); 30 | }; 31 | 32 | 33 | var cancel = function(e) { 34 | e.preventDefault(); 35 | this.close(); 36 | }; 37 | 38 | 39 | var submit = function(e) { 40 | e.preventDefault(); 41 | 42 | // Get element values 43 | var requestedDisaster = $(disasterSelectID)[0].value; 44 | this.close(requestedDisaster); 45 | }; 46 | 47 | 48 | DisasterWindow.prototype.open = function() { 49 | var i; 50 | 51 | // Ensure options have right values 52 | $('#disasterNone').attr('value', DisasterWindow.DISASTER_NONE); 53 | $('#disasterMonster').attr('value', DisasterWindow.DISASTER_MONSTER); 54 | $('#disasterFire').attr('value', DisasterWindow.DISASTER_FIRE); 55 | $('#disasterFlood').attr('value', DisasterWindow.DISASTER_FLOOD); 56 | $('#disasterCrash').attr('value', DisasterWindow.DISASTER_CRASH); 57 | $('#disasterMeltdown').attr('value', DisasterWindow.DISASTER_MELTDOWN); 58 | $('#disasterTornado').attr('value', DisasterWindow.DISASTER_TORNADO); 59 | 60 | this._toggleDisplay(); 61 | }; 62 | 63 | 64 | Object.defineProperties(DisasterWindow, 65 | {DISASTER_NONE: MiscUtils.makeConstantDescriptor('None'), 66 | DISASTER_MONSTER: MiscUtils.makeConstantDescriptor('Monster'), 67 | DISASTER_FIRE: MiscUtils.makeConstantDescriptor('Fire'), 68 | DISASTER_FLOOD: MiscUtils.makeConstantDescriptor('Flood'), 69 | DISASTER_CRASH: MiscUtils.makeConstantDescriptor('Crash'), 70 | DISASTER_MELTDOWN: MiscUtils.makeConstantDescriptor('Meltdown'), 71 | DISASTER_TORNADO: MiscUtils.makeConstantDescriptor('Tornado')}); 72 | 73 | 74 | export { DisasterWindow }; 75 | -------------------------------------------------------------------------------- /src/emergencyServices.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Position } from './position'; 11 | import { FIRESTATION, POLICESTATION } from "./tileValues"; 12 | 13 | var handleService = function(censusStat, budgetEffect, blockMap) { 14 | return function(map, x, y, simData) { 15 | simData.census[censusStat] += 1; 16 | 17 | var effect = simData.budget[budgetEffect]; 18 | var isPowered = map.getTile(x, y).isPowered(); 19 | // Unpowered buildings are half as effective 20 | if (!isPowered) 21 | effect = Math.floor(effect / 2); 22 | 23 | var pos = new Position(x, y); 24 | var connectedToRoads = simData.trafficManager.findPerimeterRoad(pos); 25 | if (!connectedToRoads) 26 | effect = Math.floor(effect / 2); 27 | 28 | var currentEffect = simData.blockMaps[blockMap].worldGet(x, y); 29 | currentEffect += effect; 30 | simData.blockMaps[blockMap].worldSet(x, y, currentEffect); 31 | }; 32 | }; 33 | 34 | 35 | var policeStationFound = handleService('policeStationPop', 'policeEffect', 'policeStationMap'); 36 | var fireStationFound = handleService('fireStationPop', 'fireEffect', 'fireStationMap'); 37 | 38 | 39 | var EmergencyServices = { 40 | registerHandlers: function(mapScanner, repairManager) { 41 | mapScanner.addAction(POLICESTATION, policeStationFound); 42 | mapScanner.addAction(FIRESTATION, fireStationFound); 43 | } 44 | }; 45 | 46 | 47 | export { EmergencyServices }; 48 | -------------------------------------------------------------------------------- /src/evaluationWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { ModalWindow } from './modalWindow'; 11 | import { EVAL_WINDOW_CLOSED } from './messages'; 12 | import { Text } from './text'; 13 | 14 | var EvaluationWindow = ModalWindow(function() { 15 | $(evaluationFormID).on('submit', submit.bind(this)); 16 | }); 17 | 18 | 19 | var evaluationFormID = '#evalButtons'; 20 | var evaluationOKID = '#evalOK'; 21 | 22 | 23 | EvaluationWindow.prototype.close = function() { 24 | this._emitEvent(EVAL_WINDOW_CLOSED); 25 | this._toggleDisplay(); 26 | }; 27 | 28 | 29 | var submit = function(e) { 30 | e.preventDefault(); 31 | this.close(); 32 | }; 33 | 34 | 35 | EvaluationWindow.prototype._populateWindow = function(evaluation) { 36 | $('#evalYes').text(evaluation.cityYes); 37 | $('#evalNo').text(100 - evaluation.cityYes); 38 | for (var i = 0; i < 4; i++) { 39 | var problemNo = evaluation.getProblemNumber(i); 40 | if (problemNo !== null) { 41 | var text = Text.problems[problemNo]; 42 | $('#evalProb' + (i + 1)).text(text); 43 | $('#evalProb' + (i + 1)).show(); 44 | } else { 45 | $('#evalProb' + (i + 1)).hide(); 46 | } 47 | } 48 | 49 | $('#evalPopulation').text(evaluation.cityPop); 50 | $('#evalMigration').text(evaluation.cityPopDelta); 51 | $('#evalValue').text(evaluation.cityAssessedValue); 52 | $('#evalLevel').text(Text.gameLevel[evaluation.gameLevel]); 53 | $('#evalClass').text(Text.cityClass[evaluation.cityClass]); 54 | $('#evalScore').text(evaluation.cityScore); 55 | $('#evalScoreDelta').text(evaluation.cityScoreDelta); 56 | }; 57 | 58 | 59 | EvaluationWindow.prototype.open = function(evaluation) { 60 | this._populateWindow(evaluation); 61 | this._toggleDisplay(); 62 | }; 63 | 64 | 65 | export { EvaluationWindow }; 66 | -------------------------------------------------------------------------------- /src/eventEmitter.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Config } from './config'; 11 | 12 | // Decorate the given object, by adding {add|remove}EventListener methods, and an internal '_emitEvent' method 13 | var EventEmitter = function(obj) { 14 | var events = {}; 15 | 16 | 17 | var addListener = function(event, listener) { 18 | if (!(event in events)) 19 | events[event] = []; 20 | 21 | var listeners = events[event]; 22 | if (listeners.indexOf(listener) === -1) 23 | listeners.push(listener); 24 | }; 25 | 26 | 27 | var removeListener = function(event, listener) { 28 | if (!(event in events)) 29 | events[event] = []; 30 | 31 | var listeners = events[event]; 32 | var index = listeners.indexOf(listener); 33 | if (index !== -1) 34 | listeners.splice(index, 1); 35 | }; 36 | 37 | 38 | var emitEvent = function(event, value) { 39 | if (event === undefined) { 40 | if (!Config.debug) 41 | console.warn('Sending undefined event!'); 42 | else 43 | throw new Error('Sending undefined event!'); 44 | } 45 | 46 | if (!(event in events)) 47 | events[event] = []; 48 | 49 | var listeners = events[event]; 50 | for (var i = 0, l = listeners.length; i < l; i++) 51 | listeners[i](value); 52 | }; 53 | 54 | 55 | var addProps = function(obj, message) { 56 | var hasExistingProp = ['addEventListener', 'removeEventListener', '_emitEvent'].some(function(prop) { 57 | return obj[prop] !== undefined; 58 | }); 59 | 60 | if (hasExistingProp) 61 | throw new Error('Cannot decorate ' + message + ': existing properties would be overwritten!'); 62 | 63 | obj.addEventListener = addListener; 64 | obj.removeEventListener = removeListener; 65 | obj._emitEvent = emitEvent; 66 | }; 67 | 68 | if (typeof(obj) === 'object') 69 | addProps(obj, 'object'); 70 | else 71 | addProps(obj.prototype, 'constructor'); 72 | 73 | return obj; 74 | }; 75 | 76 | 77 | export { EventEmitter }; 78 | -------------------------------------------------------------------------------- /src/explosionSprite.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BaseSprite } from './baseSprite'; 11 | import { EXPLOSION_REPORTED, SOUND_EXPLOSIONHIGH } from './messages'; 12 | import { MiscUtils } from './miscUtils'; 13 | import { Random } from './random'; 14 | import { SPRITE_EXPLOSION } from './spriteConstants'; 15 | import { SpriteUtils } from './spriteUtils'; 16 | import { TileUtils } from './tileUtils'; 17 | import { DIRT } from "./tileValues"; 18 | 19 | function ExplosionSprite(map, spriteManager, x, y) { 20 | this.init(SPRITE_EXPLOSION, map, spriteManager, x, y); 21 | this.width = 48; 22 | this.height = 48; 23 | this.xOffset = -24; 24 | this.yOffset = -24; 25 | this.frame = 1; 26 | } 27 | 28 | 29 | BaseSprite(ExplosionSprite); 30 | 31 | 32 | ExplosionSprite.prototype.startFire = function(x, y) { 33 | x = this.worldX; 34 | y = this.worldY; 35 | 36 | if (!this.map.testBounds(x, y)) 37 | return; 38 | 39 | var tile = this.map.getTile(x, y); 40 | var tileValue = tile.getValue(); 41 | 42 | if (!tile.isCombustible() && tileValue !== DIRT) 43 | return; 44 | 45 | if (tile.isZone()) 46 | return; 47 | 48 | this.map.setTo(x, y, TileUtils.randomFire()); 49 | }; 50 | 51 | 52 | ExplosionSprite.prototype.move = function(spriteCycle, disasterManager, blockMaps) { 53 | if ((spriteCycle & 1) === 0) { 54 | if (this.frame === 1) { 55 | // Convert sprite coordinates to tile coordinates. 56 | var explosionX = this.worldX; 57 | var explosionY = this.worldY; 58 | this._emitEvent(SOUND_EXPLOSIONHIGH); 59 | this._emitEvent(EXPLOSION_REPORTED, {x: explosionX, y: explosionY}); 60 | } 61 | 62 | this.frame++; 63 | } 64 | 65 | if (this.frame > 6) { 66 | this.frame = 0; 67 | 68 | this.startFire(this.x, this.y); 69 | this.startFire(this.x - 16, this.y - 16); 70 | this.startFire(this.x + 16, this.y + 16); 71 | this.startFire(this.x - 16, this.y + 16); 72 | this.startFire(this.x + 16, this.y + 16); 73 | } 74 | }; 75 | 76 | 77 | // Metadata for image loading 78 | Object.defineProperties(ExplosionSprite, 79 | {ID: MiscUtils.makeConstantDescriptor(7), 80 | width: MiscUtils.makeConstantDescriptor(48), 81 | frames: MiscUtils.makeConstantDescriptor(6)}); 82 | 83 | 84 | export { ExplosionSprite }; 85 | -------------------------------------------------------------------------------- /src/gameTools.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BuildingTool } from './buildingTool'; 11 | import { BulldozerTool } from './bulldozerTool'; 12 | import { EventEmitter } from './eventEmitter'; 13 | import { QUERY_WINDOW_NEEDED } from './messages'; 14 | import { MiscUtils } from './miscUtils'; 15 | import { ParkTool } from './parkTool'; 16 | import { RailTool } from './railTool'; 17 | import { RoadTool } from './roadTool'; 18 | import { QueryTool } from './queryTool'; 19 | import * as TileValues from "./tileValues"; 20 | import { WireTool } from './wireTool'; 21 | 22 | function GameTools(map) { 23 | var tools = EventEmitter({ 24 | airport: new BuildingTool(10000, TileValues.AIRPORT, map, 6, false), 25 | bulldozer: new BulldozerTool(map), 26 | coal: new BuildingTool(3000, TileValues.POWERPLANT, map, 4, false), 27 | commercial: new BuildingTool(100, TileValues.COMCLR, map, 3, false), 28 | fire: new BuildingTool(500, TileValues.FIRESTATION, map, 3, false), 29 | industrial: new BuildingTool(100, TileValues.INDCLR, map, 3, false), 30 | nuclear: new BuildingTool(5000, TileValues.NUCLEAR, map, 4, true), 31 | park: new ParkTool(map), 32 | police: new BuildingTool(500, TileValues.POLICESTATION, map, 3, false), 33 | port: new BuildingTool(3000, TileValues.PORT, map, 4, false), 34 | rail: new RailTool(map), 35 | residential: new BuildingTool(100, TileValues.FREEZ, map, 3, false), 36 | road: new RoadTool(map), 37 | query: new QueryTool(map), 38 | stadium: new BuildingTool(5000, TileValues.STADIUM, map, 4, false), 39 | wire: new WireTool(map), 40 | }); 41 | 42 | tools.query.addEventListener(QUERY_WINDOW_NEEDED, MiscUtils.reflectEvent.bind(tools, QUERY_WINDOW_NEEDED)); 43 | 44 | return tools; 45 | } 46 | 47 | 48 | export { GameTools }; 49 | -------------------------------------------------------------------------------- /src/infoBar.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import * as Messages from './messages'; 11 | import { MiscUtils } from './miscUtils'; 12 | import { Text } from './text'; 13 | 14 | // TODO L20N 15 | 16 | var InfoBar = function(classification, population, score, funds, date, name) { 17 | var classificationSelector = MiscUtils.normaliseDOMid(classification); 18 | var populationSelector = MiscUtils.normaliseDOMid(population); 19 | var scoreSelector = MiscUtils.normaliseDOMid(score); 20 | var fundsSelector = MiscUtils.normaliseDOMid(funds); 21 | var dateSelector = MiscUtils.normaliseDOMid(date); 22 | var nameSelector = MiscUtils.normaliseDOMid(name); 23 | 24 | return function(dataSource, initialValues) { 25 | $(classificationSelector).text(initialValues.classification); 26 | $(populationSelector).text(initialValues.population); 27 | $(scoreSelector).text(initialValues.score); 28 | $(fundsSelector).text(initialValues.funds); 29 | $(dateSelector).text([Text.months[initialValues.date.month], initialValues.date.year].join(' ')); 30 | $(nameSelector).text(initialValues.name); 31 | 32 | // Add the various listeners 33 | dataSource.addEventListener(Messages.CLASSIFICATION_UPDATED, function(classification) { 34 | $(classificationSelector).text(classification); 35 | }); 36 | 37 | dataSource.addEventListener(Messages.POPULATION_UPDATED, function(population) { 38 | $(populationSelector).text(population); 39 | }); 40 | 41 | dataSource.addEventListener(Messages.SCORE_UPDATED, function(score) { 42 | $(scoreSelector).text(score); 43 | }); 44 | 45 | dataSource.addEventListener(Messages.FUNDS_CHANGED, function(funds) { 46 | $(fundsSelector).text(funds); 47 | }); 48 | 49 | dataSource.addEventListener(Messages.DATE_UPDATED, function(date) { 50 | $(dateSelector).text([Text.months[date.month], date.year].join(' ')); 51 | }); 52 | }; 53 | }; 54 | 55 | 56 | export { InfoBar }; 57 | -------------------------------------------------------------------------------- /src/mapScanner.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Tile } from "./tile"; 11 | import { FLOOD } from "./tileValues"; 12 | 13 | // Tile to be filled to avoid creating lots of GC-able objects 14 | var tile = new Tile(); 15 | 16 | 17 | function MapScanner(map) { 18 | this._map = map; 19 | this._actions = []; 20 | } 21 | 22 | 23 | var isCallable = function(f) { 24 | return typeof(f) === 'function'; 25 | }; 26 | 27 | 28 | MapScanner.prototype.addAction = function(criterion, action) { 29 | this._actions.push({criterion: criterion, action: action}); 30 | }; 31 | 32 | 33 | MapScanner.prototype.mapScan = function(startX, maxX, simData) { 34 | for (var y = 0; y < this._map.height; y++) { 35 | for (var x = startX; x < maxX; x++) { 36 | this._map.getTile(x, y, tile); 37 | var tileValue = tile.getValue(); 38 | 39 | if (tileValue < FLOOD) 40 | continue; 41 | 42 | if (tile.isConductive()) 43 | simData.powerManager.setTilePower(x, y); 44 | 45 | if (tile.isZone()) { 46 | simData.repairManager.checkTile(x, y, simData.cityTime); 47 | var powered = tile.isPowered(); 48 | if (powered) 49 | simData.census.poweredZoneCount += 1; 50 | else 51 | simData.census.unpoweredZoneCount += 1; 52 | } 53 | 54 | for (var i = 0, l = this._actions.length; i < l; i++) { 55 | var current = this._actions[i]; 56 | var callable = isCallable(current.criterion); 57 | 58 | if (callable && current.criterion.call(null, tile)) { 59 | current.action.call(null, this._map, x, y, simData); 60 | break; 61 | } else if (!callable && current.criterion === tileValue) { 62 | current.action.call(null, this._map, x, y, simData); 63 | break; 64 | } 65 | } 66 | } 67 | } 68 | }; 69 | 70 | 71 | export { MapScanner }; 72 | -------------------------------------------------------------------------------- /src/messages.ts: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | export const AUTOBUDGET_CHANGED = "Autobudget changed"; 11 | export const BUDGET_NEEDED = "User needs to budget"; 12 | export const BUDGET_REQUESTED = "Budget window requested"; 13 | export const BUDGET_WINDOW_CLOSED = "Budget window closed"; 14 | export const BLACKOUTS_REPORTED = "Blackouts reported"; 15 | export const CLASSIFICATION_UPDATED = "Classification updated"; 16 | export const CONGRATS_SHOWING = "Congratulations showing"; 17 | export const CONGRATS_WINDOW_CLOSED = "Congratulations window closed"; 18 | export const DATE_UPDATED = "Date changed"; 19 | export const DEBUG_WINDOW_REQUESTED = "Debug Window Requested"; 20 | export const DEBUG_WINDOW_CLOSED = "Debug Window Closed"; 21 | export const DISASTER_REQUESTED = "Disaster Requested"; 22 | export const DISASTER_WINDOW_CLOSED = "Disaster window closed"; 23 | export const EARTHQUAKE = "Earthquake"; 24 | export const EVAL_REQUESTED = "Evaluation Requested"; 25 | export const EVAL_UPDATED = "Evaluation Updated"; 26 | export const EVAL_WINDOW_CLOSED = "Eval window closed"; 27 | export const EXPLOSION_REPORTED = "Explosion Reported"; 28 | export const FIRE_REPORTED = "Fire!"; 29 | export const FIRE_STATION_NEEDS_FUNDING = "Fire station needs funding"; 30 | export const FLOODING_REPORTED = "Flooding reported"; 31 | export const FRONT_END_MESSAGE = "Front-end Message"; 32 | export const FUNDS_CHANGED = "Total funds has changed"; 33 | export const HEAVY_TRAFFIC = "Total funds has changed"; 34 | export const HELICOPTER_CRASHED = "Helicopter crashed"; 35 | export const HIGH_CRIME = "High crime"; 36 | export const HIGH_POLLUTION = "High pollution"; 37 | export const MONSTER_SIGHTED = "Monster sighted"; 38 | export const NAG_WINDOW_CLOSED = "Nag window closed"; 39 | export const NEED_AIRPORT = "Airport needed"; 40 | export const NEED_ELECTRICITY = "More power needed"; 41 | export const NEED_FIRE_STATION = "Fire station needed"; 42 | export const NEED_MORE_COMMERCIAL = "More commercial zones needed"; 43 | export const NEED_MORE_INDUSTRIAL = "More industrial zones needed"; 44 | export const NEED_MORE_RAILS = "More railways needed"; 45 | export const NEED_MORE_RESIDENTIAL = "More residential needed"; 46 | export const NEED_MORE_ROADS = "More roads needed"; 47 | export const NEED_POLICE_STATION = "Police station needed"; 48 | export const NEED_SEAPORT = "Seaport needed"; 49 | export const NEED_STADIUM = "Stadium needed"; 50 | export const NO_MONEY = "No money"; 51 | export const NOT_ENOUGH_POWER = "Not enough power"; 52 | export const NUCLEAR_MELTDOWN = "Nuclear Meltdown"; 53 | export const PLANE_CRASHED = "Plane crashed"; 54 | export const POLICE_NEEDS_FUNDING = "Police need funding"; 55 | export const POPULATION_UPDATED = "Population updated"; 56 | export const QUERY_WINDOW_CLOSED = "Query window closed"; 57 | export const QUERY_WINDOW_NEEDED = "Query window needed"; 58 | export const REACHED_CAPITAL = "Now a capital"; 59 | export const REACHED_CITY = "Now a city"; 60 | export const REACHED_METROPOLIS = "Now a metropolis"; 61 | export const REACHED_MEGALOPOLIS = "Now a megalopolis"; 62 | export const REACHED_TOWN = "Now a town"; 63 | export const REACHED_VILLAGE = "Now a village"; 64 | export const ROAD_NEEDS_FUNDING = "Roads need funding"; 65 | export const SAVE_REQUESTED = "Save requested"; 66 | export const SAVE_WINDOW_CLOSED = "Save window closed"; 67 | export const SCORE_UPDATED = "Scoe updated"; 68 | export const SCREENSHOT_LINK_CLOSED = "Screenshot link closed"; 69 | export const SCREENSHOT_WINDOW_CLOSED = "Screenshot window closed"; 70 | export const SCREENSHOT_WINDOW_REQUESTED = "Screenshot window requested"; 71 | export const SETTINGS_WINDOW_CLOSED = "Settings window closed"; 72 | export const SETTINGS_WINDOW_REQUESTED = "Settings window requested"; 73 | export const SHIP_CRASHED = "Shipwrecked"; 74 | export const SOUND_EXPLOSIONHIGH = "Explosion! Bang!"; 75 | export const SOUND_EXPLOSIONLOW = "Explosion! Bang!"; 76 | export const SOUND_HEAVY_TRAFFIC = "Heavy Traffic sound"; 77 | export const SOUND_HONKHONK = "HonkHonk sound"; 78 | export const SOUND_MONSTER = "Monster sound"; 79 | export const SPEED_CHANGE = "Speed change"; 80 | export const SPRITE_DYING = "Sprite dying"; 81 | export const SPRITE_MOVED = "Sprite move"; 82 | export const TAX_TOO_HIGH = "Tax too high"; 83 | export const TOOL_CLICKED = "Tool clicked"; 84 | export const TORNADO_SIGHTED = "Tornado sighted"; 85 | export const TOUCH_WINDOW_CLOSED = "Touch Window closed"; 86 | export const TRAFFIC_JAMS = "Traffic jams reported"; 87 | export const TRAIN_CRASHED = "Train crashed"; 88 | export const VALVES_UPDATED = "Valves updated"; 89 | export const WELCOME = "Welcome to micropolisJS"; 90 | 91 | export const DISASTER_MESSAGES = [ 92 | EARTHQUAKE, 93 | EXPLOSION_REPORTED, 94 | FIRE_REPORTED, 95 | FLOODING_REPORTED, 96 | MONSTER_SIGHTED, 97 | NUCLEAR_MELTDOWN, 98 | TORNADO_SIGHTED, 99 | ]; 100 | 101 | export const CRASHES = [ 102 | HELICOPTER_CRASHED, 103 | PLANE_CRASHED, 104 | SHIP_CRASHED, 105 | TRAIN_CRASHED, 106 | ]; 107 | -------------------------------------------------------------------------------- /src/micropolis.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Config } from './config'; 11 | import { SplashScreen } from './splashScreen'; 12 | import { TileSet } from './tileSet'; 13 | import { TileSetURI } from './tileSetURI'; 14 | import { TileSetSnowURI } from './tileSetSnowURI'; 15 | 16 | /* 17 | * 18 | * Our task in main is to load the tile image, create a TileSet from it, and then tell the SplashScreen to display 19 | * itself. We will never return here. 20 | * 21 | */ 22 | 23 | 24 | var fallbackImage, tileSet, snowTileSet; 25 | 26 | 27 | var onTilesLoaded = function() { 28 | var snowTiles = $('#snowtiles')[1]; 29 | snowTileSet = new TileSet(snowTiles, onAllTilesLoaded, onFallbackTilesLoaded); 30 | }; 31 | 32 | 33 | var onAllTilesLoaded = function() { 34 | // Kick things off properly 35 | var sprites = $('#sprites')[0]; 36 | if (sprites.complete) { 37 | $('#loadingBanner').css('display', 'none'); 38 | var s = new SplashScreen(tileSet, snowTileSet, sprites); 39 | } else { 40 | window.setTimeout(onAllTilesLoaded, 0); 41 | } 42 | }; 43 | 44 | 45 | // XXX Replace with an error dialog 46 | var onFallbackError = function() { 47 | fallbackImage.onload = fallbackImage.onerror = null; 48 | alert('Failed to load tileset!'); 49 | }; 50 | 51 | 52 | var onFallbackSnowLoad = function() { 53 | fallbackImage.onload = fallbackImage.onerror = null; 54 | snowTileSet = new TileSet(fallbackImage, onAllTilesLoaded, onFallbackError); 55 | }; 56 | 57 | 58 | var onFallbackTilesLoaded = function() { 59 | fallbackImage = new Image(); 60 | fallbackImage.onload = onFallbackSnowLoad; 61 | fallbackImage.onerror = onFallbackError; 62 | fallbackImage.src = TileSetSnowURI; 63 | }; 64 | 65 | 66 | var onFallbackLoad = function() { 67 | fallbackImage.onload = fallbackImage.onerror = null; 68 | tileSet = new TileSet(fallbackImage, onFallbackTilesLoaded, onFallbackError); 69 | }; 70 | 71 | 72 | var tileSetError = function() { 73 | // We might be running locally in Chrome, which handles the security context of file URIs differently, which makes 74 | // things go awry when we try to create an image from a "tainted" canvas (one we've painted on). Let's try creating 75 | // the tileset by URI instead 76 | fallbackImage = new Image(); 77 | fallbackImage.onload = onFallbackLoad; 78 | fallbackImage.onerror = onFallbackError; 79 | fallbackImage.src = TileSetURI; 80 | }; 81 | 82 | 83 | // Check for debug parameter in URL 84 | Config.debug = window.location.search.slice(1).split('&').some(function(param) { 85 | return param.trim().toLowerCase() === 'debug=1'; 86 | }); 87 | 88 | 89 | var tiles = $('#tiles')[0]; 90 | tileSet = new TileSet(tiles, onTilesLoaded, tileSetError); 91 | var snowtiles = $('#snowtiles')[1]; 92 | -------------------------------------------------------------------------------- /src/miscTiles.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Random } from './random'; 11 | import { TileUtils } from './tileUtils'; 12 | import { DIRT, IZB, RADTILE } from "./tileValues"; 13 | import { ZoneUtils } from './zoneUtils'; 14 | 15 | var xDelta = [-1, 0, 1, 0 ]; 16 | var yDelta = [ 0, -1, 0, 1 ]; 17 | 18 | var fireFound = function(map, x, y, simData) { 19 | simData.census.firePop += 1; 20 | 21 | if ((Random.getRandom16() & 3) !== 0) 22 | return; 23 | 24 | // Try to set neighbouring tiles on fire as well 25 | for (var i = 0; i < 4; i++) { 26 | if (Random.getChance(7)) { 27 | var xTem = x + xDelta[i]; 28 | var yTem = y + yDelta[i]; 29 | 30 | if (map.testBounds(xTem, yTem)) { 31 | var tile = map.getTile(x, y); 32 | if (!tile.isCombustible()) 33 | continue; 34 | 35 | if (tile.isZone()) { 36 | // Neighbour is a ione and burnable 37 | ZoneUtils.fireZone(map, x, y, simData.blockMaps); 38 | 39 | // Industrial zones etc really go boom 40 | if (tile.getValue() > IZB) 41 | simData.spriteManager.makeExplosionAt(x, y); 42 | } 43 | 44 | map.setTo(tileUtils.randomFire()); 45 | } 46 | } 47 | } 48 | 49 | // Compute likelyhood of fire running out of fuel 50 | var rate = 10; // Likelyhood of extinguishing (bigger means less chance) 51 | i = simData.blockMaps.fireStationEffectMap.worldGet(x, y); 52 | 53 | if (i > 100) 54 | rate = 1; 55 | else if (i > 20) 56 | rate = 2; 57 | else if (i > 0) 58 | rate = 3; 59 | 60 | // Decide whether to put out the fire. 61 | if (Random.getRandom(rate) === 0) 62 | map.setTo(x, y, TileUtils.randomRubble()); 63 | }; 64 | 65 | 66 | var radiationFound = function(map, x, y, simData) { 67 | if (Random.getChance(4095)) 68 | map.setTile(x, y, DIRT, 0); 69 | }; 70 | 71 | 72 | var floodFound = function(map, x, y, simData) { 73 | simData.disasterManager.doFlood(x, y, simData.blockMaps); 74 | }; 75 | 76 | 77 | var MiscTiles = { 78 | registerHandlers: function(mapScanner, repairManager) { 79 | mapScanner.addAction(TileUtils.isFire, fireFound, true); 80 | mapScanner.addAction(RADTILE, radiationFound, true); 81 | mapScanner.addAction(TileUtils.isFlood, floodFound, true); 82 | } 83 | }; 84 | 85 | 86 | export { MiscTiles }; 87 | -------------------------------------------------------------------------------- /src/miscUtils.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | var clamp = function(value, min, max) { 11 | if (value < min) 12 | return min; 13 | if (value > max) 14 | return max; 15 | 16 | return value; 17 | }; 18 | 19 | 20 | var makeConstantDescriptor = function(value) { 21 | return {configurable: false, enumerable: false, 22 | writeable: false, value: value}; 23 | }; 24 | 25 | 26 | var normaliseDOMid = function(id) { 27 | return (id[0] !== '#' ? '#' : '') + id; 28 | }; 29 | 30 | 31 | var reflectEvent = function(message, value) { 32 | this._emitEvent(message, value); 33 | }; 34 | 35 | 36 | var MiscUtils = { 37 | clamp: clamp, 38 | makeConstantDescriptor: makeConstantDescriptor, 39 | normaliseDOMid: normaliseDOMid, 40 | reflectEvent: reflectEvent 41 | }; 42 | 43 | 44 | export { MiscUtils }; 45 | -------------------------------------------------------------------------------- /src/modalWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { EventEmitter } from './eventEmitter'; 11 | import { MiscUtils } from './miscUtils'; 12 | 13 | var ModalWindow = function(constructorFunction, focusID) { 14 | focusID = focusID ? MiscUtils.normaliseDOMid(focusID) : null; 15 | 16 | var newConstructor = function(opacityLayerID, windowID) { 17 | this._opacityLayer = MiscUtils.normaliseDOMid(opacityLayerID); 18 | this._windowID = MiscUtils.normaliseDOMid(windowID); 19 | constructorFunction.call(this); 20 | }; 21 | 22 | 23 | newConstructor.prototype._toggleDisplay = function() { 24 | var opacityLayer = $(this._opacityLayer); 25 | opacityLayer = opacityLayer.length === 0 ? null : opacityLayer; 26 | if (opacityLayer === null) 27 | throw new Error('Node ' + this._opacityLayer + ' not found'); 28 | 29 | var modalWindow = $(this._windowID); 30 | modalWindow = modalWindow.length === 0 ? null : modalWindow; 31 | if (modalWindow === null) 32 | throw new Error('Node ' + this._windowID + ' not found'); 33 | 34 | opacityLayer.toggle(); 35 | modalWindow.toggle(); 36 | 37 | if (focusID !== null) 38 | $(focusID).focus(); 39 | else 40 | $(this._windowID + ' input[type=submit]').focus(); 41 | }; 42 | 43 | 44 | return EventEmitter(newConstructor); 45 | }; 46 | 47 | 48 | export { ModalWindow }; 49 | -------------------------------------------------------------------------------- /src/monsterTV.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { EventEmitter } from './eventEmitter'; 11 | import { GameCanvas } from './gameCanvas'; 12 | import { SPRITE_DYING, SPRITE_MOVED } from './messages'; 13 | 14 | var TIMEOUT_SECS = 10; 15 | 16 | 17 | var MonsterTV = function(map, tileSet, spriteSheet, animationManager) { 18 | this.isOpen = false; 19 | this._tracking = null; 20 | 21 | // Need to quickly flick on the canvas container so the canvas picks up the correct dimensions (this is a bit of a 22 | // hack as we're reusing the same GameCanvas that paints the main map, but it avoids a lot of duplication) 23 | $(monsterTVID).toggle(); 24 | 25 | this.canvas = new GameCanvas(monsterTVCanvasID, monsterTVContainerID); 26 | this.canvas.init(map, tileSet, spriteSheet, animationManager); 27 | this.canvas.disallowOffMap(); 28 | 29 | this._onMove = onMove.bind(this); 30 | this._onDie = onDie.bind(this); 31 | this._timeout = null; 32 | 33 | $(monsterTVID).toggle(); 34 | this.close = close.bind(this); 35 | $(monsterTVFormID).on('submit', this.close); 36 | }; 37 | 38 | 39 | var monsterTVFormID = '#monsterTVForm'; 40 | var monsterTVContainerID = '#tvContainer'; 41 | var monsterTVCanvasID = '#tvCanvas'; 42 | var monsterTVID = '#monstertv'; 43 | 44 | 45 | var close = function(e) { 46 | if (e) 47 | e.preventDefault(); 48 | 49 | this.isOpen = false; 50 | $(monsterTVID).toggle(); 51 | }; 52 | 53 | 54 | MonsterTV.prototype.paint = function(sprite, isPaused) { 55 | if (!this.isOpen) 56 | return; 57 | 58 | this.canvas.paint(null, sprite, isPaused); 59 | }; 60 | 61 | 62 | var onMove = function(event) { 63 | var min = this.canvas.getTileOrigin(); 64 | var max = this.canvas.getMaxTile(); 65 | 66 | if (event.x < min.x || event.y < min.y || event.x >= max.x || event.y >= max.y) 67 | this.canvas.centreOn(event.x, event.y); 68 | }; 69 | 70 | 71 | var onDie = function(event) { 72 | this._tracking.removeEventListener(SPRITE_MOVED, this._onMove); 73 | this._tracking.removeEventListener(SPRITE_DYING, this._onDie); 74 | this._tracking = null; 75 | 76 | this._timeout = window.setTimeout(function() {this._timeout = null; this.close();}.bind(this), TIMEOUT_SECS * 1000); 77 | }; 78 | 79 | 80 | MonsterTV.prototype.track = function(x, y, sprite) { 81 | if (this._tracking !== null) { 82 | this._tracking.removeEventListener(SPRITE_MOVED, this._onMove); 83 | this._tracking.removeEventListener(SPRITE_DYING, this._onDie); 84 | } 85 | 86 | this._tracking = sprite; 87 | sprite.addEventListener(SPRITE_MOVED, this._onMove); 88 | sprite.addEventListener(SPRITE_DYING, this._onDie); 89 | this.canvas.centreOn(x, y); 90 | 91 | if (this._timeout !== null) { 92 | window.clearTimeout(this._timeout); 93 | this._timeout = null; 94 | } 95 | 96 | if (this.isOpen) 97 | return; 98 | 99 | this.open(); 100 | }; 101 | 102 | 103 | MonsterTV.prototype.show = function(x, y) { 104 | this.canvas.centreOn(x, y); 105 | 106 | if (this._timeout !== null) { 107 | window.clearTimeout(this._timeout); 108 | this._timeout = null; 109 | } 110 | 111 | if (this.isOpen) 112 | return; 113 | 114 | this.open(); 115 | }; 116 | 117 | 118 | MonsterTV.prototype.open = function(message) { 119 | this.isOpen = true; 120 | $(monsterTVID).toggle(); 121 | }; 122 | 123 | 124 | export { MonsterTV }; 125 | -------------------------------------------------------------------------------- /src/mouseBox.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | var MouseBox = { 11 | draw: function(c, pos, width, height, options) { 12 | var lineWidth = options.lineWidth || 3.0; 13 | var strokeStyle = options.colour || 'yellow'; 14 | var shouldOutline = (('outline' in options) && options.outline === true) || false; 15 | 16 | var startModifier = -1; 17 | var endModifier = 1; 18 | if (!shouldOutline) { 19 | startModifier = 1; 20 | endModifier = -1; 21 | } 22 | 23 | var startX = pos.x + startModifier * lineWidth / 2; 24 | width = width + endModifier * lineWidth; 25 | var startY = pos.y + startModifier * lineWidth / 2; 26 | height = height + endModifier * lineWidth; 27 | 28 | var ctx = c.getContext('2d'); 29 | ctx.lineWidth = lineWidth; 30 | ctx.strokeStyle = strokeStyle; 31 | ctx.strokeRect(startX, startY, width, height); 32 | } 33 | }; 34 | 35 | 36 | export { MouseBox }; 37 | -------------------------------------------------------------------------------- /src/nagWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { NAG_WINDOW_CLOSED } from './messages'; 11 | import { ModalWindow } from './modalWindow'; 12 | 13 | var NagWindow = ModalWindow(function() { 14 | $(nagFormID).on('submit', submit.bind(this)); 15 | }); 16 | 17 | 18 | var nagFormID = '#nagForm'; 19 | var nagOKID = '#nagOK'; 20 | 21 | 22 | var submit = function(e) { 23 | e.preventDefault(); 24 | this.close(); 25 | }; 26 | 27 | 28 | NagWindow.prototype.close = function() { 29 | this._toggleDisplay(); 30 | this._emitEvent(NAG_WINDOW_CLOSED); 31 | }; 32 | 33 | 34 | NagWindow.prototype.open = function() { 35 | this._toggleDisplay(); 36 | $(nagOKID).focus(); 37 | }; 38 | 39 | 40 | export { NagWindow }; 41 | -------------------------------------------------------------------------------- /src/notification.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { MiscUtils } from './miscUtils'; 11 | import { Text } from './text'; 12 | 13 | var TIMEOUT_SECS = 30; 14 | 15 | 16 | function Notification(element, map, initialText) { 17 | element = MiscUtils.normaliseDOMid(element); 18 | 19 | this._map = map; 20 | this._element = $(element); 21 | this._timeout = null; 22 | 23 | this._handleClick = handleClick.bind(this); 24 | 25 | // The position to centre on when the link is clicked 26 | this._x = -1; 27 | this._y = -1; 28 | 29 | this._element.click(this._handleClick); 30 | this.close = close.bind(this); 31 | if (this._element.is(':visible')) 32 | this._element.toggle(); 33 | } 34 | 35 | 36 | var close = function(e) { 37 | if (e) 38 | e.preventDefault(); 39 | 40 | if (this._element.is(':visible')) 41 | this._element.toggle(); 42 | }; 43 | 44 | 45 | Notification.prototype._displayLink = function(text, x, y) { 46 | if (this._timeout !== null) { 47 | window.clearTimeout(this._timeout); 48 | this._timeout = null; 49 | } 50 | 51 | this._element.text(text); 52 | 53 | this._x = x; 54 | this._y = y; 55 | 56 | this._element.addClass('pointer'); 57 | 58 | if (!this._element.is(':visible')) 59 | this._element.toggle(); 60 | 61 | this._timeout = window.setTimeout(function() {this._timeout = null; this.close();}.bind(this), TIMEOUT_SECS * 1000); 62 | }; 63 | 64 | 65 | Notification.prototype._displayText = function(text, x, y) { 66 | if (this._timeout !== null) { 67 | window.clearTimeout(this._timeout); 68 | this._timeout = null; 69 | } 70 | 71 | this._element.removeClass('pointer'); 72 | this._element.text(text); 73 | this._x = -1; 74 | this._y = -1; 75 | 76 | if (!this._element.is(':visible')) 77 | this._element.toggle(); 78 | 79 | this._timeout = window.setTimeout(function() {this._timeout = null; this.close();}.bind(this), TIMEOUT_SECS * 1000); 80 | }; 81 | 82 | 83 | var handleClick = function(e) { 84 | e.preventDefault(); 85 | 86 | if (this._x === -1 || this._y === -1) 87 | return; 88 | 89 | this._map.centreOn(this._x, this._y); 90 | }; 91 | 92 | 93 | Notification.prototype.createMessage = function(message) { 94 | 95 | if (message.hasOwnProperty('data') && message.data !== undefined && message.data.hasOwnProperty('x') && message.data.hasOwnProperty('y')) { 96 | this._displayLink(Text.messageText[message.subject], message.data.x, message.data.y); 97 | return; 98 | } 99 | 100 | this._displayText(Text.messageText[message.subject]); 101 | }; 102 | 103 | 104 | Notification.prototype.badNews = function(message) { 105 | this._element.removeClass('neutral'); 106 | this._element.removeClass('good'); 107 | this._element.addClass('bad'); 108 | this.createMessage(message); 109 | }; 110 | 111 | 112 | Notification.prototype.goodNews = function(message) { 113 | this._element.removeClass('neutral'); 114 | this._element.removeClass('bad'); 115 | this._element.addClass('good'); 116 | this.createMessage(message); 117 | }; 118 | 119 | 120 | Notification.prototype.news = function(message) { 121 | this._element.removeClass('good'); 122 | this._element.removeClass('bad'); 123 | this._element.addClass('neutral'); 124 | this.createMessage(message); 125 | }; 126 | 127 | 128 | export { Notification }; 129 | -------------------------------------------------------------------------------- /src/parkTool.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BaseTool } from './baseTool'; 11 | import { Random } from './random'; 12 | import { ANIMBIT, BULLBIT, BURNBIT, CONDBIT } from "./tileFlags"; 13 | import { TileUtils } from './tileUtils'; 14 | import { DIRT, FOUNTAIN, WOODS2 } from "./tileValues"; 15 | 16 | var makeTool = BaseTool.makeTool; 17 | var ParkTool = makeTool(function(map) { 18 | this.init(10, map, true); 19 | }); 20 | 21 | 22 | ParkTool.prototype.doTool = function(x, y, blockMaps) { 23 | if (this._worldEffects.getTileValue(x, y) !== DIRT) { 24 | this.result = this.TOOLRESULT_NEEDS_BULLDOZE; 25 | return; 26 | } 27 | 28 | var value = Random.getRandom(4); 29 | var tileFlags = BURNBIT | BULLBIT; 30 | var tileValue; 31 | 32 | if (value === 4) { 33 | tileValue = FOUNTAIN; 34 | tileFlags |= ANIMBIT; 35 | } else { 36 | tileValue = value + WOODS2; 37 | } 38 | 39 | this._worldEffects.setTile(x, y, tileValue, tileFlags); 40 | this.addCost(10); 41 | this.result = this.TOOLRESULT_OK; 42 | }; 43 | 44 | 45 | export { ParkTool }; 46 | -------------------------------------------------------------------------------- /src/position.ts: -------------------------------------------------------------------------------- 1 | import * as Direction from "./direction"; 2 | 3 | class DirectionDelta { 4 | 5 | constructor(readonly xDelta: number, readonly yDelta: number) {} 6 | } 7 | 8 | type MovementDirection = Direction.Direction; 9 | 10 | function getDeltaFor(direction: MovementDirection): DirectionDelta { 11 | switch (direction) { 12 | case Direction.NORTH: 13 | return new DirectionDelta(0, -1); 14 | case Direction.NORTHEAST: 15 | return new DirectionDelta(1, -1); 16 | case Direction.EAST: 17 | return new DirectionDelta(1, 0); 18 | case Direction.SOUTHEAST: 19 | return new DirectionDelta(1, 1); 20 | case Direction.SOUTH: 21 | return new DirectionDelta(0, 1); 22 | case Direction.SOUTHWEST: 23 | return new DirectionDelta(-1, 1); 24 | case Direction.WEST: 25 | return new DirectionDelta(-1, 0); 26 | case Direction.NORTHWEST: 27 | return new DirectionDelta(-1, -1); 28 | default: 29 | throw new Error(`Unexpected direction!`); 30 | } 31 | } 32 | 33 | export class Position { 34 | 35 | static move(position: Position, direction: MovementDirection): Position { 36 | const {x, y} = position; 37 | const {xDelta, yDelta} = getDeltaFor(direction); 38 | return new Position(x + xDelta, y + yDelta); 39 | } 40 | 41 | static origin(): Position { 42 | return new Position(0, 0); 43 | } 44 | 45 | constructor(readonly x: number, readonly y: number) {} 46 | 47 | toString(): string { 48 | return `(${this.x}, ${this.y})`; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/powerManager.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BlockMap } from './blockMap'; 11 | import { forEachCardinalDirection } from './direction'; 12 | import { EventEmitter } from './eventEmitter'; 13 | import { Position } from './position'; 14 | import { NOT_ENOUGH_POWER } from './messages'; 15 | import { Random } from './random'; 16 | import { ANIMBIT, BURNBIT, CONDBIT, POWERBIT } from "./tileFlags"; 17 | import { NUCLEAR, POWERPLANT } from "./tileValues"; 18 | 19 | var COAL_POWER_STRENGTH = 700; 20 | var NUCLEAR_POWER_STRENGTH = 2000; 21 | 22 | 23 | var PowerManager = EventEmitter(function(map) { 24 | this._map = map; 25 | this._powerStack = []; 26 | this.powerGridMap = new BlockMap(this._map.width, this._map.height, 1); 27 | }); 28 | 29 | 30 | PowerManager.prototype.setTilePower = function(x, y) { 31 | var tile = this._map.getTile(x, y); 32 | var tileValue = tile.getValue(); 33 | 34 | if (tileValue === NUCLEAR || tileValue === POWERPLANT || 35 | this.powerGridMap.worldGet(x, y) > 0) { 36 | tile.addFlags(POWERBIT); 37 | return; 38 | } 39 | 40 | tile.removeFlags(POWERBIT); 41 | }; 42 | 43 | 44 | PowerManager.prototype.clearPowerStack = function() { 45 | this._powerStackPointer = 0; 46 | this._powerStack = []; 47 | }; 48 | 49 | 50 | PowerManager.prototype.testForConductive = function(pos, testDir) { 51 | var movedPos = Position.move(pos, testDir); 52 | 53 | if (this._map.isPositionInBounds(movedPos)) { 54 | if (this._map.getTile(movedPos.x, movedPos.y).isConductive()) { 55 | if (this.powerGridMap.worldGet(movedPos.x, movedPos.y) === 0) 56 | return true; 57 | } 58 | } 59 | 60 | return false; 61 | }; 62 | 63 | 64 | // Note: the algorithm is buggy: if you have two adjacent power 65 | // plants, the second will be regarded as drawing power from the first 66 | // rather than as a power source itself 67 | PowerManager.prototype.doPowerScan = function(census) { 68 | // Clear power this._map. 69 | this.powerGridMap.clear(); 70 | 71 | // Power that the combined coal and nuclear power plants can deliver. 72 | var maxPower = census.coalPowerPop * COAL_POWER_STRENGTH + 73 | census.nuclearPowerPop * NUCLEAR_POWER_STRENGTH; 74 | 75 | var powerConsumption = 0; // Amount of power used. 76 | 77 | while (this._powerStack.length > 0) { 78 | var pos = this._powerStack.pop(); 79 | var anyDir = undefined; 80 | var conNum; 81 | do { 82 | powerConsumption++; 83 | if (powerConsumption > maxPower) { 84 | this._emitEvent(NOT_ENOUGH_POWER); 85 | return; 86 | } 87 | 88 | if (anyDir) 89 | pos = Position.move(pos, anyDir); 90 | 91 | this.powerGridMap.worldSet(pos.x, pos.y, 1); 92 | conNum = 0; 93 | 94 | forEachCardinalDirection(dir => { 95 | if (conNum >= 2) { 96 | return; 97 | } 98 | 99 | if (this.testForConductive(pos, dir)) { 100 | conNum++; 101 | anyDir = dir; 102 | } 103 | }); 104 | 105 | if (conNum > 1) 106 | this._powerStack.push(new Position(pos.x, pos.y)); 107 | } while (conNum); 108 | } 109 | }; 110 | 111 | 112 | PowerManager.prototype.coalPowerFound = function(map, x, y, simData) { 113 | simData.census.coalPowerPop += 1; 114 | 115 | this._powerStack.push(new Position(x, y)); 116 | 117 | // Ensure animation runs 118 | var dX = [-1, 2, 1, 2]; 119 | var dY = [-1, -1, 0, 0]; 120 | 121 | for (var i = 0; i < 4; i++) 122 | map.addTileFlags(x + dX[i], y + dY[i], ANIMBIT); 123 | }; 124 | 125 | 126 | var dX = [1, 2, 1, 2]; 127 | var dY = [-1, -1, 0, 0]; 128 | var meltdownTable = [30000, 20000, 10000]; 129 | 130 | PowerManager.prototype.nuclearPowerFound = function(map, x, y, simData) { 131 | // TODO With the auto repair system, zone gets repaired before meltdown 132 | // In original Micropolis code, we bail and don't repair if melting down 133 | if (simData.disasterManager.disastersEnabled && 134 | Random.getRandom(meltdownTable[simData.gameLevel]) === 0) { 135 | simData.disasterManager.doMeltdown(x, y); 136 | return; 137 | } 138 | 139 | simData.census.nuclearPowerPop += 1; 140 | this._powerStack.push(new Position(x, y)); 141 | 142 | // Ensure animation bits set 143 | for (var i = 0; i < 4; i++) 144 | map.addTileFlags(x, y, ANIMBIT | CONDBIT | POWERBIT | BURNBIT); 145 | }; 146 | 147 | 148 | PowerManager.prototype.registerHandlers = function(mapScanner, repairManager) { 149 | mapScanner.addAction(POWERPLANT, this.coalPowerFound.bind(this)); 150 | mapScanner.addAction(NUCLEAR, this.nuclearPowerFound.bind(this)); 151 | repairManager.addAction(POWERPLANT, 7, 4); 152 | repairManager.addAction(NUCLEAR, 7, 4); 153 | }; 154 | 155 | 156 | export { PowerManager }; 157 | -------------------------------------------------------------------------------- /src/queryWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Config } from './config'; 11 | import { QUERY_WINDOW_CLOSED } from './messages'; 12 | import { ModalWindow } from './modalWindow'; 13 | import { MiscUtils } from './miscUtils'; 14 | 15 | var QueryWindow = ModalWindow(function() { 16 | this._debugToggled = false; 17 | $(queryFormID).on('submit', submit.bind(this)); 18 | }); 19 | 20 | 21 | var queryFormID = '#queryForm'; 22 | var queryOKID = '#queryOK'; 23 | 24 | 25 | var submit = function(e) { 26 | e.preventDefault(); 27 | this.close(); 28 | }; 29 | 30 | 31 | QueryWindow.prototype.close = function() { 32 | this._toggleDisplay(); 33 | this._emitEvent(QUERY_WINDOW_CLOSED); 34 | }; 35 | 36 | 37 | QueryWindow.prototype.open = function() { 38 | if ((Config.debug || Config.queryDebug) && !this._debugToggled) { 39 | this._debugToggled = true; 40 | $('.queryDebug').removeClass('hidden'); 41 | } 42 | 43 | this._toggleDisplay(); 44 | $(queryOKID).focus(); 45 | }; 46 | 47 | 48 | export { QueryWindow }; 49 | -------------------------------------------------------------------------------- /src/railTool.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { ConnectingTool } from './connectingTool'; 11 | import { BULLBIT, BURNBIT, CONDBIT } from "./tileFlags"; 12 | import { TileUtils } from './tileUtils'; 13 | import * as TileValues from "./tileValues"; 14 | 15 | var RailTool = ConnectingTool(function(map) { 16 | this.init(20, map, true, true); 17 | }); 18 | 19 | 20 | RailTool.prototype.layRail = function(x, y) { 21 | this.doAutoBulldoze(x, y); 22 | var tile = this._worldEffects.getTileValue(x, y); 23 | tile = TileUtils.normalizeRoad(tile); 24 | var cost = this.toolCost; 25 | 26 | switch (tile) { 27 | case TileValues.DIRT: 28 | this._worldEffects.setTile(x, y, TileValues.LHRAIL, BULLBIT | BURNBIT); 29 | break; 30 | 31 | case TileValues.RIVER: 32 | case TileValues.REDGE: 33 | case TileValues.CHANNEL: 34 | cost = 100; 35 | 36 | if (x < this._map.width - 1) { 37 | tile = this._worldEffects.getTileValue(x + 1, y); 38 | tile = TileUtils.normalizeRoad(tile); 39 | if (tile == TileValues.RAILHPOWERV || tile == TileValues.HRAIL || 40 | (tile >= TileValues.LHRAIL && tile <= TileValues.HRAILROAD)) { 41 | this._worldEffects.setTile(x, y, TileValues.HRAIL, BULLBIT); 42 | break; 43 | } 44 | } 45 | 46 | if (x > 0) { 47 | tile = this._worldEffects.getTileValue(x - 1, y); 48 | tile = TileUtils.normalizeRoad(tile); 49 | if (tile == TileValues.RAILHPOWERV || tile == TileValues.HRAIL || 50 | (tile > TileValues.VRAIL && tile < TileValues.VRAILROAD)) { 51 | this._worldEffects.setTile(x, y, TileValues.HRAIL, BULLBIT); 52 | break; 53 | } 54 | } 55 | 56 | if (y < this._map.height - 1) { 57 | tile = this._worldEffects.getTileValue(x, y + 1); 58 | tile = TileUtils.normalizeRoad(tile); 59 | if (tile == TileValues.RAILVPOWERH || tile == TileValues.VRAILROAD || 60 | (tile > TileValues.HRAIL && tile < TileValues.HRAILROAD)) { 61 | this._worldEffects.setTile(x, y, TileValues.VRAIL, BULLBIT); 62 | break; 63 | } 64 | } 65 | 66 | if (y > 0) { 67 | tile = this._worldEffects.getTileValue(x, y - 1); 68 | tile = TileUtils.normalizeRoad(tile); 69 | if (tile == TileValues.RAILVPOWERH || tile == TileValues.VRAILROAD || 70 | (tile > TileValues.HRAIL && tile < TileValues.HRAILROAD)) { 71 | this._worldEffects.setTile(x, y, TileValues.VRAIL, BULLBIT); 72 | break; 73 | } 74 | } 75 | 76 | return this.TOOLRESULT_FAILED; 77 | 78 | case TileValues.LHPOWER: 79 | this._worldEffects.setTile(x, y, TileValues.RAILVPOWERH, CONDBIT | BURNBIT | BULLBIT); 80 | break; 81 | 82 | case TileValues.LVPOWER: 83 | this._worldEffects.setTile(x, y, TileValues.RAILHPOWERV, CONDBIT | BURNBIT | BULLBIT); 84 | break; 85 | 86 | case TileValues.ROADS: 87 | this._worldEffects.setTile(x, y, TileValues.VRAILROAD, BURNBIT | BULLBIT); 88 | break; 89 | 90 | case TileValues.ROADS2: 91 | this._worldEffects.setTile(x, y, TileValues.HRAILROAD, BURNBIT | BULLBIT); 92 | break; 93 | 94 | default: 95 | return this.TOOLRESULT_FAILED; 96 | } 97 | 98 | this.addCost(cost); 99 | this.checkZoneConnections(x, y); 100 | return this.TOOLRESULT_OK; 101 | }; 102 | 103 | 104 | 105 | RailTool.prototype.doTool = function(x, y, blockMaps) { 106 | this.result = this.layRail(x, y); 107 | }; 108 | 109 | 110 | export { RailTool }; 111 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | 2 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 3 | * 4 | * This code is released under the GNU GPL v3, with some additional terms. 5 | * Please see the files LICENSE and COPYING for details. Alternatively, 6 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 7 | * http://micropolisjs.graememcc.co.uk/COPYING 8 | * 9 | */ 10 | 11 | interface MathGlobal { 12 | random(): number; 13 | floor(n: number): number; 14 | } 15 | 16 | type UpperBoundedRNG = (maxValue: number) => number; 17 | 18 | type SixteenBitRNG = () => number; 19 | 20 | function getChance(chance: number, rng: SixteenBitRNG = getRandom16): boolean { 21 | // tslint:disable-next-line:no-bitwise 22 | return (rng() & chance) === 0; 23 | } 24 | 25 | function getERandom(max: number, rng: UpperBoundedRNG = getRandom): number { 26 | const firstCandidate = rng(max); 27 | const secondCandidate = rng(max); 28 | return Math.min(firstCandidate, secondCandidate); 29 | } 30 | 31 | function getRandom(max: number, mathGlobal: MathGlobal = Math): number { 32 | return mathGlobal.floor(mathGlobal.random() * (max + 1)); 33 | } 34 | 35 | function getRandom16(rng: UpperBoundedRNG = getRandom): number { 36 | return rng(65535); 37 | } 38 | 39 | function getRandom16Signed(rng: SixteenBitRNG = getRandom16) { 40 | const value = rng(); 41 | 42 | if (value < 32768) { 43 | return value; 44 | } else { 45 | return -(2 ** 16) + value; 46 | } 47 | } 48 | 49 | const Random = { 50 | getChance, 51 | getERandom, 52 | getRandom, 53 | getRandom16, 54 | getRandom16Signed, 55 | }; 56 | 57 | export { Random }; 58 | -------------------------------------------------------------------------------- /src/rci.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { VALVES_UPDATED } from './messages'; 11 | import { MiscUtils } from './miscUtils'; 12 | 13 | function RCI(parentNode, eventSource, id) { 14 | if (arguments.length < 2) 15 | throw new Error('RCI constructor called with too few arguments ' + [].toString.apply(arguments)); 16 | 17 | if (id === undefined) 18 | id = RCI.DEFAULT_ID; 19 | 20 | if (typeof(parentNode) === 'string') { 21 | var orig = parentNode; 22 | parentNode = $(MiscUtils.normaliseDOMid(parentNode)); 23 | parentNode = parentNode.length === 0 ? null : parentNode[0]; 24 | if (parentNode === null) 25 | throw new Error('Node ' + orig + ' not found'); 26 | } 27 | 28 | // Each bar is 1 unit of padding wide, and there are 2 units 29 | // of padding between the 3 bars. There are 2 units of padding 30 | // either side. So 9 units of padding total 31 | // Each bar can be at most bucket rectangles tall, but we multiply 32 | // that by 2 as we can have positive and negative directions. There 33 | // should be 1 unit of padding either side. The text box in the middle 34 | // is 1 unit of padding 35 | this._padding = 3; // 3 rectangles in each bit of padding 36 | this._buckets = 10; // 0.2000 is scaled in to 10 buckets 37 | this._rectSize = 5; // Each rect is 5px 38 | this._scale = Math.floor(2000 / this._buckets); 39 | 40 | this._canvas = $('', {id: id})[0]; 41 | 42 | // Remove any existing element with the same id 43 | var elems = $(MiscUtils.normaliseDOMid(id)); 44 | var current = elems.length > 0 ? elems[0] : null; 45 | if (current !== null) { 46 | if (current.parentNode === parentNode) 47 | parentNode.replaceChild(this._canvas, current); 48 | else 49 | throw new Error('ID ' + id + ' already exists in document!'); 50 | } else 51 | parentNode.appendChild(this._canvas); 52 | 53 | // We might be created before our container has appeared on screen 54 | this._initialisedBounds = false; 55 | 56 | eventSource.addEventListener(VALVES_UPDATED, this.update.bind(this)); 57 | } 58 | 59 | 60 | RCI.prototype._clear = function(ctx) { 61 | ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); 62 | }; 63 | 64 | 65 | RCI.prototype._drawRect = function(ctx) { 66 | // The rect is inset by one unit of padding 67 | var boxLeft = this._padding * this._rectSize; 68 | // and is the length of a bar plus a unit of padding down 69 | var boxTop = (this._buckets + this._padding) * this._rectSize; 70 | // It must accomodate 3 bars, 2 bits of internal padding 71 | // with padding either side 72 | var boxWidth = 7 * this._padding * this._rectSize; 73 | var boxHeight = this._padding * this._rectSize; 74 | 75 | ctx.fillStyle = 'rgb(192, 192, 192)'; 76 | ctx.fillRect(boxLeft, boxTop, boxWidth, boxHeight); 77 | }; 78 | 79 | 80 | RCI.prototype._drawValue = function(ctx, index, value) { 81 | // Need to scale com and ind 82 | if (index > 1) 83 | value = Math.floor(2000/1500 * value); 84 | 85 | var colours = ['rgb(0,255,0)', 'rgb(0, 0, 139)', 'rgb(255, 255, 0)']; 86 | var barHeightRect = Math.floor(Math.abs(value) / this._scale); 87 | var barStartY = (value >= 0) ? 88 | this._buckets + this._padding - barHeightRect : this._buckets + 2 * this._padding; 89 | var barStartX = 2 * this._padding + (index * 2 * this._padding); 90 | 91 | ctx.fillStyle = colours[index]; 92 | ctx.fillRect(barStartX * this._rectSize, barStartY * this._rectSize, 93 | this._padding * this._rectSize, barHeightRect * this._rectSize); 94 | }; 95 | 96 | 97 | RCI.prototype._drawLabel = function(ctx, index) { 98 | var labels = ['R', 'C', 'I']; 99 | var textLeft = 2 * this._padding + (index * 2 * this._padding) + 100 | Math.floor(this._padding/2); 101 | 102 | ctx.font = 'normal xx-small sans-serif'; 103 | ctx.fillStyle = 'rgb(0, 0, 0)'; 104 | ctx.textBaseline = 'bottom'; 105 | ctx.fillText(labels[index], textLeft * this._rectSize, 106 | (this._buckets + 2 * this._padding) * this._rectSize); 107 | }; 108 | 109 | 110 | RCI.prototype.update = function(data) { 111 | if (!this._initialised) { 112 | // The canvas is assumed to fill its container on-screen 113 | var rect = this._canvas.parentNode.getBoundingClientRect(); 114 | this._canvas.width = rect.width; 115 | this._canvas.height = rect.height; 116 | this._canvas.style.margin = '0'; 117 | this._canvas.style.padding = '0'; 118 | this._intialised = true; 119 | } 120 | 121 | var ctx = this._canvas.getContext('2d'); 122 | this._clear(ctx); 123 | this._drawRect(ctx); 124 | 125 | var values = [data.residential, data.commercial, data.industrial]; 126 | for (var i = 0; i < 3; i++) { 127 | this._drawValue(ctx, i, values[i]); 128 | this._drawLabel(ctx, i); 129 | } 130 | }; 131 | 132 | 133 | Object.defineProperty(RCI, 'DEFAULT_ID', MiscUtils.makeConstantDescriptor('RCICanvas')); 134 | 135 | export { RCI }; 136 | -------------------------------------------------------------------------------- /src/repairManager.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BURNBIT, CONDBIT } from "./tileFlags"; 11 | import { RUBBLE, ROADBASE } from "./tileValues"; 12 | 13 | function RepairManager(map) { 14 | this._map = map; 15 | this._actions = []; 16 | } 17 | 18 | 19 | var isCallable = function(f) { 20 | return typeof(f) === 'function'; 21 | }; 22 | 23 | 24 | RepairManager.prototype.addAction = function(criterion, period, zoneSize) { 25 | this._actions.push({criterion: criterion, period: period, zoneSize: zoneSize}); 26 | }; 27 | 28 | 29 | RepairManager.prototype.repairZone = function(x, y, zoneSize) { 30 | var centre = this._map.getTileValue(x, y); 31 | var tileValue = centre - zoneSize - 2; 32 | 33 | for (var yy = -1; yy < zoneSize - 1; yy++) { 34 | for (var xx = -1; xx < zoneSize - 1; xx++) { 35 | tileValue++; 36 | 37 | var current = this._map.getTile(x + xx, y + yy); 38 | if (current.isZone() || current.isAnimated()) 39 | continue; 40 | 41 | var currentValue = current.getValue(); 42 | if (currentValue < RUBBLE || currentValue >= ROADBASE) 43 | this._map.setTile(x + xx, y + yy, tileValue, CONDBIT | BURNBIT); 44 | } 45 | } 46 | }; 47 | 48 | 49 | RepairManager.prototype.checkTile = function(x, y, cityTime) { 50 | for (var i = 0, l = this._actions.length; i < l; i++) { 51 | var current = this._actions[i]; 52 | var period = current.period; 53 | 54 | if ((cityTime & period) !== 0) 55 | continue; 56 | 57 | var tile = this._map.getTile(x, y); 58 | var tileValue = tile.getValue(); 59 | 60 | var callable = isCallable(current.criterion); 61 | if (callable && current.criterion.call(null, tile)) 62 | this.repairZone(x, y, current.zoneSize); 63 | else if (!callable && current.criterion === tileValue) 64 | this.repairZone(x, y, current.zoneSize); 65 | } 66 | }; 67 | 68 | 69 | export { RepairManager }; 70 | -------------------------------------------------------------------------------- /src/roadTool.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { ConnectingTool } from './connectingTool'; 11 | import { BULLBIT, BURNBIT, CONDBIT } from "./tileFlags"; 12 | import { TileUtils } from './tileUtils'; 13 | import * as TileValues from "./tileValues"; 14 | 15 | var RoadTool = ConnectingTool(function(map) { 16 | this.init(10, map, true, true); 17 | }); 18 | 19 | 20 | RoadTool.prototype.layRoad = function(x, y) { 21 | this.doAutoBulldoze(x, y); 22 | var tile = this._worldEffects.getTileValue(x, y); 23 | var cost = this.toolCost; 24 | 25 | switch (tile) { 26 | case TileValues.DIRT: 27 | this._worldEffects.setTile(x, y, TileValues.ROADS, BULLBIT | BURNBIT); 28 | break; 29 | 30 | case TileValues.RIVER: 31 | case TileValues.REDGE: 32 | case TileValues.CHANNEL: 33 | cost = 50; 34 | 35 | if (x < this._map.width - 1) { 36 | tile = this._worldEffects.getTileValue(x + 1, y); 37 | tile = TileUtils.normalizeRoad(tile); 38 | 39 | if (tile === TileValues.VRAILROAD || tile === TileValues.HBRIDGE || 40 | (tile >= TileValues.ROADS && tile <= TileValues.HROADPOWER)) { 41 | this._worldEffects.setTile(x, y, TileValues.HBRIDGE, BULLBIT); 42 | break; 43 | } 44 | } 45 | 46 | if (x > 0) { 47 | tile = this._worldEffects.getTileValue(x - 1, y); 48 | tile = TileUtils.normalizeRoad(tile); 49 | 50 | if (tile === TileValues.VRAILROAD || tile === TileValues.HBRIDGE || 51 | (tile >= TileValues.ROADS && tile <= TileValues.INTERSECTION)) { 52 | this._worldEffects.setTile(x, y, TileValues.HBRIDGE, BULLBIT); 53 | break; 54 | } 55 | } 56 | 57 | if (y < this._map.height - 1) { 58 | tile = this._worldEffects.getTileValue(x, y + 1); 59 | tile = TileUtils.normalizeRoad(tile); 60 | 61 | if (tile === TileValues.HRAILROAD || tile === TileValues.VROADPOWER || 62 | (tile >= TileValues.VBRIDGE && tile <= TileValues.INTERSECTION)) { 63 | this._worldEffects.setTile(x, y, TileValues.VBRIDGE, BULLBIT); 64 | break; 65 | } 66 | } 67 | 68 | if (y > 0) { 69 | tile = this._worldEffects.getTileValue(x, y - 1); 70 | tile = TileUtils.normalizeRoad(tile); 71 | 72 | if (tile === TileValues.HRAILROAD || tile === TileValues.VROADPOWER || 73 | (tile >= TileValues.VBRIDGE && tile <= TileValues.INTERSECTION)) { 74 | this._worldEffects.setTile(x, y, TileValues.VBRIDGE, BULLBIT); 75 | break; 76 | } 77 | } 78 | 79 | return this.TOOLRESULT_FAILED; 80 | 81 | case TileValues.LHPOWER: 82 | this._worldEffects.setTile(x, y, TileValues.VROADPOWER, CONDBIT | BURNBIT | BULLBIT); 83 | break; 84 | 85 | case TileValues.LVPOWER: 86 | this._worldEffects.setTile(x, y, TileValues.HROADPOWER, CONDBIT | BURNBIT | BULLBIT); 87 | break; 88 | 89 | case TileValues.LHRAIL: 90 | this._worldEffects.setTile(x, y, TileValues.HRAILROAD, BURNBIT | BULLBIT); 91 | break; 92 | 93 | case TileValues.LVRAIL: 94 | this._worldEffects.setTile(x, y, TileValues.VRAILROAD, BURNBIT | BULLBIT); 95 | break; 96 | 97 | default: 98 | return this.TOOLRESULT_FAILED; 99 | } 100 | 101 | this.addCost(cost); 102 | this.checkZoneConnections(x, y); 103 | return this.TOOLRESULT_OK; 104 | }; 105 | 106 | 107 | RoadTool.prototype.doTool = function(x, y, blockMaps) { 108 | this.result = this.layRoad(x, y); 109 | }; 110 | 111 | 112 | export { RoadTool }; 113 | -------------------------------------------------------------------------------- /src/saveWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { SAVE_WINDOW_CLOSED } from './messages'; 11 | import { ModalWindow } from './modalWindow'; 12 | 13 | var SaveWindow = ModalWindow(function() { 14 | $(saveFormID).on('submit', submit.bind(this)); 15 | }); 16 | 17 | 18 | var saveFormID = '#saveForm'; 19 | var saveOKID = '#saveOK'; 20 | 21 | 22 | var submit = function(e) { 23 | e.preventDefault(); 24 | this.close(); 25 | }; 26 | 27 | 28 | SaveWindow.prototype.close = function() { 29 | this._toggleDisplay(); 30 | this._emitEvent(SAVE_WINDOW_CLOSED); 31 | }; 32 | 33 | 34 | SaveWindow.prototype.open = function() { 35 | this._toggleDisplay(); 36 | $(saveOKID).focus(); 37 | }; 38 | 39 | 40 | export { SaveWindow }; 41 | -------------------------------------------------------------------------------- /src/screenshotLinkWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { SCREENSHOT_LINK_CLOSED } from './messages'; 11 | import { ModalWindow } from './modalWindow'; 12 | import { MiscUtils } from './miscUtils'; 13 | 14 | var ScreenshotLinkWindow = ModalWindow(function() { 15 | $(screenshotLinkFormID).on('submit', submit.bind(this)); 16 | }); 17 | 18 | 19 | var screenshotLinkFormID = '#screenshotLinkForm'; 20 | var screenshotLinkOKID = '#screenshotLinkOK'; 21 | var screenshotLinkID = '#screenshotLink'; 22 | 23 | 24 | ScreenshotLinkWindow.prototype.close = function() { 25 | this._toggleDisplay(); 26 | this._emitEvent(SCREENSHOT_LINK_CLOSED); 27 | }; 28 | 29 | 30 | var cancel = function(e) { 31 | e.preventDefault(); 32 | this.close(); 33 | }; 34 | 35 | 36 | var submit = function(e) { 37 | e.preventDefault(); 38 | this.close(); 39 | }; 40 | 41 | 42 | ScreenshotLinkWindow.prototype.open = function(screenshotLink) { 43 | $(screenshotLinkID).attr('href', screenshotLink); 44 | this._toggleDisplay(); 45 | }; 46 | 47 | 48 | export { ScreenshotLinkWindow }; 49 | -------------------------------------------------------------------------------- /src/screenshotWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { SCREENSHOT_WINDOW_CLOSED } from './messages'; 11 | import { ModalWindow } from './modalWindow'; 12 | import { MiscUtils } from './miscUtils'; 13 | 14 | var ScreenshotWindow = ModalWindow(function() { 15 | $(screenshotCancelID).on('click', cancel.bind(this)); 16 | $(screenshotFormID).on('submit', submit.bind(this)); 17 | }); 18 | 19 | 20 | var screenshotCancelID = '#screenshotCancel'; 21 | var screenshotFormID = '#screenshotForm'; 22 | var screenshotOKID = '#screenshotOK'; 23 | 24 | 25 | ScreenshotWindow.prototype.close = function(action) { 26 | action = action || null; 27 | 28 | this._toggleDisplay(); 29 | this._emitEvent(SCREENSHOT_WINDOW_CLOSED, action); 30 | }; 31 | 32 | 33 | var cancel = function(e) { 34 | e.preventDefault(); 35 | this.close(null); 36 | }; 37 | 38 | 39 | var submit = function(e) { 40 | e.preventDefault(); 41 | 42 | var action = null; 43 | 44 | // Get choice 45 | var screenshotType = $('.screenshotType:checked').val(); 46 | if (screenshotType === 'visible') 47 | action = ScreenshotWindow.SCREENSHOT_VISIBLE; 48 | else 49 | action = ScreenshotWindow.SCREENSHOT_ALL; 50 | 51 | this.close(action); 52 | }; 53 | 54 | 55 | ScreenshotWindow.prototype.open = function(screenshotData) { 56 | this._toggleDisplay(); 57 | }; 58 | 59 | 60 | var defineAction = (function() { 61 | var uid = 1; 62 | 63 | return function(name) { 64 | Object.defineProperty(ScreenshotWindow, name, MiscUtils.makeConstantDescriptor(uid)); 65 | uid += 1; 66 | }; 67 | })(); 68 | 69 | 70 | defineAction('SCREENSHOT_VISIBLE'); 71 | defineAction('SCREENSHOT_ALL'); 72 | 73 | 74 | export { ScreenshotWindow }; 75 | -------------------------------------------------------------------------------- /src/settingsWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { SETTINGS_WINDOW_CLOSED } from './messages'; 11 | import { ModalWindow } from './modalWindow'; 12 | import { MiscUtils } from './miscUtils'; 13 | import { Simulation } from './simulation'; 14 | 15 | var SettingsWindow = ModalWindow(function() { 16 | $(settingsCancelID).on('click', cancel.bind(this)); 17 | $(settingsFormID).on('submit', submit.bind(this)); 18 | }); 19 | 20 | 21 | var settingsCancelID = '#settingsCancel'; 22 | var settingsFormID = '#settingsForm'; 23 | var settingsOKID = '#settingsOK'; 24 | var autoBudgetYesID = '#autoBudgetYes'; 25 | var autoBudgetNoID = '#autoBudgetNo'; 26 | var autoBulldozeYesID = '#autoBulldozeYes'; 27 | var autoBulldozeNoID = '#autoBulldozeNo'; 28 | var speedSlowID = '#speedSlow'; 29 | var speedMedID = '#speedMed'; 30 | var speedFastID = '#speedFast'; 31 | var disastersYesID = '#disastersYes'; 32 | var disastersNoID = '#disastersNo'; 33 | 34 | 35 | SettingsWindow.prototype.close = function(actions) { 36 | actions = actions || []; 37 | this._emitEvent(SETTINGS_WINDOW_CLOSED, actions); 38 | this._toggleDisplay(); 39 | }; 40 | 41 | 42 | var cancel = function(e) { 43 | e.preventDefault(); 44 | this.close([]); 45 | }; 46 | 47 | 48 | var submit = function(e) { 49 | e.preventDefault(); 50 | 51 | var actions = []; 52 | 53 | var shouldAutoBudget = $('.autoBudgetSetting:checked').val(); 54 | if (shouldAutoBudget === 'true') 55 | shouldAutoBudget = true; 56 | else 57 | shouldAutoBudget = false; 58 | actions.push({action: SettingsWindow.AUTOBUDGET, data: shouldAutoBudget}); 59 | 60 | var shouldAutoBulldoze = $('.autoBulldozeSetting:checked').val(); 61 | if (shouldAutoBulldoze === 'true') 62 | shouldAutoBulldoze = true; 63 | else 64 | shouldAutoBulldoze = false; 65 | actions.push({action: SettingsWindow.AUTOBULLDOZE, data: shouldAutoBulldoze}); 66 | 67 | var speed = $('.speedSetting:checked').val() - 0; 68 | actions.push({action: SettingsWindow.SPEED, data: speed}); 69 | 70 | var shouldEnableDisasters = $('.enableDisastersSetting:checked').val(); 71 | if (shouldEnableDisasters === 'true') 72 | shouldEnableDisasters = true; 73 | else 74 | shouldEnableDisasters = false; 75 | actions.push({action: SettingsWindow.DISASTERS_CHANGED, data: shouldEnableDisasters}); 76 | 77 | this.close(actions); 78 | }; 79 | 80 | 81 | SettingsWindow.prototype.open = function(settingsData) { 82 | if (settingsData.autoBudget) 83 | $(autoBudgetYesID).prop('checked', true); 84 | else 85 | $(autoBudgetNoID).prop('checked', true); 86 | 87 | if (settingsData.autoBulldoze) 88 | $(autoBulldozeYesID).prop('checked', true); 89 | else 90 | $(autoBulldozeNoID).prop('checked', true); 91 | 92 | if (settingsData.speed === Simulation.SPEED_SLOW) 93 | $(speedSlowID).prop('checked', true); 94 | else if (settingsData.speed === Simulation.SPEED_MED) 95 | $(speedMedID).prop('checked', true); 96 | else 97 | $(speedFastID).prop('checked', true); 98 | 99 | if (settingsData.disasters) 100 | $(disastersYesID).prop('checked', true); 101 | else 102 | $(disastersNoID).prop('checked', true); 103 | 104 | this._toggleDisplay(); 105 | }; 106 | 107 | 108 | var defineAction = (function() { 109 | var uid = 0; 110 | 111 | return function(name) { 112 | Object.defineProperty(SettingsWindow, name, MiscUtils.makeConstantDescriptor(uid)); 113 | uid += 1; 114 | }; 115 | })(); 116 | 117 | 118 | defineAction('AUTOBUDGET'); 119 | defineAction('AUTOBULLDOZE'); 120 | defineAction('SPEED'); 121 | defineAction('DISASTERS_CHANGED'); 122 | 123 | 124 | export { SettingsWindow }; 125 | -------------------------------------------------------------------------------- /src/splashCanvas.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { MiscUtils } from './miscUtils'; 11 | 12 | /* 13 | * 14 | * The SplashCanvas handles painting a canvas with the minimap that is shown to the player when selecting a map to 15 | * play on (in fact it also handles the creation of said canvas). It is a far lighter cousin of GameCanvas, as the 16 | * map only needs to be painted once (although new maps may be painted if the player generates a new map). We let 17 | * the canvas handle the scaling of the tiles. 18 | * 19 | */ 20 | 21 | 22 | // Takes the DOM id of the container in which the new canvas should be placed, and the tileset that should be used 23 | // for painting. Additionally, this function can also take an optional ID for the new canvas; SplashScreen.DEFAULT_ID 24 | // will be used if this isn't supplied. 25 | // 26 | // Handles canvas creation, throwing an error if the canvas' ID exists anywhere other than the specified parent node, 27 | // or if the given parent node is absent, or if the tileset isn't valid. This function does not paint the canvas: you 28 | // must explicitly call paint with a map. 29 | function SplashCanvas(parentID, tileSet, id) { 30 | id = id || SplashCanvas.DEFAULT_ID; 31 | 32 | if (!(this instanceof SplashCanvas)) 33 | return new SplashCanvas(parentNode, tileSet, id); 34 | 35 | if (parentID === undefined) 36 | throw new Error('No container specified'); 37 | else if (tileSet === undefined) 38 | throw new Error('No tileset specified'); 39 | else if (!tileSet.isValid) 40 | throw new Error('Tileset is not valid!'); 41 | 42 | this._tileSet = tileSet; 43 | 44 | // Check the parent container exists 45 | var parentNode = $(MiscUtils.normaliseDOMid(parentID)); 46 | parentNode = parentNode.length === 0 ? null : parentNode[0]; 47 | if (parentNode === null) 48 | throw new Error('SplashCanvas container ID ' + parentID + ' not found'); 49 | 50 | var height = SplashCanvas.DEFAULT_HEIGHT; 51 | var width = SplashCanvas.DEFAULT_WIDTH; 52 | 53 | // Create the canvas 54 | this._canvas = document.createElement('canvas'); 55 | this._canvas.id = id; 56 | this._canvas.width = width; 57 | this._canvas.height = height; 58 | 59 | // Remove any existing element with the same id 60 | var existing = document.getElementById(id); 61 | if (existing !== null) { 62 | if (existing.parentNode === parentNode) { 63 | console.warn('There was already an object with the same ID as SplashCanvas - replacing it!'); 64 | parentNode.replaceChild(this._canvas, existing); 65 | } else { 66 | console.warn('SplashCanvas id ' + id + ' already exists somewhere in document'); 67 | throw new Error('ID ' + id + ' already exists in document!'); 68 | } 69 | } else { 70 | parentNode.appendChild(this._canvas); 71 | } 72 | } 73 | 74 | 75 | // Paint an individual tile at the given map coordinates, with the tile scaled down to 3x3 76 | SplashCanvas.prototype._paintTile = function(tileVal, x, y, ctx) { 77 | var src = this._tileSet[tileVal]; 78 | ctx.drawImage(src, x * 3, y * 3, 3, 3); 79 | }; 80 | 81 | 82 | // Loop through the given map, painting each tile scaled down 83 | SplashCanvas.prototype.paint = function(map) { 84 | var ctx = this._canvas.getContext('2d'); 85 | ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); 86 | 87 | for (var y = 0; y < map.height; y++) { 88 | for (var x = 0; x < map.width; x++) { 89 | this._paintTile(map.getTileValue(x, y), x, y, ctx); 90 | } 91 | } 92 | }; 93 | 94 | 95 | SplashCanvas.DEFAULT_WIDTH = 360; 96 | SplashCanvas.DEFAULT_HEIGHT = 300; 97 | SplashCanvas.DEFAULT_ID = 'SplashCanvas'; 98 | 99 | 100 | export { SplashCanvas }; 101 | -------------------------------------------------------------------------------- /src/splashScreen.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Config } from './config'; 11 | import { Game } from './game'; 12 | import { MapGenerator } from './mapGenerator'; 13 | import { Simulation } from './simulation'; 14 | import { SplashCanvas } from './splashCanvas'; 15 | import { Storage } from './storage'; 16 | 17 | /* 18 | * 19 | * The SplashScreen is the first screen the player will see on launch. It is responsible for map generation, 20 | * placing UI on screen to allow the player to select a map or load a game, and finally launching the game. 21 | * This should not be called until the tiles and sprites have been loaded. 22 | * 23 | */ 24 | 25 | var onresize = null; 26 | 27 | 28 | // If the window is initially too small, try and relaunch if it gets bigger 29 | var makeResizeListener = function(tileSet, spriteSheet) { 30 | return function(tileSet, spriteSheet, e) { 31 | $(window).off('resize'); 32 | var s = new SplashScreen(tileSet, spriteSheet); 33 | }.bind(null, tileSet, spriteSheet); 34 | }; 35 | 36 | 37 | function SplashScreen(tileSet, snowTileSet, spriteSheet) { 38 | // We don't launch the game if the screen is too small, however, we should retain the right to do so 39 | // should the situation change... 40 | if ($('#tooSmall').is(':visible')) { 41 | onresize = makeResizeListener(tileSet, spriteSheet); 42 | $(window).on('resize', onresize); 43 | return; 44 | } 45 | 46 | this.tileSet = tileSet; 47 | this.snowTileSet = snowTileSet; 48 | this.spriteSheet = spriteSheet; 49 | this.map = MapGenerator(); 50 | 51 | // Set up listeners on buttons. When play is clicked, we will move on to get the player's desired 52 | // difficulty level and city name before launching the game properly 53 | $('#splashGenerate').click(regenerateMap.bind(this)); 54 | $('#splashPlay').click(acquireNameAndDifficulty.bind(this)); 55 | $('#splashLoad').click(handleLoad.bind(this)); 56 | 57 | // Conditionally enable load/save buttons 58 | $('#saveRequest').prop('disabled', !Storage.canStore); 59 | $('#splashLoad').prop('disabled', !(Storage.canStore && Storage.getSavedGame() !== null)); 60 | 61 | // Paint the minimap 62 | this.splashCanvas = new SplashCanvas('splashContainer', tileSet); 63 | this.splashCanvas.paint(this.map); 64 | 65 | // Let's get some bits on screen! 66 | $('.awaitGeneration').toggle(); 67 | $('#splashPlay').focus(); 68 | } 69 | 70 | 71 | // Generate a new map at the user's request, and paint it 72 | var regenerateMap = function(e) { 73 | e.preventDefault(); 74 | 75 | this.map = MapGenerator(); 76 | this.splashCanvas.paint(this.map); 77 | }; 78 | 79 | 80 | // Fetches game data from the storage manager, and launches the game. We won't return from here 81 | var handleLoad = function(e) { 82 | e.preventDefault(); 83 | 84 | var savedGame = Storage.getSavedGame(); 85 | 86 | if (savedGame === null) 87 | return; 88 | 89 | // Remove installed event listeners 90 | $('#splashLoad').off('click'); 91 | $('#splashGenerate').off('click'); 92 | $('#splashPlay').off('click'); 93 | 94 | // Hide the splashscreen UI 95 | $('#splash').toggle(); 96 | 97 | // Launch 98 | var g = new Game(savedGame, this.tileSet, this.snowTileSet, this.spriteSheet, Simulation.LEVEL_EASY, name); 99 | }; 100 | 101 | 102 | // After a map has been selected, call this function to display a form asking the user for 103 | // a city name and difficulty level. 104 | var acquireNameAndDifficulty = function(e) { 105 | e.preventDefault(); 106 | 107 | // Remove the initial event listeners 108 | $('#splashLoad').off('click'); 109 | $('#splashGenerate').off('click'); 110 | $('#splashPlay').off('click'); 111 | 112 | // Get rid of the initial splash screen 113 | $('#splash').toggle(); 114 | 115 | // As a convenience, the city name is not mandatory in debug mode 116 | if (Config.debug) 117 | $('#nameForm').removeAttr('required'); 118 | 119 | // When the form is submitted, we'll be ready to launch the game 120 | $('#playForm').submit(play.bind(this)); 121 | 122 | // Display the name and difficulty form 123 | $('#start').toggle(); 124 | $('#nameForm').focus(); 125 | }; 126 | 127 | 128 | // This function should be called after the name/difficulty form has been submitted. The game will now be launched 129 | // with the map selected earlier. 130 | var play = function(e) { 131 | e.preventDefault(); 132 | 133 | // As usual, uninstall event listeners, and hide the UI 134 | $('#playForm').off('submit'); 135 | $('#start').toggle(); 136 | 137 | // What values did the player specify? 138 | var difficulty = $('.difficulty:checked').val() - 0; 139 | var name = $('#nameForm').val(); 140 | 141 | // Launch a new game 142 | var g = new Game(this.map, this.tileSet, this.snowTileSet, this.spriteSheet, difficulty, name); 143 | }; 144 | 145 | 146 | export { SplashScreen }; 147 | -------------------------------------------------------------------------------- /src/spriteConstants.ts: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | export const SPRITE_TRAIN = 1; 11 | export const SPRITE_HELICOPTER = 2; 12 | export const SPRITE_AIRPLANE = 3; 13 | export const SPRITE_SHIP = 4; 14 | export const SPRITE_MONSTER = 5; 15 | export const SPRITE_TORNADO = 6; 16 | export const SPRTE_EXPLOSION = 7; 17 | -------------------------------------------------------------------------------- /src/spriteUtils.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { ANIMBIT, BULLBIT } from "./tileFlags"; 11 | import * as TileValues from "./tileValues"; 12 | import { ZoneUtils } from './zoneUtils'; 13 | 14 | var pixToWorld = function(p) { 15 | return p >> 4; 16 | }; 17 | 18 | 19 | var worldToPix = function(w) { 20 | return w << 4; 21 | }; 22 | 23 | 24 | // Attempt to move 45° towards the desired direction, either 25 | // clockwise or anticlockwise, whichever gets us there quicker 26 | var turnTo = function(presentDir, desiredDir) { 27 | if (presentDir === desiredDir) 28 | return presentDir; 29 | 30 | if (presentDir < desiredDir) { 31 | // select clockwise or anticlockwise 32 | if (desiredDir - presentDir < 4) 33 | presentDir++; 34 | else 35 | presentDir--; 36 | } else { 37 | if (presentDir - desiredDir < 4) 38 | presentDir--; 39 | else 40 | presentDir++; 41 | } 42 | 43 | if (presentDir > 8) 44 | presentDir = 1; 45 | 46 | if (presentDir < 1) 47 | presentDir = 8; 48 | 49 | return presentDir; 50 | }; 51 | 52 | 53 | var getTileValue = function(map, x, y) { 54 | var wX = pixToWorld(x); 55 | var wY = pixToWorld(y); 56 | 57 | if (wX < 0 || wX >= map.width || wY < 0 || wY >= map.height) 58 | return -1; 59 | 60 | return map.getTileValue(wX, wY); 61 | }; 62 | 63 | 64 | // Choose the best direction to get from the origin to the destination 65 | // If the destination is equidistant in both x and y deltas, a diagonal 66 | // will be chosen, otherwise the most 'dominant' difference will be selected 67 | // (so if a destination is 4 units north and 2 units east, north will be chosen). 68 | // This code seems to always choose south if we're already there which seems like 69 | // a bug 70 | var directionTable = [0, 3, 2, 1, 3, 4, 5, 7, 6, 5, 7, 8, 1]; 71 | 72 | var getDir = function(orgX, orgY, destX, destY) { 73 | var deltaX = destX - orgX; 74 | var deltaY = destY - orgY; 75 | var i; 76 | 77 | if (deltaX < 0) { 78 | if (deltaY < 0) { 79 | i = 11; 80 | } else { 81 | i = 8; 82 | } 83 | } else { 84 | if (deltaY < 0) { 85 | i = 2; 86 | } else { 87 | i = 5; 88 | } 89 | } 90 | 91 | deltaX = Math.abs(deltaX); 92 | deltaY = Math.abs(deltaY); 93 | 94 | if (deltaX * 2 < deltaY) 95 | i++; 96 | else if (deltaY * 2 < deltaX) 97 | i--; 98 | 99 | if (i < 0 || i > 12) 100 | i = 0; 101 | 102 | return directionTable[i]; 103 | }; 104 | 105 | 106 | var absoluteDistance = function(orgX, orgY, destX, destY) { 107 | var deltaX = destX - orgX; 108 | var deltaY = destY - orgY; 109 | return Math.abs(deltaX) + Math.abs(deltaY); 110 | }; 111 | 112 | 113 | var checkWet = function(tileValue) { 114 | if (tileValue === TileValues.HPOWER || tileValue === TileValues.VPOWER || 115 | tileValue === TileValues.HRAIL || tileValue === TileValues.VRAIL || 116 | tileValue === TileValues.BRWH || tileValue === TileValues.BRWV) 117 | return true; 118 | else 119 | return false; 120 | }; 121 | 122 | 123 | var destroyMapTile = function(spriteManager, map, blockMaps, ox, oy) { 124 | var x = pixToWorld(ox); 125 | var y = pixToWorld(oy); 126 | 127 | if (!map.testBounds(x, y)) 128 | return; 129 | 130 | var tile = map.getTile(x, y); 131 | var tileValue = tile.getValue(); 132 | 133 | if (tileValue < TileValues.TREEBASE) 134 | return; 135 | 136 | if (!tile.isCombustible()) { 137 | if (tileValue >= TileValues.ROADBASE && tileValue <= TileValues.LASTROAD) 138 | map.setTile(x, y, TileValues.RIVER, 0); 139 | 140 | return; 141 | } 142 | 143 | if (tile.isZone()) { 144 | ZoneUtils.fireZone(map, x, y, blockMaps); 145 | 146 | if (tileValue > TileValues.RZB) 147 | spriteManager.makeExplosionAt(ox, oy); 148 | } 149 | 150 | if (checkWet(tileValue)) 151 | map.setTile(x, y, TileValues.RIVER, 0); 152 | else 153 | map.setTile(x, y, TileValues.TINYEXP, BULLBIT | ANIMBIT); 154 | }; 155 | 156 | 157 | var getDistance = function(x1, y1, x2, y2) { 158 | return Math.abs(x1 - x2) + Math.abs(y1 - y2); 159 | }; 160 | 161 | 162 | var checkSpriteCollision = function(s1, s2) { 163 | return s1.frame !== 0 && s2.frame !== 0 && 164 | getDistance(s1.x, s1.y, s2.x, s2.y) < 30; 165 | }; 166 | 167 | 168 | var SpriteUtils = { 169 | absoluteDistance: absoluteDistance, 170 | checkSpriteCollision: checkSpriteCollision, 171 | destroyMapTile: destroyMapTile, 172 | getDir: getDir, 173 | getTileValue: getTileValue, 174 | turnTo: turnTo, 175 | pixToWorld: pixToWorld, 176 | worldToPix: worldToPix 177 | }; 178 | 179 | 180 | export { SpriteUtils }; 181 | -------------------------------------------------------------------------------- /src/stadia.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { ANIMBIT, POWERBIT } from "./tileFlags"; 11 | import { FOOTBALLGAME1, FOOTBALLGAME2, FULLSTADIUM, STADIUM } from "./tileValues"; 12 | 13 | var emptyStadiumFound = function(map, x, y, simData) { 14 | simData.census.stadiumPop += 1; 15 | 16 | if (map.getTile(x, y).isPowered()) { 17 | // Occasionally start the big game 18 | if (((simData.cityTime + x + y) & 31) === 0) { 19 | map.putZone(x, y, FULLSTADIUM, 4); 20 | map.addTileFlags(x, y, POWERBIT); 21 | map.setTile(x + 1, y, FOOTBALLGAME1, ANIMBIT); 22 | map.setTile(x + 1, y + 1, FOOTBALLGAME2, ANIMBIT); 23 | } 24 | } 25 | }; 26 | 27 | 28 | var fullStadiumFound = function(map, x, y, simData) { 29 | simData.census.stadiumPop += 1; 30 | var isPowered = map.getTile(x, y).isPowered(); 31 | 32 | if (((simData.cityTime + x + y) & 7) === 0) { 33 | map.putZone(x, y, STADIUM, 4); 34 | if (isPowered) 35 | map.addTileFlags(x, y, POWERBIT); 36 | } 37 | }; 38 | 39 | 40 | var Stadia = { 41 | registerHandlers: function(mapScanner, repairManager) { 42 | mapScanner.addAction(STADIUM, emptyStadiumFound); 43 | mapScanner.addAction(FULLSTADIUM, fullStadiumFound); 44 | repairManager.addAction(STADIUM, 15, 4); 45 | } 46 | }; 47 | 48 | 49 | export { Stadia }; 50 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { MiscUtils } from './miscUtils'; 11 | 12 | // A very thin wrapper around localStorage, in case we wish to move to some other storage mechanism 13 | // (such as indexedDB) in the future 14 | 15 | var getSavedGame = function() { 16 | var savedGame = window.localStorage.getItem(this.KEY); 17 | 18 | if (savedGame !== null) { 19 | savedGame = JSON.parse(savedGame); 20 | 21 | if (savedGame.version !== this.CURRENT_VERSION) 22 | this.transitionOldSave(savedGame); 23 | 24 | // Flag as a saved game for Game/Simulation etc... 25 | savedGame.isSavedGame = true; 26 | } 27 | 28 | return savedGame; 29 | }; 30 | 31 | 32 | var saveGame = function(gameData) { 33 | gameData.version = this.CURRENT_VERSION; 34 | gameData = JSON.stringify(gameData); 35 | 36 | window.localStorage.setItem(this.KEY, gameData); 37 | }; 38 | 39 | 40 | var transitionOldSave = function(savedGame) { 41 | switch (savedGame.version) { 42 | case 1: 43 | savedGame.everClicked = false; 44 | 45 | /* falls through */ 46 | case 2: 47 | savedGame.pollutionMaxX = Math.floor(savedGame.width / 2); 48 | savedGame.pollutionMaxY = Math.floor(savedGame.height / 2); 49 | savedGame.cityCentreX = Math.floor(savedGame.width / 2); 50 | savedGame.cityCentreY = Math.floor(savedGame.height / 2); 51 | 52 | break; 53 | 54 | default: 55 | throw new Error('Unknown save version!'); 56 | } 57 | }; 58 | 59 | 60 | var Storage = { 61 | getSavedGame: getSavedGame, 62 | saveGame: saveGame, 63 | transitionOldSave: transitionOldSave 64 | }; 65 | 66 | 67 | Object.defineProperty(Storage, 'CURRENT_VERSION', MiscUtils.makeConstantDescriptor(3)); 68 | Object.defineProperty(Storage, 'KEY', MiscUtils.makeConstantDescriptor('micropolisJSGame')); 69 | Object.defineProperty(Storage, 'canStore', MiscUtils.makeConstantDescriptor(window.localStorage !== undefined)); 70 | 71 | 72 | export { Storage }; 73 | -------------------------------------------------------------------------------- /src/tileFlags.ts: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | // Bit-masks for statusBits 11 | export const NOFLAGS = 0x0000; 12 | export const POWERBIT = 0x8000; // bit 15, tile has power. 13 | export const CONDBIT = 0x4000; // bit 14. tile can conduct electricity. 14 | export const BURNBIT = 0x2000; // bit 13, tile can be lit. 15 | export const BULLBIT = 0x1000; // bit 12, tile is bulldozable. 16 | export const ANIMBIT = 0x0800; // bit 11, tile is animated. 17 | export const ZONEBIT = 0x0400; // bit 10, tile is the center tile of the zone. 18 | 19 | export const BLBNBIT = BULLBIT | BURNBIT; 20 | export const BLBNCNBIT = BULLBIT | BURNBIT | CONDBIT; 21 | export const BNCNBIT = BURNBIT | CONDBIT; 22 | export const ASCBIT = ANIMBIT | CONDBIT | BURNBIT; 23 | export const ALLBITS = POWERBIT | CONDBIT | BURNBIT | BULLBIT | ANIMBIT | ZONEBIT; 24 | 25 | export const BIT_START = 0x400; 26 | export const BIT_END = 0x8000; 27 | export const BIT_MASK = BIT_START - 1; 28 | -------------------------------------------------------------------------------- /src/tileHistory.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | function TileHistory() { 11 | this.clear(); 12 | } 13 | 14 | 15 | var toKey = function(x, y) { 16 | return [x, y].join(','); 17 | }; 18 | 19 | 20 | TileHistory.prototype.clear = function() { 21 | this.data = {}; 22 | }; 23 | 24 | 25 | TileHistory.prototype.getTile = function(x, y) { 26 | var key = toKey(x, y); 27 | return this.data[key]; 28 | }; 29 | 30 | 31 | TileHistory.prototype.setTile = function(x, y, value) { 32 | var key = toKey(x, y); 33 | this.data[key] = value; 34 | }; 35 | 36 | 37 | export { TileHistory }; 38 | -------------------------------------------------------------------------------- /src/tileSet.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { TILE_COUNT } from "./tileValues"; 11 | 12 | // Tiles must be 16px square 13 | var TILE_SIZE = 16; 14 | var TILES_PER_ROW = Math.sqrt(TILE_COUNT); 15 | var ACCEPTABLE_DIMENSION = TILES_PER_ROW * TILE_SIZE; 16 | 17 | 18 | function TileSet(image, callback, errorCallback) { 19 | if (!(this instanceof TileSet)) 20 | return new TileSet(image, callback, errorCallback); 21 | 22 | if (callback === undefined || errorCallback === undefined) { 23 | if (callback === undefined && errorCallback === undefined) 24 | throw new Error('Tileset constructor called with no callback or errorCallback'); 25 | else 26 | throw new Error('Tileset constructor called with no ' + (callback === undefined ? 'callback' : 'errorCallback')); 27 | } 28 | 29 | this.isValid = false; 30 | 31 | if (!(image instanceof Image)) { 32 | // Spin the event loop 33 | window.setTimeout(errorCallback, 0); 34 | return; 35 | } 36 | 37 | this._verifyImage(image, callback, errorCallback); 38 | } 39 | 40 | 41 | TileSet.prototype._verifyImage = function(image, callback, errorCallback) { 42 | var width = image.width; 43 | var height = image.height; 44 | 45 | // We expect tilesets to be square, and of the required width/height 46 | if (width !== height || width !== ACCEPTABLE_DIMENSION) { 47 | // Spin the event loop 48 | window.setTimeout(errorCallback, 0); 49 | return; 50 | } 51 | 52 | var tileWidth = this.tileWidth = TILE_SIZE; 53 | 54 | // We paint the image onto a canvas so we can split it up 55 | var c = document.createElement('canvas'); 56 | c.width = tileWidth; 57 | c.height = tileWidth; 58 | var cx = c.getContext('2d'); 59 | 60 | // Count how many tiles we have created 61 | var tileCount = TILE_COUNT; 62 | var notifications = 0; 63 | var self = this; 64 | 65 | // Callback triggered by an image load. Checks to see if we are done creating images, 66 | // and if so notifies the caller. 67 | var imageLoad = function() { 68 | notifications++; 69 | 70 | if (notifications === tileCount) { 71 | self.isValid = true; 72 | // Spin the event loop 73 | window.setTimeout(callback, 0); 74 | return; 75 | } 76 | }; 77 | 78 | // Break up the source image into tiles by painting each tile onto a canvas, computing the dataURI 79 | // of the canvas, and using that to create a new image, which we install on ourselves as a new property 80 | for (var i = 0; i < tileCount; i++) { 81 | cx.clearRect(0, 0, tileWidth, tileWidth); 82 | 83 | var sourceX = i % TILES_PER_ROW * tileWidth; 84 | var sourceY = Math.floor(i / TILES_PER_ROW) * tileWidth; 85 | cx.drawImage(image, sourceX, sourceY, tileWidth, tileWidth, 0, 0, tileWidth, tileWidth); 86 | 87 | this[i] = new Image(); 88 | this[i].onload = imageLoad; 89 | this[i].src = c.toDataURL(); 90 | } 91 | }; 92 | 93 | 94 | export { TileSet }; 95 | -------------------------------------------------------------------------------- /src/tileUtils.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Random } from './random'; 11 | import { Tile } from './tile'; 12 | import { ANIMBIT, BULLBIT } from "./tileFlags"; 13 | import * as TileValues from "./tileValues"; 14 | 15 | var unwrapTile = function(f) { 16 | return function(tile) { 17 | if (tile instanceof Tile) 18 | tile = tile.getValue(); 19 | return f.call(null, tile); 20 | }; 21 | }; 22 | 23 | 24 | var canBulldoze = unwrapTile(function(tileValue) { 25 | return (tileValue >= TileValues.FIRSTRIVEDGE && tileValue <= TileValues.LASTRUBBLE) || 26 | (tileValue >= TileValues.POWERBASE + 2 && tileValue <= TileValues.POWERBASE + 12) || 27 | (tileValue >= TileValues.TINYEXP && tileValue <= TileValues.LASTTINYEXP + 2); 28 | }); 29 | 30 | 31 | var isCommercial = unwrapTile(function(tile) { 32 | return tile >= TileValues.COMBASE && tile < TileValues.INDBASE; 33 | }); 34 | 35 | 36 | var isCommercialZone = function(tile) { 37 | return tile.isZone() && isCommercial(tile); 38 | }; 39 | 40 | 41 | var isDriveable = unwrapTile(function(tile) { 42 | return (tile >= TileValues.ROADBASE && tile <= TileValues.LASTROAD) || 43 | (tile >= TileValues.RAILHPOWERV && tile <= TileValues.LASTRAIL); 44 | }); 45 | 46 | 47 | var isFire = unwrapTile(function(tile) { 48 | return tile >= TileValues.FIREBASE && tile < TileValues.ROADBASE; 49 | }); 50 | 51 | 52 | var isFlood = unwrapTile(function(tile) { 53 | return tile >= TileValues.FLOOD && tile < TileValues.LASTFLOOD; 54 | }); 55 | 56 | 57 | var isIndustrial = unwrapTile(function(tile) { 58 | return tile >= TileValues.INDBASE && tile < TileValues.PORTBASE; 59 | }); 60 | 61 | 62 | var isIndustrialZone = function(tile) { 63 | return tile.isZone() && isIndustrial(tile); 64 | }; 65 | 66 | 67 | var isManualExplosion = unwrapTile(function(tile) { 68 | return tile >= TileValues.TINYEXP && tile <= TileValues.LASTTINYEXP; 69 | }); 70 | 71 | 72 | var isRail = unwrapTile(function(tile) { 73 | return tile >= TileValues.RAILBASE && tile < TileValues.RESBASE; 74 | }); 75 | 76 | 77 | var isResidential = unwrapTile(function(tile) { 78 | return tile >= TileValues.RESBASE && tile < TileValues.HOSPITALBASE; 79 | }); 80 | 81 | 82 | var isResidentialZone = function(tile) { 83 | return tile.isZone() && isResidential(tile); 84 | }; 85 | 86 | 87 | var isRoad = unwrapTile(function(tile) { 88 | return tile >= TileValues.ROADBASE && tile < TileValues.POWERBASE; 89 | }); 90 | 91 | 92 | var normalizeRoad = unwrapTile(function(tile) { 93 | return (tile >= TileValues.ROADBASE && tile <= TileValues.LASTROAD + 1) ? (tile & 15) + 64 : tile; 94 | }); 95 | 96 | 97 | var randomFire = function() { 98 | return new Tile(TileValues.FIRE + (Random.getRandom16() & 3), ANIMBIT); 99 | }; 100 | 101 | 102 | var randomRubble = function() { 103 | return new Tile(TileValues.RUBBLE + (Random.getRandom16() & 3), BULLBIT); 104 | }; 105 | 106 | 107 | var TileUtils = { 108 | canBulldoze: canBulldoze, 109 | isCommercial: isCommercial, 110 | isCommercialZone: isCommercialZone, 111 | isDriveable: isDriveable, 112 | isFire: isFire, 113 | isFlood: isFlood, 114 | isIndustrial: isIndustrial, 115 | isIndustrialZone: isIndustrialZone, 116 | isManualExplosion: isManualExplosion, 117 | isRail: isRail, 118 | isResidential: isResidential, 119 | isResidentialZone: isResidentialZone, 120 | isRoad: isRoad, 121 | normalizeRoad: normalizeRoad, 122 | randomFire: randomFire, 123 | randomRubble: randomRubble 124 | }; 125 | 126 | 127 | export { TileUtils }; 128 | -------------------------------------------------------------------------------- /src/tornadoSprite.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BaseSprite } from './baseSprite'; 11 | import { SPRITE_DYING, SPRITE_MOVED } from './messages'; 12 | import { MiscUtils } from './miscUtils'; 13 | import { Random } from './random'; 14 | import { SpriteConstants } from './spriteConstants'; 15 | import { SpriteUtils } from './spriteUtils'; 16 | 17 | function TornadoSprite(map, spriteManager, x, y) { 18 | this.init(SpriteConstants.SPRITE_TORNADO, map, spriteManager, x, y); 19 | this.width = 48; 20 | this.height = 48; 21 | this.xOffset = -24; 22 | this.yOffset = -40; 23 | this.frame = 1; 24 | this.count = 200; 25 | } 26 | 27 | 28 | BaseSprite(TornadoSprite); 29 | 30 | 31 | var xDelta = [2, 3, 2, 0, -2, -3]; 32 | var yDelta = [-2, 0, 2, 3, 2, 0]; 33 | 34 | TornadoSprite.prototype.move = function(spriteCycle, disasterManager, blockMaps) { 35 | var frame = this.frame; 36 | 37 | // If middle frame, move right or left 38 | // depending on the flag value 39 | // If frame = 1, perhaps die based on flag 40 | // value 41 | if (frame === 2) { 42 | if (this.flag) 43 | frame = 3; 44 | else 45 | frame = 1; 46 | } else { 47 | if (frame === 1) 48 | this.flag = 1; 49 | else 50 | this.flag = 0; 51 | 52 | frame = 2; 53 | } 54 | 55 | if (this.count > 0) 56 | this.count--; 57 | 58 | this.frame = frame; 59 | 60 | var spriteList = this.spriteManager.getSpriteList(); 61 | for (var i = 0; i < spriteList.length; i++) { 62 | var s = spriteList[i]; 63 | 64 | // Explode vulnerable sprites 65 | if (s.frame !== 0 && 66 | (s.type === SpriteConstants.SPRITE_AIRPLANE || s.type === SpriteConstants.SPRITE_HELICOPTER || 67 | s.type === SpriteConstants.SPRITE_SHIP || s.type === SpriteConstants.SPRITE_TRAIN) && 68 | SpriteUtils.checkSpriteCollision(this, s)) { 69 | s.explodeSprite(); 70 | } 71 | } 72 | 73 | frame = Random.getRandom(5); 74 | this.x += xDelta[frame]; 75 | this.y += yDelta[frame]; 76 | 77 | if (this.spriteNotInBounds()) 78 | this.frame = 0; 79 | 80 | if (this.count !== 0 && Random.getRandom(500) === 0) 81 | this.frame = 0; 82 | 83 | if (this.frame === 0) 84 | this._emitEvent(SPRITE_DYING); 85 | 86 | SpriteUtils.destroyMapTile(this.spriteManager, this.map, blockMaps, this.x, this.y); 87 | this._emitEvent(SPRITE_MOVED, {x: this.worldX, y: this.worldY}); 88 | }; 89 | 90 | 91 | // Metadata for image loading 92 | Object.defineProperties(TornadoSprite, 93 | {ID: MiscUtils.makeConstantDescriptor(6), 94 | width: MiscUtils.makeConstantDescriptor(48), 95 | frames: MiscUtils.makeConstantDescriptor(3)}); 96 | 97 | 98 | export { TornadoSprite }; 99 | -------------------------------------------------------------------------------- /src/touchWarnWindow.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { TOUCH_WINDOW_CLOSED } from './messages'; 11 | import { ModalWindow } from './modalWindow'; 12 | 13 | var TouchWarnWindow = ModalWindow(function() { 14 | $(touchFormID).on('submit', submit.bind(this)); 15 | }); 16 | 17 | 18 | var touchFormID = '#touchForm'; 19 | var touchOKID = '#touchOK'; 20 | 21 | 22 | var submit = function(e) { 23 | e.preventDefault(); 24 | this.close(); 25 | }; 26 | 27 | 28 | TouchWarnWindow.prototype.close = function() { 29 | this._toggleDisplay(); 30 | this._emitEvent(TOUCH_WINDOW_CLOSED); 31 | }; 32 | 33 | 34 | TouchWarnWindow.prototype.open = function() { 35 | this._toggleDisplay(); 36 | $(touchOKID).focus(); 37 | }; 38 | 39 | 40 | export { TouchWarnWindow }; 41 | -------------------------------------------------------------------------------- /src/traffic.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { forEachCardinalDirection } from './direction'; 11 | import { MiscUtils } from './miscUtils'; 12 | import { Position } from './position'; 13 | import { Random } from './random'; 14 | import { SPRITE_HELICOPTER } from './spriteConstants'; 15 | import { SpriteUtils } from './spriteUtils'; 16 | import { TileUtils } from './tileUtils'; 17 | import { DIRT, POWERBASE, ROADBASE } from "./tileValues"; 18 | 19 | function Traffic(map, spriteManager) { 20 | this._map = map; 21 | this._stack = []; 22 | this._spriteManager = spriteManager; 23 | } 24 | 25 | 26 | Traffic.prototype.makeTraffic = function(x, y, blockMaps, destFn) { 27 | this._stack = []; 28 | 29 | var pos = new Position(x, y); 30 | 31 | if (this.findPerimeterRoad(pos)) { 32 | if (this.tryDrive(pos, destFn)) { 33 | this.addToTrafficDensityMap(blockMaps); 34 | return Traffic.ROUTE_FOUND; 35 | } 36 | 37 | return Traffic.NO_ROUTE_FOUND; 38 | } else { 39 | return Traffic.NO_ROAD_FOUND; 40 | } 41 | }; 42 | 43 | 44 | Traffic.prototype.addToTrafficDensityMap = function(blockMaps) { 45 | var trafficDensityMap = blockMaps.trafficDensityMap; 46 | 47 | while (this._stack.length > 0) { 48 | var pos = this._stack.pop(); 49 | 50 | // Could this happen?!? 51 | if (!this._map.testBounds(pos.x, pos.y)) 52 | continue; 53 | 54 | var tileValue = this._map.getTileValue(pos.x, pos.y); 55 | 56 | if (tileValue >= ROADBASE && tileValue < POWERBASE) { 57 | // Update traffic density. 58 | var traffic = trafficDensityMap.worldGet(pos.x, pos.y); 59 | traffic += 50; 60 | traffic = Math.min(traffic, 240); 61 | trafficDensityMap.worldSet(pos.x, pos.y, traffic); 62 | 63 | // Attract traffic copter to the traffic 64 | if (traffic >= 240 && Random.getRandom(5) === 0) { 65 | var sprite = this._spriteManager.getSprite(SPRITE_HELICOPTER); 66 | if (sprite !== null) { 67 | sprite.destX = SpriteUtils.worldToPix(pos.x); 68 | sprite.destY = SpriteUtils.worldToPix(pos.y); 69 | } 70 | } 71 | } 72 | } 73 | }; 74 | 75 | 76 | var perimX = [-1, 0, 1, 2, 2, 2, 1, 0,-1,-2,-2,-2]; 77 | var perimY = [-2,-2,-2,-1, 0, 1, 2, 2, 2, 1, 0,-1]; 78 | 79 | Traffic.prototype.findPerimeterRoad = function(pos) { 80 | for (var i = 0; i < 12; i++) { 81 | var xx = pos.x + perimX[i]; 82 | var yy = pos.y + perimY[i]; 83 | 84 | if (this._map.testBounds(xx, yy)) { 85 | if (TileUtils.isDriveable(this._map.getTileValue(xx, yy))) { 86 | pos.x = xx; 87 | pos.y = yy; 88 | return true; 89 | } 90 | } 91 | } 92 | 93 | return false; 94 | }; 95 | 96 | 97 | var MAX_TRAFFIC_DISTANCE = 30; 98 | 99 | Traffic.prototype.tryDrive = function(startPos, destFn) { 100 | var dirLast; 101 | var drivePos = new Position(startPos); 102 | 103 | /* Maximum distance to try */ 104 | for (var dist = 0; dist < MAX_TRAFFIC_DISTANCE; dist++) { 105 | var dir = this.tryGo(drivePos, dirLast); 106 | if (dir) { 107 | drivePos = Position.move(pos, dir); 108 | dirLast = dir.oppositeDirection(); 109 | 110 | if (dist & 1) 111 | this._stack.push(new Position(drivePos)); 112 | 113 | if (this.driveDone(drivePos, destFn)) 114 | return true; 115 | } else { 116 | if (this._stack.length > 0) { 117 | this._stack.pop(); 118 | dist += 3; 119 | } else { 120 | return false; 121 | } 122 | } 123 | } 124 | 125 | return false; 126 | }; 127 | 128 | 129 | Traffic.prototype.tryGo = function(pos, dirLast) { 130 | var directions = []; 131 | 132 | // Find connections from current position. 133 | var count = 0; 134 | 135 | forEachCardinalDirection(dir => { 136 | if (dir != dirLast && TileUtils.isDriveable(this._map.getTileFromMapOrDefault(pos, dir, DIRT))) { 137 | directions.push(dir); 138 | count++; 139 | } 140 | }); 141 | 142 | if (count === 0) { 143 | return; 144 | } 145 | 146 | if (count === 1) { 147 | return directions[0]; 148 | } 149 | 150 | const index = Random.getRandom(directions.length - 1); 151 | return directions[index]; 152 | }; 153 | 154 | 155 | Traffic.prototype.driveDone = function(pos, destFn) { 156 | if (pos.y > 0) { 157 | if (destFn(this._map.getTileValue(pos.x, pos.y - 1))) 158 | return true; 159 | } 160 | 161 | if (pos.x < (this._map.width - 1)) { 162 | if (destFn(this._map.getTileValue(pos.x + 1, pos.y))) 163 | return true; 164 | } 165 | 166 | if (pos.y < (this._map.height - 1)) { 167 | if (destFn(this._map.getTileValue(pos.x, pos.y + 1))) 168 | return true; 169 | } 170 | 171 | if (pos.x > 0) { 172 | if (destFn(this._map.getTileValue(pos.x - 1, pos.y))) 173 | return true; 174 | } 175 | 176 | return false; 177 | }; 178 | 179 | 180 | Object.defineProperties(Traffic, 181 | {ROUTE_FOUND: MiscUtils.makeConstantDescriptor(1), 182 | NO_ROUTE_FOUND: MiscUtils.makeConstantDescriptor(0), 183 | NO_ROAD_FOUND: MiscUtils.makeConstantDescriptor(-1)}); 184 | 185 | 186 | export { Traffic }; 187 | -------------------------------------------------------------------------------- /src/trainSprite.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { BaseSprite } from './baseSprite'; 11 | import { TRAIN_CRASHED } from './messages'; 12 | import { MiscUtils } from './miscUtils'; 13 | import { Random } from './random'; 14 | import { SPRITE_TRAIN } from './spriteConstants'; 15 | import { SpriteUtils } from './spriteUtils'; 16 | import * as TileValues from "./tileValues"; 17 | 18 | function TrainSprite(map, spriteManager, x, y) { 19 | this.init(SPRITE_TRAIN, map, 20 | spriteManager, x, y); 21 | this.width = 32; 22 | this.height = 32; 23 | this.xOffset = -16; 24 | this.yOffset = -16; 25 | this.frame = 1; 26 | this.dir = 4; 27 | } 28 | 29 | 30 | BaseSprite(TrainSprite); 31 | 32 | 33 | var tileDeltaX = [ 0, 16, 0, -16]; 34 | var tileDeltaY = [-16, 0, 16, 0 ]; 35 | var xDelta = [ 0, 4, 0, -4, 0]; 36 | var yDelta = [ -4, 0, 4, 0, 0]; 37 | 38 | var TrainPic2 = [ 1, 2, 1, 2, 5]; 39 | 40 | // Frame values 41 | var NORTHSOUTH = 1; 42 | var EASTWEST = 2; 43 | var NWSE = 3; 44 | var NESW = 4; 45 | var UNDERWATER = 5; 46 | 47 | // Direction values 48 | var NORTH = 0; 49 | var EAST = 1; 50 | var SOUTH = 2; 51 | var WEST = 3; 52 | var CANTMOVE = 4; 53 | 54 | TrainSprite.prototype.move = function(spriteCycle, disasterManager, blockMaps) { 55 | // Trains can only move in the 4 cardinal directions 56 | // Over the course of 4 frames, we move through a tile, so 57 | // ever fourth frame, we try to find a direction to move in 58 | // (excluding the opposite direction from the current direction 59 | // of travel). If there is no possible direction found, our direction 60 | // is set to CANTMOVE. (Thus, if we're in a dead end, we can start heading 61 | // backwards next time round). If we fail to find a destination after 2 attempts, 62 | // we die. 63 | 64 | if (this.frame === NWSE || this.frame === NESW) 65 | this.frame = TrainPic2[this.dir]; 66 | 67 | this.x += xDelta[this.dir]; 68 | this.y += yDelta[this.dir]; 69 | 70 | // Find a new direction. 71 | if ((spriteCycle & 3) === 0) { 72 | // Choose a random starting point for our search 73 | var dir = Random.getRandom16() & 3; 74 | 75 | for (var i = dir; i < dir + 4; i++) { 76 | var dir2 = i & 3; 77 | 78 | if (this.dir !== CANTMOVE) { 79 | // Avoid the opposite direction 80 | if (dir2 === ((this.dir + 2) & 3)) 81 | continue; 82 | } 83 | 84 | var tileValue = SpriteUtils.getTileValue(this.map, this.x + tileDeltaX[dir2], this.y + tileDeltaY[dir2]); 85 | 86 | if ((tileValue >= TileValues.RAILBASE && tileValue <= TileValues.LASTRAIL) || 87 | tileValue === TileValues.RAILVPOWERH || tileValue === TileValues.RAILHPOWERV) { 88 | if (this.dir !== dir2 && this.dir !== CANTMOVE) { 89 | if (this.dir + dir2 === WEST) 90 | this.frame = NWSE; 91 | else 92 | this.frame = NESW; 93 | } else { 94 | this.frame = TrainPic2[dir2]; 95 | } 96 | 97 | if (tileValue === TileValues.HRAIL || tileValue === TileValues.VRAIL) 98 | this.frame = UNDERWATER; 99 | 100 | this.dir = dir2; 101 | return; 102 | } 103 | } 104 | 105 | // Nowhere to go. Die. 106 | if (this.dir === CANTMOVE) { 107 | this.frame = 0; 108 | return; 109 | } 110 | 111 | // We didn't find a direction this time. We'll try the opposite 112 | // next time around 113 | this.dir = CANTMOVE; 114 | } 115 | }; 116 | 117 | 118 | TrainSprite.prototype.explodeSprite = function() { 119 | this.frame = 0; 120 | this.spriteManager.makeExplosionAt(this.x, this.y); 121 | this._emitEvent(TRAIN_CRASHED, {showable: true, x: this.worldX, y: this.worldY}); 122 | }; 123 | 124 | 125 | // Metadata for image loading 126 | Object.defineProperties(TrainSprite, 127 | {ID: MiscUtils.makeConstantDescriptor(1), 128 | width: MiscUtils.makeConstantDescriptor(32), 129 | frames: MiscUtils.makeConstantDescriptor(5)}); 130 | 131 | 132 | export { TrainSprite }; 133 | -------------------------------------------------------------------------------- /src/transport.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Random } from './random'; 11 | import { SPRITE_SHIP } from './spriteConstants'; 12 | import { ANIMBIT, CONDBIT, BURNBIT } from "./tileFlags"; 13 | import { TileUtils } from './tileUtils'; 14 | import * as TileValues from "./tileValues"; 15 | 16 | var railFound = function(map, x, y, simData) { 17 | simData.census.railTotal += 1; 18 | simData.spriteManager.generateTrain(simData.census, x, y); 19 | 20 | if (simData.budget.shouldDegradeRoad()) { 21 | if (Random.getChance(511)) { 22 | var currentTile = map.getTile(x, y); 23 | 24 | // Don't degrade tiles with power lines 25 | if (currentTile.isConductive()) 26 | return; 27 | 28 | if (simData.budget.roadEffect < (Random.getRandom16() & 31)) { 29 | var mapValue = currentTile.getValue(); 30 | 31 | // Replace bridge tiles with water, otherwise rubble 32 | if (mapValue < TileValues.RAILBASE + 2) 33 | map.setTile(x, y, TileValues.RIVER, 0); 34 | else 35 | map.setTo(x, y, TileUtils.randomRubble()); 36 | } 37 | } 38 | } 39 | }; 40 | 41 | 42 | var airportFound = function(map, x, y, simData) { 43 | simData.census.airportPop += 1; 44 | 45 | var tile = map.getTile(x, y); 46 | if (tile.isPowered()) { 47 | if (map.getTileValue(x + 1, y - 1) === TileValues.RADAR) 48 | map.setTile(x + 1, y - 1, TileValues.RADAR0, CONDBIT | ANIMBIT | BURNBIT); 49 | 50 | if (Random.getRandom(5) === 0) { 51 | simData.spriteManager.generatePlane(x, y); 52 | return; 53 | } 54 | 55 | if (Random.getRandom(12) === 0) 56 | simData.spriteManager.generateCopter(x, y); 57 | } else { 58 | map.setTile(x + 1, y - 1, TileValues.RADAR, CONDBIT | BURNBIT); 59 | } 60 | }; 61 | 62 | 63 | var portFound = function(map, x, y, simData) { 64 | simData.census.seaportPop += 1; 65 | 66 | var tile = map.getTile(x, y); 67 | if (tile.isPowered() && 68 | simData.spriteManager.getSprite(SPRITE_SHIP) === null) 69 | simData.spriteManager.generateShip(); 70 | }; 71 | 72 | 73 | var Transport = { 74 | registerHandlers: function(mapScanner, repairManager) { 75 | mapScanner.addAction(TileUtils.isRail, railFound); 76 | mapScanner.addAction(TileValues.PORT, portFound); 77 | mapScanner.addAction(TileValues.AIRPORT, airportFound); 78 | 79 | repairManager.addAction(TileValues.PORT, 15, 4); 80 | repairManager.addAction(TileValues.AIRPORT, 7, 6); 81 | } 82 | }; 83 | 84 | 85 | export { Transport }; 86 | -------------------------------------------------------------------------------- /src/valves.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { EventEmitter } from './eventEmitter'; 11 | import { VALVES_UPDATED } from './messages'; 12 | import { MiscUtils } from './miscUtils'; 13 | 14 | var Valves = EventEmitter(function () { 15 | this.resValve = 0; 16 | this.comValve = 0; 17 | this.indValve = 0; 18 | this.resCap = false; 19 | this.comCap = false; 20 | this.indCap = false; 21 | }); 22 | 23 | 24 | var RES_VALVE_RANGE = 2000; 25 | var COM_VALVE_RANGE = 1500; 26 | var IND_VALVE_RANGE = 1500; 27 | 28 | 29 | var taxTable = [ 30 | 200, 150, 120, 100, 80, 50, 30, 0, -10, -40, -100, 31 | -150, -200, -250, -300, -350, -400, -450, -500, -550, -600]; 32 | var extMarketParamTable = [1.2, 1.1, 0.98]; 33 | 34 | Valves.prototype.save = function(saveData) { 35 | saveData.resValve = this.resValve; 36 | saveData.comValve = this.comValve; 37 | saveData.indValve = this.indValve; 38 | }; 39 | 40 | 41 | Valves.prototype.load = function(saveData) { 42 | this.resValve = saveData.resValve; 43 | this.comValve = saveData.comValve; 44 | this.indValve = saveData.indValve; 45 | 46 | this._emitEvent(VALVES_UPDATED); 47 | }; 48 | 49 | 50 | Valves.prototype.setValves = function(gameLevel, census, budget) { 51 | var resPopDenom = 8; 52 | var birthRate = 0.02; 53 | var labourBaseMax = 1.3; 54 | var internalMarketDenom = 3.7; 55 | var projectedIndPopMin = 5.0; 56 | var resRatioDefault = 1.3; 57 | var resRatioMax = 2; 58 | var comRatioMax = 2; 59 | var indRatioMax = 2; 60 | var taxMax = 20; 61 | var taxTableScale = 600; 62 | var employment, labourBase; 63 | 64 | // Residential zones scale their population index when reporting it to the census 65 | var normalizedResPop = census.resPop / resPopDenom; 66 | census.totalPop = Math.round(normalizedResPop + census.comPop + census.indPop); 67 | 68 | // A lack of developed commercial and industrial zones means there are no employment opportunities, which constrain 69 | // growth. (This might hurt initially if, for example, the player lays out an initial grid, as the residential zones 70 | // will likely develop first, so the residential valve will immediately crater). 71 | if (census.resPop > 0) 72 | employment = (census.comHist10[1] + census.indHist10[1]) / normalizedResPop; 73 | else 74 | employment = 1; 75 | 76 | // Given the employment rate, calculate expected migration, add in births, and project the new population. 77 | var migration = normalizedResPop * (employment - 1); 78 | var births = normalizedResPop * birthRate; 79 | var projectedResPop = normalizedResPop + migration + births; 80 | 81 | // Examine how many zones require workers 82 | labourBase = census.comHist10[1] + census.indHist10[1]; 83 | if (labourBase > 0.0) 84 | labourBase = census.resHist10[1] / labourBase; 85 | else 86 | labourBase = 1; 87 | labourBase = MiscUtils.clamp(labourBase, 0.0, labourBaseMax); 88 | 89 | // Project future industry and commercial needs, taking into account available labour, and competition from 90 | // other global cities 91 | var internalMarket = (normalizedResPop + census.comPop + census.indPop) / internalMarketDenom; 92 | var projectedComPop = internalMarket * labourBase; 93 | var projectedIndPop = census.indPop * labourBase * extMarketParamTable[gameLevel]; 94 | projectedIndPop = Math.max(projectedIndPop, projectedIndPopMin); 95 | 96 | // Calculate the expected percentage changes in each population type 97 | var resRatio; 98 | if (normalizedResPop > 0) 99 | resRatio = projectedResPop / normalizedResPop; 100 | else 101 | resRatio = resRatioDefault; 102 | 103 | var comRatio; 104 | if (census.comPop > 0) 105 | comRatio = projectedComPop / census.comPop; 106 | else 107 | comRatio = projectedComPop; 108 | 109 | var indRatio; 110 | if (census.indPop > 0) 111 | indRatio = projectedIndPop / census.indPop; 112 | else 113 | indRatio = projectedIndPop; 114 | 115 | resRatio = Math.min(resRatio, resRatioMax); 116 | comRatio = Math.min(comRatio, comRatioMax); 117 | indRatio = Math.min(indRatio, indRatioMax); 118 | 119 | // Constrain growth according to the tax level. 120 | var z = Math.min((budget.cityTax + gameLevel), taxMax); 121 | resRatio = (resRatio - 1) * taxTableScale + taxTable[z]; 122 | comRatio = (comRatio - 1) * taxTableScale + taxTable[z]; 123 | indRatio = (indRatio - 1) * taxTableScale + taxTable[z]; 124 | 125 | this.resValve = MiscUtils.clamp(this.resValve + Math.round(resRatio), -RES_VALVE_RANGE, RES_VALVE_RANGE); 126 | this.comValve = MiscUtils.clamp(this.comValve + Math.round(comRatio), -COM_VALVE_RANGE, COM_VALVE_RANGE); 127 | this.indValve = MiscUtils.clamp(this.indValve + Math.round(indRatio), -IND_VALVE_RANGE, IND_VALVE_RANGE); 128 | 129 | if (this.resCap && this.resValve > 0) 130 | this.resValve = 0; 131 | 132 | if (this.comCap && this.comValve > 0) 133 | this.comValve = 0; 134 | 135 | if (this.indCap && this.indValve > 0) 136 | this.indValve = 0; 137 | 138 | this._emitEvent(VALVES_UPDATED); 139 | }; 140 | 141 | 142 | export { Valves }; 143 | -------------------------------------------------------------------------------- /src/wireTool.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { ConnectingTool } from './connectingTool'; 11 | import { CONDBIT, BURNBIT, BULLBIT } from "./tileFlags"; 12 | import { TileUtils } from './tileUtils'; 13 | import * as TileValues from "./tileValues"; 14 | 15 | var WireTool = ConnectingTool(function(map) { 16 | this.init(5, map, true, true); 17 | }); 18 | 19 | 20 | WireTool.prototype.layWire = function(x, y) { 21 | this.doAutoBulldoze(x, y); 22 | var cost = this.toolCost; 23 | 24 | var tile = this._worldEffects.getTileValue(x, y); 25 | tile = TileUtils.normalizeRoad(tile); 26 | 27 | switch (tile) { 28 | case TileValues.DIRT: 29 | this._worldEffects.setTile(x, y, TileValues.LHPOWER, CONDBIT | BURNBIT | BULLBIT); 30 | break; 31 | 32 | case TileValues.RIVER: 33 | case TileValues.REDGE: 34 | case TileValues.CHANNEL: 35 | cost = 25; 36 | 37 | if (x < this._map.width - 1) { 38 | tile = this._worldEffects.getTile(x + 1, y); 39 | if (tile.isConductive()) { 40 | tile = tile.getValue(); 41 | tile = TileUtils.normalizeRoad(tile); 42 | if (tile != TileValues.HROADPOWER && tile != TileValues.RAILHPOWERV && tile != TileValues.HPOWER) { 43 | this._worldEffects.setTile(x, y, TileValues.VPOWER, CONDBIT | BULLBIT); 44 | break; 45 | } 46 | } 47 | } 48 | 49 | if (x > 0) { 50 | tile = this._worldEffects.getTile(x - 1, y); 51 | if (tile.isConductive()) { 52 | tile = tile.getValue(); 53 | tile = TileUtils.normalizeRoad(tile); 54 | if (tile != TileValues.HROADPOWER && tile != TileValues.RAILHPOWERV && tile != TileValues.HPOWER) { 55 | this._worldEffects.setTile(x, y, TileValues.VPOWER, CONDBIT | BULLBIT); 56 | break; 57 | } 58 | } 59 | } 60 | 61 | if (y < this._map.height - 1) { 62 | tile = this._worldEffects.getTile(x, y + 1); 63 | if (tile.isConductive()) { 64 | tile = tile.getValue(); 65 | tile = TileUtils.normalizeRoad(tile); 66 | if (tile != TileValues.VROADPOWER && tile != TileValues.RAILVPOWERH && tile != TileValues.VPOWER) { 67 | this._worldEffects.setTile(x, y, TileValues.HPOWER, CONDBIT | BULLBIT); 68 | break; 69 | } 70 | } 71 | } 72 | 73 | if (y > 0) { 74 | tile = this._worldEffects.getTile(x, y - 1); 75 | if (tile.isConductive()) { 76 | tile = tile.getValue(); 77 | tile = TileUtils.normalizeRoad(tile); 78 | if (tile != TileValues.VROADPOWER && tile != TileValues.RAILVPOWERH && tile != TileValues.VPOWER) { 79 | this._worldEffects.setTile(x, y, TileValues.HPOWER, CONDBIT | BULLBIT); 80 | break; 81 | } 82 | } 83 | } 84 | 85 | return this.TOOLRESULT_FAILED; 86 | 87 | case TileValues.ROADS: 88 | this._worldEffects.setTile(x, y, TileValues.HROADPOWER, CONDBIT | BURNBIT | BULLBIT); 89 | break; 90 | 91 | case TileValues.ROADS2: 92 | this._worldEffects.setTile(x, y, TileValues.VROADPOWER, CONDBIT | BURNBIT | BULLBIT); 93 | break; 94 | 95 | case TileValues.LHRAIL: 96 | this._worldEffects.setTile(x, y, TileValues.RAILHPOWERV, CONDBIT | BURNBIT | BULLBIT); 97 | break; 98 | 99 | case TileValues.LVRAIL: 100 | this._worldEffects.setTile(x, y, TileValues.RAILVPOWERH, CONDBIT | BURNBIT | BULLBIT); 101 | break; 102 | 103 | default: 104 | return this.TOOLRESULT_FAILED; 105 | } 106 | 107 | this.addCost(cost); 108 | this.checkZoneConnections(x, y); 109 | return this.TOOLRESULT_OK; 110 | }; 111 | 112 | 113 | WireTool.prototype.doTool = function(x, y, blockMaps) { 114 | this.result = this.layWire(x, y); 115 | }; 116 | 117 | 118 | export { WireTool }; 119 | -------------------------------------------------------------------------------- /src/worldEffects.js: -------------------------------------------------------------------------------- 1 | /* micropolisJS. Adapted by Graeme McCutcheon from Micropolis. 2 | * 3 | * This code is released under the GNU GPL v3, with some additional terms. 4 | * Please see the files LICENSE and COPYING for details. Alternatively, 5 | * consult http://micropolisjs.graememcc.co.uk/LICENSE and 6 | * http://micropolisjs.graememcc.co.uk/COPYING 7 | * 8 | */ 9 | 10 | import { Tile } from "./tile"; 11 | 12 | function WorldEffects(map) { 13 | this._map = map; 14 | this._data = {}; 15 | } 16 | 17 | 18 | var toKey = function(x, y) { 19 | return [x, y].join(','); 20 | }; 21 | 22 | 23 | var fromKey = function(k) { 24 | k = k.split(','); 25 | return {x: k[0] - 0, y: k[1] - 0, toString: function() {return 'World effect coord: (' + k[0] + ', ' + k[1] + ')';}}; 26 | }; 27 | 28 | 29 | WorldEffects.prototype.clear = function() { 30 | this._data = []; 31 | }; 32 | 33 | 34 | WorldEffects.prototype.getTile = function(x, y) { 35 | var key = toKey(x, y); 36 | var tile = this._data[key]; 37 | if (tile === undefined) 38 | tile = this._map.getTile(x, y); 39 | return tile; 40 | }; 41 | 42 | 43 | WorldEffects.prototype.getTileValue = function(x, y) { 44 | return this.getTile(x, y).getValue(); 45 | }; 46 | 47 | 48 | WorldEffects.prototype.setTile = function(x, y, value, flags) { 49 | if (flags !== undefined && value instanceof Tile) 50 | throw new Error('Flags supplied with already defined tile'); 51 | 52 | if (!this._map.testBounds(x, y)) 53 | throw new Error('WorldEffects setTile called with invalid bounds ' + x + ', ' + y); 54 | 55 | if (flags === undefined && !(value instanceof Tile)) 56 | value = new Tile(value); 57 | else if (flags !== undefined) 58 | value = new Tile(value, flags); 59 | 60 | var key = toKey(x, y); 61 | this._data[key] = value; 62 | }; 63 | 64 | 65 | WorldEffects.prototype.apply = function() { 66 | var keys = Object.keys(this._data); 67 | for (var i = 0, l = keys.length; i < l; i++) { 68 | var coords = fromKey(keys[i]); 69 | this._map.setTo(coords, this._data[keys[i]]); 70 | } 71 | }; 72 | 73 | 74 | export { WorldEffects }; 75 | -------------------------------------------------------------------------------- /test/debugAssert.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "../src/debugAssert"; 2 | 3 | declare var global: any; 4 | 5 | describe("the debug asserter", () => { 6 | 7 | let globalAlert: any; 8 | 9 | beforeEach(() => { 10 | globalAlert = global.alert; 11 | global.alert = jest.fn().mockName("alert"); 12 | }); 13 | 14 | afterEach(() => { 15 | global.alert = globalAlert; 16 | }); 17 | 18 | it("should alert when the assertion fails", () => { 19 | const message = "foo"; 20 | 21 | assert(false, message); 22 | 23 | expect(global.alert).toHaveBeenCalledWith(expect.stringContaining(message)); 24 | }); 25 | 26 | it("should not alert when the assertion succeeds", () => { 27 | const message = "foo"; 28 | 29 | assert(true, message); 30 | 31 | expect(global.alert).not.toHaveBeenCalled(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/position.ts: -------------------------------------------------------------------------------- 1 | import * as Direction from "../src/direction"; 2 | import { Position } from "../src/position"; 3 | 4 | describe("the Position class", () => { 5 | 6 | describe("when constructing an origin position", () => { 7 | 8 | it("should construct a position with the correct coordinates", () => { 9 | const {x: originX, y: originY} = Position.origin(); 10 | 11 | expect(originX).toBe(0); 12 | expect(originY).toBe(0); 13 | }); 14 | }); 15 | 16 | describe("when moving", () => { 17 | 18 | it("should apply the correct transformation when moving north", () => { 19 | const originalPos = new Position(2, 2); 20 | 21 | const movedPos = Position.move(originalPos, Direction.NORTH); 22 | 23 | expect(movedPos.x).toBe(originalPos.x); 24 | expect(movedPos.y).toBe(originalPos.y - 1); 25 | }); 26 | 27 | it("should apply the correct transformation when moving northeast", () => { 28 | const originalPos = new Position(3, 4); 29 | 30 | const movedPos = Position.move(originalPos, Direction.NORTHEAST); 31 | 32 | expect(movedPos.x).toBe(originalPos.x + 1); 33 | expect(movedPos.y).toBe(originalPos.y - 1); 34 | }); 35 | 36 | it("should apply the correct transformation when moving east", () => { 37 | const originalPos = new Position(1, 2); 38 | 39 | const movedPos = Position.move(originalPos, Direction.EAST); 40 | 41 | expect(movedPos.x).toBe(originalPos.x + 1); 42 | expect(movedPos.y).toBe(originalPos.y); 43 | }); 44 | 45 | it("should apply the correct transformation when moving southeast", () => { 46 | const originalPos = new Position(5, 11); 47 | 48 | const movedPos = Position.move(originalPos, Direction.SOUTHEAST); 49 | 50 | expect(movedPos.x).toBe(originalPos.x + 1); 51 | expect(movedPos.y).toBe(originalPos.y + 1); 52 | }); 53 | 54 | it("should apply the correct transformation when moving south", () => { 55 | const originalPos = new Position(4, 2); 56 | 57 | const movedPos = Position.move(originalPos, Direction.SOUTH); 58 | 59 | expect(movedPos.x).toBe(originalPos.x); 60 | expect(movedPos.y).toBe(originalPos.y + 1); 61 | }); 62 | 63 | it("should apply the correct transformation when moving southwest", () => { 64 | const originalPos = new Position(7, 3); 65 | 66 | const movedPos = Position.move(originalPos, Direction.SOUTHWEST); 67 | 68 | expect(movedPos.x).toBe(originalPos.x - 1); 69 | expect(movedPos.y).toBe(originalPos.y + 1); 70 | }); 71 | 72 | it("should apply the correct transformation when moving west", () => { 73 | const originalPos = new Position(4, 6); 74 | 75 | const movedPos = Position.move(originalPos, Direction.WEST); 76 | 77 | expect(movedPos.x).toBe(originalPos.x - 1); 78 | expect(movedPos.y).toBe(originalPos.y); 79 | }); 80 | 81 | it("should apply the correct transformation when moving northwest", () => { 82 | const originalPos = new Position(8, 4); 83 | 84 | const movedPos = Position.move(originalPos, Direction.NORTHWEST); 85 | 86 | expect(movedPos.x).toBe(originalPos.x - 1); 87 | expect(movedPos.y).toBe(originalPos.y - 1); 88 | }); 89 | 90 | it("should throw an error for an unexpected direction", () => { 91 | const position = new Position(31, 11); 92 | 93 | const apiAbuse = () => Position.move(position, undefined as any); 94 | 95 | expect(apiAbuse).toThrow(); 96 | }); 97 | }); 98 | 99 | describe("the toString method", () => { 100 | 101 | it("should produce a human-readable string", () => { 102 | const x = 12; 103 | const y = 30; 104 | 105 | const pos = new Position(x, y); 106 | 107 | expect(pos.toString()).toMatch(new RegExp(`(${x}, ${y})`)); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/random.ts: -------------------------------------------------------------------------------- 1 | import { Random } from "../src/random"; 2 | 3 | function makeMathGlobal() { 4 | const math = {floor: jest.fn(), random: jest.fn()}; 5 | math.floor.mockImplementationOnce((n) => Math.floor(n)); 6 | return math; 7 | } 8 | 9 | function zeroFilledArrayOfInclusiveLength(n: number): number[] { 10 | const array = []; 11 | const limit = n + 1; 12 | 13 | for (let i = 0; i < limit; i++) { 14 | array[i] = 0; 15 | } 16 | 17 | return array; 18 | } 19 | 20 | function differenceToPreviousNeighbour(i: number, array: number[]): number { 21 | const value = array[i]; 22 | const neighbour = Math.max(i - 1, 0); 23 | return Math.abs(value - array[neighbour]); 24 | } 25 | 26 | function differenceToNextNeighbour(i: number, array: number[]): number { 27 | const value = array[i]; 28 | const neighbour = Math.min(i + 1, array.length - 1); 29 | return Math.abs(value - array[neighbour]); 30 | } 31 | 32 | describe("the getRandom function", () => { 33 | 34 | it("should be able to return 0", () => { 35 | const mathGlobal = makeMathGlobal(); 36 | mathGlobal.random.mockReturnValueOnce(0); 37 | 38 | const result = Random.getRandom(1, mathGlobal); 39 | 40 | expect(result).toBe(0); 41 | }); 42 | 43 | it("should be able to return the maximum", () => { 44 | const mathGlobal = makeMathGlobal(); 45 | mathGlobal.random.mockReturnValueOnce(0.9999999999); 46 | 47 | const result = Random.getRandom(5, mathGlobal); 48 | 49 | expect(result).toBe(5); 50 | }); 51 | 52 | it("should not exhibit significant bias", () => { 53 | const range = 1000; 54 | const buckets = zeroFilledArrayOfInclusiveLength(range); 55 | const mathGlobal = makeMathGlobal(); 56 | 57 | for (let i = 0; i < 1; i += 0.001) { 58 | buckets[Random.getRandom(range, mathGlobal)] += 1; 59 | } 60 | 61 | buckets.forEach((n) => { 62 | expect(differenceToPreviousNeighbour(n, buckets)).toBeLessThan(2); 63 | expect(differenceToNextNeighbour(n, buckets)).toBeLessThan(2); 64 | }); 65 | }); 66 | 67 | it("should not return values outside of the range", () => { 68 | const range = 1000; 69 | const buckets = zeroFilledArrayOfInclusiveLength(range); 70 | const mathGlobal = makeMathGlobal(); 71 | 72 | for (let i = 0; i < 1; i += 0.001) { 73 | buckets[Random.getRandom(range, mathGlobal)] += 1; 74 | } 75 | 76 | expect(buckets.length).toBe(range + 1); 77 | }); 78 | }); 79 | 80 | describe("the getRandom16 function", () => { 81 | 82 | it("should limit itself to 16-bit values", () => { 83 | const rng = jest.fn(); 84 | 85 | Random.getRandom16(rng); 86 | 87 | expect(rng).toHaveBeenCalledWith(2 ** 16 - 1); 88 | }); 89 | }); 90 | 91 | describe("the getRandom16Signed function", () => { 92 | 93 | it("should return positive values below 2^15", () => { 94 | const valueBelowThreshold = 32767; 95 | 96 | const rng = jest.fn().mockReturnValueOnce(valueBelowThreshold); 97 | 98 | expect(Random.getRandom16Signed(rng)).toBe(valueBelowThreshold); 99 | }); 100 | 101 | it("should return negative values in the interval [-(2^15)..-1]", () => { 102 | const threshold = 32768; 103 | const limit = 2 ** 16 - 1; 104 | const rng = jest.fn().mockReturnValueOnce(threshold).mockReturnValueOnce(limit); 105 | 106 | expect(Random.getRandom16Signed(rng)).toBeLessThan(0); 107 | expect(Random.getRandom16Signed(rng)).toBeLessThan(0); 108 | }); 109 | }); 110 | 111 | describe("the getERandom function", () => { 112 | 113 | it("getERandom should respect the given maximum", () => { 114 | const threshold = 1000; 115 | const rng = jest.fn().mockReturnValue(1); 116 | 117 | Random.getERandom(threshold, rng); 118 | 119 | expect(rng).toHaveBeenCalledWith(threshold); 120 | }); 121 | 122 | it("getERandom should exhibit a bias towards smaller numbers", () => { 123 | const threshold = 1000; 124 | const lower = 1; 125 | const upper = 2; 126 | const rng = jest.fn().mockReturnValueOnce(lower).mockReturnValueOnce(upper); 127 | 128 | expect(Random.getERandom(threshold, rng)).toBe(lower); 129 | }); 130 | }); 131 | 132 | describe("the getChance function", () => { 133 | 134 | it("getChance should return false if the least significant bits match exactly", () => { 135 | const chanceValue = 0b101; 136 | const rng = jest.fn().mockReturnValueOnce(0b1101); 137 | 138 | expect(Random.getChance(chanceValue, rng)).toBe(false); 139 | }); 140 | 141 | it("getChance should return false if some of the least significant bits match", () => { 142 | const chanceValue = 0b11; 143 | const rng = jest.fn().mockReturnValueOnce(0b1101); 144 | 145 | expect(Random.getChance(chanceValue, rng)).toBe(false); 146 | }); 147 | 148 | it("getChance should return true if none of the least significant bits match", () => { 149 | const chanceValue = 0b101; 150 | const rng = jest.fn().mockReturnValueOnce(0b1010); 151 | 152 | expect(Random.getChance(chanceValue, rng)).toBe(true); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /thirdparty/jquery/MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Paul Bakaus, http://jqueryui.com/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals (AUTHORS.txt, http://jqueryui.com/about) For exact 5 | contribution history, see the revision history and logs, available 6 | at http://jquery-ui.googlecode.com/svn/ 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | "Software"), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "outDir": "dist/src", 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "esModuleInterop": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "interface-name": ["true", "never-prefix"], 9 | "max-classes-per-file": [false], 10 | "member-access": [true, "no-public"], 11 | "no-bitwise": false, 12 | "no-console": ["false"] 13 | }, 14 | "rulesDirectory": [] 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const GitRevisionPlugin = require('git-revision-webpack-plugin'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const ScriptExtHtmlPlugin = require('script-ext-html-webpack-plugin'); 8 | const StripAssertionCode = require('ts-transformer-unassert').default; 9 | 10 | const HANDLE_TYPESCRIPT_WITH_ATL = {test: /\.ts$/, loader: "awesome-typescript-loader"}; 11 | 12 | const OUTPUT_DIRECTORY = 'dist'; 13 | 14 | function recursivelyCopy(dir) { 15 | return {from: dir, to: dir, toType: 'dir'}; 16 | } 17 | 18 | function cleanUpLeftovers() { 19 | return new CleanWebpackPlugin(OUTPUT_DIRECTORY, {}); 20 | } 21 | 22 | function copyStaticAssets() { 23 | return new CopyWebpackPlugin([ 24 | recursivelyCopy('css'), 25 | recursivelyCopy('images'), 26 | recursivelyCopy('sprites'), 27 | recursivelyCopy('thirdparty'), 28 | 'LICENSE', 29 | 'COPYING', 30 | ]); 31 | } 32 | 33 | function injectBundleIntoHTML(gitHash) { 34 | return new HtmlWebpackPlugin({ 35 | gitHash, 36 | inject: true, 37 | hash: true, 38 | template: './index.html', 39 | filename: 'index.html' 40 | }); 41 | } 42 | 43 | function injectBuildIdIntoAbout(gitHash) { 44 | return new HtmlWebpackPlugin({ 45 | gitHash, 46 | inject: false, 47 | hash: true, 48 | template: './about.html', 49 | filename: 'about.html' 50 | }); 51 | } 52 | 53 | function deferInjectedBundle() { 54 | return new ScriptExtHtmlPlugin({ 55 | defaultAttribute: 'defer' 56 | }); 57 | } 58 | 59 | function addDevelopmentConfigTo(options) { 60 | options.devServer = { 61 | contentBase: `./${OUTPUT_DIRECTORY}` 62 | }; 63 | 64 | options.devtool = 'source-maps'; 65 | options.mode = 'development'; 66 | } 67 | 68 | function addProductionConfigTo(options) { 69 | options.mode = 'production'; 70 | 71 | removeATLRuleFrom(options.module); 72 | const assertionStrippingConfig = { 73 | options: { 74 | getCustomTransformers: () => { 75 | return ({before: [StripAssertionCode]}); 76 | } 77 | } 78 | }; 79 | stripTSAssertionsRule = Object.assign(assertionStrippingConfig, HANDLE_TYPESCRIPT_WITH_ATL); 80 | options.module.rules.push(stripTSAssertionsRule); 81 | } 82 | 83 | function removeATLRuleFrom(webpackModuleOptions) { 84 | webpackModuleOptions.rules = webpackModuleOptions.rules.filter((rule) => rule !== HANDLE_TYPESCRIPT_WITH_ATL); 85 | } 86 | 87 | function getBuildId() { 88 | // Technically don't need to use the webpack plugin, as not passing it to Webpack... 89 | const gitPlugin = new GitRevisionPlugin({ 90 | commitHashCommand: `log -1 --pretty=format:'%h' master` 91 | }); 92 | 93 | return gitPlugin.commithash().slice(0, 12); 94 | } 95 | 96 | function commonOptions() { 97 | const buildId = getBuildId(); 98 | 99 | const options = { 100 | entry: './src/micropolis.js', 101 | module: { 102 | rules: [ 103 | HANDLE_TYPESCRIPT_WITH_ATL 104 | ] 105 | }, 106 | output: { 107 | path: path.resolve(__dirname, OUTPUT_DIRECTORY), 108 | filename: 'src/micropolis.js' 109 | }, 110 | plugins: [ 111 | cleanUpLeftovers(), 112 | copyStaticAssets(), 113 | injectBundleIntoHTML(buildId), 114 | injectBuildIdIntoAbout(buildId), 115 | deferInjectedBundle() 116 | ], 117 | resolve: { 118 | extensions: [ 119 | ".js", ".json", ".ts" 120 | ] 121 | } 122 | }; 123 | 124 | return options; 125 | } 126 | 127 | module.exports = function(env, argv) { 128 | const options = commonOptions(); 129 | 130 | if (env.development) { 131 | addDevelopmentConfigTo(options); 132 | } else { 133 | addProductionConfigTo(options); 134 | } 135 | 136 | return options; 137 | }; 138 | --------------------------------------------------------------------------------