diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..f6f2ae5 --- /dev/null +++ b/INSTALL @@ -0,0 +1,28 @@ +Prerequisites +============= + +To build MicropolisJ you will need + * Java Development Kit (version 7), get it from http://oracle.com/technetwork/java + * Apache Ant, get it from http://ant.apache.org + + +Building the Package +==================== + + 1. `cd' to the directory containing the package's source code. + + 2. Type `ant' to compile the package. + + +Installing MicropolisJ +====================== + +Everything you need to run the program is contained within the micropolisj.jar +file, so just copy that file to wherever you like. + + +Running MicropolisJ +=================== + +Type `java -jar micropolisj.jar` to run the program. + diff --git a/README b/README new file mode 100644 index 0000000..5ce7710 --- /dev/null +++ b/README @@ -0,0 +1,85 @@ +======================================================================== + +MicropolisJ (Micropolis in Java) + +======================================================================== + +MicropolisJ is Micropolis for the Java platform. +Copyright (C) 2013 Jason Long (jason@long.name). +Portions Copyright (C) 1989-2007 Electronic Arts Inc. + +MicropolisJ is based on Micropolis, Unix version, which was developed by +Don Hopkins (dhopkins@DonHopkins.com, http://www.DonHopkins.com) for +DUX Software under license from Maxis, in or around 1990. This version +was later modified for inclusion in the One Laptop Per Child (OLPC) +program, and released as free and open source software under the GPL in +2008. +Copyright (C) 1989-2007 Electronic Arts Inc. + +The original Micropolis game was designed and implemented by Will Wright. +Copyright (C) 2002 by Electronic Arts. + +======================================================================== + +See the INSTALL file for instructions on building the program from this +source code release. + +See the COPYING file for the full text copy of the GNU General Public +License. + +If you need assistance with this program, you may contact: + Jason Long (jason@long.name) + +======================================================================== + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. You should have received a +copy of the GNU General Public License along with this program. If +not, see . + + ADDITIONAL TERMS per GNU GPL Section 7 + +No trademark or publicity rights are granted. This license does NOT +give you any right, title or interest in the trademark SimCity or any +other Electronic Arts trademark. You may not distribute any +modification of this program using the trademark SimCity or claim any +affliation or association with Electronic Arts Inc. or its employees. +Any propagation or conveyance of this program must include this +copyright notice and these terms. + +If you convey this program (or any modifications of it) and assume +contractual liability for the program to recipients of it, you agree +to indemnify Electronic Arts for any liability that those contractual +assumptions impose on Electronic Arts. + +You may not misrepresent the origins of this program; modified +versions of the program must be marked as such and not identified as +the original program. + +This disclaimer supplements the one included in the General Public +License. TO THE FULLEST EXTENT PERMISSIBLE UNDER APPLICABLE LAW, THIS +PROGRAM IS PROVIDED TO YOU "AS IS," WITH ALL FAULTS, WITHOUT WARRANTY +OF ANY KIND, AND YOUR USE IS AT YOUR SOLE RISK. THE ENTIRE RISK OF +SATISFACTORY QUALITY AND PERFORMANCE RESIDES WITH YOU. ELECTRONIC ARTS +DISCLAIMS ANY AND ALL EXPRESS, IMPLIED OR STATUTORY WARRANTIES, +INCLUDING IMPLIED WARRANTIES OF MERCHANTABILITY, SATISFACTORY QUALITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT OF THIRD PARTY +RIGHTS, AND WARRANTIES (IF ANY) ARISING FROM A COURSE OF DEALING, +USAGE, OR TRADE PRACTICE. ELECTRONIC ARTS DOES NOT WARRANT AGAINST +INTERFERENCE WITH YOUR ENJOYMENT OF THE PROGRAM; THAT THE PROGRAM WILL +MEET YOUR REQUIREMENTS; THAT OPERATION OF THE PROGRAM WILL BE +UNINTERRUPTED OR ERROR-FREE, OR THAT THE PROGRAM WILL BE COMPATIBLE +WITH THIRD PARTY SOFTWARE OR THAT ANY ERRORS IN THE PROGRAM WILL BE +CORRECTED. NO ORAL OR WRITTEN ADVICE PROVIDED BY ELECTRONIC ARTS OR +ANY AUTHORIZED REPRESENTATIVE SHALL CREATE A WARRANTY. SOME +JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF OR LIMITATIONS ON IMPLIED +WARRANTIES OR THE LIMITATIONS ON THE APPLICABLE STATUTORY RIGHTS OF A +CONSUMER, SO SOME OR ALL OF THE ABOVE EXCLUSIONS AND LIMITATIONS MAY +NOT APPLY TO YOU. diff --git a/TODO b/TODO new file mode 100644 index 0000000..f413969 --- /dev/null +++ b/TODO @@ -0,0 +1,32 @@ +Autodoze- + *Do not autodoze for roads,rails, or wire if not enough money to actually + build it + +Budget dialog- + *Press escape to close + +Coal powerplant- + *When third tile of second row is on fire, something appears to revert it to + the animated smoke stack. + +On Game Load- + *All zones are unpowered for the first several ticks. I'd imagine there + are similar problems with the landValueMap, pollutionMap, tfDensity, + policeCoverage etc. I think what should happen is that the various methods + that update these maps should be called at load time. + *Evaluation data is not available. + *cityTime may not match history.cityTime + +Mouse pointer- + *Would be nice if tool dragging would help keep lines straight + +Disasters- + *When running FAST or SUPER_FAST, automatically pause or slow down the + simulation when a disaster occurs. + +City size notifications- + *Pause and pop up a message the first time the city reaches 2000, 10000, + 50000, etc. people. + +Code janitor- + *Rename fireZone() to killZone()... (I think that name might be better description.) diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..c0da135 --- /dev/null +++ b/build.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/micropolisj.ico b/micropolisj.ico new file mode 100755 index 0000000..6a951f5 Binary files /dev/null and b/micropolisj.ico differ diff --git a/resources/airport.png b/resources/airport.png new file mode 100755 index 0000000..10af140 Binary files /dev/null and b/resources/airport.png differ diff --git a/resources/coal.png b/resources/coal.png new file mode 100755 index 0000000..57f02a4 Binary files /dev/null and b/resources/coal.png differ diff --git a/resources/com.png b/resources/com.png new file mode 100755 index 0000000..7daca18 Binary files /dev/null and b/resources/com.png differ diff --git a/resources/demandg.png b/resources/demandg.png new file mode 100755 index 0000000..2d35059 Binary files /dev/null and b/resources/demandg.png differ diff --git a/resources/fire.png b/resources/fire.png new file mode 100755 index 0000000..89b0e87 Binary files /dev/null and b/resources/fire.png differ diff --git a/resources/grcom.png b/resources/grcom.png new file mode 100755 index 0000000..fdc593f Binary files /dev/null and b/resources/grcom.png differ diff --git a/resources/grcomhi.png b/resources/grcomhi.png new file mode 100755 index 0000000..1e29dc1 Binary files /dev/null and b/resources/grcomhi.png differ diff --git a/resources/grcrim.png b/resources/grcrim.png new file mode 100755 index 0000000..7e560e6 Binary files /dev/null and b/resources/grcrim.png differ diff --git a/resources/grcrimhi.png b/resources/grcrimhi.png new file mode 100755 index 0000000..fdabe30 Binary files /dev/null and b/resources/grcrimhi.png differ diff --git a/resources/grind.png b/resources/grind.png new file mode 100755 index 0000000..6a67b25 Binary files /dev/null and b/resources/grind.png differ diff --git a/resources/grindhi.png b/resources/grindhi.png new file mode 100755 index 0000000..e386de1 Binary files /dev/null and b/resources/grindhi.png differ diff --git a/resources/grmony.png b/resources/grmony.png new file mode 100755 index 0000000..eafa3a3 Binary files /dev/null and b/resources/grmony.png differ diff --git a/resources/grmonyhi.png b/resources/grmonyhi.png new file mode 100755 index 0000000..c492a88 Binary files /dev/null and b/resources/grmonyhi.png differ diff --git a/resources/grpoll.png b/resources/grpoll.png new file mode 100755 index 0000000..6ae873f Binary files /dev/null and b/resources/grpoll.png differ diff --git a/resources/grpollhi.png b/resources/grpollhi.png new file mode 100755 index 0000000..e5fe120 Binary files /dev/null and b/resources/grpollhi.png differ diff --git a/resources/grres.png b/resources/grres.png new file mode 100755 index 0000000..eaecec0 Binary files /dev/null and b/resources/grres.png differ diff --git a/resources/grreshi.png b/resources/grreshi.png new file mode 100755 index 0000000..1d4b22c Binary files /dev/null and b/resources/grreshi.png differ diff --git a/resources/icairp.png b/resources/icairp.png new file mode 100755 index 0000000..8f0e499 Binary files /dev/null and b/resources/icairp.png differ diff --git a/resources/icairphi.png b/resources/icairphi.png new file mode 100755 index 0000000..b546247 Binary files /dev/null and b/resources/icairphi.png differ diff --git a/resources/iccoal.png b/resources/iccoal.png new file mode 100755 index 0000000..fc4174a Binary files /dev/null and b/resources/iccoal.png differ diff --git a/resources/iccoalhi.png b/resources/iccoalhi.png new file mode 100755 index 0000000..13d957c Binary files /dev/null and b/resources/iccoalhi.png differ diff --git a/resources/iccom.png b/resources/iccom.png new file mode 100755 index 0000000..5ec433d Binary files /dev/null and b/resources/iccom.png differ diff --git a/resources/iccomhi.png b/resources/iccomhi.png new file mode 100755 index 0000000..ba32555 Binary files /dev/null and b/resources/iccomhi.png differ diff --git a/resources/icdozr.png b/resources/icdozr.png new file mode 100755 index 0000000..1404c62 Binary files /dev/null and b/resources/icdozr.png differ diff --git a/resources/icdozrhi.png b/resources/icdozrhi.png new file mode 100755 index 0000000..2f212c9 Binary files /dev/null and b/resources/icdozrhi.png differ diff --git a/resources/icfire.png b/resources/icfire.png new file mode 100755 index 0000000..031836d Binary files /dev/null and b/resources/icfire.png differ diff --git a/resources/icfirehi.png b/resources/icfirehi.png new file mode 100755 index 0000000..143a0c7 Binary files /dev/null and b/resources/icfirehi.png differ diff --git a/resources/icind.png b/resources/icind.png new file mode 100755 index 0000000..56b1ac4 Binary files /dev/null and b/resources/icind.png differ diff --git a/resources/icindhi.png b/resources/icindhi.png new file mode 100755 index 0000000..88b99a6 Binary files /dev/null and b/resources/icindhi.png differ diff --git a/resources/icnuc.png b/resources/icnuc.png new file mode 100755 index 0000000..629e15e Binary files /dev/null and b/resources/icnuc.png differ diff --git a/resources/icnuchi.png b/resources/icnuchi.png new file mode 100755 index 0000000..91d9481 Binary files /dev/null and b/resources/icnuchi.png differ diff --git a/resources/icpark.png b/resources/icpark.png new file mode 100755 index 0000000..f710bac Binary files /dev/null and b/resources/icpark.png differ diff --git a/resources/icparkhi.png b/resources/icparkhi.png new file mode 100755 index 0000000..72d7d5c Binary files /dev/null and b/resources/icparkhi.png differ diff --git a/resources/icpol.png b/resources/icpol.png new file mode 100755 index 0000000..00b2d8a Binary files /dev/null and b/resources/icpol.png differ diff --git a/resources/icpolhi.png b/resources/icpolhi.png new file mode 100755 index 0000000..030fdf3 Binary files /dev/null and b/resources/icpolhi.png differ diff --git a/resources/icqry.png b/resources/icqry.png new file mode 100755 index 0000000..d0de2a6 Binary files /dev/null and b/resources/icqry.png differ diff --git a/resources/icqryhi.png b/resources/icqryhi.png new file mode 100755 index 0000000..1f48a18 Binary files /dev/null and b/resources/icqryhi.png differ diff --git a/resources/icrail.png b/resources/icrail.png new file mode 100755 index 0000000..c845f83 Binary files /dev/null and b/resources/icrail.png differ diff --git a/resources/icrailhi.png b/resources/icrailhi.png new file mode 100755 index 0000000..27ef5c8 Binary files /dev/null and b/resources/icrailhi.png differ diff --git a/resources/icres.png b/resources/icres.png new file mode 100755 index 0000000..7b0d6b5 Binary files /dev/null and b/resources/icres.png differ diff --git a/resources/icreshi.png b/resources/icreshi.png new file mode 100755 index 0000000..a3963c4 Binary files /dev/null and b/resources/icreshi.png differ diff --git a/resources/icroad.png b/resources/icroad.png new file mode 100755 index 0000000..3174c2b Binary files /dev/null and b/resources/icroad.png differ diff --git a/resources/icroadhi.png b/resources/icroadhi.png new file mode 100755 index 0000000..21e32b8 Binary files /dev/null and b/resources/icroadhi.png differ diff --git a/resources/icseap.png b/resources/icseap.png new file mode 100755 index 0000000..48cb160 Binary files /dev/null and b/resources/icseap.png differ diff --git a/resources/icseaphi.png b/resources/icseaphi.png new file mode 100755 index 0000000..3c88684 Binary files /dev/null and b/resources/icseaphi.png differ diff --git a/resources/icstad.png b/resources/icstad.png new file mode 100755 index 0000000..e9fb524 Binary files /dev/null and b/resources/icstad.png differ diff --git a/resources/icstadhi.png b/resources/icstadhi.png new file mode 100755 index 0000000..7ac4c20 Binary files /dev/null and b/resources/icstadhi.png differ diff --git a/resources/icwire.png b/resources/icwire.png new file mode 100755 index 0000000..a3efdfa Binary files /dev/null and b/resources/icwire.png differ diff --git a/resources/icwirehi.png b/resources/icwirehi.png new file mode 100755 index 0000000..ad7898b Binary files /dev/null and b/resources/icwirehi.png differ diff --git a/resources/ind.png b/resources/ind.png new file mode 100755 index 0000000..2afe385 Binary files /dev/null and b/resources/ind.png differ diff --git a/resources/legendmm.png b/resources/legendmm.png new file mode 100755 index 0000000..c084a42 Binary files /dev/null and b/resources/legendmm.png differ diff --git a/resources/legendpm.png b/resources/legendpm.png new file mode 100755 index 0000000..0f13e40 Binary files /dev/null and b/resources/legendpm.png differ diff --git a/resources/micropolism.png b/resources/micropolism.png new file mode 100755 index 0000000..fa4bf6e Binary files /dev/null and b/resources/micropolism.png differ diff --git a/resources/nuclear.png b/resources/nuclear.png new file mode 100755 index 0000000..ebc803e Binary files /dev/null and b/resources/nuclear.png differ diff --git a/resources/obj1-0.png b/resources/obj1-0.png new file mode 100755 index 0000000..495b8e0 Binary files /dev/null and b/resources/obj1-0.png differ diff --git a/resources/obj1-1.png b/resources/obj1-1.png new file mode 100755 index 0000000..13e0689 Binary files /dev/null and b/resources/obj1-1.png differ diff --git a/resources/obj1-2.png b/resources/obj1-2.png new file mode 100755 index 0000000..16799ba Binary files /dev/null and b/resources/obj1-2.png differ diff --git a/resources/obj1-3.png b/resources/obj1-3.png new file mode 100755 index 0000000..527dd4a Binary files /dev/null and b/resources/obj1-3.png differ diff --git a/resources/obj1-4.png b/resources/obj1-4.png new file mode 100755 index 0000000..963124c Binary files /dev/null and b/resources/obj1-4.png differ diff --git a/resources/obj2-0.png b/resources/obj2-0.png new file mode 100755 index 0000000..1a45342 Binary files /dev/null and b/resources/obj2-0.png differ diff --git a/resources/obj2-1.png b/resources/obj2-1.png new file mode 100755 index 0000000..2b2035e Binary files /dev/null and b/resources/obj2-1.png differ diff --git a/resources/obj2-2.png b/resources/obj2-2.png new file mode 100755 index 0000000..e4ad890 Binary files /dev/null and b/resources/obj2-2.png differ diff --git a/resources/obj2-3.png b/resources/obj2-3.png new file mode 100755 index 0000000..5777779 Binary files /dev/null and b/resources/obj2-3.png differ diff --git a/resources/obj2-4.png b/resources/obj2-4.png new file mode 100755 index 0000000..072488d Binary files /dev/null and b/resources/obj2-4.png differ diff --git a/resources/obj2-5.png b/resources/obj2-5.png new file mode 100755 index 0000000..6286e44 Binary files /dev/null and b/resources/obj2-5.png differ diff --git a/resources/obj2-6.png b/resources/obj2-6.png new file mode 100755 index 0000000..72314b4 Binary files /dev/null and b/resources/obj2-6.png differ diff --git a/resources/obj2-7.png b/resources/obj2-7.png new file mode 100755 index 0000000..fa3d9e7 Binary files /dev/null and b/resources/obj2-7.png differ diff --git a/resources/obj3-0.png b/resources/obj3-0.png new file mode 100755 index 0000000..0f688ec Binary files /dev/null and b/resources/obj3-0.png differ diff --git a/resources/obj3-1.png b/resources/obj3-1.png new file mode 100755 index 0000000..0509c09 Binary files /dev/null and b/resources/obj3-1.png differ diff --git a/resources/obj3-10.png b/resources/obj3-10.png new file mode 100755 index 0000000..8534872 Binary files /dev/null and b/resources/obj3-10.png differ diff --git a/resources/obj3-2.png b/resources/obj3-2.png new file mode 100755 index 0000000..dafb49b Binary files /dev/null and b/resources/obj3-2.png differ diff --git a/resources/obj3-3.png b/resources/obj3-3.png new file mode 100755 index 0000000..7f81298 Binary files /dev/null and b/resources/obj3-3.png differ diff --git a/resources/obj3-4.png b/resources/obj3-4.png new file mode 100755 index 0000000..fcc3914 Binary files /dev/null and b/resources/obj3-4.png differ diff --git a/resources/obj3-5.png b/resources/obj3-5.png new file mode 100755 index 0000000..0a12dd1 Binary files /dev/null and b/resources/obj3-5.png differ diff --git a/resources/obj3-6.png b/resources/obj3-6.png new file mode 100755 index 0000000..7911107 Binary files /dev/null and b/resources/obj3-6.png differ diff --git a/resources/obj3-7.png b/resources/obj3-7.png new file mode 100755 index 0000000..5d4a28d Binary files /dev/null and b/resources/obj3-7.png differ diff --git a/resources/obj3-8.png b/resources/obj3-8.png new file mode 100755 index 0000000..c0acff2 Binary files /dev/null and b/resources/obj3-8.png differ diff --git a/resources/obj3-9.png b/resources/obj3-9.png new file mode 100755 index 0000000..45961bc Binary files /dev/null and b/resources/obj3-9.png differ diff --git a/resources/obj4-0.png b/resources/obj4-0.png new file mode 100755 index 0000000..092e725 Binary files /dev/null and b/resources/obj4-0.png differ diff --git a/resources/obj4-1.png b/resources/obj4-1.png new file mode 100755 index 0000000..70e1ce3 Binary files /dev/null and b/resources/obj4-1.png differ diff --git a/resources/obj4-2.png b/resources/obj4-2.png new file mode 100755 index 0000000..33b1993 Binary files /dev/null and b/resources/obj4-2.png differ diff --git a/resources/obj4-3.png b/resources/obj4-3.png new file mode 100755 index 0000000..d6bf269 Binary files /dev/null and b/resources/obj4-3.png differ diff --git a/resources/obj4-4.png b/resources/obj4-4.png new file mode 100755 index 0000000..53e5284 Binary files /dev/null and b/resources/obj4-4.png differ diff --git a/resources/obj4-5.png b/resources/obj4-5.png new file mode 100755 index 0000000..e2be174 Binary files /dev/null and b/resources/obj4-5.png differ diff --git a/resources/obj4-6.png b/resources/obj4-6.png new file mode 100755 index 0000000..735dd08 Binary files /dev/null and b/resources/obj4-6.png differ diff --git a/resources/obj4-7.png b/resources/obj4-7.png new file mode 100755 index 0000000..7594ced Binary files /dev/null and b/resources/obj4-7.png differ diff --git a/resources/obj5-0.png b/resources/obj5-0.png new file mode 100755 index 0000000..33a7304 Binary files /dev/null and b/resources/obj5-0.png differ diff --git a/resources/obj5-1.png b/resources/obj5-1.png new file mode 100755 index 0000000..5124bbe Binary files /dev/null and b/resources/obj5-1.png differ diff --git a/resources/obj5-10.png b/resources/obj5-10.png new file mode 100755 index 0000000..e6ecaee Binary files /dev/null and b/resources/obj5-10.png differ diff --git a/resources/obj5-11.png b/resources/obj5-11.png new file mode 100755 index 0000000..02f184e Binary files /dev/null and b/resources/obj5-11.png differ diff --git a/resources/obj5-12.png b/resources/obj5-12.png new file mode 100755 index 0000000..1428bcf Binary files /dev/null and b/resources/obj5-12.png differ diff --git a/resources/obj5-13.png b/resources/obj5-13.png new file mode 100755 index 0000000..1608933 Binary files /dev/null and b/resources/obj5-13.png differ diff --git a/resources/obj5-14.png b/resources/obj5-14.png new file mode 100755 index 0000000..bbcdc28 Binary files /dev/null and b/resources/obj5-14.png differ diff --git a/resources/obj5-15.png b/resources/obj5-15.png new file mode 100755 index 0000000..9e9674a Binary files /dev/null and b/resources/obj5-15.png differ diff --git a/resources/obj5-2.png b/resources/obj5-2.png new file mode 100755 index 0000000..d321430 Binary files /dev/null and b/resources/obj5-2.png differ diff --git a/resources/obj5-3.png b/resources/obj5-3.png new file mode 100755 index 0000000..64d0ce3 Binary files /dev/null and b/resources/obj5-3.png differ diff --git a/resources/obj5-4.png b/resources/obj5-4.png new file mode 100755 index 0000000..80795fc Binary files /dev/null and b/resources/obj5-4.png differ diff --git a/resources/obj5-5.png b/resources/obj5-5.png new file mode 100755 index 0000000..7c09dbb Binary files /dev/null and b/resources/obj5-5.png differ diff --git a/resources/obj5-6.png b/resources/obj5-6.png new file mode 100755 index 0000000..38da2e5 Binary files /dev/null and b/resources/obj5-6.png differ diff --git a/resources/obj5-7.png b/resources/obj5-7.png new file mode 100755 index 0000000..a8ef2fc Binary files /dev/null and b/resources/obj5-7.png differ diff --git a/resources/obj5-8.png b/resources/obj5-8.png new file mode 100755 index 0000000..4436a5d Binary files /dev/null and b/resources/obj5-8.png differ diff --git a/resources/obj5-9.png b/resources/obj5-9.png new file mode 100755 index 0000000..432b690 Binary files /dev/null and b/resources/obj5-9.png differ diff --git a/resources/obj6-0.png b/resources/obj6-0.png new file mode 100755 index 0000000..10a3019 Binary files /dev/null and b/resources/obj6-0.png differ diff --git a/resources/obj6-1.png b/resources/obj6-1.png new file mode 100755 index 0000000..090a507 Binary files /dev/null and b/resources/obj6-1.png differ diff --git a/resources/obj6-2.png b/resources/obj6-2.png new file mode 100755 index 0000000..81f672b Binary files /dev/null and b/resources/obj6-2.png differ diff --git a/resources/obj7-0.png b/resources/obj7-0.png new file mode 100755 index 0000000..6aad86a Binary files /dev/null and b/resources/obj7-0.png differ diff --git a/resources/obj7-1.png b/resources/obj7-1.png new file mode 100755 index 0000000..114188e Binary files /dev/null and b/resources/obj7-1.png differ diff --git a/resources/obj7-2.png b/resources/obj7-2.png new file mode 100755 index 0000000..933a464 Binary files /dev/null and b/resources/obj7-2.png differ diff --git a/resources/obj7-3.png b/resources/obj7-3.png new file mode 100755 index 0000000..c75f572 Binary files /dev/null and b/resources/obj7-3.png differ diff --git a/resources/obj7-4.png b/resources/obj7-4.png new file mode 100755 index 0000000..f480fc0 Binary files /dev/null and b/resources/obj7-4.png differ diff --git a/resources/obj7-5.png b/resources/obj7-5.png new file mode 100755 index 0000000..cafd000 Binary files /dev/null and b/resources/obj7-5.png differ diff --git a/resources/obj8-0.png b/resources/obj8-0.png new file mode 100755 index 0000000..191dab4 Binary files /dev/null and b/resources/obj8-0.png differ diff --git a/resources/obj8-1.png b/resources/obj8-1.png new file mode 100755 index 0000000..7c9ce69 Binary files /dev/null and b/resources/obj8-1.png differ diff --git a/resources/obj8-2.png b/resources/obj8-2.png new file mode 100755 index 0000000..0a3c702 Binary files /dev/null and b/resources/obj8-2.png differ diff --git a/resources/obj8-3.png b/resources/obj8-3.png new file mode 100755 index 0000000..a25ead2 Binary files /dev/null and b/resources/obj8-3.png differ diff --git a/resources/police.png b/resources/police.png new file mode 100755 index 0000000..f3d806c Binary files /dev/null and b/resources/police.png differ diff --git a/resources/res.png b/resources/res.png new file mode 100755 index 0000000..c9e92e3 Binary files /dev/null and b/resources/res.png differ diff --git a/resources/seaport.png b/resources/seaport.png new file mode 100755 index 0000000..4c87bf4 Binary files /dev/null and b/resources/seaport.png differ diff --git a/resources/sounds/bop.wav b/resources/sounds/bop.wav new file mode 100755 index 0000000..a2cd48c Binary files /dev/null and b/resources/sounds/bop.wav differ diff --git a/resources/sounds/explosion-high.wav b/resources/sounds/explosion-high.wav new file mode 100755 index 0000000..04fdc8f Binary files /dev/null and b/resources/sounds/explosion-high.wav differ diff --git a/resources/sounds/explosion-low.wav b/resources/sounds/explosion-low.wav new file mode 100755 index 0000000..d9605d7 Binary files /dev/null and b/resources/sounds/explosion-low.wav differ diff --git a/resources/sounds/heavytraffic.wav b/resources/sounds/heavytraffic.wav new file mode 100755 index 0000000..1c53cf7 Binary files /dev/null and b/resources/sounds/heavytraffic.wav differ diff --git a/resources/sounds/honkhonk-hi.wav b/resources/sounds/honkhonk-hi.wav new file mode 100755 index 0000000..9eda496 Binary files /dev/null and b/resources/sounds/honkhonk-hi.wav differ diff --git a/resources/sounds/honkhonk-high.wav b/resources/sounds/honkhonk-high.wav new file mode 100755 index 0000000..358eb55 Binary files /dev/null and b/resources/sounds/honkhonk-high.wav differ diff --git a/resources/sounds/honkhonk-low.wav b/resources/sounds/honkhonk-low.wav new file mode 100755 index 0000000..d406be7 Binary files /dev/null and b/resources/sounds/honkhonk-low.wav differ diff --git a/resources/sounds/honkhonk-med.wav b/resources/sounds/honkhonk-med.wav new file mode 100755 index 0000000..028d8e6 Binary files /dev/null and b/resources/sounds/honkhonk-med.wav differ diff --git a/resources/sounds/layzone.wav b/resources/sounds/layzone.wav new file mode 100755 index 0000000..034e236 Binary files /dev/null and b/resources/sounds/layzone.wav differ diff --git a/resources/sounds/monster.wav b/resources/sounds/monster.wav new file mode 100755 index 0000000..d9b02b4 Binary files /dev/null and b/resources/sounds/monster.wav differ diff --git a/resources/sounds/siren.wav b/resources/sounds/siren.wav new file mode 100755 index 0000000..349ddcc Binary files /dev/null and b/resources/sounds/siren.wav differ diff --git a/resources/sounds/sorry.wav b/resources/sounds/sorry.wav new file mode 100755 index 0000000..ce01b84 Binary files /dev/null and b/resources/sounds/sorry.wav differ diff --git a/resources/sounds/uhuh.wav b/resources/sounds/uhuh.wav new file mode 100755 index 0000000..b05c02d Binary files /dev/null and b/resources/sounds/uhuh.wav differ diff --git a/resources/sounds/zombie-roar-5.wav b/resources/sounds/zombie-roar-5.wav new file mode 100755 index 0000000..8182925 Binary files /dev/null and b/resources/sounds/zombie-roar-5.wav differ diff --git a/resources/stadium.png b/resources/stadium.png new file mode 100755 index 0000000..e4dd7ee Binary files /dev/null and b/resources/stadium.png differ diff --git a/resources/tiles.png b/resources/tiles.png new file mode 100755 index 0000000..678ea6a Binary files /dev/null and b/resources/tiles.png differ diff --git a/resources/tilessm.png b/resources/tilessm.png new file mode 100755 index 0000000..bc793ae Binary files /dev/null and b/resources/tilessm.png differ diff --git a/src/micropolisj/Main.java b/src/micropolisj/Main.java new file mode 100644 index 0000000..86f8652 --- /dev/null +++ b/src/micropolisj/Main.java @@ -0,0 +1,21 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj; + +import micropolisj.gui.MainWindow; + +public class Main +{ + public static void main(String [] args) + { + MainWindow win = new MainWindow(); + win.setVisible(true); + win.doNewCity(true); + } +} diff --git a/src/micropolisj/engine/AirplaneSprite.java b/src/micropolisj/engine/AirplaneSprite.java new file mode 100644 index 0000000..746bca8 --- /dev/null +++ b/src/micropolisj/engine/AirplaneSprite.java @@ -0,0 +1,87 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +public class AirplaneSprite extends Sprite +{ + int destX; + int destY; + + // Note: frames 1-8 used for regular movement + // 9-11 used for Taking off + static int [] CDx = { 0, 0, 6, 8, 6, 0, -6, -8, -6, 8, 8, 8 }; + static int [] CDy = { 0, -8, -6, 0, 6, 8, 6, 0, -6, 0, 0, 0 }; + + public AirplaneSprite(Micropolis engine, int xpos, int ypos) + { + super(engine, SpriteKind.AIR); + this.x = xpos * 16 + 8; + this.y = ypos * 16 + 8; + this.width = 48; + this.height = 48; + this.offx = -24; + this.offy = -24; + + this.destY = this.y; + if (xpos > engine.getWidth()-20) { + // not enough room to east of airport for taking off + this.destX = x - 200; + this.frame = 7; + } + else { + this.destX = x + 200; + this.frame = 11; + } + } + + @Override + public void moveImpl() + { + int z = this.frame; + + if (city.acycle % 5 == 0) { + if (z > 8) { //plane is still taking off + z--; + if (z < 9) { z = 3; } + this.frame = z; + } + else { // go to destination + int d = getDir(x, y, destX, destY); + z = turnTo(z, d); + this.frame = z; + } + } + + if (getDis(x, y, destX, destY) < 50) { // at destination + //FIXME- original code allows destination to be off-the-map + destX = city.PRNG.nextInt(city.getWidth()) * 16 + 8; + destY = city.PRNG.nextInt(city.getHeight()) * 16 + 8; + } + + if (!city.noDisasters) { + boolean explode = false; + + for (Sprite s : city.allSprites()) { + if (s != this && + (s.kind == SpriteKind.AIR || s.kind == SpriteKind.COP) && + checkSpriteCollision(s)) + { + s.explodeSprite(); + explode = true; + } + } + if (explode) { + explodeSprite(); + } + } + + this.x += CDx[z]; + this.y += CDy[z]; + } +} diff --git a/src/micropolisj/engine/Animate.java b/src/micropolisj/engine/Animate.java new file mode 100644 index 0000000..34e5ed3 --- /dev/null +++ b/src/micropolisj/engine/Animate.java @@ -0,0 +1,145 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import static micropolisj.engine.TileConstants.*; + +public class Animate +{ + static final char [] aniTile = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, + /* Fire */ + 57, 58, 59, 60, 61, 62, 63, 56, + /* No Traffic */ + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + /* Light Traffic */ + 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + /* Heavy Traffic */ + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, + 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + /* Wires & Rails */ + 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + /* Residential */ + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, + 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, + 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, + 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, + 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, + 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, + 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, + 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, + 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, + 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, + 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, + 416, 417, 418, 419, 420, 421, 422, + /* Commercial */ + 423, 424, 425, 426, 427, 428, 429, 430, 431, + 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, + 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, + 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, + 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, + 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, + 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, + 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, + 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, + 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, + 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, + 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, + 608, 609, 610, 611, + /* Industrial */ + 612, 613, 614, 615, 616, 617, 618, 619, 852, 621, 622, 623, + 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, + 640, 884, 642, 643, 888, 645, 646, 647, 648, 892, 896, 651, 652, 653, 654, 655, + 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, + 672, 673, 674, 675, 900, 904, 678, 679, 680, 681, 682, 683, 684, 685, 908, 687, + 688, 912, 690, 691, 692, + /* SeaPort */ + 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, + 704, 705, 706, 707, 708, + /* AirPort */ + 709, 710, 832, 712, 713, 714, 715, 716, 717, 718, 719, + 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, + 736, 737, 738, 739, 740, 741, 742, 743, 744, + /* Coal power */ + 745, 746, 916, 920, 749, 750, 924, + 928, 753, 754, 755, 756, 757, 758, 759, 760, + /* Fire Dept */ + 761, 762, 763, 764, 765, 766, 767, + 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, + /* Stadium */ + 779, 780, 781, 782, 783, + 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, + /* Stadium Anims */ + 795, 796, 797, 798, 799, + 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, + /* Nuclear Power */ + 811, 812, 813, 814, 815, + 816, 817, 818, 819, 952, 821, 822, 823, 824, 825, 826, + /* Power out + Bridges */ + 827, 828, 829, 830, 831, + /* Radar dish */ + 833, 834, 835, 836, 837, 838, 839, 832, + /* Fountain / Flag */ + 841, 842, 843, 840, 845, 846, 847, 848, + 849, 850, 851, 844, 853, 854, 855, 856, 857, 858, 859, 852, + /* zone destruct & rubblize */ + 861, 862, 863, 864, + 865, 866, 867, 867, + /* totally unsure */ + 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, + 880, 881, 882, 883, + /* Smoke stacks */ + 885, 886, 887, 884, 889, 890, 891, 888, 893, 894, 895, 892, + 897, 898, 899, 896, 901, 902, 903, 900, 905, 906, 907, 904, 909, 910, 911, 908, + 913, 914, 915, 912, 917, 918, 919, 916, 921, 922, 923, 920, 925, 926, 927, 924, + 929, 930, 931, 928, + /* Stadium Playfield */ + 933, 934, 935, 936, 937, 938, 939, 932, 941, 942, 943, 944, + 945, 946, 947, 940, + /* Bridge up chars */ + 948, 949, 950, 951, + /* Nuclear swirl */ + 953, 954, 955, 952, + /* */ + }; + + static class Smoke + { + // There are eight full Industry-zone images in the tiles bank. + // This array indicates which of those eight zones have an animation. + static int [] AniThis = { 1, 0, 1, 1, 0, 0, 1, 1 }; + + // Up to two tiles can be animated. Arrays DX1,DXY indicate the relative + // position of the first animated tile. + static int [] DX1 = { -1, 0, 1, 0, 0, 0, 0, 1 }; + static int [] DY1 = { -1, 0, -1, -1, 0, 0, -1, -1 }; + + // Arrays DX2,DY2 indicate the second animated tile. + static int [] DX2 = { -1, 0, 1, 1, 0, 0, 1, 1 }; + static int [] DY2 = { -1, 0, 0, -1, 0, 0, -1, 0 }; + + static int [] AniTabA = { 0, 0, 32, 40, 0, 0, 48, 56 }; + static int [] AniTabB = { 0, 0, 36, 44, 0, 0, 52, 60 }; + static int [] AniTabC = { IND1, 0, IND2, IND4, 0, 0, IND6, IND8 }; + static int [] AniTabD = { IND1, 0, IND3, IND5, 0, 0, IND7, IND9 }; + + static final int ASCBIT = (ANIMBIT | CONDBIT | BURNBIT); + static final int REGBIT = (CONDBIT | BURNBIT); + } + +} diff --git a/src/micropolisj/engine/BudgetNumbers.java b/src/micropolisj/engine/BudgetNumbers.java new file mode 100644 index 0000000..4121c2d --- /dev/null +++ b/src/micropolisj/engine/BudgetNumbers.java @@ -0,0 +1,30 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +public class BudgetNumbers +{ + public int taxRate; + public int taxIncome; + public int operatingExpenses; + public int previousBalance; + public int newBalance; + + public int roadRequest; + public int roadFunded; + public double roadPercent; + + public int fireRequest; + public int fireFunded; + public double firePercent; + + public int policeRequest; + public int policeFunded; + public double policePercent; +} diff --git a/src/micropolisj/engine/CityEval.java b/src/micropolisj/engine/CityEval.java new file mode 100644 index 0000000..6c8226d --- /dev/null +++ b/src/micropolisj/engine/CityEval.java @@ -0,0 +1,276 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import java.util.*; + +public class CityEval +{ + private final Micropolis engine; + private final Random PRNG; + + public CityEval(Micropolis engine) + { + this.engine = engine; + this.PRNG = engine.PRNG; + + assert PRNG != null; + } + + /** Percentage of population "approving" the mayor. Derived from cityScore. */ + public int cityYes; + + /** Percentage of population "disapproving" the mayor. Derived from cityScore. */ + public int cityNo; + + /** City assessment value. */ + public int cityAssValue; + + /** Player's score, 0-1000. */ + public int cityScore; + + /** Change in cityScore since last evaluation. */ + public int deltaCityScore; + + /** City population as of current evaluation. */ + public int cityPop; + + /** Change in cityPopulation since last evaluation. */ + public int deltaCityPop; + + /** Classification of city size. 0==village, 1==town, etc. */ + public int cityClass; // 0..5 + + /** City's top 4 (or fewer) problems as reported by citizens. */ + public CityProblem [] problemOrder = new CityProblem[0]; + + /** Number of votes given for the various problems identified by problemOrder[]. */ + public EnumMap problemVotes = new EnumMap<>(CityProblem.class); + + /** Score for various problems. */ + public EnumMap problemTable = new EnumMap<>(CityProblem.class); + + void cityEvaluation() + { + if (engine.totalPop != 0) { + calculateAssValue(); + doPopNum(); + doProblems(); + calculateScore(); + doVotes(); + } else { + evalInit(); + } + engine.fireEvaluationChanged(); + } + + /** Evaluate an empty city. */ + void evalInit() + { + cityYes = 0; + cityNo = 0; + cityAssValue = 0; + cityClass = 0; + cityScore = 500; + deltaCityScore = 0; + problemVotes.clear(); + problemOrder = new CityProblem[0]; + } + + void calculateAssValue() + { + int z = 0; + z += engine.roadTotal * 5; + z += engine.railTotal * 10; + z += engine.policeCount * 1000; + z += engine.fireStationCount * 1000; + z += engine.hospitalCount * 400; + z += engine.stadiumCount * 3000; + z += engine.seaportCount * 5000; + z += engine.airportCount * 10000; + z += engine.coalCount * 3000; + z += engine.nuclearCount * 6000; + cityAssValue = z * 1000; + } + + void doPopNum() + { + int oldCityPop = cityPop; + cityPop = engine.getCityPopulation(); + deltaCityPop = cityPop - oldCityPop; + + cityClass = + cityPop > 500000 ? 5 : //megalopolis + cityPop > 100000 ? 4 : //metropolis + cityPop > 50000 ? 3 : //capital + cityPop > 10000 ? 2 : //city + cityPop > 2000 ? 1 : //town + 0; //village + } + + void doProblems() + { + problemTable.clear(); + problemTable.put(CityProblem.CRIME, engine.crimeAverage); + problemTable.put(CityProblem.POLLUTION, engine.pollutionAverage); + problemTable.put(CityProblem.HOUSING, (int)Math.round(engine.landValueAverage * 0.7)); + problemTable.put(CityProblem.TAXES, engine.cityTax * 10); + problemTable.put(CityProblem.TRAFFIC, averageTrf()); + problemTable.put(CityProblem.UNEMPLOYMENT, getUnemployment()); + problemTable.put(CityProblem.FIRE, getFire()); + + problemVotes = voteProblems(problemTable); + + CityProblem [] probOrder = CityProblem.values(); + Arrays.sort(probOrder, new Comparator() { + public int compare(CityProblem a, CityProblem b) { + return -(problemVotes.get(a).compareTo(problemVotes.get(b))); + }}); + + int c = 0; + while (c < probOrder.length && + problemVotes.get(probOrder[c]).intValue() != 0 && + c < 4) + c++; + + problemOrder = new CityProblem[c]; + for (int i = 0; i < c; i++) { + problemOrder[i] = probOrder[i]; + } + } + + EnumMap voteProblems(Map probTab) + { + CityProblem [] pp = CityProblem.values(); + int [] votes = new int[pp.length]; + + int countVotes = 0; + for (int i = 0; i < 600; i++) { + if (PRNG.nextInt(301) < probTab.get(pp[i%pp.length])) { + votes[i%pp.length]++; + countVotes++; + if (countVotes >= 100) + break; + } + } + + EnumMap rv = new EnumMap<>(CityProblem.class); + for (int i = 0; i < pp.length; i++) { + rv.put(pp[i], votes[i]); + } + return rv; + } + + int averageTrf() + { + int count = 1; + int total = 0; + + for (int hy = 0; hy < engine.trfDensity.length; hy++) { + for (int hx = 0; hx < engine.trfDensity[hy].length; hx++) { + if (engine.landValueMem[hy][hx] != 0) { + total += engine.trfDensity[hy][hx]; + count++; + } + } + } + + engine.trafficAverage = (int)Math.round(((double)total / (double)count) * 2.4); + return engine.trafficAverage; + } + + int getUnemployment() + { + int b = (engine.comPop + engine.indPop) * 8; + if (b == 0) + return 0; + + double r = (double)engine.resPop / (double)b; + b = (int)Math.floor((r-1.0)*255); + if (b > 255) { + b = 255; + } + return b; + } + + int getFire() + { + int z = engine.firePop * 5; + return Math.min(255, z); + } + + static double clamp(double x, double min, double max) + { + return Math.max(min, Math.min(max, x)); + } + + void calculateScore() + { + int oldCityScore = cityScore; + + int x = 0; + for (Integer z : problemTable.values()) { + x += z.intValue(); + } + + x /= 3; + x = Math.min(256, x); + + double z = clamp((256 - x) * 4, 0, 1000); + + if (engine.resCap) { z = 0.85 * z; } + if (engine.comCap) { z = 0.85 * z; } + if (engine.indCap) { z = 0.85 * z; } + if (engine.roadEffect < 32) { z -= (32 - engine.roadEffect); } + if (engine.policeEffect < 1000) { z *= (0.9 + (engine.policeEffect / 10000.1)); } + if (engine.fireEffect < 1000) { z *= (0.9 + (engine.fireEffect / 10000.1)); } + if (engine.resValve < -1000) { z *= 0.85; } + if (engine.comValve < -1000) { z *= 0.85; } + if (engine.indValve < -1000) { z *= 0.85; } + + double SM = 1.0; + if (cityPop == 0 && deltaCityPop == 0) { + SM = 1.0; + } + else if (deltaCityPop == cityPop) { + SM = 1.0; + } + else if (deltaCityPop > 0) { + SM = (double)deltaCityPop / (double)cityPop + 1.0; + } + else if (deltaCityPop < 0) { + SM = 0.95 + ((double)deltaCityPop / (double)(cityPop-deltaCityPop)); + } + z *= SM; + z -= getFire(); + z -= engine.cityTax; + + int TM = engine.unpoweredZoneCount + engine.poweredZoneCount; + SM = TM != 0 ? ((double)engine.poweredZoneCount / (double)TM) : 1.0; + z *= SM; + + z = clamp(z, 0, 1000); + + cityScore = (int)Math.round((cityScore + z) / 2.0); + deltaCityScore = cityScore - oldCityScore; + } + + void doVotes() + { + cityYes = cityNo = 0; + for (int i = 0; i < 100; i++) { + if (PRNG.nextInt(1001) < cityScore) { + cityYes++; + } else { + cityNo++; + } + } + } + +} diff --git a/src/micropolisj/engine/CityLocation.java b/src/micropolisj/engine/CityLocation.java new file mode 100644 index 0000000..91faaa6 --- /dev/null +++ b/src/micropolisj/engine/CityLocation.java @@ -0,0 +1,45 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +public class CityLocation +{ + public int x; + public int y; + + public CityLocation(int x, int y) + { + this.x = x; + this.y = y; + } + + @Override + public int hashCode() + { + return x*33+y; + } + + @Override + public boolean equals(Object obj) + { + if (obj instanceof CityLocation) { + CityLocation rhs = (CityLocation)obj; + return this.x == rhs.x && this.y == rhs.y; + } + else { + return false; + } + } + + @Override + public String toString() + { + return "("+x+","+y+")"; + } +} diff --git a/src/micropolisj/engine/CityProblem.java b/src/micropolisj/engine/CityProblem.java new file mode 100644 index 0000000..5ea91e7 --- /dev/null +++ b/src/micropolisj/engine/CityProblem.java @@ -0,0 +1,23 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +/** + * Enumeration of various city problems that the citizens complain about. + */ +public enum CityProblem +{ + CRIME, + POLLUTION, + HOUSING, + TAXES, + TRAFFIC, + UNEMPLOYMENT, + FIRE; +} diff --git a/src/micropolisj/engine/Disaster.java b/src/micropolisj/engine/Disaster.java new file mode 100644 index 0000000..4b8baa6 --- /dev/null +++ b/src/micropolisj/engine/Disaster.java @@ -0,0 +1,22 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +/** + * Lists the disasters that the user can invoke. + */ +public enum Disaster +{ + MONSTER, + FIRE, + FLOOD, + MELTDOWN, + TORNADO, + EARTHQUAKE; +} diff --git a/src/micropolisj/engine/EarthquakeListener.java b/src/micropolisj/engine/EarthquakeListener.java new file mode 100644 index 0000000..a2dfc90 --- /dev/null +++ b/src/micropolisj/engine/EarthquakeListener.java @@ -0,0 +1,17 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +/** + * The listener interface for receiving earthquake notifications. + */ +public interface EarthquakeListener +{ + void earthquakeStarted(); +} diff --git a/src/micropolisj/engine/ExplosionSprite.java b/src/micropolisj/engine/ExplosionSprite.java new file mode 100644 index 0000000..d36105a --- /dev/null +++ b/src/micropolisj/engine/ExplosionSprite.java @@ -0,0 +1,63 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import static micropolisj.engine.TileConstants.*; + +public class ExplosionSprite extends Sprite +{ + public ExplosionSprite(Micropolis engine, int x, int y) + { + super(engine, SpriteKind.EXP); + this.x = x; + this.y = y; + this.width = 48; + this.height = 48; + this.offx = -24; + this.offy = -24; + this.frame = 1; + } + + @Override + public void moveImpl() + { + if (city.acycle % 2 == 0) { + if (this.frame == 1) { + city.makeSound(x/16, y/16, Sound.EXPLOSION_HIGH); + city.sendMessageAt(MicropolisMessage.EXPLOSION_REPORT, x/16, y/16); + } + this.frame++; + } + + if (this.frame > 6) { + this.frame = 0; + + startFire(x/16, y/16); + startFire(x/16-1, y/16-1); + startFire(x/16+1, y/16-1); + startFire(x/16-1, y/16+1); + startFire(x/16+1, y/16+1); + return; + } + } + + void startFire(int xpos, int ypos) + { + if (!city.testBounds(xpos, ypos)) + return; + + int z = city.getTile(xpos, ypos); + int t = z & LOMASK; + if ((z & BURNBIT) == 0 && t != DIRT) + return; + if ((z & ZONEBIT) != 0) + return; + city.setTile(xpos, ypos, (char)(FIRE + city.PRNG.nextInt(4) + ANIMBIT)); + } +} diff --git a/src/micropolisj/engine/GameLevel.java b/src/micropolisj/engine/GameLevel.java new file mode 100644 index 0000000..739c520 --- /dev/null +++ b/src/micropolisj/engine/GameLevel.java @@ -0,0 +1,34 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +public class GameLevel +{ + public static final int MIN_LEVEL = 0; + public static final int MAX_LEVEL = 2; + + public static boolean isValid(int lev) + { + return lev >= MIN_LEVEL && lev <= MAX_LEVEL; + } + + public static int getStartingFunds(int lev) + { + switch (lev) { + case 0: return 20000; + case 1: return 10000; + case 2: return 5000; + default: + throw new Error("unexpected game level: "+lev); + } + } + + //prevent this class from being instantiated + private GameLevel() {} +} diff --git a/src/micropolisj/engine/HelicopterSprite.java b/src/micropolisj/engine/HelicopterSprite.java new file mode 100644 index 0000000..2971935 --- /dev/null +++ b/src/micropolisj/engine/HelicopterSprite.java @@ -0,0 +1,101 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +public class HelicopterSprite extends Sprite +{ + int count; + int destX; + int destY; + int origX; + int origY; + + static int [] CDx = { 0, 0, 3, 5, 3, 0, -3, -5, -3 }; + static int [] CDy = { 0, -5, -3, 0, 3, 5, 3, 0, -3 }; + static final int SOUND_FREQ = 200; + + public HelicopterSprite(Micropolis engine, int xpos, int ypos) + { + super(engine, SpriteKind.COP); + this.x = xpos * 16 + 8; + this.y = ypos * 16 + 8; + this.width = 32; + this.height = 32; + this.offx = -16; + this.offy = -16; + + this.destX = city.PRNG.nextInt(city.getWidth()) * 16 + 8; + this.destY = city.PRNG.nextInt(city.getHeight()) * 16 + 8; + + this.origX = x; + this.origY = y; + this.count = 1500; + this.frame = 5; + } + + @Override + public void moveImpl() + { + if (this.count > 0) { + this.count--; + } + + if (this.count == 0) { + + // attract copter to monster and tornado so it blows up more often + if (city.hasSprite(SpriteKind.GOD)) { + + MonsterSprite monster = (MonsterSprite) city.getSprite(SpriteKind.GOD); + this.destX = monster.x; + this.destY = monster.y; + + } + else if (city.hasSprite(SpriteKind.TOR)) { + + TornadoSprite tornado = (TornadoSprite) city.getSprite(SpriteKind.TOR); + this.destX = tornado.x; + this.destY = tornado.y; + + } + else { + this.destX = origX; + this.destY = origY; + } + + if (getDis(x, y, origX, origY) < 30) { + // made it back to airport, go ahead and land. + this.frame = 0; + return; + } + } + + if (city.acycle % SOUND_FREQ == 0) { + // send report, if hovering over high traffic area + int xpos = this.x / 16; + int ypos = this.y / 16; + + if (city.getTrafficDensity(xpos, ypos) > 170 && + city.PRNG.nextInt(8) == 0) + { + city.sendMessageAt(MicropolisMessage.HEAVY_TRAFFIC_REPORT, + xpos, ypos); + city.makeSound(xpos, ypos, Sound.HEAVYTRAFFIC); + } + } + + int z = this.frame; + if (city.acycle % 3 == 0) { + int d = getDir(x, y, destX, destY); + z = turnTo(z, d); + this.frame = z; + } + x += CDx[z]; + y += CDy[z]; + } +} diff --git a/src/micropolisj/engine/MapGenerator.java b/src/micropolisj/engine/MapGenerator.java new file mode 100644 index 0000000..9919380 --- /dev/null +++ b/src/micropolisj/engine/MapGenerator.java @@ -0,0 +1,541 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import java.util.*; + +import static micropolisj.engine.TileConstants.*; + +public class MapGenerator +{ + Micropolis engine; + char [][] map; + Random PRNG; + + /** + * Three settings on whether to generate a new map as an island. + */ + static enum CreateIsland + { + NEVER, + ALWAYS, + SELDOM; // seldom == 10% of the time + } + CreateIsland createIsland = CreateIsland.SELDOM; + + public MapGenerator(Micropolis engine) + { + assert engine != null; + this.engine = engine; + this.map = engine.map; + } + + private int getWidth() + { + return map[0].length; + } + + private int getHeight() + { + return map.length; + } + + public void generateNewCity() + { + long r = Micropolis.DEFAULT_PRNG.nextLong(); + generateSomeCity(r); + } + + public void generateSomeCity(long r) + { + engine.totalFunds = GameLevel.getStartingFunds(engine.gameLevel); + generateMap(r); + engine.fireWholeMapChanged(); + } + + /** + * Level for tree creation. + * If positive, this is (roughly) the number of trees to randomly place. + * If negative, then the number of trees is randomly chosen. + * If zero, then no trees are generated. + */ + int treeLevel = -1; //level for tree creation + + int curveLevel = -1; //level for river curviness; -1==auto, 0==none, >0==level + + int lakeLevel = -1; //level for lake creation; -1==auto, 0==none, >0==level + + void generateMap(long r) + { + PRNG = new Random(r); + + if (createIsland == CreateIsland.SELDOM) + { + if (PRNG.nextInt(100) < 10) //chance that island is generated + { + makeIsland(); + return; + } + } + + if (createIsland == CreateIsland.ALWAYS) + { + makeNakedIsland(); + } + else + { + clearMap(); + } + + getRandStart(); + + if (curveLevel != 0) + { + doRivers(); + } + + if (lakeLevel != 0) + { + makeLakes(); + } + + smoothRiver(); + + if (treeLevel != 0) + { + doTrees(); + } + } + + private void makeIsland() + { + makeNakedIsland(); + smoothRiver(); + doTrees(); + } + + private int erand(int limit) + { + return Math.min( + PRNG.nextInt(limit), + PRNG.nextInt(limit) + ); + } + + private void makeNakedIsland() + { + final int ISLAND_RADIUS = 18; + final int WORLD_X = getWidth(); + final int WORLD_Y = getHeight(); + + for (int y = 0; y < WORLD_Y; y++) + { + for (int x = 0; x < WORLD_X; x++) + { + map[y][x] = RIVER; + } + } + + for (int y = 5; y < WORLD_Y - 5; y++) + { + for (int x = 5; x < WORLD_X - 5; x++) + { + map[y][x] = DIRT; + } + } + + for (int x = 0; x < WORLD_X - 5; x += 2) + { + mapX = x; + mapY = erand(ISLAND_RADIUS+1); + BRivPlop(); + mapY = (WORLD_Y - 10) - erand(ISLAND_RADIUS+1); + BRivPlop(); + mapY = 0; + SRivPlop(); + mapY = WORLD_Y - 6; + SRivPlop(); + } + + for (int y = 0; y < WORLD_Y - 5; y += 2) + { + mapY = y; + mapX = erand(ISLAND_RADIUS+1); + BRivPlop(); + mapX = (WORLD_X - 10) - erand(ISLAND_RADIUS+1); + BRivPlop(); + mapX = 0; + SRivPlop(); + mapX = (WORLD_X - 6); + SRivPlop(); + } + } + + private void clearMap() + { + for (int y = 0; y < map.length; y++) + { + for (int x = 0; x < map[y].length; x++) + { + map[y][x] = DIRT; + } + } + } + + int xStart; + int yStart; + int mapX; + int mapY; + int dir; + int lastDir; + + private void getRandStart() + { + xStart = 40 + PRNG.nextInt(getWidth() - 79); + yStart = 33 + PRNG.nextInt(getHeight() - 66); + + mapX = xStart; + mapY = yStart; + } + + private void makeLakes() + { + int lim1; + if (lakeLevel < 0) + lim1 = PRNG.nextInt(11); + else + lim1 = lakeLevel / 2; + + for (int t = 0; t < lim1; t++) + { + int x = PRNG.nextInt(getWidth() - 20) + 10; + int y = PRNG.nextInt(getHeight() - 19) + 10; + int lim2 = PRNG.nextInt(13) + 2; + + for (int z = 0; z < lim2; z++) + { + mapX = x - 6 + PRNG.nextInt(13); + mapY = y - 6 + PRNG.nextInt(13); + + if (PRNG.nextInt(5) != 0) + SRivPlop(); + else + BRivPlop(); + } + } + } + + private void doRivers() + { + dir = lastDir = PRNG.nextInt(4); + doBRiv(); + + mapX = xStart; + mapY = yStart; + dir = lastDir = lastDir ^ 4; + doBRiv(); + + mapX = xStart; + mapY = yStart; + lastDir = PRNG.nextInt(4); + doSRiv(); + } + + private void doBRiv() + { + int r1, r2; + if (curveLevel < 0) + { + r1 = 100; + r2 = 200; + } + else + { + r1 = curveLevel + 10; + r2 = curveLevel + 100; + } + + while (engine.testBounds(mapX + 4, mapY + 4)) + { + BRivPlop(); + if (PRNG.nextInt(r1+1) < 10) + { + dir = lastDir; + } + else + { + if (PRNG.nextInt(r2+1) > 90) + { + dir++; + } + if (PRNG.nextInt(r2+1) > 90) + { + dir--; + } + } + moveMap(dir); + } + } + + private void doSRiv() + { + int r1, r2; + if (curveLevel < 0) + { + r1 = 100; + r2 = 200; + } + else + { + r1 = curveLevel + 10; + r2 = curveLevel + 100; + } + + while (engine.testBounds(mapX + 3, mapY + 3)) + { + SRivPlop(); + if (PRNG.nextInt(r1+1) < 10) + { + dir = lastDir; + } + else + { + if (PRNG.nextInt(r2+1) > 90) + { + dir++; + } + if (PRNG.nextInt(r2+1) > 90) + { + dir--; + } + } + moveMap(dir); + } + } + + static final char [][] BRMatrix = new char[][] { + { 0, 0, 0, 3, 3, 3, 0, 0, 0 }, + { 0, 0, 3, 2, 2, 2, 3, 0, 0 }, + { 0, 3, 2, 2, 2, 2, 2, 3, 0 }, + { 3, 2, 2, 2, 2, 2, 2, 2, 3 }, + { 3, 2, 2, 2, 4, 2, 2, 2, 3 }, + { 3, 2, 2, 2, 2, 2, 2, 2, 3 }, + { 0, 3, 2, 2, 2, 2, 2, 3, 0 }, + { 0, 0, 3, 2, 2, 2, 3, 0, 0 }, + { 0, 0, 0, 3, 3, 3, 0, 0, 0 } + }; + + private void BRivPlop() + { + for (int x = 0; x < 9; x++) + { + for (int y = 0; y < 9; y++) + { + putOnMap(BRMatrix[y][x], x, y); + } + } + } + + static final char [][] SRMatrix = new char[][] { + { 0, 0, 3, 3, 0, 0 }, + { 0, 3, 2, 2, 3, 0 }, + { 3, 2, 2, 2, 2, 3 }, + { 3, 2, 2, 2, 2, 3 }, + { 0, 3, 2, 2, 3, 0 }, + { 0, 0, 3, 3, 0, 0 } + }; + + private void SRivPlop() + { + for (int x = 0; x < 6; x++) + { + for (int y = 0; y < 6; y++) + { + putOnMap(SRMatrix[y][x], x, y); + } + } + } + + private void putOnMap(char mapChar, int xoff, int yoff) + { + if (mapChar == 0) + return; + + int xloc = mapX + xoff; + int yloc = mapY + yoff; + + if (!engine.testBounds(xloc, yloc)) + return; + + char tmp = map[yloc][xloc]; + if (tmp != DIRT) + { + tmp &= LOMASK; + if (tmp == RIVER && mapChar != CHANNEL) + return; + if (tmp == CHANNEL) + return; + } + map[yloc][xloc] = mapChar; + } + + static final char [] REdTab = new char[] { + 13 + BULLBIT, 13 + BULLBIT, 17 + BULLBIT, 15 + BULLBIT, + 5 + BULLBIT, 2, 19 + BULLBIT, 17 + BULLBIT, + 9 + BULLBIT, 11 + BULLBIT, 2, 13 + BULLBIT, + 7 + BULLBIT, 9 + BULLBIT, 5 + BULLBIT, 2 + }; + + private void smoothRiver() + { + for (int mapY = 0; mapY < map.length; mapY++) + { + for (int mapX = 0; mapX < map[mapY].length; mapX++) + { + if (map[mapY][mapX] == REDGE) + { + int bitindex = 0; + + for (int z = 0; z < 4; z++) + { + bitindex <<= 1; + int xtem = mapX + DX[z]; + int ytem = mapY + DY[z]; + if (engine.testBounds(xtem, ytem) && + ((map[ytem][xtem] & LOMASK) != DIRT) && + (((map[ytem][xtem] & LOMASK) < WOODS_LOW) || + ((map[ytem][xtem] & LOMASK) > WOODS_HIGH))) + { + bitindex |= 1; + } + } + + char temp = REdTab[bitindex & 15]; + if ((temp != RIVER) && PRNG.nextInt(2) != 0) + temp++; + map[mapY][mapX] = temp; + } + } + } + } + + private void doTrees() + { + int amount; + + if (treeLevel < 0) + { + amount = PRNG.nextInt(101) + 50; + } + else + { + amount = treeLevel + 3; + } + + for (int x = 0; x < amount; x++) + { + int xloc = PRNG.nextInt(getWidth()); + int yloc = PRNG.nextInt(getHeight()); + treeSplash(xloc, yloc); + } + + smoothTrees(); + smoothTrees(); + } + + private void treeSplash(int xloc, int yloc) + { + int dis; + if (treeLevel < 0) + { + dis = PRNG.nextInt(151) + 50; + } + else + { + dis = PRNG.nextInt(101 + (treeLevel*2)) + 50; + } + + mapX = xloc; + mapY = yloc; + + for (int z = 0; z < dis; z++) + { + int dir = PRNG.nextInt(8); + moveMap(dir); + + if (!engine.testBounds(mapX, mapY)) + return; + + if ((map[mapY][mapX] & LOMASK) == DIRT) + { + map[mapY][mapX] = WOODS + BLBNBIT; + } + } + } + + static final int [] DIRECTION_TABX = new int[] { 0, 1, 1, 1, 0, -1, -1, -1 }; + static final int [] DIRECTION_TABY = new int[] { -1, -1, 0, 1, 1, 1, 0, -1 }; + private void moveMap(int dir) + { + dir = dir & 7; + mapX += DIRECTION_TABX[dir]; + mapY += DIRECTION_TABY[dir]; + } + + static final int [] DX = new int[] { -1, 0, 1, 0 }; + static final int [] DY = new int[] { 0, 1, 0, -1 }; + static final char [] TEdTab = new char[] { + 0, 0, 0, 34, + 0, 0, 36, 35, + 0, 32, 0, 33, + 30, 31, 29, 37 + }; + + private void smoothTrees() + { + for (int mapY = 0; mapY < map.length; mapY++) + { + for (int mapX = 0; mapX < map[mapY].length; mapX++) + { + if (isTree(map[mapY][mapX])) + { + int bitindex = 0; + for (int z = 0; z < 4; z++) + { + bitindex <<= 1; + int xtem = mapX + DX[z]; + int ytem = mapY + DY[z]; + if (engine.testBounds(xtem, ytem) && + isTree(map[ytem][xtem])) + { + bitindex |= 1; + } + } + char temp = TEdTab[bitindex & 15]; + if (temp != 0) + { + if (temp != WOODS) + { + if (((mapX + mapY) & 1) != 0) + { + temp -= 8; + } + } + map[mapY][mapX] = (char)(temp + BLBNBIT); + } + else + { + map[mapY][mapX] = temp; + } + } + } + } + } + +} diff --git a/src/micropolisj/engine/MapListener.java b/src/micropolisj/engine/MapListener.java new file mode 100644 index 0000000..73fcade --- /dev/null +++ b/src/micropolisj/engine/MapListener.java @@ -0,0 +1,28 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +/** + * The listener interface for receiving notifications whenever a tile on + * the city map changes, or when a sprite moves or changes. + */ +public interface MapListener +{ + /** Called whenever data for a specific overlay has changed. */ + void mapOverlayDataChanged(MapState overlayDataType); + + /** Called when a sprite moves. */ + void spriteMoved(Sprite sprite); + + /** Called when a map tile changes, including for animations. */ + void tileChanged(int xpos, int ypos); + + /** Called when the entire map should be reread and rendered. */ + void wholeMapChanged(); +} diff --git a/src/micropolisj/engine/MapScanner.java b/src/micropolisj/engine/MapScanner.java new file mode 100644 index 0000000..8a5397c --- /dev/null +++ b/src/micropolisj/engine/MapScanner.java @@ -0,0 +1,1234 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import java.util.*; + +import static micropolisj.engine.TileConstants.*; +import static micropolisj.engine.Micropolis.ZoneType; +import static micropolisj.engine.Animate.Smoke; + +class MapScanner +{ + final Micropolis city; + final Random PRNG; + int xpos; + int ypos; + char cchr; + char cchr9; + + MapScanner(Micropolis city) + { + this.city = city; + this.PRNG = city.PRNG; + } + + public void scanTile() + { + cchr9 = (char) (cchr & LOMASK); + + if (cchr9 >= FLOOD) + { + if (cchr9 < ROADBASE) + { + if (cchr9 >= FIREBASE) + { + city.firePop++; + if (PRNG.nextInt(4) == 0) + { + // one in four times + doFire(); + } + return; + } + + if (cchr9 < RADTILE) + { + doFlood(); + } + else + { + doRadioactiveTile(); + } + return; + } + + if (city.newPower && ((cchr & CONDBIT) != 0)) + { + setZonePower(); + } + + if (cchr9 >= ROADBASE && cchr9 < POWERBASE) + { + doRoad(); + return; + } + + if ((cchr & ZONEBIT) != 0) + { + doZone(); + return; + } + + if (cchr9 >= RAILBASE && cchr9 < RESBASE) + { + doRail(); + return; + } + + if (cchr9 >= SOMETINYEXP && cchr9 <= LASTTINYEXP) + { + // clear AniRubble + city.setTile(xpos, ypos, (char)(RUBBLE + PRNG.nextInt(4) + BULLBIT)); + } + } + } + + void doRadioactiveTile() + { + if (PRNG.nextInt(4096) == 0) + { + // radioactive decay + city.setTile(xpos, ypos, DIRT); + } + } + + static int [] TRAFFIC_DENSITY_TAB = { ROADBASE, LTRFBASE, HTRFBASE }; + void doRoad() + { + city.roadTotal++; + + if (city.roadEffect < 30) + { + // deteriorating roads + if (PRNG.nextInt(512) == 0) + { + if ((cchr & CONDBIT) == 0) + { + if (city.roadEffect < PRNG.nextInt(32)) + { + if ((cchr & 15) < 2 || (cchr & 15) == 15) + city.setTile(xpos, ypos, RIVER); + else + city.setTile(xpos, ypos, (char)(RUBBLE + PRNG.nextInt(4) + BULLBIT)); + return; + } + } + } + } + + if ((cchr & BURNBIT) == 0) //bridge + { + city.roadTotal += 4; + if (doBridge()) + return; + } + + int tden; + if ((cchr & LOMASK) < LTRFBASE) + tden = 0; + else if ((cchr & LOMASK) < HTRFBASE) + tden = 1; + else { + city.roadTotal++; + tden = 2; + } + + int trafficDensity = city.trfDensity[ypos/2][xpos/2]; + int newLevel = trafficDensity < 64 ? 0 : + trafficDensity < 192 ? 1 : 2; + + assert newLevel >= 0 && newLevel < TRAFFIC_DENSITY_TAB.length; + + if (tden != newLevel) + { + int z = (((cchr & LOMASK) - ROADBASE) & 15) + TRAFFIC_DENSITY_TAB[newLevel]; + z += cchr & (ALLBITS - ANIMBIT); + + if (newLevel != 0) + z |= ANIMBIT; + + city.setTile(xpos, ypos, (char) z); + } + } + + void doFire() + { + final int [] DX = { 0, 1, 0, -1 }; + final int [] DY = { -1, 0, 1, 0 }; + + for (int dir = 0; dir < 4; dir++) + { + if (PRNG.nextInt(8) == 0) + { + int xtem = xpos + DX[dir]; + int ytem = ypos + DY[dir]; + if (!city.testBounds(xtem, ytem)) + continue; + + int c = city.map[ytem][xtem]; + if ((c & BURNBIT) != 0) { + if ((c & ZONEBIT) != 0) { + city.fireZone(xtem, ytem, c); + if ((c & LOMASK) > IZB) { //explode + city.makeExplosion(xtem, ytem); + } + } + city.setTile(xtem, ytem, (char)(FIRE + PRNG.nextInt(4) + ANIMBIT)); + } + } + } + + int cov = city.fireRate[ypos/8][xpos/8]; //fire station coverage + int rate = cov > 100 ? 1 : + cov > 20 ? 2 : + cov != 0 ? 3 : 10; + + if (PRNG.nextInt(rate+1) == 0) { + city.setTile(xpos, ypos, (char)(RUBBLE + PRNG.nextInt(4) + BULLBIT)); + } + } + + void doFlood() + { + final int [] DX = { 0, 1, 0, -1 }; + final int [] DY = { -1, 0, 1, 0 }; + + if (city.floodCnt != 0) + { + for (int z = 0; z < 4; z++) + { + if (PRNG.nextInt(8) == 0) { + int xx = xpos + DX[z]; + int yy = ypos + DY[z]; + if (city.testBounds(xx, yy)) { + int c = city.getTile(xx, yy); + int t = c & LOMASK; + if (((c & BURNBIT) != 0) || c == DIRT || + (t >= WOODS5 && t < FLOOD)) + { + if ((c & ZONEBIT) != 0) { + city.fireZone(xx, yy, c); + } + city.setTile(xx, yy, (char)(FLOOD + PRNG.nextInt(3))); + } + } + } + } + } + else { + if (PRNG.nextInt(16) == 0) { + city.setTile(xpos, ypos, DIRT); + } + } + } + + void doRail() + { + city.railTotal++; + city.generateTrain(xpos, ypos); + + if (city.roadEffect < 30) { // deteriorating rail + if (PRNG.nextInt(512) == 0) { + if ((cchr & CONDBIT) == 0) { + if (city.roadEffect < PRNG.nextInt(32)) { + if (cchr9 < RAILBASE+2) { + city.setTile(xpos,ypos,RIVER); + } else { + city.setTile(xpos,ypos,(char)(RUBBLE + PRNG.nextInt(4)+BULLBIT)); + } + } + } + } + } + } + + boolean doBridge() + { + final int HDx[] = { -2, 2, -2, -1, 0, 1, 2 }; + final int HDy[] = { -1, -1, 0, 0, 0, 0, 0 }; + final char HBRTAB[] = { + HBRDG1 | BULLBIT, HBRDG3 | BULLBIT, + HBRDG0 | BULLBIT, RIVER, + BRWH | BULLBIT, RIVER, + HBRDG2 | BULLBIT }; + final char HBRTAB2[] = { + RIVER, RIVER, + HBRIDGE | BULLBIT, HBRIDGE | BULLBIT, + HBRIDGE | BULLBIT, HBRIDGE | BULLBIT, + HBRIDGE | BULLBIT }; + + final int VDx[] = { 0, 1, 0, 0, 0, 0, 1 }; + final int VDy[] = { -2, -2, -1, 0, 1, 2, 2 }; + final char VBRTAB[] = { + VBRDG0 | BULLBIT, VBRDG1 | BULLBIT, + RIVER, BRWV | BULLBIT, + RIVER, VBRDG2 | BULLBIT, + VBRDG3 | BULLBIT }; + final char VBRTAB2[] = { + VBRIDGE | BULLBIT, RIVER, + VBRIDGE | BULLBIT, VBRIDGE | BULLBIT, + VBRIDGE | BULLBIT, VBRIDGE | BULLBIT, + RIVER }; + + if (cchr9 == BRWV) { + // vertical bridge, open + if (PRNG.nextInt(4) == 0 && getBoatDis() > 340/16) { + //close the bridge + applyBridgeChange(VDx, VDy, VBRTAB, VBRTAB2); + } + return true; + } + else if (cchr9 == BRWH) { + // horizontal bridge, open + if (PRNG.nextInt(4) == 0 && getBoatDis() > 340/16) { + // close the bridge + applyBridgeChange(HDx, HDy, HBRTAB, HBRTAB2); + } + return true; + } + + if (getBoatDis() < 300/16 && PRNG.nextInt(8) == 0) { + if ((cchr & 1) != 0) { + // vertical bridge + if (xpos < city.getWidth()-1) { + // look for CHANNEL tile to right of + // bridge. the CHANNEL tiles are only + // found in the very center of the + // river + if (city.getTile(xpos+1,ypos) == CHANNEL) { + // vertical bridge, open it up + applyBridgeChange(VDx, VDy, VBRTAB2, VBRTAB); + return true; + } + } + return false; + } + else { + // horizontal bridge + if (ypos > 0) { + // look for CHANNEL tile just above + // bridge. the CHANNEL tiles are only + // found in the very center of the + // river + if (city.getTile(xpos, ypos-1) == CHANNEL) { + // open it up + applyBridgeChange(HDx, HDy, HBRTAB2, HBRTAB); + return true; + } + } + return false; + } + } + + return false; + } + + private void applyBridgeChange(int [] Dx, int [] Dy, char [] fromTab, char [] toTab) + { + for (int z = 0; z < 7; z++) { + int x = xpos + Dx[z]; + int y = ypos + Dy[z]; + if (city.testBounds(x,y)) { + if ((city.map[y][x] & LOMASK) == (fromTab[z] & LOMASK) || + (city.map[y][x] == CHANNEL) + ) { + city.setTile(x, y, toTab[z]); + } + } + } + } + + int getBoatDis() + { + int dist = 99999; + for (Sprite s : city.sprites) + { + if (s.isVisible() && s.kind == SpriteKind.SHI) + { + int x = s.x / 16; + int y = s.y / 16; + int d = Math.abs(xpos-x) + Math.abs(ypos-y); + dist = Math.min(d, dist); + } + } + return dist; + } + + void doAirport() + { + if (PRNG.nextInt(6) == 0) { + city.generatePlane(xpos, ypos); + } + + if (PRNG.nextInt(13) == 0) { + city.generateCopter(xpos, ypos); + } + } + + void doZone() + { + // set power bit in map, from powermap + boolean zonePwrFlag = setZonePower(); + + if (zonePwrFlag) + { + city.poweredZoneCount++; + } + else + { + city.unpoweredZoneCount++; + } + + if (cchr9 > PORTBASE) + { + doSpecialZone(zonePwrFlag); + return; + } + + if (cchr9 < HOSPITAL) + { + doResidential(zonePwrFlag); + return; + } + + if (cchr9 < COMBASE) + { + doHospitalChurch(zonePwrFlag); + return; + } + + if (cchr9 < INDBASE) + { + doCommercial(zonePwrFlag); + return; + } + + doIndustrial(zonePwrFlag); + return; + } + + boolean setZonePower() + { + // refresh cchr, cchr9, since this can get called after the + // tile's been changed + cchr = city.map[ypos][xpos]; + cchr9 = (char) (cchr & LOMASK); + + if (cchr9 == NUCLEAR || + cchr9 == POWERPLANT || + city.hasPower(xpos,ypos)) + { + city.setTile(xpos, ypos, (char) (cchr | PWRBIT)); + return true; + } + else + { + city.setTile(xpos, ypos, (char) (cchr & (~PWRBIT))); + return false; + } + } + + boolean zonePlop(int xpos, int ypos, int base) + { + if (!city.testBounds(xpos-1, ypos-1)) + return false; + if (!city.testBounds(xpos+1, ypos+1)) + return false; + + for (int y = ypos-1; y <= ypos+1; y++) + { + for (int x = xpos-1; x <= xpos+1; x++) + { + char c = (char)(city.map[y][x] & LOMASK); + if (c >= FLOOD && c < ROADBASE) + return false; + } + } + + for (int y = ypos-1; y <= ypos+1; y++) + { + for (int x = xpos-1; x <= xpos+1; x++) + { + city.setTile(x, y, (char)(base | BNCNBIT | (x == xpos && y == ypos ? ZONEBIT + BULLBIT : 0))); + base++; + } + } + + setZonePower(); + return true; + } + + void doSpecialZone(boolean powerOn) + { + switch (cchr9) + { + case POWERPLANT: + city.coalCount++; + if ((city.cityTime % 8) == 0) { + repairZone(xpos, ypos, POWERPLANT, 4); + } + + city.powerPlants.add(new CityLocation(xpos,ypos)); + coalSmoke(xpos, ypos); + return; + + case NUCLEAR: + if (!city.noDisasters && PRNG.nextInt(city.MltdwnTab[city.gameLevel]+1) == 0) { + city.doMeltdown(xpos, ypos); + return; + } + + city.nuclearCount++; + if ((city.cityTime % 8) == 0) { + repairZone(xpos, ypos, NUCLEAR, 4); + } + + city.powerPlants.add(new CityLocation(xpos, ypos)); + return; + + case FIRESTATION: + { + city.fireStationCount++; + if ((city.cityTime % 8) == 0) { + repairZone(xpos, ypos, FIRESTATION, 3); + } + + int z; + if (powerOn) { + z = city.fireEffect; //if powered, get effect + } else { + z = city.fireEffect/2; // from the funding ratio + } + + city.traffic.mapX = xpos; + city.traffic.mapY = ypos; + if (!city.traffic.findPerimeterRoad()) { + z /= 2; + } + + city.fireStMap[ypos/8][xpos/8] += z; + return; + } + + case POLICESTATION: + { + city.policeCount++; + if ((city.cityTime % 8) == 0) { + repairZone(xpos, ypos, POLICESTATION, 3); + } + + int z; + if (powerOn) { + z = city.policeEffect; + } else { + z = city.policeEffect / 2; + } + + city.traffic.mapX = xpos; + city.traffic.mapY = ypos; + if (!city.traffic.findPerimeterRoad()) { + z /= 2; + } + + city.policeMap[ypos/8][xpos/8] += z; + return; + } + + case STADIUM: + city.stadiumCount++; + if ((city.cityTime % 16) == 0) { + repairZone(xpos, ypos, STADIUM, 4); + } + + if (powerOn) + { + if (((city.cityTime + xpos + ypos) % 32) == 0) { + drawStadium(xpos, ypos, FULLSTADIUM); + city.setTile(xpos+1,ypos, (char)(FOOTBALLGAME1 | ANIMBIT)); + city.setTile(xpos+1,ypos+1,(char)(FOOTBALLGAME2 | ANIMBIT)); + } + } + return; + + case FULLSTADIUM: + city.stadiumCount++; + if (((city.cityTime + xpos + ypos) % 8) == 0) { + drawStadium(xpos, ypos, STADIUM); + } + return; + + case AIRPORT: + city.airportCount++; + if ((city.cityTime % 8) == 0) { + repairZone(xpos, ypos, AIRPORT, 6); + } + + if (powerOn) + { + if ((city.map[ypos-1][xpos+1] & LOMASK) == RADAR) { + city.setTile(xpos+1,ypos-1, (char) + (RADAR + ANIMBIT + CONDBIT + BURNBIT) + ); + } + } + else + { + city.setTile(xpos+1,ypos-1,(char)(RADAR + CONDBIT + BURNBIT)); + } + + if (powerOn) { + doAirport(); + } + return; + + case PORT: + city.seaportCount++; + if ((city.cityTime % 16) == 0) { + repairZone(xpos, ypos, PORT, 4); + } + + if (powerOn && !city.hasSprite(SpriteKind.SHI)) { + city.generateShip(); + } + return; + + default: + // should not happen + assert false; + } + } + + void makeHospital() + { + if (city.needHospital > 0) + { + zonePlop(xpos, ypos, HOSPITAL - 4); + city.needHospital = 0; + } + + if (city.needChurch > 0) + { + zonePlop(xpos, ypos, CHURCH - 4); + city.needChurch = 0; + } + } + + void doHospitalChurch(boolean powerOn) + { + if (cchr9 == HOSPITAL) + { + city.hospitalCount++; + + if (city.cityTime % 16 == 0) + { + repairZone(xpos, ypos, HOSPITAL, 3); + } + if (city.needHospital == -1) //too many hospitals + { + if (PRNG.nextInt(21) == 0) + { + zonePlop(xpos, ypos, RESBASE); + } + } + } + else if (cchr9 == CHURCH) + { + city.churchCount++; + + if (city.cityTime % 16 == 0) + { + repairZone(xpos, ypos, CHURCH, 3); + } + if (city.needChurch == -1) //too many churches + { + if (PRNG.nextInt(21) == 0) + { + zonePlop(xpos, ypos, RESBASE); + } + } + } + } + + void repairZone(int xpos, int ypos, char zoneCenter, int zoneSize) + { + int cnt=0; + for (int y = 0; y < zoneSize; y++) + { + for (int x = 0; x < zoneSize; x++) + { + int xx = xpos - 1 + x; + int yy = ypos - 1 + y; + + cnt++; + + if (city.testBounds(xx, yy)) + { + int thCh = city.map[yy][xx]; + if ((thCh & ZONEBIT) != 0) + continue; + + if ((thCh & ANIMBIT) != 0) + continue; + + thCh &= LOMASK; + if (thCh < RUBBLE || thCh >= ROADBASE) + { //not rubble, radiactive, on fire or flooded + + city.setTile(xx,yy,(char) + (zoneCenter-2-zoneSize+cnt+CONDBIT+BURNBIT) + ); + } + } + } + } + } + + void doCommercial(boolean powerOn) + { + city.comZoneCount++; + + int tpop = city.commercialZonePop(cchr9); + city.comPop += tpop; + + int trafficGood; + if (tpop > PRNG.nextInt(6)) + { + trafficGood = city.makeTraffic(xpos, ypos, ZoneType.COMMERCIAL); + } + else + { + trafficGood = 1; + } + + if (trafficGood == -1) + { + int value = getCRValue(xpos, ypos); + doCommercialOut(xpos, ypos, tpop, value); + return; + } + + if (PRNG.nextInt(8) == 0) + { + int locValve = evalCommercial(xpos, ypos, trafficGood); + int zscore = city.comValve + locValve; + + if (!powerOn) + zscore = -500; + + if (trafficGood != 0 && + zscore > -350 && + zscore - 26380 > (PRNG.nextInt(0x10000)-0x8000)) + { + int value = getCRValue(xpos, ypos); + doCommercialIn(xpos, ypos, tpop, value); + return; + } + + if (zscore < 350 && zscore + 26380 < (PRNG.nextInt(0x10000)-0x8000)) + { + int value = getCRValue(xpos, ypos); + doCommercialOut(xpos, ypos, tpop, value); + } + } + } + + void setSmoke(int xpos, int ypos, boolean powerOn) + { + int cchr9 = city.map[ypos][xpos] & LOMASK; + + if (cchr9 < IZB) + return; + + int z = ((cchr9 - IZB) / 8) % 8; + if (Smoke.AniThis[z] != 0) + { + int xx = xpos + Smoke.DX1[z]; + int yy = ypos + Smoke.DY1[z]; + + if (city.testBounds(xx, yy)) + { + int t = city.map[yy][xx] & LOMASK; + + if (powerOn) { + if (t == Smoke.AniTabC[z]) //expected non-animated tile + { + city.setTile(xx,yy,(char)(Smoke.ASCBIT | (SMOKEBASE + Smoke.AniTabA[z]))); + } + } + else { + if (t > Smoke.AniTabC[z]) { + city.setTile(xx,yy,(char)(Smoke.REGBIT | Smoke.AniTabC[z])); + } + } + } + + xx = xpos + Smoke.DX2[z]; + yy = ypos + Smoke.DY2[z]; + if (city.testBounds(xx, yy) && !(Smoke.DX1[z] == Smoke.DX2[z] && Smoke.DY1[z] == Smoke.DY2[z])) + { + int t = city.map[yy][xx] & LOMASK; + if (powerOn) { + if (t == Smoke.AniTabD[z]) { + city.setTile(xx,yy,(char)(Smoke.ASCBIT | (SMOKEBASE + Smoke.AniTabB[z]))); + } + } + else { + if (t > Smoke.AniTabD[z]) { + city.setTile(xx,yy,(char)(Smoke.REGBIT | Smoke.AniTabD[z])); + } + } + } + } + } + + void doIndustrial(boolean powerOn) + { + city.indZoneCount++; + setSmoke(xpos, ypos, powerOn); + + int tpop = city.industrialZonePop(cchr9); + city.indPop += tpop; + + int trafficGood; + if (tpop > PRNG.nextInt(6)) + { + trafficGood = city.makeTraffic(xpos, ypos, ZoneType.INDUSTRIAL); + } + else + { + trafficGood = 1; + } + + if (trafficGood == -1) + { + doIndustrialOut(xpos, ypos, tpop, PRNG.nextInt(2)); + return; + } + + if (PRNG.nextInt(8) == 0) + { + int locValve = evalIndustrial(xpos, ypos, trafficGood); + int zscore = city.indValve + locValve; + + if (!powerOn) + zscore = -500; + + if (zscore > -350 && + zscore - 26380 > (PRNG.nextInt(0x10000)-0x8000)) + { + int value = PRNG.nextInt(2); + doIndustrialIn(xpos, ypos, tpop, value); + return; + } + + if (zscore < 350 && zscore + 26380 < (PRNG.nextInt(0x10000)-0x8000)) + { + int value = PRNG.nextInt(2); + doIndustrialOut(xpos, ypos, tpop, value); + } + } + } + + void doResidential(boolean powerOn) + { + city.resZoneCount++; + + int tpop; //population of this zone + if (cchr9 == FREEZ) + { + tpop = city.doFreePop(xpos, ypos); + } + else + { + tpop = city.residentialZonePop(cchr9); + } + + city.resPop += tpop; + + int trafficGood; + if (tpop > PRNG.nextInt(36)) + { + trafficGood = city.makeTraffic(xpos, ypos, ZoneType.RESIDENTIAL); + } + else + { + trafficGood = 1; + } + + if (trafficGood == -1) + { + int value = getCRValue(xpos, ypos); + doResidentialOut(xpos, ypos, tpop, value); + return; + } + + if (cchr9 == FREEZ || PRNG.nextInt(8) == 0) + { + int locValve = evalResidential(xpos, ypos, trafficGood); + int zscore = city.resValve + locValve; + + if (!powerOn) + zscore = -500; + + if (zscore > -350 && zscore - 26380 > (PRNG.nextInt(0x10000)-0x8000)) + { + if (tpop == 0 && PRNG.nextInt(4) == 0) + { + makeHospital(); + return; + } + + int value = getCRValue(xpos, ypos); + doResidentialIn(xpos, ypos, tpop, value); + return; + } + + if (zscore < 350 && zscore + 26380 < (PRNG.nextInt(0x10000)-0x8000)) + { + int value = getCRValue(xpos, ypos); + doResidentialOut(xpos, ypos, tpop, value); + } + } + } + + int evalLot(int x, int y) + { + // test for clear lot + int tmp = city.getTile(x,y) & LOMASK; + + if (tmp != DIRT && (tmp < RESBASE || tmp > RESBASE+8)) + { + return -1; + } + + int score = 1; + + final int [] DX = { 0, 1, 0, -1 }; + final int [] DY = { -1, 0, 1, 0 }; + for (int z = 0; z < 4; z++) + { + int xx = x + DX[z]; + int yy = y + DY[z]; + + if (city.testBounds(xx, yy) && + city.map[yy][xx] != DIRT && + ((city.map[yy][xx] & LOMASK) <= LASTROAD)) //look for road + { + score++; + } + } + + return score; + } + + private void buildHouse(int xpos, int ypos, int value) + { + assert value >= 0 && value <= 3; + + final int [] ZeX = { 0, -1, 0, 1, -1, 1, -1, 0, 1 }; + final int [] ZeY = { 0, -1, -1, -1, 0, 0, 1, 1, 1 }; + + int bestLoc = 0; + int hscore = 0; + + for (int z = 1; z < 9; z++) + { + int xx = xpos + ZeX[z]; + int yy = ypos + ZeY[z]; + + if (city.testBounds(xx, yy)) + { + int score = evalLot(xx, yy); + + if (score != 0) + { + if (score > hscore) + { + hscore = score; + bestLoc = z; + } + + if ((score == hscore) && PRNG.nextInt(8) == 0) + { + bestLoc = z; + } + } + } + } + + if (bestLoc != 0) + { + int xx = xpos + ZeX[bestLoc]; + int yy = ypos + ZeY[bestLoc]; + int houseNumber = value * 3 + PRNG.nextInt(3); + assert houseNumber >= 0 && houseNumber < 12; + + assert city.testBounds(xx, yy); + city.setTile(xx, yy, (char)(HOUSE + houseNumber + BLBNCNBIT)); + } + } + + private void doCommercialIn(int xpos, int ypos, int pop, int value) + { + int z = city.landValueMem[ypos/2][xpos/2] / 32; + if (pop > z) + return; + + if (pop < 5) + { + comPlop(xpos, ypos, pop, value); + incrementROG(xpos, ypos, 8); + } + } + + private void doIndustrialIn(int xpos, int ypos, int pop, int value) + { + if (pop < 4) + { + indPlop(xpos, ypos, pop, value); + incrementROG(xpos, ypos, 8); + } + } + + private void doResidentialIn(int xpos, int ypos, int pop, int value) + { + assert value >= 0 && value <= 3; + + int z = city.pollutionMem[ypos/2][xpos/2]; + if (z > 128) + return; + + char cchr9 = (char)(city.map[ypos][xpos] & LOMASK); + if (cchr9 == FREEZ) + { + if (pop < 8) + { + buildHouse(xpos, ypos, value); + incrementROG(xpos, ypos, 1); + return; + } + + if (city.getPopulationDensity(xpos, ypos) > 64) + { + residentialPlop(xpos, ypos, 0, value); + incrementROG(xpos, ypos, 8); + return; + } + return; + } + + if (pop < 40) + { + residentialPlop(xpos, ypos, pop / 8 - 1, value); + incrementROG(xpos, ypos, 8); + } + } + + void comPlop(int xpos, int ypos, int density, int value) + { + int base = (value * 5 + density) * 9 + CZB - 4; + zonePlop(xpos, ypos, base); + } + + void indPlop(int xpos, int ypos, int density, int value) + { + int base = (value * 4 + density) * 9 + (IZB - 4); + zonePlop(xpos, ypos, base); + } + + void residentialPlop(int xpos, int ypos, int density, int value) + { + int base = (value * 4 + density) * 9 + RZB - 4; + zonePlop(xpos, ypos, base); + } + + private void doCommercialOut(int xpos, int ypos, int pop, int value) + { + if (pop > 1) + { + comPlop(xpos, ypos, pop-2, value); + incrementROG(xpos, ypos, -8); + } + else if (pop == 1) + { + zonePlop(xpos, ypos, COMBASE); + incrementROG(xpos, ypos, -8); + } + } + + private void doIndustrialOut(int xpos, int ypos, int pop, int value) + { + if (pop > 1) + { + indPlop(xpos, ypos, pop-2, value); + incrementROG(xpos, ypos, -8); + } + else if (pop == 1) + { + zonePlop(xpos, ypos, INDCLR-4); + incrementROG(xpos, ypos, -8); + } + } + + private void doResidentialOut(int xpos, int ypos, int pop, int value) + { + assert value >= 0 && value < 4; + + final char [] Brdr = { 0, 3, 6, 1, 4, 7, 2, 5, 8 }; + + if (pop == 0) + return; + + if (pop > 16) + { + // downgrade to a lower-density full-size residential zone + residentialPlop(xpos, ypos, (pop-24) / 8, value); + incrementROG(xpos, ypos, -8); + return; + } + + if (pop == 16) + { + // downgrade from full-size zone to 8 little houses + + city.setTile(xpos, ypos, (char)(FREEZ | BLBNCNBIT | ZONEBIT)); + for (int x = xpos-1; x <= xpos+1; x++) + { + for (int y = ypos-1; y <= ypos+1; y++) + { + if (city.testBounds(x,y)) + { + if (!(x == xpos && y == ypos)) + { + // pick a random small house + int houseNumber = value * 3 + PRNG.nextInt(3); + city.setTile(x, y, (char) (HOUSE + houseNumber + BLBNCNBIT)); + } + } + } + } + + incrementROG(xpos, ypos, -8); + return; + } + + if (pop < 16) + { + // remove one little house + incrementROG(xpos, ypos, -1); + int z = 0; + + for (int x = xpos-1; x <= xpos+1; x++) + { + for (int y = ypos-1; y <= ypos+1; y++) + { + if (city.testBounds(x,y)) + { + int loc = city.map[y][x] & LOMASK; + if (loc >= LHTHR && loc <= HHTHR) + { //little house + city.setTile(x, y, (char)(Brdr[z] + BLBNCNBIT + FREEZ - 4)); + return; + } + } + z++; + } + } + } + } + + // returns integer between -3000 and 3000 + int evalCommercial(int x, int y, int traf) + { + if (traf < 0) + return -3000; + + return city.comRate[y/8][x/8]; + } + + // returns integer between -3000 and 3000 + int evalIndustrial(int x, int y, int traf) + { + if (traf < 0) + return -1000; + else + return 0; + } + + // returns integer between -3000 and 3000 + int evalResidential(int x, int y, int traf) + { + if (traf < 0) + return -3000; + + int value = city.landValueMem[y/2][x/2]; + value -= city.pollutionMem[y/2][x/2]; + + if (value < 0) + value = 0; //cap at 0 + else + value *= 32; + + if (value > 6000) + value = 6000; //cap at 6000 + + return value - 3000; + } + + // applies to commercial and residential zones? + // returns integer from 0 to 3 + int getCRValue(int x, int y) + { + int lval = city.landValueMem[y/2][x/2]; + lval -= city.pollutionMem[y/2][x/2]; + + if (lval < 30) + return 0; + + if (lval < 80) + return 1; + + if (lval < 150) + return 2; + + return 3; + } + + void coalSmoke(int mx, int my) + { + final int [] SmTb = { COALSMOKE1, COALSMOKE2, COALSMOKE3, COALSMOKE4 }; + final int [] dx = { 1, 2, 1, 2 }; + final int [] dy = { -1, -1, 0, 0 }; + + for (int z = 0; z < 4; z++) { + city.setTile(mx + dx[z], my + dy[z], + (char) (SmTb[z] | ANIMBIT | CONDBIT | PWRBIT | BURNBIT) + ); + } + } + + void incrementROG(int xpos, int ypos, int amount) + { + city.rateOGMem[ypos/8][xpos/8] += 4*amount; + } + + void drawStadium(int mapx, int mapy, int z) + { + z -= 5; + + for (int y = mapy-1; y < mapy+3; y++) { + for (int x = mapx-1; x < mapx+3; x++) { + city.setTile(x, y, (char) (z | BNCNBIT | (x == mapx && y == mapy ? (ZONEBIT|PWRBIT) : 0))); + z++; + } + } + } + +} diff --git a/src/micropolisj/engine/MapState.java b/src/micropolisj/engine/MapState.java new file mode 100644 index 0000000..2191e5f --- /dev/null +++ b/src/micropolisj/engine/MapState.java @@ -0,0 +1,30 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +/** + * Lists the various map overlay options that are available. + */ +public enum MapState +{ + ALL, //ALMAP + RESIDENTIAL, //REMAP + COMMERCIAL, //COMAP + INDUSTRIAL, //INMAP + TRANSPORT, //RDMAP + POPDEN_OVERLAY, //PDMAP + GROWTHRATE_OVERLAY, //RGMAP + LANDVALUE_OVERLAY, //LVMAP + CRIME_OVERLAY, //CRMAP + POLLUTE_OVERLAY, //PLMAP + TRAFFIC_OVERLAY, //TDMAP + POWER_OVERLAY, //PRMAP + FIRE_OVERLAY, //FIMAP + POLICE_OVERLAY; //POMAP +} diff --git a/src/micropolisj/engine/Micropolis.java b/src/micropolisj/engine/Micropolis.java new file mode 100644 index 0000000..8cdf793 --- /dev/null +++ b/src/micropolisj/engine/Micropolis.java @@ -0,0 +1,2575 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import java.io.*; +import java.util.*; + +import static micropolisj.engine.TileConstants.*; + +public class Micropolis +{ + static final Random DEFAULT_PRNG = new Random(); + + Random PRNG; + + // full size arrays + char [][] map; + boolean [][] powerMap; + + // half-size arrays + public int [][] landValueMem; + public int [][] pollutionMem; + public int [][] crimeMem; //updated each cycle by crimeScan(); affects land value + public int [][] popDensity; + public int [][] trfDensity; + int [][] tem; + + // quarter-size arrays + int [][] terrainMem; + int [][] qtem; + + // eighth-size arrays + public int [][] rateOGMem; //rate of growth? + int [][] fireStMap; //firestations- cleared and rebuilt each sim cycle + public int [][] fireRate; //firestations reach- used for overlay graphs + int [][] policeMap; //police stations- cleared and rebuilt each sim cycle + public int [][] policeMapEffect;//police stations reach- used for overlay graphs + + /** For each 8x8 section of city, this is an integer between 0 and 64, + * with higher numbers being closer to the center of the city. */ + int [][] comRate; + + static final int DEFAULT_WIDTH = 120; + static final int DEFAULT_HEIGHT = 100; + + public int totalFunds; + public boolean autoBulldoze = true; + public boolean autoBudget = false; + public Speed simSpeed = Speed.NORMAL; + public boolean noDisasters = false; + + public static final int MIN_LEVEL = 0; + public static final int MAX_LEVEL = 2; + public int gameLevel; + + boolean autoGo; + + // census numbers, reset in phase 0 of each cycle, summed during map scan + int poweredZoneCount; + int unpoweredZoneCount; + int roadTotal; + int railTotal; + int firePop; + int resZoneCount; + int comZoneCount; + int indZoneCount; + int resPop; + int comPop; + int indPop; + int hospitalCount; + int churchCount; + int policeCount; + int fireStationCount; + int stadiumCount; + int coalCount; + int nuclearCount; + int seaportCount; + int airportCount; + + int totalPop; + int lastCityPop; + + int trafficMaxLocationX; + int trafficMaxLocationY; + int pollutionMaxLocationX; + int pollutionMaxLocationY; + int crimeMaxLocationX; + int crimeMaxLocationY; + public int centerMassX; + public int centerMassY; + CityLocation meltdownLocation; //may be null + CityLocation crashLocation; //may be null + + int needHospital; // -1 too many already, 0 just right, 1 not enough + int needChurch; // -1 too many already, 0 just right, 1 not enough + + int crimeAverage; + int pollutionAverage; + int landValueAverage; + int trafficAverage; + + int resValve; // ranges between -2000 and 2000, updated by setValves + int comValve; // ranges between -1500 and 1500 + int indValve; // ranges between -1500 and 1500 + + boolean resCap; // residents demand a stadium, caps resValve at 0 + boolean comCap; // commerce demands airport, caps comValve at 0 + boolean indCap; // industry demands sea port, caps indValve at 0 + int crimeRamp; + int polluteRamp; + + // + // budget stuff + // + public int cityTax = 7; + public double roadPercent = 1.0; + public double policePercent = 1.0; + public double firePercent = 1.0; + + int taxEffect = 7; + int roadEffect = 32; + int policeEffect = 1000; + int fireEffect = 1000; + + int taxFund; + int roadFundEscrow; + int fireFundEscrow; + int policeFundEscrow; + int cashFlow; //net change in totalFunds in previous year + + boolean newPower; + + int floodCnt; //number of turns the flood will last + int floodX; + int floodY; + + public int cityTime; //counts "weeks" (actually, 1/48'ths years) + int scycle; //same as cityTime, except mod 1024 + int fcycle; //counts simulation steps (mod 1024) + public int acycle; //animation cycle (mod 960) + + public CityEval evaluation; + + ArrayList sprites = new ArrayList<>(); + + static final int VALVERATE = 2; + public static final int CENSUSRATE = 4; + static final int TAXFREQ = 48; + + public void spend(int amount) + { + totalFunds -= amount; + fireFundsChanged(); + } + + public Micropolis() + { + PRNG = DEFAULT_PRNG; + evaluation = new CityEval(this); + init(); + } + + protected void init() + { + int width = DEFAULT_WIDTH; + int height = DEFAULT_HEIGHT; + + map = new char[height][width]; + powerMap = new boolean[height][width]; + + int hX = (width+1)/2; + int hY = (height+1)/2; + + landValueMem = new int[hY][hX]; + pollutionMem = new int[hY][hX]; + crimeMem = new int[hY][hX]; + popDensity = new int[hY][hX]; + trfDensity = new int[hY][hX]; + tem = new int[hY][hX]; + + int qX = (width+3)/4; + int qY = (height+3)/4; + + terrainMem = new int[qY][qX]; + qtem = new int[qY][qX]; + + int smX = (width+7)/8; + int smY = (height+7)/8; + + rateOGMem = new int[smY][smX]; + fireStMap = new int[smY][smX]; + policeMap = new int[smY][smX]; + policeMapEffect = new int[smY][smX]; + fireRate = new int[smY][smX]; + comRate = new int[smY][smX]; + + centerMassX = hX; + centerMassY = hY; + } + + void fireCensusChanged() + { + for (Listener l : listeners) { + l.censusChanged(); + } + } + + void fireCityMessage(MicropolisMessage message, CityLocation loc, boolean isPic) + { + for (Listener l : listeners) { + l.cityMessage(message, loc, isPic); + } + } + + void fireCitySound(Sound sound, CityLocation loc) + { + for (Listener l : listeners) { + l.citySound(sound, loc); + } + } + + void fireDemandChanged() + { + for (Listener l : listeners) { + l.demandChanged(); + } + } + + void fireEarthquakeStarted() + { + for (EarthquakeListener l : earthquakeListeners) { + l.earthquakeStarted(); + } + } + + void fireEvaluationChanged() + { + for (Listener l : listeners) { + l.evaluationChanged(); + } + } + + void fireFundsChanged() + { + for (Listener l : listeners) { + l.fundsChanged(); + } + } + + void fireMapOverlayDataChanged(MapState overlayDataType) + { + for (MapListener l : mapListeners) { + l.mapOverlayDataChanged(overlayDataType); + } + } + + void fireOptionsChanged() + { + for (Listener l : listeners) + { + l.optionsChanged(); + } + } + + void fireSpriteMoved(Sprite sprite) + { + for (MapListener l : mapListeners) + { + l.spriteMoved(sprite); + } + } + + void fireTileChanged(int xpos, int ypos) + { + for (MapListener l : mapListeners) + { + l.tileChanged(xpos, ypos); + } + } + + void fireWholeMapChanged() + { + for (MapListener l : mapListeners) + { + l.wholeMapChanged(); + } + } + + ArrayList listeners = new ArrayList<>(); + ArrayList mapListeners = new ArrayList<>(); + ArrayList earthquakeListeners = new ArrayList<>(); + + public void addListener(Listener l) + { + this.listeners.add(l); + } + + public void removeListener(Listener l) + { + this.listeners.remove(l); + } + + public void addEarthquakeListener(EarthquakeListener l) + { + this.earthquakeListeners.add(l); + } + + public void removeEarthquakeListener(EarthquakeListener l) + { + this.earthquakeListeners.remove(l); + } + + public void addMapListener(MapListener l) + { + this.mapListeners.add(l); + } + + public void removeMapListener(MapListener l) + { + this.mapListeners.remove(l); + } + + /** + * The listener interface for receiving miscellaneous events that occur + * in the Micropolis city. + * Use the Micropolis class's addListener interface to register an object + * that implements this interface. + */ + public interface Listener + { + void cityMessage(MicropolisMessage message, CityLocation loc, boolean isPic); + void citySound(Sound sound, CityLocation loc); + + /** + * Fired whenever the "census" is taken, and the various historical + * counters have been updated. (Once a month in game.) + */ + void censusChanged(); + + /** + * Fired whenever resValve, comValve, or indValve changes. + * (Twice a month in game.) */ + void demandChanged(); + + /** + * Fired whenever the city evaluation is recalculated. + * (Once a year.) + */ + void evaluationChanged(); + + /** + * Fired whenever the mayor's money changes. + */ + void fundsChanged(); + + /** + * Fired whenever autoBulldoze, autoBudget, noDisasters, + * or simSpeed change. + */ + void optionsChanged(); + } + + public int getWidth() + { + return map[0].length; + } + + public int getHeight() + { + return map.length; + } + + public char getTile(int xpos, int ypos) + { + return map[ypos][xpos]; + } + + public void setTile(int xpos, int ypos, char newTile) + { + if (map[ypos][xpos] != newTile) + { + map[ypos][xpos] = newTile; + fireTileChanged(xpos, ypos); + } + } + + final boolean testBounds(int xpos, int ypos) + { + return xpos >= 0 && xpos < getWidth() && + ypos >= 0 && ypos < getHeight(); + } + + final boolean hasPower(int x, int y) + { + return powerMap[y][x]; + } + + /** + * Checks whether the next call to step() will collect taxes and + * process the budget. + */ + public boolean isBudgetTime() + { + return ( + cityTime != 0 && + (cityTime % TAXFREQ) == 0 && + ((fcycle + 1) % 16) == 10 + ); + } + + public void step() + { + fcycle = (fcycle + 1) % 1024; + simulate(fcycle % 16); + } + + void clearCensus() + { + poweredZoneCount = 0; + unpoweredZoneCount = 0; + firePop = 0; + roadTotal = 0; + railTotal = 0; + resPop = 0; + comPop = 0; + indPop = 0; + resZoneCount = 0; + comZoneCount = 0; + indZoneCount = 0; + hospitalCount = 0; + churchCount = 0; + policeCount = 0; + fireStationCount = 0; + stadiumCount = 0; + coalCount = 0; + nuclearCount = 0; + seaportCount = 0; + airportCount = 0; + powerPlants.clear(); + + for (int y = 0; y < fireStMap.length; y++) { + for (int x = 0; x < fireStMap[y].length; x++) { + fireStMap[y][x] = 0; + policeMap[y][x] = 0; + } + } + } + + // period of powerScan() + static final int [] spdPwr = { 1, 1, 1, 2, 5 }; + // period of ptlScan() + static final int [] spdPtl = { 1, 1, 1, 2, 5 }; + // period of crimeScan() + static final int [] spdCri = { 1, 1, 1, 2, 5 }; + // period of popDenScan() + static final int [] spdPop = { 1, 1, 1, 2, 5 }; + // period of fireAnalysis() + static final int [] spdFir = { 1, 1, 1, 2, 5 }; + + void simulate(int mod16) + { + final int band = getWidth() / 8; + + switch (mod16) + { + case 0: + scycle = (scycle + 1) % 1024; + cityTime++; + if (scycle % 2 == 0) { + setValves(); + } + clearCensus(); + break; + + case 1: + mapScan(0 * band, 1 * band); + break; + + case 2: + mapScan(1 * band, 2 * band); + break; + + case 3: + mapScan(2 * band, 3 * band); + break; + + case 4: + mapScan(3 * band, 4 * band); + break; + + case 5: + mapScan(4 * band, 5 * band); + break; + + case 6: + mapScan(5 * band, 6 * band); + break; + + case 7: + mapScan(6 * band, 7 * band); + break; + + case 8: + mapScan(7 * band, getWidth()); + break; + + case 9: + if (cityTime % CENSUSRATE == 0) { + takeCensus(); + + if (cityTime % (CENSUSRATE*12) == 0) { + takeCensus2(); + } + + fireCensusChanged(); + } + + collectTaxPartial(); + + if (cityTime % TAXFREQ == 0) { + collectTax(); + evaluation.cityEvaluation(); + } + break; + + case 10: + if (scycle % 5 == 0) { // every ~10 weeks + decROGMem(); + } + decTrafficMem(); + fireMapOverlayDataChanged(MapState.TRAFFIC_OVERLAY); //TDMAP + fireMapOverlayDataChanged(MapState.TRANSPORT); //RDMAP + fireMapOverlayDataChanged(MapState.ALL); //ALMAP + fireMapOverlayDataChanged(MapState.RESIDENTIAL); //REMAP + fireMapOverlayDataChanged(MapState.COMMERCIAL); //COMAP + fireMapOverlayDataChanged(MapState.INDUSTRIAL); //INMAP + doMessages(); + break; + + case 11: + if (scycle % spdPwr[simSpeed.ordinal()] == 0) { + powerScan(); + fireMapOverlayDataChanged(MapState.POWER_OVERLAY); + newPower = true; + } + break; + + case 12: + if (scycle % spdPtl[simSpeed.ordinal()] == 0) { + ptlScan(); + } + break; + + case 13: + if (scycle % spdCri[simSpeed.ordinal()] == 0) { + crimeScan(); + } + break; + + case 14: + if (scycle % spdPop[simSpeed.ordinal()] == 0) { + popDenScan(); + } + break; + + case 15: + if (scycle % spdFir[simSpeed.ordinal()] == 0) { + fireAnalysis(); + } + doDisasters(); + break; + + default: + throw new Error("unreachable"); + } + } + + private int computePopDen(int x, int y, char tile) + { + if (tile == FREEZ) + return doFreePop(x, y); + + if (tile < COMBASE) + return residentialZonePop(tile); + + if (tile < INDBASE) + return commercialZonePop(tile) * 8; + + if (tile < PORTBASE) + return industrialZonePop(tile) * 8; + + return 0; + } + + private static int [][] doSmooth(int [][] tem) + { + final int h = tem.length; + final int w = tem[0].length; + int [][] tem2 = new int[h][w]; + + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + int z = tem[y][x]; + if (x > 0) + z += tem[y][x-1]; + if (x + 1 < w) + z += tem[y][x+1]; + if (y > 0) + z += tem[y-1][x]; + if (y + 1 < h) + z += tem[y+1][x]; + z /= 4; + if (z > 255) + z = 255; + tem2[y][x] = z; + } + } + + return tem2; + } + + public void calculateCenterMass() + { + popDenScan(); + } + + private void popDenScan() + { + int xtot = 0; + int ytot = 0; + int zoneCount = 0; + int width = getWidth(); + int height = getHeight(); + int [][] tem = new int[(height+1)/2][(width+1)/2]; + + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + char tile = map[y][x]; + if ((tile & ZONEBIT) != 0) + { + tile &= LOMASK; + int den = computePopDen(x, y, (char)tile) * 8; + if (den > 254) + den = 254; + tem[y/2][x/2] = den; + xtot += x; + ytot += y; + zoneCount++; + } + } + } + + tem = doSmooth(tem); + tem = doSmooth(tem); + tem = doSmooth(tem); + + for (int x = 0; x < (width+1)/2; x++) + { + for (int y = 0; y < (height+1)/2; y++) + { + popDensity[y][x] = 2 * tem[y][x]; + } + } + + distIntMarket(); //set ComRate + + // find center of mass for city + if (zoneCount != 0) + { + centerMassX = xtot / zoneCount; + centerMassY = ytot / zoneCount; + } + else + { + centerMassX = (width+1)/2; + centerMassY = (height+1)/2; + } + + fireMapOverlayDataChanged(MapState.POPDEN_OVERLAY); //PDMAP + fireMapOverlayDataChanged(MapState.GROWTHRATE_OVERLAY); //RGMAP + } + + private void distIntMarket() + { + for (int y = 0; y < comRate.length; y++) + { + for (int x = 0; x < comRate[y].length; x++) + { + int z = getDisCC(x*4, y*4); + z /= 4; + z = 64 - z; + comRate[y][x] = z; + } + } + } + + //tends to empty RateOGMem[][] + private void decROGMem() + { + for (int y = 0; y < rateOGMem.length; y++) + { + for (int x = 0; x < rateOGMem[y].length; x++) + { + int z = rateOGMem[y][x]; + if (z == 0) + continue; + + if (z > 0) + { + rateOGMem[y][x]--; + if (z > 200) + { + rateOGMem[y][x] = 200; //prevent overflow? + } + continue; + } + + if (z < 0) + { + rateOGMem[y][x]++; + if (z < -200) + { + rateOGMem[y][x] = -200; + } + continue; + } + } + } + } + + //tends to empty trfDensity + private void decTrafficMem() + { + for (int y = 0; y < trfDensity.length; y++) + { + for (int x = 0; x < trfDensity[y].length; x++) + { + int z = trfDensity[y][x]; + if (z != 0) + { + if (z > 200) + trfDensity[y][x] = z - 34; + else if (z > 24) + trfDensity[y][x] = z - 24; + else + trfDensity[y][x] = 0; + } + } + } + } + + void crimeScan() + { + policeMap = smoothFirePoliceMap(policeMap); + policeMap = smoothFirePoliceMap(policeMap); + policeMap = smoothFirePoliceMap(policeMap); + + for (int sy = 0; sy < policeMap.length; sy++) { + for (int sx = 0; sx < policeMap[sy].length; sx++) { + policeMapEffect[sy][sx] = policeMap[sy][sx]; + } + } + + int count = 0; + int sum = 0; + int cmax = 0; + for (int hy = 0; hy < landValueMem.length; hy++) { + for (int hx = 0; hx < landValueMem[hy].length; hx++) { + int val = landValueMem[hy][hx]; + if (val != 0) { + count++; + int z = 128 - val + popDensity[hy][hx]; + z = Math.min(300, z); + z -= policeMap[hy/4][hx/4]; + z = Math.min(250, z); + z = Math.max(0, z); + crimeMem[hy][hx] = z; + + sum += z; + if (z > cmax || (z == cmax && PRNG.nextInt(4) == 0)) { + cmax = z; + crimeMaxLocationX = hx*2; + crimeMaxLocationY = hy*2; + } + } + else { + crimeMem[hy][hx] = 0; + } + } + } + + if (count != 0) + crimeAverage = sum / count; + else + crimeAverage = 0; + + fireMapOverlayDataChanged(MapState.POLICE_OVERLAY); + } + + void doDisasters() + { + if (floodCnt > 0) { + floodCnt--; + } + + final int [] DisChance = { 480, 240, 60 }; + if (noDisasters) + return; + + if (PRNG.nextInt(DisChance[gameLevel]+1) != 0) + return; + + switch (PRNG.nextInt(9)) + { + case 0: + case 1: + setFire(); + break; + case 2: + case 3: + makeFlood(); + break; + case 4: + break; + case 5: + makeTornado(); + break; + case 6: + makeEarthquake(); + break; + case 7: + case 8: + if (pollutionAverage > 60) { + makeMonster(); + } + break; + } + } + + private int[][] smoothFirePoliceMap(int[][] omap) + { + int smX = omap[0].length; + int smY = omap.length; + int[][] nmap = new int[smY][smX]; + for (int sy = 0; sy < smY; sy++) { + for (int sx = 0; sx < smX; sx++) { + int edge = 0; + if (sx > 0) { edge += omap[sy][sx-1]; } + if (sx + 1 < smX) { edge += omap[sy][sx+1]; } + if (sy > 0) { edge += omap[sy-1][sx]; } + if (sy + 1 < smY) { edge += omap[sy+1][sx]; } + edge = edge / 4 + omap[sy][sx]; + nmap[sy][sx] = edge / 2; + } + } + return nmap; + } + + void fireAnalysis() + { + fireStMap = smoothFirePoliceMap(fireStMap); + fireStMap = smoothFirePoliceMap(fireStMap); + fireStMap = smoothFirePoliceMap(fireStMap); + for (int sy = 0; sy < fireStMap.length; sy++) { + for (int sx = 0; sx < fireStMap[sy].length; sx++) { + fireRate[sy][sx] = fireStMap[sy][sx]; + } + } + + fireMapOverlayDataChanged(MapState.FIRE_OVERLAY); + } + + private boolean isConductive(int x, int y) + { + return (map[y][x] & CONDBIT) != 0; + } + + private boolean testForCond(CityLocation loc, int dir) + { + int xsave = loc.x; + int ysave = loc.y; + + boolean rv = false; + if (movePowerLocation(loc,dir)) + { + rv = ( + isConductive(loc.x, loc.y) && + map[loc.y][loc.x] != NUCLEAR && + map[loc.y][loc.x] != POWERPLANT && + !hasPower(loc.x, loc.y) + ); + } + + loc.x = xsave; + loc.y = ysave; + return rv; + } + + private boolean movePowerLocation(CityLocation loc, int dir) + { + switch(dir) + { + case 0: + if (loc.y > 0) + { + loc.y--; + return true; + } + else + return false; + case 1: + if (loc.x + 1 < getWidth()) + { + loc.x++; + return true; + } + else + return false; + case 2: + if (loc.y + 1 < getHeight()) + { + loc.y++; + return true; + } + else + return false; + case 3: + if (loc.x > 0) + { + loc.x--; + return true; + } + else + return false; + case 4: + return true; + } + return false; + } + + void powerScan() + { + // clear powerMap + for (boolean [] bb : powerMap) + { + Arrays.fill(bb, false); + } + + // + // Note: brownouts are based on total number of power plants, not the number + // of powerplants connected to your city. + // + + int maxPower = coalCount * 700 + nuclearCount * 2000; + int numPower = 0; + + // This is kind of odd algorithm, but I haven't the heart to rewrite it at + // this time. + + while (!powerPlants.isEmpty()) + { + CityLocation loc = powerPlants.pop(); + + int aDir = 4; + int conNum; + do + { + if (++numPower > maxPower) + { + // trigger notification + sendMessage(MicropolisMessage.BROWNOUTS_REPORT); + return; + } + movePowerLocation(loc, aDir); + powerMap[loc.y][loc.x] = true; + + conNum = 0; + int dir = 0; + while (dir < 4 && conNum < 2) + { + if (testForCond(loc, dir)) + { + conNum++; + aDir = dir; + } + else + { + } + dir++; + } + if (conNum > 1) + { + powerPlants.add(new CityLocation(loc.x,loc.y)); + } + } + while (conNum != 0); + } + } + + static int getPollutionValue(int tile) + { + if (tile < POWERBASE) + { + if (tile >= HTRFBASE) + return 75; //heavy traffic + if (tile >= LTRFBASE) + return 50; //light traffic + + if (tile < ROADBASE) + { + if (tile > FIREBASE) + return 90; + + if (tile >= RADTILE) + return 255; //radioactivity + } + + return 0; + } + + if (tile <= LASTIND) + return 0; + + if (tile < PORTBASE) + return 50; + + if (tile <= LASTPOWERPLANT) + return 100; + + return 0; + } + + public int getTrafficDensity(int xpos, int ypos) + { + if (testBounds(xpos, ypos)) { + return trfDensity[ypos/2][xpos/2]; + } else { + return 0; + } + } + + //power, terrain, land value + void ptlScan() + { + for (int [] aa : qtem) + Arrays.fill(aa, 0); + + int landValueTotal = 0; + int landValueCount = 0; + + final int HWLDX = (getWidth()+1)/2; + final int HWLDY = (getHeight()+1)/2; + for (int x = 0; x < HWLDX; x++) + { + for (int y = 0; y < HWLDY; y++) + { + int plevel = 0; + int lvflag = 0; + int zx = 2*x; + int zy = 2*y; + + for (int mx = zx; mx <= zx+1; mx++) + { + for (int my = zy; my <= zy+1; my++) + { + int tile = (map[my][mx] & LOMASK); + if (tile != DIRT) + { + if (tile < RUBBLE) //natural land features + { + //inc terrainMem + qtem[y/2][x/2] += 15; + continue; + } + plevel += getPollutionValue(tile); + if (tile >= ROADBASE) + lvflag++; + } + } + } + + if (plevel < 0) + plevel = 250; //? + + if (plevel > 255) + plevel = 255; + + tem[y][x] = plevel; + + if (lvflag != 0) + { + //land value equation + + + int dis = 34 - getDisCC(x, y); + dis *= 4; + dis += terrainMem[y/2][x/2]; + dis -= pollutionMem[y][x]; + if (crimeMem[y][x] > 190) { + dis -= 20; + } + if (dis > 250) + dis = 250; + if (dis < 1) + dis = 1; + landValueMem[y][x] = dis; + landValueTotal += dis; + landValueCount++; + } + else + { + landValueMem[y][x] = 0; + } + } + } + + landValueAverage = landValueCount != 0 ? (landValueTotal/landValueCount) : 0; + + tem = doSmooth(tem); + tem = doSmooth(tem); + + int pcount = 0; + int ptotal = 0; + int pmax = 0; + for (int x = 0; x < HWLDX; x++) + { + for (int y = 0; y < HWLDY; y++) + { + int z = tem[y][x]; + pollutionMem[y][x] = z; + + if (z != 0) + { + pcount++; + ptotal += z; + + if (z > pmax || + (z == pmax && PRNG.nextInt(4) == 0)) + { + pmax = z; + pollutionMaxLocationX = 2*x; + pollutionMaxLocationY = 2*y; + } + } + } + } + + pollutionAverage = pcount != 0 ? (ptotal / pcount) : 0; + + smoothTerrain(qtem); + + fireMapOverlayDataChanged(MapState.POLLUTE_OVERLAY); //PLMAP + fireMapOverlayDataChanged(MapState.LANDVALUE_OVERLAY); //LVMAP + } + + public CityLocation getLocationOfMaxPollution() + { + return new CityLocation(pollutionMaxLocationX, pollutionMaxLocationY); + } + + static final int [] TaxTable = { + 200, 150, 120, 100, 80, 50, 30, 0, -10, -40, -100, + -150, -200, -250, -300, -350, -400, -450, -500, -550, -600 }; + + public static class History + { + public int cityTime; + public int [] res = new int[240]; + public int [] com = new int[240]; + public int [] ind = new int[240]; + public int [] money = new int[240]; + public int [] pollution = new int[240]; + public int [] crime = new int[240]; + int resMax; + int comMax; + int indMax; + } + public History history = new History(); + + static class MiscHistory + { + int resPop; + int comPop; + int indPop; + int resValve; + int comValve; + int indValve; + int crimeRamp; + int polluteRamp; + int landValueAverage; + int crimeAverage; + int pollutionAverage; + int gameLevel; + int cityClass; + int cityScore; + } + + void setValves() + { + MiscHistory hist = new MiscHistory(); + hist.resPop = resPop; + hist.comPop = comPop; + hist.indPop = indPop; + hist.resValve = resValve; + hist.comValve = comValve; + hist.indValve = indValve; + hist.crimeRamp = crimeRamp; + hist.polluteRamp = polluteRamp; + hist.landValueAverage = landValueAverage; + hist.crimeAverage = crimeAverage; + hist.pollutionAverage = pollutionAverage; + hist.gameLevel = gameLevel; + hist.cityClass = evaluation.cityClass; + hist.cityScore = evaluation.cityScore; + + double normResPop = (double)resPop / 8.0; + totalPop = (int) (normResPop + comPop + indPop); + + double employment; + if (normResPop != 0.0) + { + employment = (history.com[1] + history.ind[1]) / normResPop; + } + else + { + employment = 1; + } + + double migration = normResPop * (employment - 1); + final double BIRTH_RATE = 0.02; + double births = (double)normResPop * BIRTH_RATE; + double projectedResPop = normResPop + migration + births; + + double temp = (history.com[1] + history.ind[1]); + double laborBase; + if (temp != 0.0) + { + laborBase = history.res[1] / temp; + } + else + { + laborBase = 1; + } + + // clamp laborBase to between 0.0 and 1.3 + laborBase = Math.max(0.0, Math.min(1.3, laborBase)); + + double internalMarket = (double)(normResPop + comPop + indPop) / 3.7; + double projectedComPop = internalMarket * laborBase; + + int z = gameLevel; + temp = 1.0; + switch (z) + { + case 0: temp = 1.2; break; + case 1: temp = 1.1; break; + case 2: temp = 0.98; break; + } + + double projectedIndPop = indPop * laborBase * temp; + if (projectedIndPop < 5.0) + projectedIndPop = 5.0; + + double resRatio; + if (normResPop != 0) + { + resRatio = (double)projectedResPop / (double)normResPop; + } + else + { + resRatio = 1.3; + } + + double comRatio; + if (comPop != 0) + comRatio = (double)projectedComPop / (double)comPop; + else + comRatio = projectedComPop; + + double indRatio; + if (indPop != 0) + indRatio = (double)projectedIndPop / (double)indPop; + else + indRatio = projectedIndPop; + + if (resRatio > 2.0) + resRatio = 2.0; + + if (comRatio > 2.0) + comRatio = 2.0; + + if (indRatio > 2.0) + indRatio = 2.0; + + int z2 = taxEffect + gameLevel; + if (z2 > 20) + z2 = 20; + + resRatio = (resRatio - 1) * 600 + TaxTable[z]; + comRatio = (comRatio - 1) * 600 + TaxTable[z]; + indRatio = (indRatio - 1) * 600 + TaxTable[z]; + + // ratios are velocity changes to valves + resValve += (int) resRatio; + comValve += (int) comRatio; + indValve += (int) indRatio; + + if (resValve > 2000) + resValve = 2000; + else if (resValve < -2000) + resValve = -2000; + + if (comValve > 1500) + comValve = 1500; + else if (comValve < -1500) + comValve = -1500; + + if (indValve > 1500) + indValve = 1500; + else if (indValve < -1500) + indValve = -1500; + + + if (resCap && resValve > 0) { + // residents demand stadium + resValve = 0; + } + + if (comCap && comValve > 0) { + // commerce demands airport + comValve = 0; + } + + if (indCap && indValve > 0) { + // industry demands sea port + indValve = 0; + } + + fireDemandChanged(); + } + + void smoothTerrain(int [][] qtem) + { + final int QWX = qtem[0].length; + final int QWY = qtem.length; + + for (int y = 0; y < QWY; y++) + { + for (int x = 0; x < QWX; x++) + { + int z = 0; + if (x > 0) + z += qtem[y][x-1]; + if (x+1 < QWX) + z += qtem[y][x+1]; + if (y > 0) + z += qtem[y-1][x]; + if (y+1 < QWY) + z += qtem[y+1][x]; + terrainMem[y][x] = z / 4 + qtem[y][x] / 2; + } + } + } + + // calculate manhatten distance (in 2-units) from center of city + // capped at 32 + int getDisCC(int x, int y) + { + assert x >= 0 && x <= getWidth()/2; + assert y >= 0 && y <= getHeight()/2; + + int xdis = Math.abs(x - centerMassX/2); + int ydis = Math.abs(y - centerMassY/2); + + int z = (xdis + ydis); + if (z > 32) + return 32; + else + return z; + } + + void mapScan(int x0, int x1) + { + MapScanner scanner = new MapScanner(this); + + for (int x = x0; x < x1; x++) + { + scanner.xpos = x; + + for (int y = 0; y < getHeight(); y++) + { + scanner.ypos = y; + scanner.cchr = map[y][x]; + scanner.scanTile(); + } + } + } + + void generateShip() + { + int edge = PRNG.nextInt(4); + + if (edge == 0) { + for (int x = 4; x < getWidth() - 2; x++) { + if (getTile(x,0) == CHANNEL) { + makeShipAt(x, 0, ShipSprite.NORTH_EDGE); + return; + } + } + } + else if (edge == 1) { + for (int y = 1; y < getHeight() - 2; y++) { + if (getTile(0,y) == CHANNEL) { + makeShipAt(0, y, ShipSprite.EAST_EDGE); + return; + } + } + } + else if (edge == 2) { + for (int x = 4; x < getWidth() - 2; x++) { + if (getTile(x, getHeight()-1) == CHANNEL) { + makeShipAt(x, getHeight()-1, ShipSprite.SOUTH_EDGE); + return; + } + } + } + else { + for (int y = 1; y < getHeight() - 2; y++) { + if (getTile(getWidth()-1, y) == CHANNEL) { + makeShipAt(getWidth()-1, y, ShipSprite.EAST_EDGE); + return; + } + } + } + } + + Sprite getSprite(SpriteKind kind) + { + for (Sprite s : sprites) { + if (s.kind == kind) + return s; + } + return null; + } + + boolean hasSprite(SpriteKind kind) + { + return getSprite(kind) != null; + } + + void makeShipAt(int xpos, int ypos, int edge) + { + assert !hasSprite(SpriteKind.SHI); + + sprites.add(new ShipSprite(this, xpos, ypos, edge)); + } + + void generateCopter(int xpos, int ypos) + { + if (!hasSprite(SpriteKind.COP)) { + sprites.add(new HelicopterSprite(this, xpos, ypos)); + } + } + + void generatePlane(int xpos, int ypos) + { + if (!hasSprite(SpriteKind.AIR)) { + sprites.add(new AirplaneSprite(this, xpos, ypos)); + } + } + + void generateTrain(int xpos, int ypos) + { + if (totalPop > 20 && + !hasSprite(SpriteKind.TRA) && + PRNG.nextInt(26) == 0) + { + sprites.add(new TrainSprite(this, xpos, ypos)); + } + } + + Stack powerPlants = new Stack(); + + // counts the population in a certain type of residential zone + int doFreePop(int xpos, int ypos) + { + int count = 0; + + for (int x = xpos - 1; x <= xpos + 1; x++) + { + for (int y = ypos - 1; y <= ypos + 1; y++) + { + if (testBounds(x,y)) + { + char loc = (char) (map[y][x] & LOMASK); + if (loc >= LHTHR && loc <= HHTHR) + count++; + } + } + } + + return count; + } + + // counts the population in a certain type of residential zone + // a.k.a. RZPop + int residentialZonePop(char tile) + { + int czDen = ((tile - RZB) / 9) % 4; + return czDen * 8 + 16; + } + + int commercialZonePop(int tile) + { + if (tile == COMCLR) + return 0; + + int czDen = ((tile - CZB) / 9) % 5 + 1; + return czDen; + } + + int industrialZonePop(int tile) + { + if (tile == INDCLR) + return 0; + + int czDen = ((tile - IZB) / 9) % 4 + 1; + return czDen; + } + + // called every several cycles; this takes the census data collected in this + // cycle and records it to the history + // + void takeCensus() + { + int resMax = 0; + int comMax = 0; + int indMax = 0; + + for (int i = 118; i >= 0; i--) + { + if (history.res[i] > resMax) + resMax = history.res[i]; + if (history.com[i] > comMax) + comMax = history.res[i]; + if (history.ind[i] > indMax) + indMax = history.ind[i]; + + history.res[i + 1] = history.res[i]; + history.com[i + 1] = history.com[i]; + history.ind[i + 1] = history.ind[i]; + history.crime[i + 1] = history.crime[i]; + history.pollution[i + 1] = history.pollution[i]; + history.money[i + 1] = history.money[i]; + } + + history.resMax = resMax; + history.comMax = comMax; + history.indMax = indMax; + + //graph10max = Math.max(resMax, Math.max(comMax, indMax)); + + history.res[0] = resPop / 8; + history.com[0] = comPop; + history.ind[0] = indPop; + + crimeRamp += (crimeAverage - crimeRamp) / 4; + history.crime[0] = Math.min(255, crimeRamp); + + polluteRamp += (pollutionAverage - polluteRamp) / 4; + history.pollution[0] = Math.min(255, polluteRamp); + + int moneyScaled = cashFlow / 20 + 128; + if (moneyScaled < 0) + moneyScaled = 0; + if (moneyScaled > 255) + moneyScaled = 255; + history.money[0] = moneyScaled; + + history.cityTime = cityTime; + + if (hospitalCount < resPop / 256) + { + needHospital = 1; + } + else if (hospitalCount > resPop / 256) + { + needHospital = -1; + } + else + { + needHospital = 0; + } + + if (churchCount < resPop / 256) + { + needChurch = 1; + } + else if (churchCount > resPop / 256) + { + needChurch = -1; + } + else + { + needChurch = 0; + } + } + + void takeCensus2() + { + // update long term graphs + int resMax = 0; + int comMax = 0; + int indMax = 0; + + for (int i = 238; i >= 120; i--) + { + if (history.res[i] > resMax) + resMax = history.res[i]; + if (history.com[i] > comMax) + comMax = history.res[i]; + if (history.ind[i] > indMax) + indMax = history.ind[i]; + + history.res[i + 1] = history.res[i]; + history.com[i + 1] = history.com[i]; + history.ind[i + 1] = history.ind[i]; + history.crime[i + 1] = history.crime[i]; + history.pollution[i + 1] = history.pollution[i]; + history.money[i + 1] = history.money[i]; + } + + history.res[120] = resPop / 8; + history.com[120] = comPop; + history.ind[120] = indPop; + history.crime[120] = history.crime[0]; + history.pollution[120] = history.pollution[0]; + history.money[120] = history.money[0]; + } + + /** Road/rail maintenance cost multiplier, for various difficulty settings. + */ + static final double [] RLevels = { 0.7, 0.9, 1.2 }; + + /** Tax income multiplier, for various difficulty settings. + */ + static final double [] FLevels = { 1.4, 1.2, 0.8 }; + + void collectTaxPartial() + { + BudgetNumbers b = generateBudget(); + + taxFund += b.taxIncome; + roadFundEscrow -= b.roadFunded; + fireFundEscrow -= b.fireFunded; + policeFundEscrow -= b.policeFunded; + + taxEffect = b.taxRate; + roadEffect = b.roadRequest != 0 ? + (int)Math.floor(32.0 * (double)b.roadFunded / (double)b.roadRequest) : + 32; + policeEffect = b.policeRequest != 0 ? + (int)Math.floor(1000.0 * (double)b.policeFunded / (double)b.policeRequest) : + 1000; + fireEffect = b.fireRequest != 0 ? + (int)Math.floor(1000.0 * (double)b.fireFunded / (double)b.fireRequest) : + 1000; + } + + public static class FinancialHistory + { + public int cityTime; + public int totalFunds; + public int taxIncome; + public int operatingExpenses; + } + public ArrayList financialHistory = new ArrayList<>(); + + void collectTax() + { + int revenue = taxFund / TAXFREQ; + int expenses = -(roadFundEscrow + fireFundEscrow + policeFundEscrow) / TAXFREQ; + + FinancialHistory hist = new FinancialHistory(); + hist.cityTime = cityTime; + hist.taxIncome = revenue; + hist.operatingExpenses = expenses; + + cashFlow = revenue - expenses; + spend(-cashFlow); + + hist.totalFunds = totalFunds; + financialHistory.add(0,hist); + + taxFund = 0; + roadFundEscrow = 0; + fireFundEscrow = 0; + policeFundEscrow = 0; + } + + public BudgetNumbers generateBudget() + { + BudgetNumbers b = new BudgetNumbers(); + b.taxRate = Math.max(0, cityTax); + b.roadPercent = Math.max(0.0, roadPercent); + b.firePercent = Math.max(0.0, firePercent); + b.policePercent = Math.max(0.0, policePercent); + + b.previousBalance = totalFunds; + b.taxIncome = (int)Math.round(totalPop * landValueAverage / 120 * b.taxRate * FLevels[gameLevel]); + assert b.taxIncome >= 0; + + b.roadRequest = (int)Math.round((roadTotal + railTotal * 2) * RLevels[gameLevel]); + b.fireRequest = 100 * fireStationCount; + b.policeRequest = 100 * policeCount; + + b.roadFunded = (int)Math.round(b.roadRequest * b.roadPercent); + b.fireFunded = (int)Math.round(b.fireRequest * b.firePercent); + b.policeFunded = (int)Math.round(b.policeRequest * b.policePercent); + + int yumDuckets = totalFunds + b.taxIncome; + assert yumDuckets >= 0; + + if (yumDuckets >= b.roadFunded) + { + yumDuckets -= b.roadFunded; + if (yumDuckets >= b.fireFunded) + { + yumDuckets -= b.fireFunded; + if (yumDuckets >= b.policeFunded) + { + yumDuckets -= b.policeFunded; + } + else + { + assert b.policeRequest != 0; + + b.policeFunded = yumDuckets; + b.policePercent = (double)b.policeFunded / (double)b.policeRequest; + yumDuckets = 0; + } + } + else + { + assert b.fireRequest != 0; + + b.fireFunded = yumDuckets; + b.firePercent = (double)b.fireFunded / (double)b.fireRequest; + b.policeFunded = 0; + b.policePercent = 0.0; + yumDuckets = 0; + } + } + else + { + assert b.roadRequest != 0; + + b.roadFunded = yumDuckets; + b.roadPercent = (double)b.roadFunded / (double)b.roadRequest; + b.fireFunded = 0; + b.firePercent = 0.0; + b.policeFunded = 0; + b.policePercent = 0.0; + } + + b.operatingExpenses = b.roadFunded + b.fireFunded + b.policeFunded; + b.newBalance = b.previousBalance + b.taxIncome - b.operatingExpenses; + + return b; + } + + /** + * The three main types of zones found in Micropolis. + */ + static enum ZoneType + { + RESIDENTIAL, COMMERCIAL, INDUSTRIAL; + } + + TrafficGen traffic = new TrafficGen(this); + + /** + * @return 1 if traffic "passed", 0 if traffic "failed", -1 if no roads found + */ + int makeTraffic(int xpos, int ypos, ZoneType zoneType) + { + traffic.mapX = xpos; + traffic.mapY = ypos; + traffic.sourceZone = zoneType; + return traffic.makeTraffic(); + } + + int getPopulationDensity(int xpos, int ypos) + { + return popDensity[ypos/2][xpos/2]; + } + + void doMeltdown(int xpos, int ypos) + { + meltdownLocation = new CityLocation(xpos, ypos); + + makeExplosion(xpos - 1, ypos - 1); + makeExplosion(xpos - 1, ypos + 2); + makeExplosion(xpos + 2, ypos - 1); + makeExplosion(xpos + 2, ypos + 2); + + for (int x = xpos - 1; x < xpos + 3; x++) { + for (int y = ypos - 1; y < ypos + 3; y++) { + setTile(x, y, (char)(FIRE + PRNG.nextInt(4) + ANIMBIT)); + } + } + + for (int z = 0; z < 200; z++) { + int x = xpos - 20 + PRNG.nextInt(41); + int y = ypos - 15 + PRNG.nextInt(31); + if (!testBounds(x,y)) + continue; + + int t = map[y][x]; + if ((t & ZONEBIT) != 0) + continue; + if ((t & BURNBIT) != 0 || t == DIRT) { + setTile(x, y, RADTILE); + } + } + + clearMes(); + sendMessageAtPic(MicropolisMessage.MELTDOWN_REPORT, xpos, ypos); + } + + static final int [] MltdwnTab = { 30000, 20000, 10000 }; + + void loadHistoryArray(int [] array, DataInputStream dis) + throws IOException + { + for (int i = 0; i < 240; i++) + { + array[i] = dis.readShort(); + } + } + + void writeHistoryArray(int [] array, DataOutputStream out) + throws IOException + { + for (int i = 0; i < 240; i++) + { + out.writeShort(array[i]); + } + } + + void loadMisc(DataInputStream dis) + throws IOException + { + dis.readShort(); //[0]... ignored? + dis.readShort(); //[1] externalMarket, ignored + resPop = dis.readShort(); //[2-4] populations + comPop = dis.readShort(); + indPop = dis.readShort(); + resValve = dis.readShort(); //[5-7] valves + comValve = dis.readShort(); + indValve = dis.readShort(); + cityTime = dis.readInt(); //[8-9] city time + crimeRamp = dis.readShort(); //[10] + polluteRamp = dis.readShort(); + landValueAverage = dis.readShort(); //[12] + crimeAverage = dis.readShort(); + pollutionAverage = dis.readShort(); //[14] + gameLevel = dis.readShort(); + evaluation.cityClass = dis.readShort(); //[16] + evaluation.cityScore = dis.readShort(); + + for (int i = 18; i < 50; i++) + { + dis.readShort(); + } + + totalFunds = dis.readInt(); //[50-51] total funds + autoBulldoze = dis.readShort() != 0; //52 + autoBudget = dis.readShort() != 0; + autoGo = dis.readShort() != 0; //54 + dis.readShort(); // userSoundOn (this setting not saved to game file + // in this edition of the game) + cityTax = dis.readShort(); //56 + taxEffect = cityTax; + int simSpeedAsInt = dis.readShort(); + if (simSpeedAsInt >= 0 || simSpeedAsInt <= 4) + simSpeed = Speed.values()[simSpeedAsInt]; + else + simSpeed = Speed.NORMAL; + + // read budget numbers, convert them to percentages + // + long n = dis.readInt(); //58,59... police percent + policePercent = (double)n / 65536.0; + n = dis.readInt(); //60,61... fire percent + firePercent = (double)n / 65536.0; + n = dis.readInt(); //62,63... road percent + roadPercent = (double)n / 65536.0; + + for (int i = 64; i < 120; i++) + { + dis.readShort(); + } + + if (cityTime < 0) { cityTime = 0; } + if (cityTax < 0 || cityTax > 20) { cityTax = 7; } + if (gameLevel < 0 || gameLevel > 2) { gameLevel = 0; } + if (evaluation.cityClass < 0 || evaluation.cityClass > 5) { evaluation.cityClass = 0; } + if (evaluation.cityScore < 1 || evaluation.cityScore > 999) { evaluation.cityScore = 500; } + + resCap = false; + comCap = false; + indCap = false; + } + + void writeMisc(DataOutputStream out) + throws IOException + { + out.writeShort(0); + out.writeShort(0); + out.writeShort(resPop); + out.writeShort(comPop); + out.writeShort(indPop); + out.writeShort(resValve); + out.writeShort(comValve); + out.writeShort(indValve); + //8 + out.writeInt(cityTime); + out.writeShort(crimeRamp); + out.writeShort(polluteRamp); + //12 + out.writeShort(landValueAverage); + out.writeShort(crimeAverage); + out.writeShort(pollutionAverage); + out.writeShort(gameLevel); + //16 + out.writeShort(evaluation.cityClass); + out.writeShort(evaluation.cityScore); + //18 + for (int i = 18; i < 50; i++) { + out.writeShort(0); + } + //50 + out.writeInt(totalFunds); + out.writeShort(autoBulldoze ? 1 : 0); + out.writeShort(autoBudget ? 1 : 0); + //54 + out.writeShort(autoGo ? 1 : 0); + out.writeShort(1); //userSoundOn + out.writeShort(cityTax); + out.writeShort(simSpeed.ordinal()); + + //58 + out.writeInt((int)(policePercent * 65536)); + out.writeInt((int)(firePercent * 65536)); + out.writeInt((int)(roadPercent * 65536)); + + //64 + for (int i = 64; i < 120; i++) { + out.writeShort(0); + } + } + + void loadMap(DataInputStream dis) + throws IOException + { + for (int x = 0; x < DEFAULT_WIDTH; x++) + { + for (int y = 0; y < DEFAULT_HEIGHT; y++) + { + map[y][x] = (char) dis.readShort(); + } + } + } + + void writeMap(DataOutputStream out) + throws IOException + { + for (int x = 0; x < DEFAULT_WIDTH; x++) + { + for (int y = 0; y < DEFAULT_HEIGHT; y++) + { + out.writeShort(map[y][x]); + } + } + } + + public void load(File filename) + throws IOException + { + load(new FileInputStream(filename)); + } + + void checkPowerMap() + { + coalCount = 0; + nuclearCount = 0; + + powerPlants.clear(); + for (int y = 0; y < map.length; y++) { + for (int x = 0; x < map[y].length; x++) { + int tile = getTile(x,y); + if ((tile & LOMASK) == NUCLEAR) { + nuclearCount++; + powerPlants.add(new CityLocation(x,y)); + } + else if ((tile & LOMASK) == POWERPLANT) { + coalCount++; + powerPlants.add(new CityLocation(x,y)); + } + } + } + + powerScan(); + newPower = true; + + assert powerPlants.isEmpty(); + } + + public void load(InputStream inStream) + throws IOException + { + DataInputStream dis = new DataInputStream(inStream); + loadHistoryArray(history.res, dis); + loadHistoryArray(history.com, dis); + loadHistoryArray(history.ind, dis); + loadHistoryArray(history.crime, dis); + loadHistoryArray(history.pollution, dis); + loadHistoryArray(history.money, dis); + loadMisc(dis); + loadMap(dis); + dis.close(); + + checkPowerMap(); + + fireWholeMapChanged(); + fireDemandChanged(); + fireFundsChanged(); + } + + public void save(File filename) + throws IOException + { + save(new FileOutputStream(filename)); + } + + public void save(OutputStream outStream) + throws IOException + { + DataOutputStream out = new DataOutputStream(outStream); + writeHistoryArray(history.res, out); + writeHistoryArray(history.com, out); + writeHistoryArray(history.ind, out); + writeHistoryArray(history.crime, out); + writeHistoryArray(history.pollution, out); + writeHistoryArray(history.money, out); + writeMisc(out); + writeMap(out); + out.close(); + } + + public void toggleAutoBudget() + { + autoBudget = !autoBudget; + fireOptionsChanged(); + } + + public void toggleAutoBulldoze() + { + autoBulldoze = !autoBulldoze; + fireOptionsChanged(); + } + + public void toggleDisasters() + { + noDisasters = !noDisasters; + fireOptionsChanged(); + } + + public void setSpeed(Speed newSpeed) + { + simSpeed = newSpeed; + fireOptionsChanged(); + } + + public void animate() + { + moveObjects(); + animateTiles(); + } + + public Sprite [] allSprites() + { + return sprites.toArray(new Sprite[0]); + } + + void moveObjects() + { + for (Sprite sprite : allSprites()) + { + sprite.move(); + + if (sprite.frame == 0) { + sprites.remove(sprite); + } + } + } + + void animateTiles() + { + for (int y = 0; y < map.length; y++) + { + for (int x = 0; x < map[y].length; x++) + { + char tilevalue = map[y][x]; + if ((tilevalue & ANIMBIT) != 0) + { + int flags = tilevalue & ALLBITS; + setTile(x, y, (char) + (Animate.aniTile[tilevalue & LOMASK] | flags) + ); + } + } + } + } + + public int getCityPopulation() + { + return lastCityPop; + } + + void makeSound(int x, int y, Sound sound) + { + fireCitySound(sound, new CityLocation(x,y)); + } + + public void makeEarthquake() + { + makeSound(centerMassX, centerMassY, Sound.EXPLOSION_LOW); + fireEarthquakeStarted(); + + sendMessageAtPic(MicropolisMessage.EARTHQUAKE_REPORT, centerMassX, centerMassY); + int time = PRNG.nextInt(701) + 300; + for (int z = 0; z < time; z++) { + int x = PRNG.nextInt(getWidth()); + int y = PRNG.nextInt(getHeight()); + assert testBounds(x, y); + + if (TileConstants.isVulnerable(getTile(x, y))) { + if (PRNG.nextInt(4) != 0) { + setTile(x, y, (char)(RUBBLE + BULLBIT + PRNG.nextInt(4))); + } else { + setTile(x, y, (char)(FIRE + ANIMBIT + PRNG.nextInt(8))); + } + } + } + } + + void setFire() + { + int x = PRNG.nextInt(getWidth()); + int y = PRNG.nextInt(getHeight()); + int t = getTile(x, y); + + if (TileConstants.isArsonable(t)) { + setTile(x, y, (char)(FIRE + ANIMBIT + PRNG.nextInt(8))); + crashLocation = new CityLocation(x, y); + sendMessageAtPic(MicropolisMessage.FIRE_REPORT, x, y); + } + } + + public void makeFire() + { + // forty attempts at finding place to start fire + for (int t = 0; t < 40; t++) + { + int x = PRNG.nextInt(getWidth()); + int y = PRNG.nextInt(getHeight()); + int tile = map[y][x]; + if ((tile & ZONEBIT) == 0 && (tile & BURNBIT) != 0) + { + tile &= LOMASK; + if (tile > 21 && tile < LASTZONE) { + setTile(x, y, (char)(FIRE + ANIMBIT + PRNG.nextInt(8))); + sendMessageAt(MicropolisMessage.FIRE_REPORT, x, y); + return; + } + } + } + } + + /** + * Force a meltdown to occur. + * @return true if a metldown was initiated. + */ + public boolean makeMeltdown() + { + ArrayList candidates = new ArrayList<>(); + for (int y = 0; y < map.length; y++) { + for (int x = 0; x < map[y].length; x++) { + if ((map[y][x] & LOMASK) == NUCLEAR) { + candidates.add(new CityLocation(x,y)); + } + } + } + + if (candidates.isEmpty()) { + // tell caller that no nuclear plants were found + return false; + } + + int i = PRNG.nextInt(candidates.size()); + CityLocation p = candidates.get(i); + doMeltdown(p.x, p.y); + return true; + } + + public void makeMonster() + { + MonsterSprite monster = (MonsterSprite) getSprite(SpriteKind.GOD); + if (monster != null) { + // already have a monster in town + monster.soundCount = 1; + monster.count = 1000; + monster.flag = false; + monster.destX = pollutionMaxLocationX; + monster.destY = pollutionMaxLocationY; + return; + } + + // try to find a suitable starting spot for monster + + for (int i = 0; i < 300; i++) { + int x = PRNG.nextInt(getWidth() - 19) + 10; + int y = PRNG.nextInt(getHeight() - 9) + 5; + int t = getTile(x, y); + if ((t & LOMASK) == RIVER) { + makeMonsterAt(x, y); + return; + } + } + + // no "nice" location found, just start in center of map then + makeMonsterAt(getWidth()/2, getHeight()/2); + } + + void makeMonsterAt(int xpos, int ypos) + { + assert !hasSprite(SpriteKind.GOD); + sprites.add(new MonsterSprite(this, xpos, ypos)); + } + + public void makeTornado() + { + TornadoSprite tornado = (TornadoSprite) getSprite(SpriteKind.TOR); + if (tornado != null) { + // already have a tornado, so extend the length of the + // existing tornado + tornado.count = 200; + return; + } + + //FIXME- this is not exactly like the original code + int xpos = PRNG.nextInt(getWidth() - 19) + 10; + int ypos = PRNG.nextInt(getHeight() - 19) + 10; + sprites.add(new TornadoSprite(this, xpos, ypos)); + sendMessageAtPic(MicropolisMessage.TORNADO_REPORT, xpos, ypos); + } + + public void makeFlood() + { + final int [] DX = { 0, 1, 0, -1 }; + final int [] DY = { -1, 0, 1, 0 }; + + for (int z = 0; z < 300; z++) { + int x = PRNG.nextInt(getWidth()); + int y = PRNG.nextInt(getHeight()); + int tile = map[y][x] & LOMASK; + if (isRiverEdge(tile)) + { + for (int t = 0; t < 4; t++) { + int xx = x + DX[t]; + int yy = y + DY[t]; + if (testBounds(xx,yy)) { + int c = map[yy][xx]; + if (isFloodable(c)) { + setTile(xx, yy, FLOOD); + floodCnt = 30; + sendMessageAtPic(MicropolisMessage.FLOOD_REPORT, xx, yy); + floodX = xx; + floodY = yy; + return; + } + } + } + } + } + } + + /** + * Makes all component tiles of a zone bulldozable. + * Should be called whenever the key zone tile of a zone is destroyed, + * since otherwise the user would no longer have a way of destroying + * the zone. + */ + void fireZone(int xpos, int ypos, int zoneTile) + { + rateOGMem[ypos/8][xpos/8] -= 20; + + int sz = TileConstants.getZoneSizeFor(zoneTile); + for (int x = 0; x < sz; x++) { + for (int y = 0; y < sz; y++) { + int xtem = xpos - 1 + x; + int ytem = ypos - 1 + y; + if (!testBounds(xtem, ytem)) + continue; + + int t = getTile(xtem, ytem); + if ((t & LOMASK) >= ROADBASE) { + setTile(xtem, ytem, (char)(t | BULLBIT)); + } + } + } + } + + void makeExplosion(int xpos, int ypos) + { + makeExplosionAt(xpos*16+8, ypos*16+8); + } + + /** + * Uses x,y coordinates as 1/16th-length tiles. + */ + void makeExplosionAt(int x, int y) + { + sprites.add(new ExplosionSprite(this, x, y)); + } + + void checkGrowth() + { + if (cityTime % 4 == 0) { + int newPop = (resPop + comPop * 8 + indPop * 8) * 20; + if (lastCityPop != 0) { + MicropolisMessage z = null; + if (lastCityPop < 500000 && newPop >= 500000) { + z = MicropolisMessage.POP_500K_REACHED; + } else if (lastCityPop < 100000 && newPop >= 100000) { + z = MicropolisMessage.POP_100K_REACHED; + } else if (lastCityPop < 50000 && newPop >= 50000) { + z = MicropolisMessage.POP_50K_REACHED; + } else if (lastCityPop < 10000 && newPop >= 10000) { + z = MicropolisMessage.POP_10K_REACHED; + } else if (lastCityPop < 2000 && newPop >= 2000) { + z = MicropolisMessage.POP_2K_REACHED; + } + if (z != null) { + sendMessage(z, true); + } + } + lastCityPop = newPop; + } + } + + void doMessages() + { + //MORE (scenario stuff) + + checkGrowth(); + + int totalZoneCount = resZoneCount + comZoneCount + indZoneCount; + int powerCount = nuclearCount + coalCount; + + int z = cityTime % 64; + switch (z) { + case 1: + if (totalZoneCount / 4 >= resZoneCount) { + sendMessage(MicropolisMessage.NEED_RES); + } + break; + case 5: + if (totalZoneCount / 8 >= comZoneCount) { + sendMessage(MicropolisMessage.NEED_COM); + } + break; + case 10: + if (totalZoneCount / 8 >= indZoneCount) { + sendMessage(MicropolisMessage.NEED_IND); + } + break; + case 14: + if (totalZoneCount > 10 && totalZoneCount * 2 > roadTotal) { + sendMessage(MicropolisMessage.NEED_ROADS); + } + break; + case 18: + if (totalZoneCount > 50 && totalZoneCount > railTotal) { + sendMessage(MicropolisMessage.NEED_RAILS); + } + break; + case 22: + if (totalZoneCount > 10 && powerCount == 0) { + sendMessage(MicropolisMessage.NEED_POWER); + } + break; + case 26: + resCap = (resPop > 500 && stadiumCount == 0); + if (resCap) { + sendMessage(MicropolisMessage.NEED_STADIUM); + } + break; + case 28: + indCap = (indPop > 70 && seaportCount == 0); + if (indCap) { + sendMessage(MicropolisMessage.NEED_SEAPORT); + } + break; + case 30: + comCap = (comPop > 100 && airportCount == 0); + if (comCap) { + sendMessage(MicropolisMessage.NEED_AIRPORT); + } + break; + case 32: + int TM = unpoweredZoneCount + poweredZoneCount; + if (TM != 0) { + if ((double)poweredZoneCount / (double)TM < 0.7) { + sendMessage(MicropolisMessage.BLACKOUTS); + } + } + break; + case 35: + if (pollutionAverage > 60) { // FIXME, consider changing threshold to 80 + sendMessage(MicropolisMessage.HIGH_POLLUTION, true); + } + break; + case 42: + if (crimeAverage > 100) { + sendMessage(MicropolisMessage.HIGH_CRIME, true); + } + break; + case 45: + if (totalPop > 60 && fireStationCount == 0) { + sendMessage(MicropolisMessage.NEED_FIRESTATION); + } + break; + case 48: + if (totalPop > 60 && policeCount == 0) { + sendMessage(MicropolisMessage.NEED_POLICE); + } + break; + case 51: + if (cityTax > 12) { + sendMessage(MicropolisMessage.HIGH_TAXES); + } + break; + case 54: + if (roadEffect < 20 && roadTotal > 30) { + sendMessage(MicropolisMessage.ROADS_NEED_FUNDING); + } + break; + case 57: + if (fireEffect < 700 && totalPop > 20) { + sendMessage(MicropolisMessage.FIRE_NEED_FUNDING); + } + break; + case 60: + if (policeEffect < 700 && totalPop > 20) { + sendMessage(MicropolisMessage.POLICE_NEED_FUNDING); + } + break; + case 63: + if (trafficAverage > 60) { + sendMessage(MicropolisMessage.HIGH_TRAFFIC); + } + break; + default: + //nothing + } + } + + void clearMes() + { + //TODO. + // What this does in the original code is clears the 'last message' + // properties, ensuring that the next message will be delivered even + // if it is a repeat. + } + + void sendMessage(MicropolisMessage message) + { + fireCityMessage(message, null, false); + } + + void sendMessage(MicropolisMessage message, boolean isPic) + { + fireCityMessage(message, null, true); + } + + void sendMessageAt(MicropolisMessage message, int x, int y) + { + fireCityMessage(message, new CityLocation(x,y), false); + } + + void sendMessageAtPic(MicropolisMessage message, int x, int y) + { + fireCityMessage(message, new CityLocation(x,y), true); + } + + public ZoneStatus queryZoneStatus(int xpos, int ypos) + { + ZoneStatus zs = new ZoneStatus(); + zs.building = getBuildingId(getTile(xpos, ypos)); + + int z; + z = (popDensity[ypos/2][xpos/2] / 64) % 4; + zs.popDensity = z + 1; + + z = landValueMem[ypos/2][xpos/2]; + z = z < 30 ? 4 : z < 80 ? 5 : z < 150 ? 6 : 7; + zs.landValue = z + 1; + + z = ((crimeMem[ypos/2][xpos/2] / 64) % 4) + 8; + zs.crimeLevel = z + 1; + + z = Math.max(13,((pollutionMem[ypos/2][xpos/2] / 64) % 4) + 12); + zs.pollution = z + 1; + + z = rateOGMem[ypos/8][xpos/8]; + z = z < 0 ? 16 : z == 0 ? 17 : z <= 100 ? 18 : 19; + zs.growthRate = z + 1; + + return zs; + } + + public int getResValve() + { + return resValve; + } + + public int getComValve() + { + return comValve; + } + + public int getIndValve() + { + return indValve; + } + + public void setGameLevel(int newLevel) + { + assert GameLevel.isValid(newLevel); + + int origFunds = GameLevel.getStartingFunds(gameLevel); + int newFunds = GameLevel.getStartingFunds(newLevel); + int delta = origFunds - newFunds; + + gameLevel = newLevel; + fireOptionsChanged(); + + if (totalFunds > delta) { + spend(delta); + } + } +} diff --git a/src/micropolisj/engine/MicropolisMessage.java b/src/micropolisj/engine/MicropolisMessage.java new file mode 100644 index 0000000..ae1669c --- /dev/null +++ b/src/micropolisj/engine/MicropolisMessage.java @@ -0,0 +1,64 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +/** + * Enumeration of every possible message for the user generated by the game engine. + */ +public enum MicropolisMessage +{ + //orig_num generated last tested/verified + NEED_RES, // 1 doMessages 1/19 + NEED_COM, // 2 doMessages 1/19 + NEED_IND, // 3 doMessages 1/19 + NEED_ROADS, // 4 doMessages 1/19 + NEED_RAILS, // 5 doMessages 1/20 + NEED_POWER, // 6 doMessages 1/19 + NEED_STADIUM, // 7 doMessages 1/20 + NEED_SEAPORT, // 8 doMessages 1/20 + NEED_AIRPORT, // 9 doMessages + HIGH_POLLUTION, // 10 doMessages 1/20 + HIGH_CRIME, // 11 doMessages 1/19 + HIGH_TRAFFIC, // 12 doMessages 1/20 + NEED_FIRESTATION, // 13 doMessages 1/19 + NEED_POLICE, // 14 doMessages 1/19 + BLACKOUTS, // 15 doMessages 1/19 + HIGH_TAXES, // 16 doMessages 1/19 + ROADS_NEED_FUNDING, // 17 doMessages + FIRE_NEED_FUNDING, // 18 doMessages + POLICE_NEED_FUNDING, // 19 doMessages + FIRE_REPORT, // 20 + MONSTER_REPORT, + TORNADO_REPORT, + EARTHQUAKE_REPORT, // 23 makeEarthquake + PLANECRASH_REPORT, + SHIPWRECK_REPORT, + TRAIN_CRASH_REPORT, + COPTER_CRASH_REPORT, + HIGH_UNEMPLOYMENT, + OUT_OF_FUNDS_REPORT, + FIREBOMBING_REPORT, //30 + NEED_PARKS, + EXPLOSION_REPORT, + INSUFFICIENT_FUNDS, // 33 MainWindow.applyCurrentTool + BULLDOZE_FIRST, // 34 MainWindow.applyCurrentTool + POP_2K_REACHED, // 35 checkGrowth 1/19 + POP_10K_REACHED, // 36 checkGrowth + POP_50K_REACHED, // 37 checkGrowth + POP_100K_REACHED, // 38 checkGrowth + POP_500K_REACHED, // 39 checkGrowth + BROWNOUTS_REPORT, // 40 1/20 + HEAVY_TRAFFIC_REPORT, // 41 HelicopterSprite + FLOOD_REPORT, + MELTDOWN_REPORT, // 43 doMeltdown + RIOTING_REPORT, + + // added by Jason + NO_NUCLEAR_PLANTS; +} diff --git a/src/micropolisj/engine/MicropolisTool.java b/src/micropolisj/engine/MicropolisTool.java new file mode 100644 index 0000000..5c7bb55 --- /dev/null +++ b/src/micropolisj/engine/MicropolisTool.java @@ -0,0 +1,1138 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import static micropolisj.engine.TileConstants.*; + +/** + * Enumerates the various tools that can be applied to the map by the user. + * Call the tool's apply() method to actually use the tool on the map. + */ +public enum MicropolisTool +{ + BULLDOZER, + WIRE, + ROADS, + RAIL, + RESIDENTIAL, + COMMERCIAL, + INDUSTRIAL, + FIRE, + POLICE, + STADIUM, + PARK, + SEAPORT, + POWERPLANT, + NUCLEAR, + AIRPORT, + QUERY; + + public int getWidth() + { + switch(this) + { + case RESIDENTIAL: + case COMMERCIAL: + case INDUSTRIAL: + case FIRE: + case POLICE: + return 3; + + case STADIUM: + case SEAPORT: + case POWERPLANT: + case NUCLEAR: + return 4; + + case AIRPORT: + return 6; + + default: + return 1; + } + } + + public int getHeight() + { + return getWidth(); + } + + public ToolResult apply(Micropolis engine, int xpos, int ypos) + { + switch (this) + { + case BULLDOZER: + return applyBulldozer(engine, xpos, ypos); + + case RAIL: + return applyRailTool(engine, xpos, ypos); + + case ROADS: + return applyRoadTool(engine, xpos, ypos); + + case WIRE: + return applyWireTool(engine, xpos, ypos); + + case PARK: + return applyParkTool(engine, xpos, ypos); + + case RESIDENTIAL: + return apply3x3buildingTool(engine, xpos, ypos, RESBASE); + + case COMMERCIAL: + return apply3x3buildingTool(engine, xpos, ypos, COMBASE); + + case INDUSTRIAL: + return apply3x3buildingTool(engine, xpos, ypos, INDBASE); + + case FIRE: + return apply3x3buildingTool(engine, xpos, ypos, FIRESTBASE); + + case POLICE: + return apply3x3buildingTool(engine, xpos, ypos, POLICESTBASE); + + case POWERPLANT: + return apply4x4buildingTool(engine, xpos, ypos, COALBASE); + + case STADIUM: + return apply4x4buildingTool(engine, xpos, ypos, STADIUMBASE); + + case SEAPORT: + return apply4x4buildingTool(engine, xpos, ypos, PORTBASE); + + case NUCLEAR: + return apply4x4buildingTool(engine, xpos, ypos, NUCLEARBASE); + + case AIRPORT: + return apply6x6buildingTool(engine, xpos, ypos, AIRPORTBASE); + + default: + // not expected + return ToolResult.UH_OH; + } + } + + ToolResult apply3x3buildingTool(Micropolis engine, int xpos, int ypos, char tileBase) + { + int mapH = xpos - 1; + int mapV = ypos - 1; + + if (!(mapH >= 0 && mapH + 2 < engine.getWidth())) + return ToolResult.UH_OH; + if (!(mapV >= 0 && mapV + 2 < engine.getHeight())) + return ToolResult.UH_OH; + + int cost = 0; + boolean canBuild = true; + for (int rowNum = 0; rowNum <= 2; rowNum++) + { + for (int columnNum = 0; columnNum <= 2; columnNum++) + { + int x = mapH + columnNum; + int y = mapV + rowNum; + char tileValue = (char) (engine.getTile(x,y) & LOMASK); + + if (tileValue != DIRT) + { + if (engine.autoBulldoze) + { + if (canAutoBulldoze(tileValue)) + cost++; + else + canBuild = false; + } + else + canBuild = false; + } + } + } + + if (!canBuild) + return ToolResult.UH_OH; + + cost += getToolCost(); + + if (engine.totalFunds < cost) + return ToolResult.INSUFFICIENT_FUNDS; + + // take care of the money situation here + engine.spend(cost); + + for (int rowNum = 0; rowNum <= 2; rowNum++) + { + for (int columnNum = 0; columnNum <= 2; columnNum++) + { + engine.setTile(mapH + columnNum, mapV + rowNum, (char) ( + tileBase + BNCNBIT + + (columnNum == 1 && rowNum == 1 ? ZONEBIT : 0) + )); + tileBase++; + } + } + + fixBorder(engine, mapH, mapV, mapH + 2, mapV + 2); + return ToolResult.SUCCESS; + } + + ToolResult apply4x4buildingTool(Micropolis engine, int xpos, int ypos, char tileBase) + { + int mapH = xpos - 1; + int mapV = ypos - 1; + + if (!(mapH >= 0 && mapH + 3 < engine.getWidth())) + return ToolResult.UH_OH; + if (!(mapV >= 0 && mapV + 3 < engine.getHeight())) + return ToolResult.UH_OH; + + int cost = 0; + boolean canBuild = true; + for (int rowNum = 0; rowNum <= 3; rowNum++) + { + for (int columnNum = 0; columnNum <= 3; columnNum++) + { + int x = mapH + columnNum; + int y = mapV + rowNum; + char tileValue = (char) (engine.getTile(x,y) & LOMASK); + + if (tileValue != DIRT) + { + if (engine.autoBulldoze) + { + if (canAutoBulldoze(tileValue)) + cost++; + else + canBuild = false; + } + else + canBuild = false; + } + } + } + + if (!canBuild) + return ToolResult.UH_OH; + + cost += getToolCost(); + + if (engine.totalFunds < cost) + return ToolResult.INSUFFICIENT_FUNDS; + + // take care of the money situation here + engine.spend(cost); + + for (int rowNum = 0; rowNum <= 3; rowNum++) + { + for (int columnNum = 0; columnNum <= 3; columnNum++) + { + engine.setTile(mapH + columnNum, mapV + rowNum, (char) ( + tileBase + BNCNBIT + + (columnNum == 1 && rowNum == 1 ? ZONEBIT : 0) + + (columnNum == 1 && rowNum == 2 ? ANIMBIT : 0) + )); + tileBase++; + } + } + + fixBorder(engine, mapH, mapV, mapH + 3, mapV + 3); + return ToolResult.SUCCESS; + } + + ToolResult apply6x6buildingTool(Micropolis engine, int xpos, int ypos, char tileBase) + { + int mapH = xpos - 1; + int mapV = ypos - 1; + + if (!(mapH >= 0 && mapH + 5 < engine.getWidth())) + return ToolResult.UH_OH; + if (!(mapV >= 0 && mapV + 5 < engine.getHeight())) + return ToolResult.UH_OH; + + int cost = 0; + boolean canBuild = true; + for (int rowNum = 0; rowNum <= 5; rowNum++) + { + for (int columnNum = 0; columnNum <= 5; columnNum++) + { + int x = mapH + columnNum; + int y = mapV + rowNum; + char tileValue = (char) (engine.getTile(x,y) & LOMASK); + + if (tileValue != DIRT) + { + if (engine.autoBulldoze) + { + if (canAutoBulldoze(tileValue)) + cost++; + else + canBuild = false; + } + else + canBuild = false; + } + } + } + + if (!canBuild) + return ToolResult.UH_OH; + + cost += getToolCost(); + + if (engine.totalFunds < cost) + return ToolResult.INSUFFICIENT_FUNDS; + + // take care of the money situation here + engine.spend(cost); + + for (int rowNum = 0; rowNum <= 5; rowNum++) + { + for (int columnNum = 0; columnNum <= 5; columnNum++) + { + engine.setTile(mapH + columnNum, mapV + rowNum, (char) ( + tileBase + BNCNBIT + + (columnNum == 1 && rowNum == 1 ? ZONEBIT : 0) + )); + tileBase++; + } + } + + fixBorder(engine, mapH, mapV, mapH + 5, mapV + 5); + return ToolResult.SUCCESS; + } + + public int getToolCost() + { + switch (this) + { + case BULLDOZER: return 1; + case WIRE: return 5; //25 for underwater + case ROADS: return 10; //50 for over water + case RAIL: return 20; //100 for underwater + case RESIDENTIAL: return 100; + case COMMERCIAL: return 100; + case INDUSTRIAL: return 100; + case FIRE: return 500; + case POLICE: return 500; + case STADIUM: return 5000; + case PARK: return 10; + case SEAPORT: return 3000; + case POWERPLANT: return 3000; + case NUCLEAR: return 5000; + case AIRPORT: return 10000; + case QUERY: return 0; + default: + assert false; + return 1; + } + } + + static boolean canAutoBulldoze(char tileValue) + { + // can we autobulldoze this tile? + if ((tileValue >= FIRSTRIVEDGE && tileValue <= LASTRUBBLE) || + (tileValue >= POWERBASE + 2 && tileValue <= POWERBASE + 12) || + (tileValue >= TINYEXP && tileValue <= LASTTINYEXP + 2)) + { + return true; + } + else + { + return false; + } + } + + void fixBorder(Micropolis engine, int left, int top, int right, int bottom) + { + for (int x = left; x <= right; x++) + { + fixZone(engine, x, top); + fixZone(engine, x, bottom); + } + for (int y = top + 1; y <= bottom - 1; y++) + { + fixZone(engine, left, y); + fixZone(engine, right, y); + } + } + + ToolResult applyBulldozer(Micropolis engine, int xpos, int ypos) + { + if (!engine.testBounds(xpos, ypos)) + return ToolResult.UH_OH; + + char currTile = engine.getTile(xpos, ypos); + char tmp = (char)(currTile & LOMASK); + + if ((currTile & ZONEBIT) != 0) + { + // zone center bit is set + if (engine.totalFunds >= 1) + { + engine.spend(1); + switch (checkSize(tmp)) + { + case 3: + engine.makeSound(xpos, ypos, Sound.EXPLOSION_HIGH); + putRubble(engine, xpos, ypos, 3, 3); + break; + case 4: + engine.makeSound(xpos, ypos, Sound.EXPLOSION_LOW); + putRubble(engine, xpos, ypos, 4, 4); + break; + case 6: + engine.makeSound(xpos, ypos, Sound.EXPLOSION_BOTH); + putRubble(engine, xpos, ypos, 6, 6); + break; + default: + assert false; + break; + } + return ToolResult.SUCCESS; + } + else + { + return ToolResult.INSUFFICIENT_FUNDS; + } + } + else if (false && isBigZone(tmp)) + { + // The GPL Micropolis will uses a bunch of code to find + // the center of this zone, and then converts it to rubble + // the same as clicking the center of the zone. + // I prefer to make the user click the critical spot of + // the zone to destroy it. + return ToolResult.UH_OH; + } + else if (tmp == RIVER || + tmp == REDGE || + tmp == CHANNEL) + { + if (engine.totalFunds >= 6) + { + ToolResult result = layDoze(engine, xpos, ypos); + if (tmp != (engine.getTile(xpos, ypos) & LOMASK)) + { + // tile changed + engine.spend(5); + fixZone(engine, xpos, ypos); + } + return result; + } + else { + return ToolResult.INSUFFICIENT_FUNDS; + } + } + else + { + ToolResult result = layDoze(engine, xpos, ypos); + fixZone(engine, xpos, ypos); + return result; + } + } + + void autoDoze(Micropolis engine, int xpos, int ypos) + { + if (engine.autoBulldoze && engine.totalFunds > 0) + { + char tile = engine.getTile(xpos, ypos); + char ntile = neutralizeRoad(tile); + + if ((tile & BULLBIT) != 0 && + ((ntile >= TINYEXP && + ntile <= LASTTINYEXP) || + (ntile < HBRIDGE && ntile != DIRT) + ) + ) + { + engine.spend(1); + engine.setTile(xpos, ypos, DIRT); + } + } + } + + ToolResult applyRailTool(Micropolis engine, int xpos, int ypos) + { + if (!engine.testBounds(xpos, ypos)) + return ToolResult.UH_OH; + + autoDoze(engine, xpos, ypos); + ToolResult result = layRail(engine, xpos, ypos); + fixZone(engine, xpos, ypos); + return result; + } + + ToolResult applyRoadTool(Micropolis engine, int xpos, int ypos) + { + if (!engine.testBounds(xpos, ypos)) + return ToolResult.UH_OH; + + autoDoze(engine, xpos, ypos); + ToolResult result = layRoad(engine, xpos, ypos); + fixZone(engine, xpos, ypos); + return result; + } + + ToolResult applyParkTool(Micropolis engine, int xpos, int ypos) + { + if (!engine.testBounds(xpos, ypos)) + return ToolResult.UH_OH; + + int cost = getToolCost(); + + if (engine.getTile(xpos, ypos) != DIRT) { + // some sort of bulldozing is necessary + if (!engine.autoBulldoze) { + return ToolResult.UH_OH; + } + + if (isRubble(engine.getTile(xpos, ypos))) { + // this tile can be auto-bulldozed + cost++; + } + else { + // cannot be auto-bulldozed + return ToolResult.UH_OH; + } + } + + if (engine.totalFunds < cost) { + return ToolResult.INSUFFICIENT_FUNDS; + } + + int z = engine.PRNG.nextInt(5); + int tile; + if (z < 4) { + tile = (WOODS2 + z) | BURNBIT | BULLBIT; + } else { + tile = FOUNTAIN | BURNBIT | BULLBIT | ANIMBIT; + } + + engine.spend(cost); + engine.setTile(xpos, ypos, (char) tile); + return ToolResult.SUCCESS; + } + + ToolResult applyWireTool(Micropolis engine, int xpos, int ypos) + { + if (!engine.testBounds(xpos, ypos)) + return ToolResult.UH_OH; + + autoDoze(engine, xpos, ypos); + ToolResult result = layWire(engine, xpos, ypos); + fixZone(engine, xpos, ypos); + return result; + } + + static char neutralizeRoad(char tile) + { + tile &= LOMASK; + if (tile >= 64 && tile <= 207) + tile = (char)( (tile & 0xf) + 64 ); + return tile; + } + + private ToolResult layDoze(Micropolis engine, int xpos, int ypos) + { + if (engine.totalFunds <= 0) + return ToolResult.INSUFFICIENT_FUNDS; + + char tile = engine.getTile(xpos, ypos); + + // check dozeable bit + if ((tile & BULLBIT) == 0) + return ToolResult.NONE; + + tile = neutralizeRoad(tile); + if (isOverWater(tile)) + { + // dozing over water, replace with water. + engine.setTile(xpos, ypos, RIVER); + } + else + { + // dozing on land, replace with land. Simple, eh? + engine.setTile(xpos, ypos, DIRT); + } + + engine.spend(1); + return ToolResult.SUCCESS; + } + + private ToolResult layRail(Micropolis engine, int xpos, int ypos) + { + final int RAIL_COST = 20; + final int TUNNEL_COST = 100; + + int cost = RAIL_COST; + if (engine.totalFunds < cost) + return ToolResult.INSUFFICIENT_FUNDS; + + char tile = (char) (engine.getTile(xpos, ypos) & LOMASK); + switch (tile) + { + case DIRT: //rail on dirt + engine.setTile(xpos, ypos, (char) (LHRAIL | BULLBIT | BURNBIT)); + break; + + case RIVER: // rail on water + case REDGE: + case CHANNEL: + + cost = TUNNEL_COST; + if (engine.totalFunds < cost) + return ToolResult.INSUFFICIENT_FUNDS; + + if (xpos + 1 < engine.getWidth()) + { + char eTile = neutralizeRoad(engine.getTile(xpos + 1, ypos)); + if (eTile == RAILHPOWERV || + eTile == HRAIL || + (eTile >= LHRAIL && eTile <= HRAILROAD)) + { + engine.setTile(xpos, ypos, (char) (HRAIL | BULLBIT)); + break; + } + } + + if (xpos > 0) + { + char wTile = neutralizeRoad(engine.getTile(xpos - 1, ypos)); + if (wTile == RAILHPOWERV || + wTile == HRAIL || + (wTile > VRAIL && wTile < VRAILROAD)) + { + engine.setTile(xpos, ypos, (char) (HRAIL | BULLBIT)); + break; + } + } + + if (ypos + 1 < engine.getHeight()) + { + char sTile = neutralizeRoad(engine.getTile(xpos, ypos + 1)); + if (sTile == RAILVPOWERH || + sTile == VRAILROAD || + (sTile > HRAIL && sTile < HRAILROAD)) + { + engine.setTile(xpos, ypos, (char) (VRAIL | BULLBIT)); + break; + } + } + + if (ypos > 0) + { + char nTile = neutralizeRoad(engine.getTile(xpos, ypos - 1)); + if (nTile == RAILVPOWERH || + nTile == VRAILROAD || + (nTile > HRAIL && nTile < HRAILROAD)) + { + engine.setTile(xpos, ypos, (char) (VRAIL | BULLBIT)); + break; + } + } + + // cannot do road here + return ToolResult.NONE; + + case LHPOWER: // rail on power + engine.setTile(xpos, ypos, (char) (RAILVPOWERH | CONDBIT | BURNBIT | BULLBIT)); + break; + + case LVPOWER: // rail on power + engine.setTile(xpos, ypos, (char) (RAILHPOWERV | CONDBIT | BURNBIT | BULLBIT)); + break; + + case TileConstants.ROADS: // rail on road (case 1) + engine.setTile(xpos, ypos, (char) (VRAILROAD | BURNBIT | BULLBIT)); + break; + + case ROADS2: // rail on road (case 2) + engine.setTile(xpos, ypos, (char) (HRAILROAD | BURNBIT | BULLBIT)); + break; + + default: + return ToolResult.NONE; + } + + engine.spend(cost); + return ToolResult.SUCCESS; + } + + private ToolResult layRoad(Micropolis engine, int xpos, int ypos) + { + final int ROAD_COST = 10; + final int BRIDGE_COST = 50; + + int cost = ROAD_COST; + if (engine.totalFunds < cost) + return ToolResult.INSUFFICIENT_FUNDS; + + char tile = (char) (engine.getTile(xpos, ypos) & LOMASK); + switch (tile) + { + case DIRT: + engine.setTile(xpos, ypos, (char) (TileConstants.ROADS | BULLBIT | BURNBIT)); + break; + + case RIVER: // road on water + case REDGE: + case CHANNEL: // check how to build bridges, if possible. + + cost = BRIDGE_COST; + if (engine.totalFunds < cost) + return ToolResult.INSUFFICIENT_FUNDS; + + if (xpos + 1 < engine.getWidth()) + { + char eTile = neutralizeRoad(engine.getTile(xpos + 1, ypos)); + if (eTile == VRAILROAD || + eTile == HBRIDGE || + (eTile >= TileConstants.ROADS && eTile <= HROADPOWER)) + { + engine.setTile(xpos, ypos, (char) (HBRIDGE | BULLBIT)); + break; + } + } + + if (xpos > 0) + { + char wTile = neutralizeRoad(engine.getTile(xpos - 1, ypos)); + if (wTile == VRAILROAD || + wTile == HBRIDGE || + (wTile >= TileConstants.ROADS && wTile <= INTERSECTION)) + { + engine.setTile(xpos, ypos, (char) (HBRIDGE | BULLBIT)); + break; + } + } + + if (ypos + 1 < engine.getHeight()) + { + char sTile = neutralizeRoad(engine.getTile(xpos, ypos + 1)); + if (sTile == HRAILROAD || + sTile == VROADPOWER || + (sTile >= VBRIDGE && sTile <= INTERSECTION)) + { + engine.setTile(xpos, ypos, (char) (VBRIDGE | BULLBIT)); + break; + } + } + + if (ypos > 0) + { + char nTile = neutralizeRoad(engine.getTile(xpos, ypos - 1)); + if (nTile == HRAILROAD || + nTile == VROADPOWER || + (nTile >= VBRIDGE && nTile <= INTERSECTION)) + { + engine.setTile(xpos, ypos, (char) (VBRIDGE | BULLBIT)); + break; + } + } + + // cannot do road here + return ToolResult.NONE; + + case LHPOWER: //road on power + engine.setTile(xpos, ypos, (char) (VROADPOWER | CONDBIT | BURNBIT | BULLBIT)); + break; + + case LVPOWER: //road on power #2 + engine.setTile(xpos, ypos, (char) (HROADPOWER | CONDBIT | BURNBIT | BULLBIT)); + break; + + case LHRAIL: //road on rail + engine.setTile(xpos, ypos, (char) (HRAILROAD | BURNBIT | BULLBIT)); + break; + + case LVRAIL: //road on rail #2 + engine.setTile(xpos, ypos, (char) (VRAILROAD | BURNBIT | BULLBIT)); + break; + + default: + return ToolResult.NONE; + } + + engine.spend(cost); + return ToolResult.SUCCESS; + } + + private ToolResult layWire(Micropolis engine, int xpos, int ypos) + { + final int WIRE_COST = 5; + final int UNDERWATER_WIRE_COST = 25; + + int cost = WIRE_COST; + if (engine.totalFunds < cost) + return ToolResult.INSUFFICIENT_FUNDS; + + char tile = (char) (engine.getTile(xpos, ypos) & LOMASK); + tile = neutralizeRoad(tile); + + switch (tile) + { + case DIRT: //wire on dirt + engine.setTile(xpos, ypos, (char) (LHPOWER | CONDBIT | BULLBIT | BURNBIT)); + break; + + case RIVER: // wire on water + case REDGE: + case CHANNEL: + + cost = UNDERWATER_WIRE_COST; + if (engine.totalFunds < cost) + return ToolResult.INSUFFICIENT_FUNDS; + + if (xpos + 1 < engine.getWidth()) + { + char tmp = engine.getTile(xpos + 1, ypos); + char tmpn = neutralizeRoad(tmp); + + if ((tmp & CONDBIT) != 0 && + tmpn != HROADPOWER && + tmpn != RAILHPOWERV && + tmpn != HPOWER) + { + engine.setTile(xpos, ypos, (char) (VPOWER | CONDBIT | BULLBIT)); + break; + } + } + + if (xpos > 0) + { + char tmp = engine.getTile(xpos - 1, ypos); + char tmpn = neutralizeRoad(tmp); + + if ((tmp & CONDBIT) != 0 && + tmpn != HROADPOWER && + tmpn != RAILHPOWERV && + tmpn != HPOWER) + { + engine.setTile(xpos, ypos, (char) (VPOWER | CONDBIT | BULLBIT)); + break; + } + } + + if (ypos + 1 < engine.getHeight()) + { + char tmp = engine.getTile(xpos, ypos + 1); + char tmpn = neutralizeRoad(tmp); + + if ((tmp & CONDBIT) != 0 && + tmpn != VROADPOWER && + tmpn != RAILVPOWERH && + tmpn != VPOWER) + { + engine.setTile(xpos, ypos, (char) (HPOWER | CONDBIT | BULLBIT)); + break; + } + } + + if (ypos > 0) + { + char tmp = engine.getTile(xpos, ypos - 1); + char tmpn = neutralizeRoad(tmp); + + if ((tmp & CONDBIT) != 0 && + tmpn != VROADPOWER && + tmpn != RAILVPOWERH && + tmpn != VPOWER) + { + engine.setTile(xpos, ypos, (char) (HPOWER | CONDBIT | BULLBIT)); + break; + } + } + + // cannot do wire here + return ToolResult.NONE; + + case TileConstants.ROADS: // wire on E/W road + engine.setTile(xpos, ypos, (char) (HROADPOWER | CONDBIT | BURNBIT | BULLBIT)); + break; + + case ROADS2: // wire on N/S road + engine.setTile(xpos, ypos, (char) (VROADPOWER | CONDBIT | BURNBIT | BULLBIT)); + break; + + case LHRAIL: // wire on E/W railroad tracks + engine.setTile(xpos, ypos, (char) (RAILHPOWERV | CONDBIT | BURNBIT | BULLBIT)); + break; + + case LVRAIL: // wire on N/S railroad tracks + engine.setTile(xpos, ypos, (char) (RAILVPOWERH | CONDBIT | BURNBIT | BULLBIT)); + break; + + default: //cannot do wire here + return ToolResult.NONE; + } + + engine.spend(cost); + return ToolResult.SUCCESS; + } + + private void fixZone(Micropolis engine, int xpos, int ypos) + { + fixSingle(engine, xpos, ypos); + if (ypos > 0) + fixSingle(engine, xpos, ypos - 1); + if (xpos > 0) + fixSingle(engine, xpos - 1, ypos); + if (xpos + 1 < engine.getWidth()) + fixSingle(engine, xpos + 1, ypos); + if (ypos + 1 < engine.getHeight()) + fixSingle(engine, xpos, ypos + 1); + } + + private void fixSingle(Micropolis engine, int xpos, int ypos) + { + char tile = (char) (engine.getTile(xpos, ypos) & LOMASK); + tile = neutralizeRoad(tile); + + if (tile >= TileConstants.ROADS && tile <= INTERSECTION) + { + // cleanup road + int adjTile = 0; + + if (ypos > 0) + { + tile = engine.getTile(xpos, ypos - 1); + tile = neutralizeRoad(tile); + if (((tile == HRAILROAD) || + (tile >= ROADBASE && tile <= VROADPOWER) + ) && + (tile != HROADPOWER) && + (tile != VRAILROAD) && + (tile != ROADBASE)) + { + adjTile |= 1; + } + } + + if (xpos + 1 < engine.getWidth()) + { + tile = engine.getTile(xpos + 1, ypos); + tile = neutralizeRoad(tile); + if (((tile == VRAILROAD) || + (tile >= ROADBASE && tile <= VROADPOWER) + ) && + (tile != VROADPOWER) && + (tile != HRAILROAD) && + (tile != VBRIDGE)) + { + adjTile |= 2; + } + } + + if (ypos + 1 < engine.getHeight()) + { + tile = engine.getTile(xpos, ypos + 1); + tile = neutralizeRoad(tile); + if (((tile == HRAILROAD) || + (tile >= ROADBASE && tile <= VROADPOWER) + ) && + (tile != HROADPOWER) && + (tile != VRAILROAD) && + (tile != ROADBASE)) + { + adjTile |= 4; + } + + } + + if (xpos > 0) + { + tile = engine.getTile(xpos - 1, ypos); + tile = neutralizeRoad(tile); + if (((tile == VRAILROAD) || + (tile >= ROADBASE && tile <= VROADPOWER) + ) && + (tile != VROADPOWER) && + (tile != HRAILROAD) && + (tile != VBRIDGE)) + { + adjTile |= 8; + } + } + + engine.setTile(xpos, ypos, (char)(RoadTable[adjTile] | BULLBIT | BURNBIT)); + return; + } //endif on a road tile + + if (tile >= LHRAIL && tile <= LVRAIL10) + { + // cleanup Rail + int adjTile = 0; + + if (ypos > 0) + { + tile = engine.getTile(xpos, ypos - 1); + tile = neutralizeRoad(tile); + if (tile >= RAILHPOWERV && tile <= VRAILROAD && + tile != RAILHPOWERV && + tile != HRAILROAD && + tile != HRAIL) + { + adjTile |= 1; + } + } + + if (xpos + 1 < engine.getWidth()) + { + tile = engine.getTile(xpos + 1, ypos); + tile = neutralizeRoad(tile); + if (tile >= RAILHPOWERV && tile <= VRAILROAD && + tile != RAILVPOWERH && + tile != VRAILROAD && + tile != VRAIL) + { + adjTile |= 2; + } + } + + if (ypos + 1 < engine.getHeight()) + { + tile = engine.getTile(xpos, ypos + 1); + tile = neutralizeRoad(tile); + if (tile >= RAILHPOWERV && tile <= VRAILROAD && + tile != RAILHPOWERV && + tile != HRAILROAD && + tile != HRAIL) + { + adjTile |= 4; + } + } + + if (xpos > 0) + { + tile = engine.getTile(xpos - 1, ypos); + tile = neutralizeRoad(tile); + if (tile >= RAILHPOWERV && tile <= VRAILROAD && + tile != RAILVPOWERH && + tile != VRAILROAD && + tile != VRAIL) + { + adjTile |= 8; + } + } + + engine.setTile(xpos, ypos, (char)(RailTable[adjTile] | BULLBIT | BURNBIT)); + return; + } //end if on a rail tile + + if (tile >= LHPOWER && tile <= LVPOWER10) + { + // Cleanup Wire + int adjTile = 0; + + if (ypos > 0) + { + tile = engine.getTile(xpos, ypos - 1); + char ntile = neutralizeRoad(tile); + if ((tile & CONDBIT) != 0 && + ntile != VPOWER && + ntile != VROADPOWER && + ntile != RAILVPOWERH) + { + adjTile |= 1; + } + } + + if (xpos + 1 < engine.getWidth()) + { + tile = engine.getTile(xpos + 1, ypos); + char ntile = neutralizeRoad(tile); + if ((tile & CONDBIT) != 0 && + ntile != HPOWER && + ntile != HROADPOWER && + ntile != RAILHPOWERV) + { + adjTile |= 2; + } + } + + if (ypos + 1 < engine.getHeight()) + { + tile = engine.getTile(xpos, ypos + 1); + char ntile = neutralizeRoad(tile); + if ((tile & CONDBIT) != 0 && + ntile != VPOWER && + ntile != VROADPOWER && + ntile != RAILVPOWERH) + { + adjTile |= 4; + } + } + + if (xpos > 0) + { + tile = engine.getTile(xpos - 1, ypos); + char ntile = neutralizeRoad(tile); + if ((tile & CONDBIT) != 0 && + ntile != HPOWER && + ntile != HROADPOWER && + ntile != RAILHPOWERV) + { + adjTile |= 8; + } + } + + engine.setTile(xpos, ypos, (char)(WireTable[adjTile] | BULLBIT | BURNBIT | CONDBIT)); + return; + } //end if on a rail tile + } + + void putRubble(Micropolis engine, int xpos, int ypos, int w, int h) + { + for (int xx = xpos - 1; xx <= xpos + w-2; xx++) { + for (int yy = ypos - 1; yy <= ypos + h-2; yy++) { + if (engine.testBounds(xx, yy)) { + int tile = engine.getTile(xx,yy) & LOMASK; + if (tile != RADTILE && tile != DIRT) { + int nTile = (TINYEXP + engine.PRNG.nextInt(3)) + | ANIMBIT | BULLBIT; + engine.setTile(xx, yy, (char)nTile); + } + } + } + } + } + + boolean isBigZone(int tile) + { + if (tile >= RESBASE && tile <= LASTZONE) + return true; + else if (tile >= SMOKEBASE && tile < TINYEXP) + return true; + else if (tile >= COALSMOKE1) + return true; + else + return false; + } + + int checkSize(int tile) + { + if ((tile >= (RESBASE-1) && tile <= (PORTBASE-1)) || + (tile >= (LASTPOWERPLANT+1) && tile <= (POLICESTATION+4))) + { + return 3; + } + else if ((tile >= PORTBASE && tile <= LASTPORT) || + (tile >= COALBASE && tile <= LASTPOWERPLANT) || + (tile >= STADIUMBASE && tile <= LASTZONE)) + { + return 4; + } + else if (tile == TileConstants.AIRPORT) + { + return 6; + } + else + { + return 0; + } + } +} diff --git a/src/micropolisj/engine/MonsterSprite.java b/src/micropolisj/engine/MonsterSprite.java new file mode 100644 index 0000000..9f6d3a9 --- /dev/null +++ b/src/micropolisj/engine/MonsterSprite.java @@ -0,0 +1,181 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import static micropolisj.engine.TileConstants.*; + +public class MonsterSprite extends Sprite +{ + int count; + int soundCount; + int destX; + int destY; + int origX; + int origY; + int step; + boolean flag; //true if the monster wants to return home + + //GODZILLA FRAMES + // 1...3 : northeast + // 4...6 : southeast + // 7...9 : southwest + // 10..12 : northwest + // 13 : north + // 14 : east + // 15 : south + // 16 : west + + // movement deltas + static int [] Gx = { 2, 2, -2, -2, 0 }; + static int [] Gy = { -2, 2, 2, -2, 0 }; + + static int [] ND1 = { 0, 1, 2, 3 }; + static int [] ND2 = { 1, 2, 3, 0 }; + static int [] nn1 = { 2, 5, 8, 11 }; + static int [] nn2 = { 11, 2, 5, 8 }; + + public MonsterSprite(Micropolis engine, int xpos, int ypos) + { + super(engine, SpriteKind.GOD); + this.x = xpos * 16 + 8; + this.y = ypos * 16 + 8; + this.width = 48; + this.height = 48; + this.offx = -24; + this.offy = -24; + + this.origX = x; + this.origY = y; + + this.frame = xpos > city.getWidth() / 2 ? + (ypos > city.getHeight() / 2 ? 10 : 7) : + (ypos > city.getHeight() / 2 ? 1 : 4); + + this.count = 1000; + CityLocation p = city.getLocationOfMaxPollution(); + this.destX = p.x * 16 + 8; + this.destY = p.y * 16 + 8; + this.flag = false; + this.step = 1; + } + + @Override + public void moveImpl() + { + if (this.frame == 0) { + return; + } + + if (soundCount > 0) { + soundCount--; + } + + int d = (this.frame - 1) / 3; // basic direction + int z = (this.frame - 1) % 3; // step index (only valid for d<4) + + if (d < 4) { //turn n s e w + assert step == -1 || step == 1; + if (z == 2) step = -1; + if (z == 0) step = 1; + z += step; + + if (getDis(x, y, destX, destY) < 60) { + + // reached destination + + if (!flag) { + // destination was the pollution center; + // now head for home + flag = true; + destX = origX; + destY = origY; + } + else { + // destination was origX, origY; + // hide the sprite + this.frame = 0; + return; + } + } + + int c = getDir(x, y, destX, destY); + c = (c - 1) / 2; //convert to one of four basic headings + assert c >= 0 && c < 4; + + if ((c != d) && city.PRNG.nextInt(11) == 0) { + // randomly determine direction to turn + if (city.PRNG.nextInt(2) == 0) { + z = ND1[d]; + } + else { + z = ND2[d]; + } + d = 4; //transition heading + + if (soundCount == 0) { + city.makeSound(x/16, y/16, Sound.MONSTER); + soundCount = 50 + city.PRNG.nextInt(101); + } + } + } + else { + assert this.frame >= 13 && this.frame <= 16; + + int z2 = (this.frame - 13) % 4; + + if (city.PRNG.nextInt(4) == 0) { + int newFrame; + if (city.PRNG.nextInt(2) == 0) { + newFrame = nn1[z2]; + } else { + newFrame = nn2[z2]; + } + d = (newFrame-1) / 3; + z = (newFrame-1) % 3; + + assert d < 4; + } + else { + d = 4; + } + } + + this.frame = ((d * 3) + z) + 1; + + assert this.frame >= 1 && this.frame <= 16; + + this.x += Gx[d]; + this.y += Gy[d]; + + if (this.count > 0) { + this.count--; + } + + int c = getChar(x, y); + if (c == -1 || + (c == RIVER && this.count != 0 && false) + ) { + this.frame = 0; //kill zilla + } + + for (Sprite s : city.allSprites()) + { + if (checkSpriteCollision(s) && + (s.kind == SpriteKind.AIR || + s.kind == SpriteKind.COP || + s.kind == SpriteKind.SHI || + s.kind == SpriteKind.TRA) + ) { + s.explodeSprite(); + } + } + + destroyTile(x / 16, y / 16); + } +} diff --git a/src/micropolisj/engine/ShipSprite.java b/src/micropolisj/engine/ShipSprite.java new file mode 100644 index 0000000..9224039 --- /dev/null +++ b/src/micropolisj/engine/ShipSprite.java @@ -0,0 +1,136 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import static micropolisj.engine.TileConstants.*; + +public class ShipSprite extends Sprite +{ + static int [] BDx = { 0, 0, 1, 1, 1, 0, -1, -1, -1 }; + static int [] BDy = { 0, -1, -1, 0, 1, 1, 1, 0, -1 }; + static int [] BPx = { 0, 0, 2, 2, 2, 0, -2, -2, -2 }; + static int [] BPy = { 0, -2, -2, 0, 2, 2, 2, 0, -2 }; + static int [] BtClrTab = { RIVER, CHANNEL, POWERBASE, POWERBASE+1, + RAILBASE, RAILBASE+1, BRWH, BRWV }; + + int newDir; + int count; + int soundCount; + + public static final int NORTH_EDGE = 5; + public static final int EAST_EDGE = 7; + public static final int SOUTH_EDGE = 1; + public static final int WEST_EDGE = 3; + + public ShipSprite(Micropolis engine, int xpos, int ypos, int edge) + { + super(engine, SpriteKind.SHI); + this.x = xpos * 16 + 8; + this.y = ypos * 16 + 8; + this.width = 48; + this.height = 48; + this.offx = -24; + this.offy = -24; + this.frame = edge; + this.newDir = edge; + this.dir = 10; + this.count = 1; + } + + @Override + public void moveImpl() + { + int t = RIVER; + + this.soundCount--; + if (this.soundCount <= 0) { + if (city.PRNG.nextInt(4) == 0) { + city.makeSound(x/16,y/16,Sound.HONKHONK_LOW); + } + this.soundCount = 200; + } + + this.count--; + if (this.count <= 0) { + this.count = 9; + if (this.newDir != this.frame) { + this.frame = turnTo(this.frame, this.newDir); + return; + } + int tem = city.PRNG.nextInt(8); + int pem; + for (pem = tem; pem < (tem + 8); pem++) { + int z = (pem % 8) + 1; + if (z == this.dir) + continue; + + int xpos = this.x / 16 + BDx[z]; + int ypos = this.y / 16 + BDy[z]; + + if (city.testBounds(xpos, ypos)) { + t = city.getTile(xpos, ypos) & LOMASK; + if ((t == CHANNEL) || (t == BRWH) || (t == BRWV) || + tryOther(t, this.dir, z)) + { + this.newDir = z; + this.frame = turnTo(this.frame, this.newDir); + this.dir = z + 4; + if (this.dir > 8) { this.dir -= 8; } + break; + } + } + } + + if (pem == (tem + 8)) { + this.dir = 10; + this.newDir = city.PRNG.nextInt(8)+1; + } + } + else { + int z = this.frame; + if (z == this.newDir) { + this.x += BPx[z]; + this.y += BPy[z]; + } + } + + if (!spriteInBounds()) { + this.frame = 0; + return; + } + + boolean found = false; + for (int z : BtClrTab) { + if (t == z) { + found = true; + } + } + if (!found) { + explodeSprite(); + destroyTile(x/16, y/16); + } + } + + boolean tryOther(int tile, int oldDir, int newDir) + { + int z = oldDir + 4; + if (z > 8) z -= 8; + if (newDir != z) return false; + + return (tile == POWERBASE || tile == POWERBASE+1 || + tile == RAILBASE || tile == RAILBASE+1); + } + + boolean spriteInBounds() + { + int xpos = x / 16; + int ypos = y / 16; + return city.testBounds(xpos, ypos); + } +} diff --git a/src/micropolisj/engine/Sound.java b/src/micropolisj/engine/Sound.java new file mode 100644 index 0000000..2715a1f --- /dev/null +++ b/src/micropolisj/engine/Sound.java @@ -0,0 +1,47 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import java.net.URL; + +/** + * Enumerates the various sounds that the city may produce. + * The engine is not responsible for actually playing the sound. That task + * belongs to the front-end (i.e. the user interface). + */ +public enum Sound +{ + EXPLOSION_LOW ("explosion-low"), + EXPLOSION_HIGH("explosion-high"), + EXPLOSION_BOTH("explosion-low"), + UHUH ("bop"), + SORRY ("bop"), + BUILD ("layzone"), + BULLDOZE (null), + HONKHONK_LOW ("honkhonk-low"), + HONKHONK_MED ("honkhonk-med"), + HONKHONK_HIGH ("honkhonk-high"), + HONKHONK_HI ("honkhonk-hi"), + SIREN ("siren"), + HEAVYTRAFFIC ("heavytraffic"), + MONSTER ("zombie-roar-5"); + + String wavName; + private Sound(String wavName) + { + this.wavName = wavName; + } + + public URL getAudioFile() + { + String n2 = "/sounds/"+wavName+".wav"; + URL u = Sound.class.getResource(n2); + return u; + } +} diff --git a/src/micropolisj/engine/Speed.java b/src/micropolisj/engine/Speed.java new file mode 100644 index 0000000..e33760e --- /dev/null +++ b/src/micropolisj/engine/Speed.java @@ -0,0 +1,37 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +/** + * Lists the simulation speeds available. + */ +public enum Speed +{ + PAUSED ( 999,999, 0), + SLOW ( 500, 5, 1), //one step every 2500 ms + NORMAL ( 125, 2, 1), //one step every 250 ms + FAST ( 50, 1, 2), //one step every 25 ms + SUPER_FAST( 25, 1, 10); //one step every 2.5 ms + + /** The animation speed, expressed as an interval in milliseconds. */ + public final int animationDelay; + /** For slower speeds, how many animation occur for every simulation step. + * Faster speeds should set this to one. */ + public final int aniFramesPerStep; + /** For faster speeds, how many simulation steps should occur for every + * update to the screen. */ + public final int simStepsPerUpdate; + + private Speed(int delay, int aniFrames, int simSteps) + { + this.animationDelay = delay; + this.aniFramesPerStep = aniFrames; + this.simStepsPerUpdate = simSteps; + } +} diff --git a/src/micropolisj/engine/Sprite.java b/src/micropolisj/engine/Sprite.java new file mode 100644 index 0000000..004a118 --- /dev/null +++ b/src/micropolisj/engine/Sprite.java @@ -0,0 +1,201 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import static micropolisj.engine.TileConstants.*; + +public abstract class Sprite +{ + Micropolis city; + + //TODO- enforce read-only nature of the following properties + // (i.e. do not let them be modified directly by other classes) + + public SpriteKind kind; + + public int offx; + public int offy; + public int width = 32; + public int height = 32; + + public int frame; + public int x; + public int y; + + public int lastX; + public int lastY; + + int dir; + + protected Sprite(Micropolis engine, SpriteKind kind) + { + this.city = engine; + this.kind = kind; + } + + protected final int getChar(int x, int y) + { + int xpos = x / 16; + int ypos = y / 16; + if (city.testBounds(xpos, ypos)) { + return (city.getTile(xpos, ypos) & LOMASK); + } else { + return -1; + } + } + + /** + * For subclasses to override. Actually does the movement and animation + * of this particular sprite. Setting this.frame to zero will cause the + * sprite to be unallocated. + */ + protected abstract void moveImpl(); + + public final void move() + { + lastX = x; + lastY = y; + moveImpl(); + city.fireSpriteMoved(this); + } + + public final boolean isVisible() + { + return this.frame != 0; + } + + /** + * Computes direction from one point to another. + * @return integer between 1 and 8, with + * 1 == north, + * 3 == east, + * 5 == south, + * 7 == west. + */ + static final int getDir(int orgX, int orgY, int desX, int desY) + { + final int Gdtab [] = { 0, 3, 2, 1, 3, 4, 5, 7, 6, 5, 7, 8, 1 }; + int dispX = desX - orgX; + int dispY = desY - orgY; + + int z = dispX < 0 ? (dispY < 0 ? 11 : 8) : (dispY < 0 ? 2 : 5); + + dispX = Math.abs(dispX); + dispY = Math.abs(dispY); + int absDist = dispX + dispY; + + if (dispX * 2 < dispY) z++; + else if (dispY * 2 < dispX) z--; + + if (z >= 1 && z <= 12) { + return Gdtab[z]; + } + else { + assert false; + return 0; + } + } + + /** + * Computes manhatten distance between two points. + */ + static final int getDis(int x0, int y0, int x1, int y1) + { + return Math.abs(x0-x1) + Math.abs(y0-y1); + } + + final void explodeSprite() + { + this.frame = 0; + + city.makeExplosionAt(x, y); + int xpos = x/16; + int ypos = y/16; + + switch (kind) { + case AIR: + city.crashLocation = new CityLocation(xpos, ypos); + city.sendMessageAtPic(MicropolisMessage.PLANECRASH_REPORT, xpos, ypos); + break; + case SHI: + city.crashLocation = new CityLocation(xpos, ypos); + city.sendMessageAtPic(MicropolisMessage.SHIPWRECK_REPORT, xpos, ypos); + break; + case TRA: + case BUS: + city.crashLocation = new CityLocation(xpos, ypos); + city.sendMessageAtPic(MicropolisMessage.TRAIN_CRASH_REPORT, xpos, ypos); + break; + case COP: + city.crashLocation = new CityLocation(xpos, ypos); + city.sendMessageAtPic(MicropolisMessage.COPTER_CRASH_REPORT, xpos, ypos); + break; + } + + city.makeSound(xpos, ypos, Sound.EXPLOSION_HIGH); + } + + final boolean checkSpriteCollision(Sprite otherSprite) + { + if (!isVisible()) return false; + if (!otherSprite.isVisible()) return false; + + return (getDis(this.x, this.y, otherSprite.x, otherSprite.y) < 30); + } + + final void destroyTile(int xpos, int ypos) + { + if (!city.testBounds(xpos, ypos)) + return; + + int z = city.getTile(xpos, ypos); + int t = z & LOMASK; + + if (t >= TREEBASE) { + if (TileConstants.isBridge(z)) { + city.setTile(xpos, ypos, RIVER); + return; + } + if ((z & BURNBIT) == 0) { + return; //cannot destroy it + } + if ((z & ZONEBIT) != 0) { + city.fireZone(xpos, ypos, z); + if (t > RZB) { + city.makeExplosion(xpos, ypos); + } + } + if (TileConstants.checkWet(t)) { + city.setTile(xpos, ypos, RIVER); + } + else { + city.setTile(xpos, ypos, + (char) (TINYEXP | BULLBIT | ANIMBIT)); + } + } + } + + static final int turnTo(int p, int d) + { + if (p == d) + return p; + if (p < d) { + if (d - p < 4) p++; + else p--; + } + else { + if (p - d < 4) p--; + else p++; + } + if (p > 8) return 1; + if (p < 1) return 8; + return p; + } + +} diff --git a/src/micropolisj/engine/SpriteKind.java b/src/micropolisj/engine/SpriteKind.java new file mode 100644 index 0000000..3b3b926 --- /dev/null +++ b/src/micropolisj/engine/SpriteKind.java @@ -0,0 +1,33 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +/** + * Enumeration of the various kinds of sprites that may appear in the city. + */ +public enum SpriteKind +{ + TRA(1,5), + COP(2,8), + AIR(3,11), + SHI(4,8), + GOD(5,16), + TOR(6,3), + EXP(7,6), + BUS(8,4); + + public final int objectId; + public final int numFrames; + + private SpriteKind(int objectId, int numFrames) + { + this.objectId = objectId; + this.numFrames = numFrames; + } +} diff --git a/src/micropolisj/engine/TileConstants.java b/src/micropolisj/engine/TileConstants.java new file mode 100644 index 0000000..3c04873 --- /dev/null +++ b/src/micropolisj/engine/TileConstants.java @@ -0,0 +1,327 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import java.util.Arrays; + +public class TileConstants +{ + // + // terrain mapping + // + public static final char DIRT = 0; + public static final char RIVER = 2; + public static final char REDGE = 3; + public static final char CHANNEL = 4; + public static final char FIRSTRIVEDGE = 5; + public static final char LASTRIVEDGE = 20; + public static final char TREEBASE = 21; + public static final char WOODS_LOW = TREEBASE; + public static final char WOODS = 37; + public static final char UNUSED_TRASH2 = 39; + public static final char WOODS_HIGH = UNUSED_TRASH2; + public static final char WOODS2 = 40; + public static final char WOODS5 = 43; + public static final char RUBBLE = 44; + public static final char LASTRUBBLE = 47; + public static final char FLOOD = 48; + public static final char LASTFLOOD = 51; + public static final char RADTILE = 52; + public static final char FIRE = 56; + public static final char FIREBASE = 56; + public static final char ROADBASE = 64; + public static final char HBRIDGE = 64; + public static final char VBRIDGE = 65; + public static final char ROADS = 66; + public static final char ROADS2 = 67; + public static final char ROADS3 = 68; + public static final char ROADS4 = 69; + public static final char ROADS5 = 70; + public static final char ROADS6 = 71; + public static final char ROADS7 = 72; + public static final char ROADS8 = 73; + public static final char ROADS9 = 74; + public static final char ROADS10 = 75; + public static final char INTERSECTION = 76; + public static final char HROADPOWER = 77; + public static final char VROADPOWER = 78; + public static final char BRWH = 79; + public static final char LTRFBASE = 80; + public static final char BRWV = 95; + public static final char HTRFBASE = 144; + public static final char LASTROAD = 206; + public static final char POWERBASE = 208; + public static final char HPOWER = 208; + public static final char VPOWER = 209; + public static final char LHPOWER = 210; + public static final char LVPOWER = 211; + public static final char LVPOWER2 = 212; + public static final char LVPOWER3 = 213; + public static final char LVPOWER4 = 214; + public static final char LVPOWER5 = 215; + public static final char LVPOWER6 = 216; + public static final char LVPOWER7 = 217; + public static final char LVPOWER8 = 218; + public static final char LVPOWER9 = 219; + public static final char LVPOWER10 = 220; + public static final char RAILHPOWERV = 221; + public static final char RAILVPOWERH = 222; + public static final char LASTPOWER = 222; + public static final char RAILBASE = 224; + public static final char HRAIL = 224; + public static final char VRAIL = 225; + public static final char LHRAIL = 226; + public static final char LVRAIL = 227; + public static final char LVRAIL2 = 228; + public static final char LVRAIL3 = 229; + public static final char LVRAIL4 = 230; + public static final char LVRAIL5 = 231; + public static final char LVRAIL6 = 232; + public static final char LVRAIL7 = 233; + public static final char LVRAIL8 = 234; + public static final char LVRAIL9 = 235; + public static final char LVRAIL10 = 236; + public static final char HRAILROAD = 237; + public static final char VRAILROAD = 238; + public static final char LASTRAIL = 238; + public static final char RESBASE = 240; + public static final char FREEZ = 244; //free zone? + public static final char HOUSE = 249; + public static final char LHTHR = 249; //12 house tiles + public static final char HHTHR = 260; + public static final char RZB = 265; //residential zone base + public static final char HOSPITAL = 409; + public static final char CHURCH = 418; + public static final char COMBASE = 423; + public static final char COMCLR = 427; + public static final char CZB = 436; //commercial zone base + public static final char COMLAST = 609; + public static final char INDBASE = 612; + public static final char INDCLR = 616; + public static final char LASTIND = 620; + public static final char IND1 = 621; + public static final char IZB = 625; + public static final char IND2 = 641; + public static final char IND3 = 644; + public static final char IND4 = 649; + public static final char IND5 = 650; + public static final char IND6 = 676; + public static final char IND7 = 677; + public static final char IND8 = 686; + public static final char IND9 = 689; + public static final char PORTBASE = 693; + public static final char PORT = 698; + public static final char LASTPORT = 708; + public static final char AIRPORTBASE = 709; + public static final char RADAR = 711; + public static final char AIRPORT = 716; + public static final char COALBASE = 745; + public static final char POWERPLANT = 750; + public static final char LASTPOWERPLANT = 760; + public static final char FIRESTBASE = 761; + public static final char FIRESTATION = 765; + public static final char POLICESTBASE = 770; + public static final char POLICESTATION = 774; + public static final char STADIUMBASE = 779; + public static final char STADIUM = 784; + public static final char FULLSTADIUM = 800; + public static final char NUCLEARBASE = 811; + public static final char NUCLEAR = 816; + public static final char LASTZONE = 826; + public static final char LIGHTNINGBOLT = 827; + public static final char HBRDG0 = 828; + public static final char HBRDG1 = 829; + public static final char HBRDG2 = 830; + public static final char HBRDG3 = 831; + public static final char RADAR_ANIM = 832; + public static final char FOUNTAIN = 840; + public static final char INDBASE2 = 844; + public static final char SMOKEBASE = 852; + public static final char TINYEXP = 860; + public static final char SOMETINYEXP = 864; + public static final char LASTTINYEXP = 867; + public static final char COALSMOKE1 = 916; + public static final char COALSMOKE2 = 920; + public static final char COALSMOKE3 = 924; + public static final char COALSMOKE4 = 928; + public static final char FOOTBALLGAME1 = 932; + public static final char FOOTBALLGAME2 = 940; + public static final char VBRDG0 = 948; + public static final char VBRDG1 = 949; + public static final char VBRDG2 = 950; + public static final char VBRDG3 = 951; + public static final char URANIUM_FUEL = 952; + public static final char LAST_TILE = 956; + + static final char [] RoadTable = new char[] { + ROADS, ROADS2, ROADS, ROADS3, + ROADS2, ROADS2, ROADS4, ROADS8, + ROADS, ROADS6, ROADS, ROADS7, + ROADS5, ROADS10, ROADS9, INTERSECTION + }; + + static final char [] RailTable = new char[] { + LHRAIL, LVRAIL, LHRAIL, LVRAIL2, + LVRAIL, LVRAIL, LVRAIL3, LVRAIL7, + LHRAIL, LVRAIL5, LHRAIL, LVRAIL6, + LVRAIL4, LVRAIL9, LVRAIL8, LVRAIL10 + }; + + static final char [] WireTable = new char[] { + LHPOWER, LVPOWER, LHPOWER, LVPOWER2, + LVPOWER, LVPOWER, LVPOWER3, LVPOWER7, + LHPOWER, LVPOWER5, LHPOWER, LVPOWER6, + LVPOWER4, LVPOWER9, LVPOWER8, LVPOWER10 + }; + + // + // status bits + // + public static final char PWRBIT = 32768; // bit 15 ... currently powered + public static final char CONDBIT = 16384; // bit 14 ... can conduct power + public static final char BURNBIT = 8192; // bit 13 ... is combustible + public static final char BULLBIT = 4096; // bit 12 ... is bulldozable + public static final char ANIMBIT = 2048; // bit 11 ... animates + public static final char ZONEBIT = 1024; // bit 10 ... is the special tile for a zone + + public static final char ALLBITS = 64512; // mask for upper 6 bits + public static final char LOMASK = 1023; //mask for low 10 bits + + public static final char BLBNBIT = (BULLBIT | BURNBIT); + public static final char BLBNCNBIT = (BULLBIT | BURNBIT | CONDBIT); + public static final char BNCNBIT = (BURNBIT | CONDBIT); + + private TileConstants() {} + + private static int [] buildingBases = { + DIRT, RIVER, TREEBASE, RUBBLE, + FLOOD, RADTILE, FIRE, ROADBASE, + POWERBASE, RAILBASE, RESBASE, COMBASE, + INDBASE, PORTBASE, AIRPORTBASE, COALBASE, + FIRESTBASE, POLICESTBASE, STADIUMBASE, NUCLEARBASE, + HBRDG0, RADAR_ANIM, FOUNTAIN, INDBASE2, + FOOTBALLGAME1, VBRDG0, URANIUM_FUEL, LAST_TILE + }; + + //used by queryZoneStatus + public static int getBuildingId(int tile) + { + tile &= LOMASK; + int i = Arrays.binarySearch(buildingBases, tile); + if (i >= 0) { + return i; + } else { + return -i - 2; + } + } + + //used by setFire() + public static boolean isArsonable(int tile) + { + return ( + (tile & ZONEBIT) == 0 && + (tile & LOMASK) >= LHTHR && + (tile & LOMASK) <= LASTZONE + ); + } + + //used by Sprite::destroyTile + public static boolean isBridge(int tile) + { + return (((tile & LOMASK) >= ROADBASE && (tile & LOMASK) <= LASTROAD) + && ((tile & BURNBIT) == 0)); + } + + public static boolean isOverWater(char cell) + { + switch (cell & LOMASK) + { + case HBRIDGE: + case VBRIDGE: + case BRWV: + case BRWH: + case HBRDG0: + case HBRDG1: + case HBRDG2: + case HBRDG3: + case VBRDG0: + case VBRDG1: + case VBRDG2: + case VBRDG3: + case HPOWER: + case VPOWER: + case HRAIL: + case VRAIL: + return true; + default: + return false; + } + } + + public static boolean isRubble(char cell) + { + return (((cell & LOMASK) >= RUBBLE) && + ((cell & LOMASK) <= LASTRUBBLE)); + } + + public static boolean isTree(char cell) + { + return (((cell & LOMASK) >= WOODS_LOW) && + ((cell & LOMASK) <= WOODS_HIGH)); + } + + //used by makeEarthquake + public static boolean isVulnerable(int tile) + { + int tem2 = tile & LOMASK; + if (tem2 < RESBASE || + tem2 > LASTZONE || + (tile & ZONEBIT) != 0 + ) { + return false; + } else { + return true; + } + } + + public static boolean checkWet(int tile) + { + int x = tile & LOMASK; + return (x == POWERBASE || + x == POWERBASE+1 || + x == RAILBASE || + x == RAILBASE + 1 || + x == BRWH || + x == BRWV); + } + + public static int getZoneSizeFor(int tile) + { + int ch = tile & LOMASK; + if (ch < PORTBASE) { + return 3; + } + else if (ch == AIRPORT) { + return 6; + } + else { + return 4; + } + } + + static boolean isRiverEdge(int tile) + { + return (tile & LOMASK) > 4 && (tile & LOMASK) < 21; + } + + static boolean isFloodable(int tile) + { + return (tile == DIRT || ((tile & BULLBIT) != 0 && (tile & BURNBIT) != 0)); + } +} diff --git a/src/micropolisj/engine/ToolResult.java b/src/micropolisj/engine/ToolResult.java new file mode 100644 index 0000000..43c9f5a --- /dev/null +++ b/src/micropolisj/engine/ToolResult.java @@ -0,0 +1,20 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +/** + * Lists the various results that may occur when applying a tool. + */ +public enum ToolResult +{ + SUCCESS, // 1 + NONE, // 0 + UH_OH, // -1; invalid position + INSUFFICIENT_FUNDS; // -2 +} diff --git a/src/micropolisj/engine/TornadoSprite.java b/src/micropolisj/engine/TornadoSprite.java new file mode 100644 index 0000000..cfdca3d --- /dev/null +++ b/src/micropolisj/engine/TornadoSprite.java @@ -0,0 +1,82 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +public class TornadoSprite extends Sprite +{ + static int [] CDx = { 2, 3, 2, 0, -2, -3 }; + static int [] CDy = { -2, 0, 2, 3, 2, 0 }; + + boolean flag; + int count; + + public TornadoSprite(Micropolis engine, int xpos, int ypos) + { + super(engine, SpriteKind.TOR); + this.x = xpos * 16 + 8; + this.y = ypos * 16 + 8; + this.width = 48; + this.height = 48; + this.offx = -24; + this.offy = -40; + + this.frame = 1; + this.count = 200; + } + + @Override + public void moveImpl() + { + int z = this.frame; + + if (z == 2) { + //cycle animation + + if (this.flag) + z = 3; + else + z = 1; + } + else { + this.flag = (z == 1); + z = 2; + } + + if (this.count > 0) { + this.count--; + } + + this.frame = z; + + for (Sprite s : city.allSprites()) { + if (checkSpriteCollision(s) && + (s.kind == SpriteKind.AIR || + s.kind == SpriteKind.COP || + s.kind == SpriteKind.SHI || + s.kind == SpriteKind.TRA) + ) { + s.explodeSprite(); + } + } + + int zz = city.PRNG.nextInt(CDx.length); + x += CDx[zz]; + y += CDy[zz]; + + if (!city.testBounds(x/16, y/16)) { + // out of bounds + this.frame = 0; + return; + } + + // FIXME- the original code checks here for an ending condition + + destroyTile(x/16, y/16); + } +} diff --git a/src/micropolisj/engine/TrafficGen.java b/src/micropolisj/engine/TrafficGen.java new file mode 100644 index 0000000..bbace53 --- /dev/null +++ b/src/micropolisj/engine/TrafficGen.java @@ -0,0 +1,245 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import java.util.*; +import static micropolisj.engine.TileConstants.*; + +class TrafficGen +{ + Micropolis engine; + int mapX; + int mapY; + Micropolis.ZoneType sourceZone; + + int lastdir; + Stack positions = new Stack<>(); + + static final int MAX_TRAFFIC_DISTANCE = 30; + + public TrafficGen(Micropolis engine) + { + this.engine = engine; + } + + int makeTraffic() + { + if (findPerimeterRoad()) //look for road on this zone's perimeter + { + if (tryDrive()) //attempt to drive somewhere + { + // success; incr trafdensity + setTrafficMem(); + return 1; + } + + return 0; + } + else + { + // no road found + return -1; + } + } + + void setTrafficMem() + { + while (!positions.isEmpty()) + { + Position pos = positions.pop(); + mapX = pos.x; + mapY = pos.y; + assert engine.testBounds(mapX, mapY); + + int tile = engine.getTile(mapX, mapY) & LOMASK; + if (tile >= ROADBASE && tile < POWERBASE) + { + // check for rail + int z = engine.trfDensity[mapY/2][mapX/2]; + z += 50; + + if (z > 240 && engine.PRNG.nextInt(6) == 0) + { + z = 240; + engine.trafficMaxLocationX = mapX; + engine.trafficMaxLocationY = mapY; + + HelicopterSprite copter = (HelicopterSprite) engine.getSprite(SpriteKind.COP); + if (copter != null) { + copter.destX = mapX; + copter.destY = mapY; + } + } + + engine.trfDensity[mapY/2][mapX/2] = z; + } + } + } + + static final int [] PerimX = { -1, 0, 1, 2, 2, 2, 1, 0,-1, -2,-2,-2 }; + static final int [] PerimY = { -2,-2,-2, -1, 0, 1, 2, 2, 2, 1, 0,-1 }; + boolean findPerimeterRoad() + { + for (int z = 0; z < 12; z++) + { + int tx = mapX + PerimX[z]; + int ty = mapY + PerimY[z]; + + if (engine.testBounds(tx, ty) + && roadTest(tx, ty)) + { + mapX = tx; + mapY = ty; + return true; + } + } + return false; + } + + boolean roadTest(int tx, int ty) + { + char c = engine.getTile(tx, ty); + c &= LOMASK; + + if (c < ROADBASE) + return false; + else if (c > LASTRAIL) + return false; + else if (c >= POWERBASE && c < LASTPOWER) + return false; + else + return true; + } + + boolean tryDrive() + { + lastdir = 5; + positions.clear(); + + for (int z = 0; z < MAX_TRAFFIC_DISTANCE; z++) //maximum distance to try + { + if (tryGo(z)) + { + // got a road + if (driveDone()) + { + // destination reached + return true; + } + } + else + { + // deadend, try backing up + if (!positions.isEmpty()) + { + positions.pop(); + z += 3; + } + else + { + return false; + } + } + } + + // gone maxdis + return false; + } + + static final int [] DX = { 0, 1, 0, -1 }; + static final int [] DY = { -1, 0, 1, 0 }; + boolean tryGo(int z) + { + // random starting direction + int rdir = engine.PRNG.nextInt(4); + + for (int d = rdir; d < rdir + 4; d++) + { + int realdir = d % 4; + if (realdir == lastdir) + continue; + + if (roadTest(mapX + DX[realdir], mapY + DY[realdir])) + { + mapX += DX[realdir]; + mapY += DY[realdir]; + lastdir = (realdir + 2) % 4; + + if (z % 2 == 1) + { + // save pos every other move + positions.push(new Position(mapX, mapY)); + } + + return true; + } + } + + return false; + } + + static class Position + { + int x; + int y; + Position(int x, int y) + { + this.x = x; + this.y = y; + } + } + + boolean driveDone() + { + int low, high; + switch (sourceZone) + { + case RESIDENTIAL: + low = COMBASE; + high = NUCLEAR; + break; + case COMMERCIAL: + low = LHTHR; + high = PORT; + break; + case INDUSTRIAL: + low = LHTHR; + high = COMBASE; + break; + default: + throw new Error("unreachable"); + } + + if (mapY > 0) + { + int tile = engine.getTile(mapX, mapY-1) & LOMASK; + if (tile >= low && tile <= high) + return true; + } + if (mapX + 1 < engine.getWidth()) + { + int tile = engine.getTile(mapX + 1, mapY) & LOMASK; + if (tile >= low && tile <= high) + return true; + } + if (mapY + 1 < engine.getHeight()) + { + int tile = engine.getTile(mapX, mapY + 1) & LOMASK; + if (tile >= low && tile <= high) + return true; + } + if (mapX > 0) + { + int tile = engine.getTile(mapX - 1, mapY) & LOMASK; + if (tile >= low && tile <= high) + return true; + } + return false; + } +} diff --git a/src/micropolisj/engine/TrainSprite.java b/src/micropolisj/engine/TrainSprite.java new file mode 100644 index 0000000..9ca9ef3 --- /dev/null +++ b/src/micropolisj/engine/TrainSprite.java @@ -0,0 +1,90 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +import static micropolisj.engine.TileConstants.*; + +public class TrainSprite extends Sprite +{ + static int [] Cx = { 0, 16, 0, -16 }; + static int [] Cy = { -16, 0, 16, 0 }; + static int [] Dx = { 0, 4, 0, -4, 0 }; + static int [] Dy = { -4, 0, 4, 0, 0 }; + static int [] TrainPic2 = { 1, 2, 1, 2, 5 }; + static final int TRA_GROOVE_X = 8; + static final int TRA_GROOVE_Y = 8; + + static final int FRAME_NORTHSOUTH = 1; + static final int FRAME_EASTWEST = 2; + static final int FRAME_NW_SE = 3; + static final int FRAME_SW_NE = 4; + static final int FRAME_UNDERWATER = 5; + + public TrainSprite(Micropolis engine, int xpos, int ypos) + { + super(engine, SpriteKind.TRA); + this.x = xpos * 16 + TRA_GROOVE_X; + this.y = ypos * 16 + TRA_GROOVE_Y; + this.offx = -16; + this.offy = -16; + this.dir = 4; //not moving + } + + @Override + public void moveImpl() + { + if (frame == 3 || frame == 4) { + frame = TrainPic2[this.dir]; + } + x += Dx[this.dir]; + y += Dy[this.dir]; + if (city.acycle % 4 == 0) { + // should be at the center of a cell, if not, correct it + x = (x/16) * 16 + TRA_GROOVE_X; + y = (y/16) * 16 + TRA_GROOVE_Y; + int d1 = city.PRNG.nextInt(4); + for (int z = d1; z < d1 + 4; z++) { + int d2 = z % 4; + if (this.dir != 4) { //impossible? + if (d2 == (this.dir + 2) % 4) + continue; + } + + int c = getChar(this.x + Cx[d2], this.y + Cy[d2]); + if (((c >= RAILBASE) && (c <= LASTRAIL)) || //track? + (c == RAILVPOWERH) || + (c == RAILHPOWERV)) + { + if ((this.dir != d2) && (this.dir != 4)) { + if (this.dir + d2 == 3) + this.frame = 3; + else + this.frame = 4; + } + else { + this.frame = TrainPic2[d2]; + } + + if ((c == RAILBASE) || (c == (RAILBASE+1))) { + //underwater + this.frame = 5; + } + this.dir = d2; + return; + } + } + if (this.dir == 4) { + // train has nowhere to go, so retire + this.frame = 0; + return; + } + this.dir = 4; + } + } +} diff --git a/src/micropolisj/engine/ZoneStatus.java b/src/micropolisj/engine/ZoneStatus.java new file mode 100644 index 0000000..8e89d30 --- /dev/null +++ b/src/micropolisj/engine/ZoneStatus.java @@ -0,0 +1,19 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.engine; + +public class ZoneStatus +{ + public int building; + public int popDensity; + public int landValue; + public int crimeLevel; + public int pollution; + public int growthRate; +} diff --git a/src/micropolisj/engine/package.html b/src/micropolisj/engine/package.html new file mode 100644 index 0000000..283bb23 --- /dev/null +++ b/src/micropolisj/engine/package.html @@ -0,0 +1,4 @@ + + +

Contains the backend classes that implement the actual city simulation.

+ diff --git a/src/micropolisj/gui/BudgetDialog.java b/src/micropolisj/gui/BudgetDialog.java new file mode 100644 index 0000000..86ad9b1 --- /dev/null +++ b/src/micropolisj/gui/BudgetDialog.java @@ -0,0 +1,335 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.event.*; +import java.text.NumberFormat; +import java.util.*; + +import micropolisj.engine.*; +import static micropolisj.gui.MainWindow.formatFunds; +import static micropolisj.gui.MainWindow.formatGameDate; + +public class BudgetDialog extends JDialog +{ + Micropolis engine; + + JSpinner taxRateEntry; + int origTaxRate; + double origRoadPct; + double origFirePct; + double origPolicePct; + + JLabel roadFundAlloc = new JLabel(); + JSpinner roadFundEntry; + + JLabel policeFundAlloc = new JLabel(); + JSpinner policeFundEntry; + + JLabel fireFundAlloc = new JLabel(); + JSpinner fireFundEntry; + + JLabel taxRevenueLbl = new JLabel(); + + static ResourceBundle strings = MainWindow.strings; + + JCheckBox autoBudgetBtn = new JCheckBox(strings.getString("budgetdlg.auto_budget")); + JCheckBox pauseBtn = new JCheckBox(strings.getString("budgetdlg.pause_game")); + + private void applyChange() + { + int newTaxRate = ((Number) taxRateEntry.getValue()).intValue(); + int newRoadPct = ((Number) roadFundEntry.getValue()).intValue(); + int newPolicePct = ((Number) policeFundEntry.getValue()).intValue(); + int newFirePct = ((Number) fireFundEntry.getValue()).intValue(); + + engine.cityTax = newTaxRate; + engine.roadPercent = (double)newRoadPct / 100.0; + engine.policePercent = (double)newPolicePct / 100.0; + engine.firePercent = (double)newFirePct / 100.0; + + loadBudgetNumbers(false); + } + + private void loadBudgetNumbers(boolean updateEntries) + { + BudgetNumbers b = engine.generateBudget(); + if (updateEntries) + { + taxRateEntry.setValue(b.taxRate); + roadFundEntry.setValue((int)Math.round(b.roadPercent*100.0)); + policeFundEntry.setValue((int)Math.round(b.policePercent*100.0)); + fireFundEntry.setValue((int)Math.round(b.firePercent*100.0)); + } + + taxRevenueLbl.setText(formatFunds(b.taxIncome)); + roadFundAlloc.setText(formatFunds(b.roadFunded)); + policeFundAlloc.setText(formatFunds(b.policeFunded)); + fireFundAlloc.setText(formatFunds(b.fireFunded)); + } + + public BudgetDialog(Window owner, Micropolis engine) + { + super(owner); + setTitle(strings.getString("budgetdlg.title")); + + this.engine = engine; + this.origTaxRate = engine.cityTax; + this.origRoadPct = engine.roadPercent; + this.origFirePct = engine.firePercent; + this.origPolicePct = engine.policePercent; + + // give text fields of the fund-level spinners a minimum size + taxRateEntry = new JSpinner(new SpinnerNumberModel(7,0,20,1)); + roadFundEntry = new JSpinner(new SpinnerNumberModel(100,0,100,1)); + fireFundEntry = new JSpinner(new SpinnerNumberModel(1,0,100,1)); + policeFundEntry = new JSpinner(new SpinnerNumberModel(10,0,100,1)); + + ChangeListener change = new ChangeListener() { + public void stateChanged(ChangeEvent ev) { + applyChange(); + } + }; + taxRateEntry.addChangeListener(change); + roadFundEntry.addChangeListener(change); + fireFundEntry.addChangeListener(change); + policeFundEntry.addChangeListener(change); + + Box mainBox = new Box(BoxLayout.Y_AXIS); + mainBox.setBorder(BorderFactory.createEmptyBorder(8,8,8,8)); + add(mainBox, BorderLayout.CENTER); + + mainBox.add(makeTaxPane()); + + JSeparator sep = new JSeparator(SwingConstants.HORIZONTAL); + mainBox.add(sep); + + JPanel fundingRatesPane = new JPanel(new GridBagLayout()); + fundingRatesPane.setBorder(BorderFactory.createEmptyBorder(8,0,8,0)); + mainBox.add(fundingRatesPane); + + GridBagConstraints c0 = new GridBagConstraints(); + c0.gridx = 0; + c0.weightx = 0.25; + c0.anchor = GridBagConstraints.WEST; + GridBagConstraints c1 = new GridBagConstraints(); + c1.gridx = 1; + c1.weightx = 0.25; + c1.anchor = GridBagConstraints.EAST; + GridBagConstraints c2 = new GridBagConstraints(); + c2.gridx = 2; + c2.weightx = 0.5; + c2.anchor = GridBagConstraints.EAST; + + c1.gridy = c2.gridy = 0; + fundingRatesPane.add(new JLabel(strings.getString("budgetdlg.funding_level_hdr")), c1); + fundingRatesPane.add(new JLabel(strings.getString("budgetdlg.allocation_hdr")), c2); + + c0.gridy = c1.gridy = c2.gridy = 1; + fundingRatesPane.add(new JLabel(strings.getString("budgetdlg.road_fund")), c0); + fundingRatesPane.add(roadFundEntry, c1); + fundingRatesPane.add(roadFundAlloc, c2); + + c0.gridy = c1.gridy = c2.gridy = 2; + fundingRatesPane.add(new JLabel(strings.getString("budgetdlg.police_fund")), c0); + fundingRatesPane.add(policeFundEntry, c1); + fundingRatesPane.add(policeFundAlloc, c2); + + c0.gridy = c1.gridy = c2.gridy = 3; + fundingRatesPane.add(new JLabel(strings.getString("budgetdlg.fire_fund")), c0); + fundingRatesPane.add(fireFundEntry, c1); + fundingRatesPane.add(fireFundAlloc, c2); + + JSeparator sep1 = new JSeparator(SwingConstants.HORIZONTAL); + mainBox.add(sep1); + + JPanel balancePane = new JPanel(new GridBagLayout()); + balancePane.setBorder(BorderFactory.createEmptyBorder(8,24,8,24)); + mainBox.add(balancePane); + + makeBalancePane(balancePane); + + JSeparator sep2 = new JSeparator(SwingConstants.HORIZONTAL); + mainBox.add(sep2); + + JPanel optionsPane = new JPanel(new GridBagLayout()); + optionsPane.setBorder(BorderFactory.createEmptyBorder(8,0,0,0)); + mainBox.add(optionsPane); + + c0.anchor = c1.anchor = GridBagConstraints.WEST; + c0.gridy = c1.gridy = 0; + c0.weightx = c1.weightx = 0.5; + optionsPane.add(autoBudgetBtn, c0); + optionsPane.add(pauseBtn, c1); + + autoBudgetBtn.setSelected(engine.autoBudget); + pauseBtn.setSelected(engine.simSpeed == Speed.PAUSED); + + JPanel buttonPane = new JPanel(); + add(buttonPane, BorderLayout.SOUTH); + + JButton continueBtn = new JButton(strings.getString("budgetdlg.continue")); + continueBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) { + onContinueClicked(); + }}); + buttonPane.add(continueBtn); + + JButton resetBtn = new JButton(strings.getString("budgetdlg.reset")); + resetBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) { + onResetClicked(); + }}); + buttonPane.add(resetBtn); + + loadBudgetNumbers(true); + setAutoRequestFocus(false); + pack(); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setLocationRelativeTo(owner); + } + + private JComponent makeTaxPane() + { + JPanel pane = new JPanel(new GridBagLayout()); + pane.setBorder(BorderFactory.createEmptyBorder(0,0,8,0)); + + GridBagConstraints c0 = new GridBagConstraints(); + GridBagConstraints c1 = new GridBagConstraints(); + GridBagConstraints c2 = new GridBagConstraints(); + + c0.gridx = 0; + c0.anchor = GridBagConstraints.WEST; + c0.weightx = 0.25; + c1.gridx = 1; + c1.anchor = GridBagConstraints.EAST; + c1.weightx = 0.25; + c2.gridx = 2; + c2.anchor = GridBagConstraints.EAST; + c2.weightx = 0.5; + + c0.gridy = c1.gridy = c2.gridy = 0; + pane.add(new JLabel(strings.getString("budgetdlg.tax_rate_hdr")), c1); + pane.add(new JLabel(strings.getString("budgetdlg.annual_receipts_hdr")), c2); + + c0.gridy = c1.gridy = c2.gridy = 1; + pane.add(new JLabel(strings.getString("budgetdlg.tax_revenue")), c0); + pane.add(taxRateEntry, c1); + pane.add(taxRevenueLbl, c2); + + return pane; + } + + private void onContinueClicked() + { + if (autoBudgetBtn.isSelected() != engine.autoBudget) { + engine.toggleAutoBudget(); + } + if (pauseBtn.isSelected() && engine.simSpeed != Speed.PAUSED) { + engine.setSpeed(Speed.PAUSED); + } + else if (!pauseBtn.isSelected() && engine.simSpeed == Speed.PAUSED) { + engine.setSpeed(Speed.NORMAL); + } + + dispose(); + } + + private void onResetClicked() + { + engine.cityTax = this.origTaxRate; + engine.roadPercent = this.origRoadPct; + engine.firePercent = this.origFirePct; + engine.policePercent = this.origPolicePct; + loadBudgetNumbers(true); + } + + private JComponent makeBalancePane(JPanel balancePane) + { + GridBagConstraints c0 = new GridBagConstraints(); + GridBagConstraints c1 = new GridBagConstraints(); + + c0.anchor = GridBagConstraints.WEST; + c0.weightx = 0.5; + c0.gridx = 0; + c0.gridy = 0; + + JLabel thLbl = new JLabel(strings.getString("budgetdlg.period_ending")); + Font origFont = thLbl.getFont(); + Font headFont = origFont.deriveFont(Font.ITALIC); + thLbl.setFont(headFont); + thLbl.setForeground(Color.MAGENTA); + balancePane.add(thLbl, c0); + + c0.gridy++; + balancePane.add(new JLabel(strings.getString("budgetdlg.cash_begin")), c0); + c0.gridy++; + balancePane.add(new JLabel(strings.getString("budgetdlg.taxes_collected")), c0); + c0.gridy++; + balancePane.add(new JLabel(strings.getString("budgetdlg.capital_expenses")), c0); + c0.gridy++; + balancePane.add(new JLabel(strings.getString("budgetdlg.operating_expenses")), c0); + c0.gridy++; + balancePane.add(new JLabel(strings.getString("budgetdlg.cash_end")), c0); + + c1.anchor = GridBagConstraints.EAST; + c1.weightx = 0.25; + c1.gridx = 0; + + for (int i = 0; i < 2; i++) { + + if (i + 1 >= engine.financialHistory.size()) { + break; + } + + Micropolis.FinancialHistory f = engine.financialHistory.get(i); + Micropolis.FinancialHistory fPrior = engine.financialHistory.get(i+1); + int cashFlow = f.totalFunds - fPrior.totalFunds; + int capExpenses = -(cashFlow - f.taxIncome + f.operatingExpenses); + + c1.gridx++; + c1.gridy = 0; + + thLbl = new JLabel(formatGameDate(f.cityTime-1)); + thLbl.setFont(headFont); + thLbl.setForeground(Color.MAGENTA); + balancePane.add(thLbl, c1); + + c1.gridy++; + JLabel previousBalanceLbl = new JLabel(); + previousBalanceLbl.setText(formatFunds(fPrior.totalFunds)); + balancePane.add(previousBalanceLbl, c1); + + c1.gridy++; + JLabel taxIncomeLbl = new JLabel(); + taxIncomeLbl.setText(formatFunds(f.taxIncome)); + balancePane.add(taxIncomeLbl, c1); + + c1.gridy++; + JLabel capExpensesLbl = new JLabel(); + capExpensesLbl.setText(formatFunds(capExpenses)); + balancePane.add(capExpensesLbl, c1); + + c1.gridy++; + JLabel opExpensesLbl = new JLabel(); + opExpensesLbl.setText(formatFunds(f.operatingExpenses)); + balancePane.add(opExpensesLbl, c1); + + c1.gridy++; + JLabel newBalanceLbl = new JLabel(); + newBalanceLbl.setText(formatFunds(f.totalFunds)); + balancePane.add(newBalanceLbl, c1); + } + + return balancePane; + } +} diff --git a/src/micropolisj/gui/ColorParser.java b/src/micropolisj/gui/ColorParser.java new file mode 100644 index 0000000..6cde0dc --- /dev/null +++ b/src/micropolisj/gui/ColorParser.java @@ -0,0 +1,35 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.Color; + +public class ColorParser +{ + private ColorParser() {} + + static Color parseColor(String str) + { + if (str.startsWith("#") && str.length() == 7) { + return new Color(Integer.parseInt(str.substring(1), 16)); + } + else if (str.startsWith("rgba(") && str.endsWith(")")) { + String [] parts = str.substring(5,str.length()-1).split(","); + int r = Integer.parseInt(parts[0]); + int g = Integer.parseInt(parts[1]); + int b = Integer.parseInt(parts[2]); + double aa = Double.parseDouble(parts[3]); + int a = Math.min(255, (int)Math.floor(aa*256.0)); + return new Color(r,g,b,a); + } + else { + throw new Error("invalid color format: "+str); + } + } +} diff --git a/src/micropolisj/gui/DemandIndicator.java b/src/micropolisj/gui/DemandIndicator.java new file mode 100644 index 0000000..7b48735 --- /dev/null +++ b/src/micropolisj/gui/DemandIndicator.java @@ -0,0 +1,146 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.*; +import java.awt.image.*; +import java.net.URL; +import javax.swing.*; + +import micropolisj.engine.*; + +public class DemandIndicator extends JComponent + implements Micropolis.Listener +{ + Micropolis engine; + + public DemandIndicator() + { + } + + public void setEngine(Micropolis newEngine) + { + if (engine != null) { //old engine + engine.removeListener(this); + } + + engine = newEngine; + + if (engine != null) { //new engine + engine.addListener(this); + } + repaint(); + } + + static final BufferedImage backgroundImage = loadImage("/demandg.png"); + static BufferedImage loadImage(String resourceName) + { + URL iconUrl = MicropolisDrawingArea.class.getResource(resourceName); + Image refImage = new ImageIcon(iconUrl).getImage(); + + BufferedImage bi = new BufferedImage(refImage.getWidth(null), refImage.getHeight(null), + BufferedImage.TYPE_INT_RGB); + Graphics2D gr = bi.createGraphics(); + gr.drawImage(refImage, 0, 0, null); + + return bi; + } + + static final Dimension MY_SIZE = new Dimension( + backgroundImage.getWidth(), + backgroundImage.getHeight() + ); + + @Override + public Dimension getMinimumSize() + { + return MY_SIZE; + } + + @Override + public Dimension getPreferredSize() + { + return MY_SIZE; + } + + @Override + public Dimension getMaximumSize() + { + return MY_SIZE; + } + + static final int UPPER_EDGE = 19; + static final int LOWER_EDGE = 28; + static final int MAX_LENGTH = 16; + + public void paintComponent(Graphics gr1) + { + Graphics2D gr = (Graphics2D) gr1; + gr.drawImage(backgroundImage, 0, 0, null); + + if (engine == null) + return; + + int resValve = engine.getResValve(); + int ry0 = resValve <= 0 ? LOWER_EDGE : UPPER_EDGE; + int ry1 = ry0 - resValve/100; + + if (ry1 - ry0 > MAX_LENGTH) { ry1 = ry0 + MAX_LENGTH; } + if (ry1 - ry0 < -MAX_LENGTH) { ry1 = ry0 - MAX_LENGTH; } + + int comValve = engine.getComValve(); + int cy0 = comValve <= 0 ? LOWER_EDGE : UPPER_EDGE; + int cy1 = cy0 - comValve/100; + + int indValve = engine.getIndValve(); + int iy0 = indValve <= 0 ? LOWER_EDGE : UPPER_EDGE; + int iy1 = iy0 - indValve/100; + + if (ry0 != ry1) + { + Rectangle resRect = new Rectangle(8, Math.min(ry0,ry1), 6, Math.abs(ry1-ry0)); + gr.setColor(Color.GREEN); + gr.fill(resRect); + gr.setColor(Color.BLACK); + gr.draw(resRect); + } + + if (cy0 != cy1) + { + Rectangle comRect = new Rectangle(17, Math.min(cy0,cy1), 6, Math.abs(cy1-cy0)); + gr.setColor(Color.BLUE); + gr.fill(comRect); + gr.setColor(Color.BLACK); + gr.draw(comRect); + } + + if (iy0 != iy1) + { + Rectangle indRect = new Rectangle(26, Math.min(iy0,iy1), 6, Math.abs(iy1-iy0)); + gr.setColor(Color.YELLOW); + gr.fill(indRect); + gr.setColor(Color.BLACK); + gr.draw(indRect); + } + } + + //implements Micropolis.Listener + public void demandChanged() + { + repaint(); + } + + //implements Micropolis.Listener + public void cityMessage(MicropolisMessage m, CityLocation p, boolean x) { } + public void citySound(Sound sound, CityLocation p) { } + public void censusChanged() { } + public void evaluationChanged() { } + public void fundsChanged() { } + public void optionsChanged() { } +} diff --git a/src/micropolisj/gui/EvaluationPane.java b/src/micropolisj/gui/EvaluationPane.java new file mode 100644 index 0000000..7ad9d84 --- /dev/null +++ b/src/micropolisj/gui/EvaluationPane.java @@ -0,0 +1,298 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.*; +import java.awt.event.*; +import java.text.*; +import java.util.*; +import javax.swing.*; + +import micropolisj.engine.*; +import static micropolisj.gui.MainWindow.formatFunds; + +public class EvaluationPane extends JPanel + implements Micropolis.Listener +{ + Micropolis engine; + + JLabel yesLbl; + JLabel noLbl; + JLabel [] voterProblemLbl; + JLabel [] voterCountLbl; + JLabel popLbl; + JLabel deltaLbl; + JLabel assessLbl; + JLabel cityClassLbl; + JLabel gameLevelLbl; + JLabel scoreLbl; + JLabel scoreDeltaLbl; + + static ResourceBundle cstrings = ResourceBundle.getBundle("micropolisj.CityStrings"); + static ResourceBundle gstrings = MainWindow.strings; + + public EvaluationPane(Micropolis _engine) + { + super(new BorderLayout()); + + JButton dismissBtn = new JButton(gstrings.getString("dismiss-evaluation")); + dismissBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onDismissClicked(); + }}); + add(dismissBtn, BorderLayout.SOUTH); + + Box b1 = new Box(BoxLayout.X_AXIS); + add(b1, BorderLayout.CENTER); + + b1.add(makePublicOpinionPane()); + b1.add(new JSeparator(SwingConstants.VERTICAL)); + b1.add(makeStatisticsPane()); + + assert _engine != null; + setEngine(_engine); + } + + public void setEngine(Micropolis newEngine) + { + if (engine != null) { //old engine + engine.removeListener(this); + } + engine = newEngine; + if (engine != null) { //new engine + engine.addListener(this); + loadEvaluation(); + } + } + + private void onDismissClicked() + { + setVisible(false); + } + + private JComponent makePublicOpinionPane() + { + JPanel me = new JPanel(new GridBagLayout()); + GridBagConstraints c1 = new GridBagConstraints(); + GridBagConstraints c2 = new GridBagConstraints(); + GridBagConstraints c3 = new GridBagConstraints(); + + // c1 is for the full-width headers + c1.gridx = c1.gridy = 0; + c1.gridwidth = 2; + c1.gridheight = 1; + c1.weightx = 1.0; + c1.fill = GridBagConstraints.NONE; + c1.anchor = GridBagConstraints.NORTH; + + JLabel headerLbl = new JLabel(gstrings.getString("public-opinion")); + Font curFont = headerLbl.getFont(); + headerLbl.setFont( + curFont.deriveFont(curFont.getStyle() | Font.BOLD, (float)(curFont.getSize() * 1.2)) + ); + me.add(headerLbl, c1); + + c1.gridy = 1; + c1.insets = new Insets(3, 0, 3, 0); + me.add(new JLabel(gstrings.getString("public-opinion-1")), c1); + + c1.gridy = 4; + me.add(new JLabel(gstrings.getString("public-opinion-2")), c1); + + c2.gridx = 0; + c2.gridy = 2; + c2.gridwidth = c2.gridheight = 1; + c2.weightx = 1.0; + c2.anchor = GridBagConstraints.EAST; + c2.insets = new Insets(0, 0, 0, 4); + + me.add(new JLabel(gstrings.getString("public-opinion-yes")), c2); + + c2.gridy = 3; + me.add(new JLabel(gstrings.getString("public-opinion-no")), c2); + + c3.gridx = 1; + c3.gridwidth = c3.gridheight = 1; + c3.weightx = 1.0; + c3.anchor = GridBagConstraints.WEST; + c3.insets = new Insets(0, 4, 0, 0); + + c3.gridy = 2; + yesLbl = new JLabel(); + me.add(yesLbl, c3); + + c3.gridy = 3; + noLbl = new JLabel(); + me.add(noLbl, c3); + + c2.gridy = c3.gridy = 5; + + final int NUM_PROBS = 4; + voterProblemLbl = new JLabel[NUM_PROBS]; + voterCountLbl = new JLabel[NUM_PROBS]; + for (int i = 0; i < NUM_PROBS; i++) { + voterProblemLbl[i] = new JLabel(); + me.add(voterProblemLbl[i], c2); + + voterCountLbl[i] = new JLabel(); + me.add(voterCountLbl[i], c3); + + c2.gridy = ++c3.gridy; + } + + // add glue so that everything will align towards the top + c1.gridy = 999; + c1.weighty = 1.0; + me.add(new JLabel(), c1); + + return me; + } + + private JComponent makeStatisticsPane() + { + JPanel me = new JPanel(new GridBagLayout()); + GridBagConstraints c1 = new GridBagConstraints(); + GridBagConstraints c2 = new GridBagConstraints(); + GridBagConstraints c3 = new GridBagConstraints(); + + c1.gridx = c1.gridy = 0; + c1.gridwidth = 2; + c1.gridheight = 1; + c1.weightx = 1.0; + c1.fill = GridBagConstraints.NONE; + c1.anchor = GridBagConstraints.NORTH; + c1.insets = new Insets(0,0,3,0); + + JLabel headerLbl = new JLabel(gstrings.getString("statistics-head")); + Font curFont = headerLbl.getFont(); + headerLbl.setFont( + curFont.deriveFont(curFont.getStyle() | Font.BOLD, (float)(curFont.getSize() * 1.2)) + ); + me.add(headerLbl, c1); + + c1.gridy = 20; + c1.insets = new Insets(9, 0, 3, 0); + c1.fill = GridBagConstraints.VERTICAL; + JLabel header2Lbl = new JLabel(gstrings.getString("city-score-head")); + me.add(header2Lbl, c1); + + c2.gridx = 0; + c2.gridwidth = c2.gridheight = 1; + c2.weightx = 0.5; + c2.anchor = GridBagConstraints.EAST; + c2.insets = new Insets(0, 0, 0, 4); + + c3.gridx = 1; + c3.gridwidth = c3.gridheight = 1; + c3.weightx = 0.5; + c3.anchor = GridBagConstraints.WEST; + c3.insets = new Insets(0, 4, 0, 0); + + c2.gridy = c3.gridy = 1; + me.add(new JLabel(gstrings.getString("stats-population")), c2); + popLbl = new JLabel(); + me.add(popLbl, c3); + + c2.gridy = ++c3.gridy; + me.add(new JLabel(gstrings.getString("stats-net-migration")), c2); + deltaLbl = new JLabel(); + me.add(deltaLbl, c3); + + c2.gridy = ++c3.gridy; + me.add(new JLabel(gstrings.getString("stats-last-year")), c2); + + c2.gridy = ++c3.gridy; + me.add(new JLabel(gstrings.getString("stats-assessed-value")), c2); + assessLbl = new JLabel(); + me.add(assessLbl, c3); + + c2.gridy = ++c3.gridy; + me.add(new JLabel(gstrings.getString("stats-category")), c2); + cityClassLbl = new JLabel(); + me.add(cityClassLbl, c3); + + c2.gridy = ++c3.gridy; + me.add(new JLabel(gstrings.getString("stats-game-level")), c2); + gameLevelLbl = new JLabel(); + me.add(gameLevelLbl, c3); + + c2.gridy = c3.gridy = 21; + me.add(new JLabel(gstrings.getString("city-score-current")), c2); + scoreLbl = new JLabel(); + me.add(scoreLbl, c3); + + c2.gridy = ++c3.gridy; + me.add(new JLabel(gstrings.getString("city-score-change")), c2); + scoreDeltaLbl = new JLabel(); + me.add(scoreDeltaLbl, c3); + + // add glue so that everything will align towards the top + c1.gridy = 999; + c1.weighty = 1.0; + c1.insets = new Insets(0,0,0,0); + me.add(new JLabel(), c1); + + return me; + } + + //implements Micropolis.Listener + public void cityMessage(MicropolisMessage message, CityLocation loc, boolean isPic) {} + public void citySound(Sound sound, CityLocation loc) {} + public void censusChanged() {} + public void demandChanged() {} + public void fundsChanged() {} + public void optionsChanged() {} + + //implements Micropolis.Listener + public void evaluationChanged() + { + loadEvaluation(); + } + + private void loadEvaluation() + { + NumberFormat pctFmt = NumberFormat.getPercentInstance(); + yesLbl.setText(pctFmt.format(0.01 * engine.evaluation.cityYes)); + noLbl.setText(pctFmt.format(0.01 * engine.evaluation.cityNo)); + + for (int i = 0; i < voterProblemLbl.length; i++) { + CityProblem p = i < engine.evaluation.problemOrder.length ? engine.evaluation.problemOrder[i] : null; + int numVotes = p != null ? engine.evaluation.problemVotes.get(p) : 0; + + if (numVotes != 0) { + voterProblemLbl[i].setText(cstrings.getString("problem."+p.name())); + voterCountLbl[i].setText(pctFmt.format(0.01 * numVotes)); + voterProblemLbl[i].setVisible(true); + voterCountLbl[i].setVisible(true); + } else { + voterProblemLbl[i].setVisible(false); + voterCountLbl[i].setVisible(false); + } + } + + NumberFormat nf = NumberFormat.getInstance(); + popLbl.setText(nf.format(engine.evaluation.cityPop)); + deltaLbl.setText(nf.format(engine.evaluation.deltaCityPop)); + assessLbl.setText(formatFunds(engine.evaluation.cityAssValue)); + cityClassLbl.setText(getCityClassName(engine.evaluation.cityClass)); + gameLevelLbl.setText(getGameLevelName(engine.gameLevel)); + scoreLbl.setText(nf.format(engine.evaluation.cityScore)); + scoreDeltaLbl.setText(nf.format(engine.evaluation.deltaCityScore)); + } + + static String getCityClassName(int cityClass) + { + return cstrings.getString("class."+cityClass); + } + + static String getGameLevelName(int gameLevel) + { + return cstrings.getString("level."+gameLevel); + } +} diff --git a/src/micropolisj/gui/GraphsPane.java b/src/micropolisj/gui/GraphsPane.java new file mode 100644 index 0000000..8e371ca --- /dev/null +++ b/src/micropolisj/gui/GraphsPane.java @@ -0,0 +1,320 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Path2D; +import java.text.*; +import java.util.*; +import javax.swing.*; + +import micropolisj.engine.*; +import static micropolisj.gui.ColorParser.parseColor; + +public class GraphsPane extends JPanel + implements Micropolis.Listener +{ + Micropolis engine; + + JToggleButton tenYearsBtn; + JToggleButton onetwentyYearsBtn; + GraphArea graphArea; + + static enum TimePeriod + { + TEN_YEARS, + ONETWENTY_YEARS; + } + + static enum GraphData + { + RESPOP, + COMPOP, + INDPOP, + MONEY, + CRIME, + POLLUTION; + } + EnumMap dataBtns = new EnumMap<>(GraphData.class); + + static ResourceBundle strings = MainWindow.strings; + static final int LEFT_MARGIN = 4; + static final int RIGHT_MARGIN = 4; + static final int TOP_MARGIN = 2; + static final int BOTTOM_MARGIN = 2; + static final int LEGEND_PADDING = 6; + + public GraphsPane(Micropolis engine) + { + super(new BorderLayout()); + + assert engine != null; + this.engine = engine; + engine.addListener(this); + + JButton dismissBtn = new JButton(strings.getString("dismiss_graph")); + dismissBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onDismissClicked(); + }}); + add(dismissBtn, BorderLayout.SOUTH); + + JPanel b1 = new JPanel(new BorderLayout()); + add(b1, BorderLayout.CENTER); + + JPanel toolsPane = new JPanel(new GridBagLayout()); + b1.add(toolsPane, BorderLayout.WEST); + + GridBagConstraints c = new GridBagConstraints(); + c.gridx = c.gridy = 0; + c.gridwidth = 2; + c.fill = GridBagConstraints.BOTH; + c.insets = new Insets(1,1,1,1); + tenYearsBtn = new JToggleButton(strings.getString("ten_years")); + tenYearsBtn.setMargin(new Insets(0,0,0,0)); + tenYearsBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + setTimePeriod(TimePeriod.TEN_YEARS); + }}); + toolsPane.add(tenYearsBtn, c); + + c.gridy++; + onetwentyYearsBtn = new JToggleButton(strings.getString("onetwenty_years")); + onetwentyYearsBtn.setMargin(new Insets(0,0,0,0)); + onetwentyYearsBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + setTimePeriod(TimePeriod.ONETWENTY_YEARS); + }}); + toolsPane.add(onetwentyYearsBtn, c); + + c.gridx = 0; + c.gridy = 2; + c.gridwidth = 1; + c.anchor = GridBagConstraints.NORTH; + c.weightx = 0.5; + toolsPane.add(makeDataBtn(GraphData.RESPOP), c); + + c.gridy = 3; + toolsPane.add(makeDataBtn(GraphData.COMPOP), c); + + c.gridy = 4; + toolsPane.add(makeDataBtn(GraphData.INDPOP), c); + + c.gridx = 1; + c.gridy = 2; + toolsPane.add(makeDataBtn(GraphData.MONEY), c); + + c.gridy = 3; + toolsPane.add(makeDataBtn(GraphData.CRIME), c); + + c.gridy = 4; + toolsPane.add(makeDataBtn(GraphData.POLLUTION), c); + + graphArea = new GraphArea(); + b1.add(graphArea, BorderLayout.CENTER); + + setTimePeriod(TimePeriod.TEN_YEARS); + dataBtns.get(GraphData.MONEY).setSelected(true); + dataBtns.get(GraphData.POLLUTION).setSelected(true); + } + + public void setEngine(Micropolis newEngine) + { + if (engine != null) { //old engine + engine.removeListener(this); + } + engine = newEngine; + if (engine != null) { //new engine + engine.addListener(this); + graphArea.repaint(); + } + } + + private void onDismissClicked() + { + setVisible(false); + } + + //implements Micropolis.Listener + public void cityMessage(MicropolisMessage message, CityLocation loc, boolean isPic) {} + public void citySound(Sound sound, CityLocation loc) {} + public void demandChanged() {} + public void evaluationChanged() {} + public void fundsChanged() {} + public void optionsChanged() {} + + //implements Micropolis.Listener + public void censusChanged() + { + graphArea.repaint(); + } + + private JToggleButton makeDataBtn(GraphData graph) + { + String icon1name = strings.getString("graph_button."+graph.name()); + String icon2name = strings.getString("graph_button."+graph.name()+".selected"); + + ImageIcon icon1 = new ImageIcon(getClass().getResource("/"+icon1name)); + ImageIcon icon2 = new ImageIcon(getClass().getResource("/"+icon2name)); + + JToggleButton btn = new JToggleButton(); + btn.setIcon(icon1); + btn.setSelectedIcon(icon2); + btn.setBorder(null); + btn.setBorderPainted(false); + btn.setFocusPainted(false); + btn.setContentAreaFilled(false); + btn.setMargin(new Insets(0,0,0,0)); + + btn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + graphArea.repaint(); + }}); + + dataBtns.put(graph, btn); + return btn; + } + + int getHistoryValue(GraphData graph, int pos) + { + assert pos >= 0 && pos < 240; + switch(graph) { + case RESPOP: return engine.history.res[pos]; + case COMPOP: return engine.history.com[pos]; + case INDPOP: return engine.history.ind[pos]; + case MONEY: return engine.history.money[pos]; + case CRIME: return engine.history.crime[pos]; + case POLLUTION: return engine.history.pollution[pos]; + default: throw new Error("unexpected"); + } + } + + void setTimePeriod(TimePeriod period) + { + tenYearsBtn.setSelected(period == TimePeriod.TEN_YEARS); + onetwentyYearsBtn.setSelected(period == TimePeriod.ONETWENTY_YEARS); + graphArea.repaint(); + } + + class GraphArea extends JComponent + { + GraphArea() + { + setBorder(BorderFactory.createLoweredBevelBorder()); + } + + @Override + public void paintComponent(Graphics gr1) + { + Graphics2D gr = (Graphics2D)gr1; + FontMetrics fm = gr.getFontMetrics(); + + gr.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + gr.setColor(Color.WHITE); + gr.fill(gr.getClipBounds()); + + // determine length of longest label + int maxLabelWidth = 0; + for (GraphData gd : GraphData.values()) + { + String labelStr = strings.getString("graph_label."+gd.name()); + int adv = fm.stringWidth(labelStr); + if (adv > maxLabelWidth) { + maxLabelWidth = adv; + } + } + + int leftEdge = getInsets().left + LEFT_MARGIN; + int topEdge = getInsets().top + TOP_MARGIN + fm.getHeight()*2; + int bottomEdge = getHeight() - getInsets().bottom - getInsets().top - BOTTOM_MARGIN; + int rightEdge = getWidth() - getInsets().right - getInsets().left - RIGHT_MARGIN - maxLabelWidth - LEGEND_PADDING; + + // draw graph lower, upper borders + gr.setColor(Color.BLACK); + gr.drawLine(leftEdge,topEdge,rightEdge,topEdge); + gr.drawLine(leftEdge,bottomEdge,rightEdge,bottomEdge); + + // draw vertical bars and label the dates + boolean isOneTwenty = onetwentyYearsBtn.isSelected(); + int unitPeriod = isOneTwenty ? 12*Micropolis.CENSUSRATE : Micropolis.CENSUSRATE; + int hashPeriod = isOneTwenty ? 10*unitPeriod : 12*unitPeriod; + int startTime = ((engine.history.cityTime / unitPeriod) - 119) * unitPeriod; + + double x_interval = (rightEdge - leftEdge) / 120.0; + for (int i = 0; i < 120; i++) { + int t = startTime + i * unitPeriod; // t might be negative + if (t % hashPeriod == 0) { + // year + int year = 1900+(t/(12*Micropolis.CENSUSRATE)); + int numHashes = t/hashPeriod; + int x = (int)Math.round(leftEdge+i*x_interval); + int y = getInsets().top + TOP_MARGIN + + (numHashes % 2 == 0 ? fm.getHeight() : 0) + + fm.getAscent(); + gr.drawString(Integer.toString(year), x, y); + gr.drawLine(x,topEdge,x,bottomEdge); + } + } + + int H = isOneTwenty ? 239 : 119; + final HashMap paths = new HashMap<>(); + for (GraphData gd : GraphData.values()) + { + if (dataBtns.get(gd).isSelected()) { + + Path2D.Double path = new Path2D.Double(); + for (int i = 0; i < 120; i++) { + double xp = leftEdge + i * x_interval; + double yp = bottomEdge - getHistoryValue(gd,H-i) * (bottomEdge-topEdge) / 256.0; + if (i == 0) { + path.moveTo(xp, yp); + } else { + path.lineTo(xp, yp); + } + } + paths.put(gd, path); + } + } + + GraphData [] myGraphs = paths.keySet().toArray(new GraphData[0]); + Arrays.sort(myGraphs, new Comparator() { + public int compare(GraphData a, GraphData b) { + double y0 = paths.get(a).getCurrentPoint().getY(); + double y1 = paths.get(b).getCurrentPoint().getY(); + return -Double.compare(y0,y1); + }}); + + int lbottom = bottomEdge; + for (GraphData gd : myGraphs) + { + String labelStr = strings.getString("graph_label."+gd.name()); + String colStr = strings.getString("graph_color."+gd.name()); + Color col = parseColor(colStr); + Path2D.Double path = paths.get(gd); + + gr.setColor(col); + gr.setStroke(new BasicStroke(2)); + gr.draw(path); + + int x = rightEdge + LEGEND_PADDING; + int y = (int)Math.round(path.getCurrentPoint().getY()+fm.getAscent()/2); + y = Math.min(lbottom, y); + lbottom = y - fm.getAscent(); + + gr.setColor(col); + gr.drawString(labelStr, x-1, y); + gr.drawString(labelStr, x, y-1); + + gr.setColor(Color.BLACK); + gr.drawString(labelStr, x, y); + } + } + } +} diff --git a/src/micropolisj/gui/MainWindow.java b/src/micropolisj/gui/MainWindow.java new file mode 100644 index 0000000..39dde93 --- /dev/null +++ b/src/micropolisj/gui/MainWindow.java @@ -0,0 +1,1365 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.net.URL; +import java.text.MessageFormat; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.prefs.*; +import javax.sound.sampled.*; +import javax.swing.*; +import javax.swing.Timer; +import javax.swing.filechooser.FileNameExtensionFilter; + +import micropolisj.engine.*; +import static micropolisj.gui.ColorParser.parseColor; + +public class MainWindow extends JFrame + implements Micropolis.Listener, EarthquakeListener +{ + Micropolis engine; + MicropolisDrawingArea drawingArea; + JScrollPane drawingAreaScroll; + DemandIndicator demandInd; + MessagesPane messagesPane; + JLabel mapLegendLbl; + OverlayMapView mapView; + NotificationPane notificationPane; + EvaluationPane evaluationPane; + GraphsPane graphsPane; + JLabel dateLbl; + JLabel fundsLbl; + JLabel popLbl; + JLabel currentToolLbl; + JLabel currentToolCostLbl; + Map toolBtns; + EnumMap mapStateMenuItems = new EnumMap<>(MapState.class); + MicropolisTool currentTool; + File currentFile; + boolean doSounds = true; + boolean dirty1 = false; //indicates if a tool was successfully applied since last save + boolean dirty2 = false; //indicates if simulator took a step since last save + long lastSavedTime = 0; //real-time clock of when file was last saved + + static ImageIcon appIcon; + static { + appIcon = new ImageIcon(MainWindow.class.getResource("/micropolism.png")); + } + + static ResourceBundle strings = ResourceBundle.getBundle("micropolisj.GuiStrings"); + static final String PRODUCT_NAME = strings.getString("PRODUCT"); + + public MainWindow() + { + this(new Micropolis()); + } + + public MainWindow(Micropolis engine) + { + setIconImage(appIcon.getImage()); + + this.engine = engine; + + JPanel mainArea = new JPanel(new BorderLayout()); + add(mainArea, BorderLayout.CENTER); + + drawingArea = new MicropolisDrawingArea(engine); + drawingAreaScroll = new JScrollPane(drawingArea); + mainArea.add(drawingAreaScroll); + + makeMenu(); + JToolBar tb = makeToolbar(); + mainArea.add(tb, BorderLayout.WEST); + + Box evalGraphsBox = new Box(BoxLayout.Y_AXIS); + mainArea.add(evalGraphsBox, BorderLayout.SOUTH); + + graphsPane = new GraphsPane(engine); + graphsPane.setVisible(false); + evalGraphsBox.add(graphsPane); + + evaluationPane = new EvaluationPane(engine); + evaluationPane.setVisible(false); + evalGraphsBox.add(evaluationPane, BorderLayout.SOUTH); + + JPanel leftPane = new JPanel(new GridBagLayout()); + add(leftPane, BorderLayout.WEST); + + GridBagConstraints c = new GridBagConstraints(); + c.gridx = c.gridy = 0; + c.anchor = GridBagConstraints.SOUTHWEST; + c.insets = new Insets(4,4,4,4); + c.weightx = 1.0; + + demandInd = new DemandIndicator(); + leftPane.add(demandInd, c); + + c.gridx = 1; + c.weightx = 0.0; + c.fill = GridBagConstraints.BOTH; + c.insets = new Insets(4, 20, 4, 4); + + leftPane.add(makeDateFunds(), c); + + c.gridx = 0; + c.gridy = 1; + c.gridwidth = 2; + c.weighty = 0.0; + c.anchor = GridBagConstraints.NORTH; + c.insets = new Insets(0,0,0,0); + + JPanel mapViewContainer = new JPanel(new BorderLayout()); + mapViewContainer.setBorder(BorderFactory.createLineBorder(Color.BLACK)); + leftPane.add(mapViewContainer, c); + + JMenuBar mapMenu = new JMenuBar(); + mapViewContainer.add(mapMenu, BorderLayout.NORTH); + + JMenu zonesMenu = new JMenu(strings.getString("menu.zones")); + mapMenu.add(zonesMenu); + + zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.ALL"), MapState.ALL)); + zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.RESIDENTIAL"), MapState.RESIDENTIAL)); + zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.COMMERCIAL"), MapState.COMMERCIAL)); + zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.INDUSTRIAL"), MapState.INDUSTRIAL)); + zonesMenu.add(makeMapStateMenuItem(strings.getString("menu.zones.TRANSPORT"), MapState.TRANSPORT)); + + JMenu overlaysMenu = new JMenu(strings.getString("menu.overlays")); + mapMenu.add(overlaysMenu); + + overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.POPDEN_OVERLAY"), MapState.POPDEN_OVERLAY)); + overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.GROWTHRATE_OVERLAY"), MapState.GROWTHRATE_OVERLAY)); + overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.LANDVALUE_OVERLAY"), MapState.LANDVALUE_OVERLAY)); + overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.CRIME_OVERLAY"), MapState.CRIME_OVERLAY)); + overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.POLLUTE_OVERLAY"), MapState.POLLUTE_OVERLAY)); + overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.TRAFFIC_OVERLAY"), MapState.TRAFFIC_OVERLAY)); + overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.POWER_OVERLAY"), MapState.POWER_OVERLAY)); + overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.FIRE_OVERLAY"), MapState.FIRE_OVERLAY)); + overlaysMenu.add(makeMapStateMenuItem(strings.getString("menu.overlays.POLICE_OVERLAY"), MapState.POLICE_OVERLAY)); + + mapMenu.add(Box.createHorizontalGlue()); + mapLegendLbl = new JLabel(); + mapMenu.add(mapLegendLbl); + + mapView = new OverlayMapView(engine); + mapView.connectView(drawingArea, drawingAreaScroll); + mapViewContainer.add(mapView, BorderLayout.CENTER); + + setMapState(MapState.ALL); + + c.gridx = 0; + c.gridy = 2; + c.gridwidth = 2; + c.weighty = 1.0; + c.fill = GridBagConstraints.BOTH; + c.insets = new Insets(0,0,0,0); + + messagesPane = new MessagesPane(); + JScrollPane scroll2 = new JScrollPane(messagesPane); + scroll2.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + scroll2.setPreferredSize(new Dimension(0,0)); + scroll2.setMinimumSize(new Dimension(0,0)); + leftPane.add(scroll2, c); + + c.gridy = 3; + c.weighty = 0.0; + notificationPane = new NotificationPane(engine); + leftPane.add(notificationPane, c); + + pack(); + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + setLocationRelativeTo(null); + + MouseAdapter mouse = new MouseAdapter() { + public void mousePressed(MouseEvent ev) + { + onToolDown(ev); + } + public void mouseReleased(MouseEvent ev) + { + onToolUp(ev); + } + public void mouseDragged(MouseEvent ev) + { + onToolDrag(ev); + } + public void mouseMoved(MouseEvent ev) + { + onToolHover(ev); + } + public void mouseExited(MouseEvent ev) + { + onToolExited(ev); + } + }; + drawingArea.addMouseListener(mouse); + drawingArea.addMouseMotionListener(mouse); + + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent ev) + { + closeWindow(); + } + public void windowClosed(WindowEvent ev) + { + onWindowClosed(ev); + } + }); + + Preferences prefs = Preferences.userNodeForPackage(MainWindow.class); + doSounds = prefs.getBoolean(SOUNDS_PREF, true); + + // start things up + mapView.setEngine(engine); + engine.addListener(this); + engine.addEarthquakeListener(this); + reloadFunds(); + reloadOptions(); + startTimer(); + makeClean(); + } + + public void setEngine(Micropolis newEngine) + { + if (engine != null) { // old engine + engine.removeListener(this); + engine.removeEarthquakeListener(this); + } + + engine = newEngine; + + if (engine != null) { // new engine + engine.addListener(this); + engine.addEarthquakeListener(this); + } + + boolean timerEnabled = isTimerActive(); + if (timerEnabled) { + stopTimer(); + } + stopEarthquake(); + + drawingArea.setEngine(engine); + mapView.setEngine(engine); //must change mapView after drawingArea + evaluationPane.setEngine(engine); + demandInd.setEngine(engine); + graphsPane.setEngine(engine); + reloadFunds(); + reloadOptions(); + notificationPane.setVisible(false); + + if (timerEnabled) { + startTimer(); + } + } + + boolean needsSaved() + { + if (dirty1) //player has built something since last save + return true; + + if (!dirty2) //no simulator ticks since last save + return false; + + // simulation time has passed since last save, but the player + // hasn't done anything. Whether we need to prompt for save + // will depend on how much real time has elapsed. + // The threshold is 30 seconds. + + return (System.currentTimeMillis() - lastSavedTime > 30000); + } + + boolean maybeSaveCity() + { + if (needsSaved()) + { + try { + stopTimer(); + + int rv = JOptionPane.showConfirmDialog( + this, + strings.getString("main.save_query"), + PRODUCT_NAME, + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + if (rv == JOptionPane.CANCEL_OPTION) + return false; + + if (rv == JOptionPane.YES_OPTION) { + if (!onSaveCityClicked()) { + // canceled save dialog + return false; + } + } + } + finally { + startTimer(); + } + } + return true; + } + + void closeWindow() + { + if (maybeSaveCity()) { + dispose(); + } + } + + JComponent makeDateFunds() + { + JPanel pane = new JPanel(new GridBagLayout()); + GridBagConstraints c0 = new GridBagConstraints(); + GridBagConstraints c1 = new GridBagConstraints(); + + c0.gridx = 0; + c1.gridx = 1; + c0.gridy = c1.gridy = 0; + c0.weightx = 1.0; + c0.weighty = c1.weighty = 1.0; + c0.anchor = GridBagConstraints.WEST; + c1.anchor = GridBagConstraints.EAST; + + pane.add(new JLabel(strings.getString("main.date_label")), c0); + dateLbl = new JLabel(); + pane.add(dateLbl, c1); + + c0.gridy = c1.gridy = 1; + + pane.add(new JLabel(strings.getString("main.funds_label")), c0); + fundsLbl = new JLabel(); + pane.add(fundsLbl, c1); + + c0.gridy = c1.gridy = 2; + + pane.add(new JLabel(strings.getString("main.population_label")), c0); + popLbl = new JLabel(); + pane.add(popLbl, c1); + + return pane; + } + + private void makeMenu() + { + JMenuBar menuBar = new JMenuBar(); + + JMenu gameMenu = new JMenu(strings.getString("menu.game")); + menuBar.add(gameMenu); + + JMenuItem menuItem; + menuItem = new JMenuItem(strings.getString("menu.game.new")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onNewCityClicked(); + } + }); + gameMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.game.load")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onLoadGameClicked(); + } + }); + gameMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.game.save")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onSaveCityClicked(); + } + }); + gameMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.game.save_as")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onSaveCityAsClicked(); + } + }); + gameMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.game.exit")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + closeWindow(); + } + }); + gameMenu.add(menuItem); + + JMenu optionsMenu = new JMenu(strings.getString("menu.options")); + menuBar.add(optionsMenu); + + JMenu levelMenu = new JMenu(strings.getString("menu.difficulty")); + optionsMenu.add(levelMenu); + + difficultyMenuItems = new HashMap(); + for (int i = GameLevel.MIN_LEVEL; i <= GameLevel.MAX_LEVEL; i++) + { + final int level = i; + menuItem = new JRadioButtonMenuItem(strings.getString("menu.difficulty."+level)); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onDifficultyClicked(level); + } + }); + levelMenu.add(menuItem); + difficultyMenuItems.put(level, menuItem); + } + + autoBudgetMenuItem = new JCheckBoxMenuItem(strings.getString("menu.options.auto_budget")); + autoBudgetMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onAutoBudgetClicked(); + } + }); + optionsMenu.add(autoBudgetMenuItem); + + autoBulldozeMenuItem = new JCheckBoxMenuItem(strings.getString("menu.options.auto_bulldoze")); + autoBulldozeMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onAutoBulldozeClicked(); + } + }); + optionsMenu.add(autoBulldozeMenuItem); + + disastersMenuItem = new JCheckBoxMenuItem(strings.getString("menu.options.disasters")); + disastersMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onDisastersClicked(); + } + }); + optionsMenu.add(disastersMenuItem); + + soundsMenuItem = new JCheckBoxMenuItem(strings.getString("menu.options.sound")); + soundsMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onSoundClicked(); + } + }); + optionsMenu.add(soundsMenuItem); + + JMenu disastersMenu = new JMenu(strings.getString("menu.disasters")); + menuBar.add(disastersMenu); + + menuItem = new JMenuItem(strings.getString("menu.disasters.MONSTER")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onInvokeDisasterClicked(Disaster.MONSTER); + } + }); + disastersMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.disasters.FIRE")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onInvokeDisasterClicked(Disaster.FIRE); + } + }); + disastersMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.disasters.FLOOD")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onInvokeDisasterClicked(Disaster.FLOOD); + } + }); + disastersMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.disasters.MELTDOWN")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onInvokeDisasterClicked(Disaster.MELTDOWN); + } + }); + disastersMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.disasters.TORNADO")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onInvokeDisasterClicked(Disaster.TORNADO); + } + }); + disastersMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.disasters.EARTHQUAKE")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onInvokeDisasterClicked(Disaster.EARTHQUAKE); + } + }); + disastersMenu.add(menuItem); + + JMenu priorityMenu = new JMenu(strings.getString("menu.speed")); + menuBar.add(priorityMenu); + + priorityMenuItems = new EnumMap(Speed.class); + menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.SUPER_FAST")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onPriorityClicked(Speed.SUPER_FAST); + } + }); + priorityMenu.add(menuItem); + priorityMenuItems.put(Speed.SUPER_FAST, menuItem); + + menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.FAST")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onPriorityClicked(Speed.FAST); + } + }); + priorityMenu.add(menuItem); + priorityMenuItems.put(Speed.FAST, menuItem); + + menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.NORMAL")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onPriorityClicked(Speed.NORMAL); + } + }); + priorityMenu.add(menuItem); + priorityMenuItems.put(Speed.NORMAL, menuItem); + + menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.SLOW")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onPriorityClicked(Speed.SLOW); + } + }); + priorityMenu.add(menuItem); + priorityMenuItems.put(Speed.SLOW, menuItem); + + menuItem = new JRadioButtonMenuItem(strings.getString("menu.speed.PAUSED")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onPriorityClicked(Speed.PAUSED); + } + }); + priorityMenu.add(menuItem); + priorityMenuItems.put(Speed.PAUSED, menuItem); + + JMenu windowsMenu = new JMenu(strings.getString("menu.windows")); + menuBar.add(windowsMenu); + + menuItem = new JMenuItem(strings.getString("menu.windows.budget")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onViewBudgetClicked(); + } + }); + windowsMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.windows.evaluation")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onViewEvaluationClicked(); + } + }); + windowsMenu.add(menuItem); + + menuItem = new JMenuItem(strings.getString("menu.windows.graph")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onViewGraphClicked(); + } + }); + windowsMenu.add(menuItem); + + JMenu helpMenu = new JMenu(strings.getString("menu.help")); + menuBar.add(helpMenu); + + menuItem = new JMenuItem(strings.getString("menu.help.about")); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + onAboutClicked(); + } + }); + helpMenu.add(menuItem); + + setJMenuBar(menuBar); + } + + private Micropolis getEngine() + { + return engine; + } + + JMenuItem autoBudgetMenuItem; + JMenuItem autoBulldozeMenuItem; + JMenuItem disastersMenuItem; + JMenuItem soundsMenuItem; + Map priorityMenuItems; + Map difficultyMenuItems; + + private void onAutoBudgetClicked() + { + dirty1 = true; + getEngine().toggleAutoBudget(); + } + + private void onAutoBulldozeClicked() + { + dirty1 = true; + getEngine().toggleAutoBulldoze(); + } + + private void onDisastersClicked() + { + dirty1 = true; + getEngine().toggleDisasters(); + } + + static final String SOUNDS_PREF = "enable_sounds"; + private void onSoundClicked() + { + doSounds = !doSounds; + Preferences prefs = Preferences.userNodeForPackage(MainWindow.class); + prefs.putBoolean(SOUNDS_PREF, doSounds); + reloadOptions(); + } + + void makeClean() + { + dirty1 = false; + dirty2 = false; + lastSavedTime = System.currentTimeMillis(); + if (currentFile != null) { + String fileName = currentFile.getName(); + if (fileName.endsWith("."+EXTENSION)) { + fileName = fileName.substring(0, fileName.length() - 1 - EXTENSION.length()); + } + setTitle(MessageFormat.format(strings.getString("main.caption_named_city"), fileName)); + } + else { + setTitle(strings.getString("main.caption_unnamed_city")); + } + } + + private boolean onSaveCityClicked() + { + if (currentFile == null) + { + return onSaveCityAsClicked(); + } + + try + { + getEngine().save(currentFile); + makeClean(); + return true; + } + catch (IOException e) + { + e.printStackTrace(System.err); + JOptionPane.showMessageDialog(this, e, strings.getString("main.error_caption"), + JOptionPane.ERROR_MESSAGE); + return false; + } + } + + static final String EXTENSION = "cty"; + private boolean onSaveCityAsClicked() + { + stopTimer(); + try + { + JFileChooser fc = new JFileChooser(); + FileNameExtensionFilter filter1 = new FileNameExtensionFilter(strings.getString("cty_file"), EXTENSION); + fc.setFileFilter(filter1); + int rv = fc.showSaveDialog(this); + if (rv == JFileChooser.APPROVE_OPTION) { + currentFile = fc.getSelectedFile(); + if (!currentFile.getName().endsWith("."+EXTENSION)) { + currentFile = new File(currentFile.getPath()+"."+EXTENSION); + } + getEngine().save(currentFile); + makeClean(); + return true; + } + } + catch (Exception e) + { + e.printStackTrace(System.err); + JOptionPane.showMessageDialog(this, e, strings.getString("main.error_caption"), + JOptionPane.ERROR_MESSAGE); + } + finally + { + startTimer(); + } + return false; + } + + private void onLoadGameClicked() + { + // check if user wants to save their current city + if (!maybeSaveCity()) { + return; + } + + try + { + JFileChooser fc = new JFileChooser(); + FileNameExtensionFilter filter1 = new FileNameExtensionFilter(strings.getString("cty_file"), EXTENSION); + fc.setFileFilter(filter1); + + stopTimer(); + int rv = fc.showOpenDialog(this); + startTimer(); + + if (rv == JFileChooser.APPROVE_OPTION) { + File file = fc.getSelectedFile(); + Micropolis newEngine = new Micropolis(); + newEngine.load(file); + setEngine(newEngine); + currentFile = file; + makeClean(); + } + } + catch (Exception e) + { + e.printStackTrace(System.err); + JOptionPane.showMessageDialog(this, e, strings.getString("main.error_caption"), + JOptionPane.ERROR_MESSAGE); + } + } + + private JToggleButton makeToolBtn(final MicropolisTool tool) + { + JToggleButton btn = new JToggleButton(); + btn.setIcon(new ImageIcon(MainWindow.class.getResource(strings.getString("tool."+tool.name()+".icon")))); + btn.setSelectedIcon(new ImageIcon(MainWindow.class.getResource(strings.getString("tool."+tool.name()+".selected_icon")))); + btn.setToolTipText(strings.getString("tool."+tool.name()+".tip")); + btn.setMargin(new Insets(0,0,0,0)); + btn.setBorderPainted(false); + btn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) + { + selectTool(tool); + } + }); + toolBtns.put(tool, btn); + return btn; + } + + private JToolBar makeToolbar() + { + toolBtns = new EnumMap(MicropolisTool.class); + + JToolBar toolBar = new JToolBar(strings.getString("main.tools_caption"), JToolBar.VERTICAL); + toolBar.setFloatable(false); + toolBar.setRollover(false); + + JPanel gridBox = new JPanel(new GridBagLayout()); + toolBar.add(gridBox); + + GridBagConstraints c = new GridBagConstraints(); + c.gridx = c.gridy = 0; + c.anchor = GridBagConstraints.NORTH; + c.insets = new Insets(8,0,0,0); + currentToolLbl = new JLabel(" "); + gridBox.add(currentToolLbl, c); + + c.gridy = 1; + c.insets = new Insets(0,0,12,0); + currentToolCostLbl = new JLabel(" "); + gridBox.add(currentToolCostLbl, c); + + c.gridy++; + c.fill = GridBagConstraints.NONE; + c.weightx = 1.0; + c.insets = new Insets(0,0,0,0); + Box b0 = new Box(BoxLayout.X_AXIS); + gridBox.add(b0,c); + + b0.add(makeToolBtn(MicropolisTool.BULLDOZER)); + b0.add(makeToolBtn(MicropolisTool.WIRE)); + b0.add(makeToolBtn(MicropolisTool.PARK)); + + c.gridy++; + Box b1 = new Box(BoxLayout.X_AXIS); + gridBox.add(b1,c); + + b1.add(makeToolBtn(MicropolisTool.ROADS)); + b1.add(makeToolBtn(MicropolisTool.RAIL)); + + c.gridy++; + Box b2 = new Box(BoxLayout.X_AXIS); + gridBox.add(b2,c); + + b2.add(makeToolBtn(MicropolisTool.RESIDENTIAL)); + b2.add(makeToolBtn(MicropolisTool.COMMERCIAL)); + b2.add(makeToolBtn(MicropolisTool.INDUSTRIAL)); + + c.gridy++; + Box b3 = new Box(BoxLayout.X_AXIS); + gridBox.add(b3,c); + + b3.add(makeToolBtn(MicropolisTool.FIRE)); + b3.add(makeToolBtn(MicropolisTool.QUERY)); + b3.add(makeToolBtn(MicropolisTool.POLICE)); + + c.gridy++; + Box b4 = new Box(BoxLayout.X_AXIS); + gridBox.add(b4,c); + + b4.add(makeToolBtn(MicropolisTool.POWERPLANT)); + b4.add(makeToolBtn(MicropolisTool.NUCLEAR)); + + c.gridy++; + Box b5 = new Box(BoxLayout.X_AXIS); + gridBox.add(b5,c); + + b5.add(makeToolBtn(MicropolisTool.STADIUM)); + b5.add(makeToolBtn(MicropolisTool.SEAPORT)); + + c.gridy++; + Box b6 = new Box(BoxLayout.X_AXIS); + gridBox.add(b6,c); + + b6.add(makeToolBtn(MicropolisTool.AIRPORT)); + + // add glue to make all elements align toward top + c.gridy++; + c.weighty = 1.0; + gridBox.add(new JLabel(), c); + + return toolBar; + } + + private void selectTool(MicropolisTool newTool) + { + toolBtns.get(newTool).setSelected(true); + if (newTool == currentTool) { + return; + } + + if (currentTool != null) { + toolBtns.get(currentTool).setSelected(false); + } + + currentTool = newTool; + + currentToolLbl.setText(strings.getString("tool."+currentTool.name()+".name")); + + int cost = currentTool.getToolCost(); + currentToolCostLbl.setText(cost != 0 ? formatFunds(cost) : " "); + } + + private void onNewCityClicked() + { + if (maybeSaveCity()) { + doNewCity(false); + } + } + + public void doNewCity(boolean firstTime) + { + stopTimer(); + new NewCityDialog(this, !firstTime).setVisible(true); + startTimer(); + } + + void doQueryTool(int xpos, int ypos) + { + ZoneStatus z = engine.queryZoneStatus(xpos, ypos); + notificationPane.showZoneStatus(engine, xpos, ypos, z); + } + + // where the tool was first pressed + int origX; + int origY; + + // where the tool was last applied during the current drag + int lastX; + int lastY; + + private void onToolDown(MouseEvent ev) + { + if (ev.getButton() == MouseEvent.BUTTON3) { + doQueryTool(ev.getX() / MicropolisDrawingArea.TILE_WIDTH, + ev.getY() / MicropolisDrawingArea.TILE_HEIGHT); + return; + } + + if (ev.getButton() != MouseEvent.BUTTON1) + return; + + if (currentTool == null) + return; + + int x = ev.getX() / MicropolisDrawingArea.TILE_WIDTH; + int y = ev.getY() / MicropolisDrawingArea.TILE_HEIGHT; + lastX = x; + lastY = y; + origX = x; + origY = y; + + applyCurrentTool(x, y, false); + } + + private void onToolUp(MouseEvent ev) + { + } + + private void onToolDrag(MouseEvent ev) + { + onToolHover(ev); + + if (currentTool == null) + return; + if ((ev.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0) + return; + + int x = ev.getX() / MicropolisDrawingArea.TILE_WIDTH; + int y = ev.getY() / MicropolisDrawingArea.TILE_HEIGHT; + if (x == lastX && y == lastY) + return; + + while (lastX != x || lastY != y) + { + if (Math.abs(lastX-x) >= Math.abs(lastY-y)) + { + lastX += lastX < x ? 1 : -1; + } + else + { + lastY += lastY < y ? 1 : -1; + } + applyCurrentTool(lastX, lastY, true); + } + + assert lastX == x; + assert lastY == y; + } + + private void onToolHover(MouseEvent ev) + { + if (currentTool == null || currentTool == MicropolisTool.QUERY) + { + drawingArea.setToolPreview(null); + return; + } + + int x = ev.getX() / MicropolisDrawingArea.TILE_WIDTH; + int y = ev.getY() / MicropolisDrawingArea.TILE_HEIGHT; + int w = currentTool.getWidth(); + int h = currentTool.getHeight(); + + if (w >= 3) + x--; + if (h >= 3) + y--; + + drawingArea.setToolPreview(new Rectangle(x,y,w,h), + parseColor(strings.getString("tool."+currentTool.name()+".border")) + ); + drawingArea.toolPreview.fillColor = parseColor(strings.getString("tool."+currentTool.name()+".bgcolor")); + } + + private void onToolExited(MouseEvent ev) + { + drawingArea.setToolPreview(null); + } + + private void applyCurrentTool(int x, int y, boolean drag) + { + if (currentTool == MicropolisTool.QUERY) { + doQueryTool(x, y); + return; + } + + ToolResult result = currentTool.apply(engine, x, y); + + switch (result) { + case SUCCESS: + citySound(currentTool == MicropolisTool.BULLDOZER ? Sound.BULLDOZE : Sound.BUILD, new CityLocation(x, y)); + dirty1 = true; + break; + + case NONE: break; + case UH_OH: + if (!drag) { + messagesPane.appendCityMessage(MicropolisMessage.BULLDOZE_FIRST); + citySound(Sound.UHUH, new CityLocation(x, y)); + } + break; + case INSUFFICIENT_FUNDS: + if (!drag) { + messagesPane.appendCityMessage(MicropolisMessage.INSUFFICIENT_FUNDS); + citySound(Sound.SORRY, new CityLocation(x, y)); + } + break; + default: + assert false; + } + } + + public static String formatFunds(int funds) + { + return MessageFormat.format( + strings.getString("funds"), funds + ); + } + + public static String formatGameDate(int cityTime) + { + Calendar c = Calendar.getInstance(); + c.set(1900 + cityTime/48, + (cityTime%48)/4, + (cityTime%4)*7 + 1 + ); + + return MessageFormat.format( + strings.getString("citytime"), + c.getTime() + ); + } + + private void updateDateLabel() + { + dateLbl.setText(formatGameDate(engine.cityTime)); + + NumberFormat nf = NumberFormat.getInstance(); + popLbl.setText(nf.format(getEngine().getCityPopulation())); + } + + Timer simTimer; + Timer shakeTimer; + + private void startTimer() + { + final Micropolis engine = getEngine(); + final int updateCycle = engine.simSpeed.aniFramesPerStep; + final int count = engine.simSpeed.simStepsPerUpdate; + + assert !isTimerActive(); + + if (engine.simSpeed == Speed.PAUSED) + return; + + if (currentEarthquake != null) + { + int interval = 3000 / MicropolisDrawingArea.SHAKE_STEPS; + shakeTimer = new Timer(interval, new ActionListener() { + public void actionPerformed(ActionEvent evt) { + currentEarthquake.oneStep(); + if (currentEarthquake.count == 0) { + stopTimer(); + currentEarthquake = null; + startTimer(); + } + }}); + shakeTimer.start(); + return; + } + + ActionListener taskPerformer = new ActionListener() { + public void actionPerformed(ActionEvent evt) + { + engine.acycle = (engine.acycle+1) % 960; + if (engine.acycle % updateCycle == 0) + { + for (int i = 0; i < count; i++) + { + if (!engine.autoBudget && engine.isBudgetTime()) + { + stopTimer(); //redundant + showBudgetWindow(true); + return; + } + engine.step(); + dirty2 = true; + } + } + engine.animate(); + updateDateLabel(); + }}; + + assert simTimer == null; + simTimer = new Timer(engine.simSpeed.animationDelay, taskPerformer); + simTimer.start(); + } + + class EarthquakeStepper + { + int count = 0; + void oneStep() + { + count = (count + 1) % MicropolisDrawingArea.SHAKE_STEPS; + drawingArea.shake(count); + } + } + EarthquakeStepper currentEarthquake; + + //implements EarthquakeListener + public void earthquakeStarted() + { + stopTimer(); + currentEarthquake = new EarthquakeStepper(); + currentEarthquake.oneStep(); + startTimer(); + } + + void stopEarthquake() + { + drawingArea.shake(0); + currentEarthquake = null; + } + + private void stopTimer() + { + if (simTimer != null) { + simTimer.stop(); + simTimer = null; + } + if (shakeTimer != null) { + shakeTimer.stop(); + shakeTimer = null; + } + } + + boolean isTimerActive() + { + return simTimer != null || shakeTimer != null; + } + + private void onWindowClosed(WindowEvent ev) + { + stopTimer(); + } + + private void onDifficultyClicked(int newDifficulty) + { + getEngine().setGameLevel(newDifficulty); + } + + private void onPriorityClicked(Speed newSpeed) + { + stopTimer(); + getEngine().setSpeed(newSpeed); + startTimer(); + } + + private void onInvokeDisasterClicked(Disaster disaster) + { + dirty1 = true; + switch (disaster) { + case FIRE: + getEngine().makeFire(); + break; + case FLOOD: + getEngine().makeFlood(); + break; + case MONSTER: + getEngine().makeMonster(); + break; + case MELTDOWN: + if (!getEngine().makeMeltdown()) { + messagesPane.appendCityMessage(MicropolisMessage.NO_NUCLEAR_PLANTS); + } + break; + case TORNADO: + getEngine().makeTornado(); + break; + case EARTHQUAKE: + getEngine().makeEarthquake(); + break; + default: + assert false; //unknown disaster + } + } + + private void reloadFunds() + { + fundsLbl.setText(formatFunds(getEngine().totalFunds)); + } + + //implements Micropolis.Listener + public void cityMessage(MicropolisMessage m, CityLocation p, boolean pictureMessage) + { + messagesPane.appendCityMessage(m); + + if (pictureMessage && p != null) + { + notificationPane.showMessage(engine, m, p.x, p.y); + } + } + + //implements Micropolis.Listener + public void fundsChanged() + { + reloadFunds(); + } + + //implements Micropolis.Listener + public void optionsChanged() + { + reloadOptions(); + } + + private void reloadOptions() + { + autoBudgetMenuItem.setSelected(getEngine().autoBudget); + autoBulldozeMenuItem.setSelected(getEngine().autoBulldoze); + disastersMenuItem.setSelected(!getEngine().noDisasters); + soundsMenuItem.setSelected(doSounds); + for (Speed spd : priorityMenuItems.keySet()) + { + priorityMenuItems.get(spd).setSelected(getEngine().simSpeed == spd); + } + for (int i = Micropolis.MIN_LEVEL; i <= Micropolis.MAX_LEVEL; i++) + { + difficultyMenuItems.get(i).setSelected(getEngine().gameLevel == i); + } + } + + //implements Micropolis.Listener + public void citySound(Sound sound, CityLocation loc) + { + if (!doSounds) + return; + + URL afile = sound.getAudioFile(); + if (afile == null) + return; + + boolean isOnScreen = drawingAreaScroll.getViewport().getViewRect().contains( + drawingArea.getTileBounds(loc.x, loc.y) + ); + if (sound == Sound.HONKHONK_LOW && !isOnScreen) + return; + + try + { + Clip clip = AudioSystem.getClip(); + clip.open(AudioSystem.getAudioInputStream(afile)); + clip.start(); + } + catch (Exception e) + { + e.printStackTrace(System.err); + } + } + + //implements Micropolis.Listener + public void censusChanged() { } + public void demandChanged() { } + public void evaluationChanged() { } + + void onViewBudgetClicked() + { + dirty1 = true; + showBudgetWindow(false); + } + + void onViewEvaluationClicked() + { + evaluationPane.setVisible(true); + } + + void onViewGraphClicked() + { + graphsPane.setVisible(true); + } + + private void showBudgetWindow(boolean isEndOfYear) + { + stopTimer(); + BudgetDialog dlg = new BudgetDialog(this, getEngine()); + dlg.setModal(true); + dlg.setVisible(true); + + if (isEndOfYear) { + getEngine().step(); + } + + startTimer(); + } + + private JMenuItem makeMapStateMenuItem(String caption, final MapState state) + { + JMenuItem menuItem = new JRadioButtonMenuItem(caption); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + setMapState(state); + } + }); + mapStateMenuItems.put(state, menuItem); + return menuItem; + } + + private void setMapState(MapState state) + { + mapStateMenuItems.get(mapView.getMapState()).setSelected(false); + mapStateMenuItems.get(state).setSelected(true); + mapView.setMapState(state); + setMapLegend(state); + } + + private void setMapLegend(MapState state) + { + String k = "legend_image."+state.name(); + java.net.URL iconUrl = null; + if (strings.containsKey(k)) { + String iconName = strings.getString(k); + iconUrl = MainWindow.class.getResource(iconName); + } + if (iconUrl != null) { + mapLegendLbl.setIcon(new ImageIcon(iconUrl)); + } + else { + mapLegendLbl.setIcon(null); + } + } + + private void onAboutClicked() + { + String version = getClass().getPackage().getImplementationVersion(); + String versionStr = MessageFormat.format(strings.getString("main.version_string"), version); + JLabel appNameLbl = new JLabel(versionStr); + JLabel appDetailsLbl = new JLabel(strings.getString("main.about_text")); + JComponent [] inputs = new JComponent[] { appNameLbl, appDetailsLbl }; + JOptionPane.showMessageDialog(this, + inputs, + strings.getString("main.about_caption"), + JOptionPane.PLAIN_MESSAGE, + appIcon); + } +} diff --git a/src/micropolisj/gui/MessagesPane.java b/src/micropolisj/gui/MessagesPane.java new file mode 100644 index 0000000..5b935d8 --- /dev/null +++ b/src/micropolisj/gui/MessagesPane.java @@ -0,0 +1,44 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.util.*; +import javax.swing.*; +import javax.swing.text.*; + +import micropolisj.engine.*; + +public class MessagesPane extends JTextPane +{ + static ResourceBundle cityMessageStrings = ResourceBundle.getBundle("micropolisj.CityMessages"); + + public MessagesPane() + { + setEditable(false); + } + + public void appendCityMessage(MicropolisMessage message) + { + appendMessageText(cityMessageStrings.getString(message.name())); + } + + void appendMessageText(String messageText) + { + try { + StyledDocument doc = getStyledDocument(); + if (doc.getLength() != 0) { + doc.insertString(doc.getLength(), "\n", null); + } + doc.insertString(doc.getLength(), messageText, null); + } + catch (BadLocationException e) { + throw new Error("unexpected", e); + } + } +} diff --git a/src/micropolisj/gui/MicropolisDrawingArea.java b/src/micropolisj/gui/MicropolisDrawingArea.java new file mode 100644 index 0000000..476d97c --- /dev/null +++ b/src/micropolisj/gui/MicropolisDrawingArea.java @@ -0,0 +1,379 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import java.net.URL; +import java.util.*; +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.Timer; + +import micropolisj.engine.*; +import static micropolisj.engine.TileConstants.*; + +public class MicropolisDrawingArea extends JComponent + implements Scrollable, MapListener +{ + Micropolis m; + boolean blinkUnpoweredZones = true; + HashSet unpoweredZones = new HashSet<>(); + boolean blink; + Timer blinkTimer; + ToolPreview toolPreview; + int shakeStep; + + static final Dimension PREFERRED_VIEWPORT_SIZE = new Dimension(640,640); + + public MicropolisDrawingArea(Micropolis engine) + { + this.m = engine; + m.addMapListener(this); + + addAncestorListener(new AncestorListener() { + public void ancestorAdded(AncestorEvent evt) { + startBlinkTimer(); + } + public void ancestorRemoved(AncestorEvent evt) { + stopBlinkTimer(); + } + public void ancestorMoved(AncestorEvent evt) {} + }); + } + + @Override + public Dimension getPreferredSize() + { + assert this.m != null; + + return new Dimension(TILE_WIDTH*m.getWidth(),TILE_HEIGHT*m.getHeight()); + } + + public void setEngine(Micropolis newEngine) + { + assert newEngine != null; + + if (this.m != null) { //old engine + this.m.removeMapListener(this); + } + this.m = newEngine; + if (this.m != null) { //new engine + this.m.addMapListener(this); + } + + // size may have changed + invalidate(); + repaint(); + } + + static Image [] tileImages = loadTileImages("/tiles.png"); + public static final int TILE_WIDTH = 16; + public static final int TILE_HEIGHT = 16; + + static Image [] loadTileImages(String resourceName) + { + URL iconUrl = MicropolisDrawingArea.class.getResource(resourceName); + Image refImage = new ImageIcon(iconUrl).getImage(); + + GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice dev = env.getDefaultScreenDevice(); + GraphicsConfiguration conf = dev.getDefaultConfiguration(); + + Image [] images = new Image[refImage.getHeight(null) / TILE_HEIGHT]; + for (int i = 0; i < images.length; i++) + { + BufferedImage bi = conf.createCompatibleImage(TILE_WIDTH, TILE_HEIGHT, Transparency.OPAQUE); + Graphics2D gr = bi.createGraphics(); + gr.drawImage(refImage, 0, 0, TILE_WIDTH, TILE_HEIGHT, + 0, i * TILE_HEIGHT, + TILE_WIDTH, (i+1)*TILE_HEIGHT, + null); + + images[i] = bi; + } + return images; + } + + static Map > spriteImages; + static { + spriteImages = new EnumMap >(SpriteKind.class); + for (SpriteKind kind : SpriteKind.values()) + { + HashMap imgs = new HashMap<>(); + for (int i = 0; i < kind.numFrames; i++) { + Image img = loadSpriteImage(kind, i); + if (img != null) { + imgs.put(i, img); + } + } + spriteImages.put(kind, imgs); + } + } + + static Image loadSpriteImage(SpriteKind kind, int frameNo) + { + String resourceName = "/obj"+kind.objectId+"-"+frameNo+".png"; + URL iconUrl = MicropolisDrawingArea.class.getResource(resourceName); + if (iconUrl == null) + return null; + + return new ImageIcon(iconUrl).getImage(); + } + + void drawSprite(Graphics gr, Sprite sprite) + { + assert sprite.isVisible(); + + Image img = spriteImages.get(sprite.kind).get(sprite.frame-1); + if (img != null) { + gr.drawImage(img, sprite.x + sprite.offx, sprite.y + sprite.offy, null); + } + else { + gr.setColor(Color.RED); + gr.fillRect(sprite.x, sprite.y, 16, 16); + gr.setColor(Color.WHITE); + gr.drawString(Integer.toString(sprite.frame-1),sprite.x,sprite.y); + } + } + + public void paintComponent(Graphics gr) + { + final int width = m.getWidth(); + final int height = m.getHeight(); + + Rectangle clipRect = gr.getClipBounds(); + int minX = Math.max(0, clipRect.x / TILE_WIDTH); + int minY = Math.max(0, clipRect.y / TILE_HEIGHT); + int maxX = Math.min(width, 1 + (clipRect.x + clipRect.width-1) / TILE_WIDTH); + int maxY = Math.min(height, 1 + (clipRect.y + clipRect.height-1) / TILE_HEIGHT); + + for (int y = minY; y < maxY; y++) + { + for (int x = minX; x < maxX; x++) + { + int cell = m.getTile(x,y); + int tile = (cell & LOMASK) % tileImages.length; + if (blinkUnpoweredZones && + (cell & ZONEBIT) != 0 && + (cell & PWRBIT) == 0) + { + unpoweredZones.add(new Point(x,y)); + if (blink) + tile = LIGHTNINGBOLT; + } + + gr.drawImage(tileImages[tile], + x*TILE_WIDTH + (shakeStep != 0 ? getShakeModifier(y) : 0), + y*TILE_HEIGHT, + null); + } + } + + for (Sprite sprite : m.allSprites()) + { + if (sprite.isVisible()) + { + drawSprite(gr, sprite); + } + } + + if (toolPreview != null) + { + int x0 = toolPreview.rect.x * TILE_WIDTH; + int x1 = (toolPreview.rect.x + toolPreview.rect.width) * TILE_WIDTH; + int y0 = toolPreview.rect.y * TILE_HEIGHT; + int y1 = (toolPreview.rect.y + toolPreview.rect.height) * TILE_HEIGHT; + + gr.setColor(Color.BLACK); + gr.drawLine(x0-1,y0-1,x0-1,y1-1); + gr.drawLine(x0-1,y0-1,x1-1,y0-1); + gr.drawLine(x1+3,y0-3,x1+3,y1+3); + gr.drawLine(x0-3,y1+3,x1+3,y1+3); + + gr.setColor(Color.WHITE); + gr.drawLine(x0-4,y0-4,x1+3,y0-4); + gr.drawLine(x0-4,y0-4,x0-4,y1+3); + gr.drawLine(x1, y0-1,x1, y1 ); + gr.drawLine(x0-1,y1, x1, y1 ); + + gr.setColor(toolPreview.borderColor); + gr.drawRect(x0-3,y0-3,x1-x0+5,y1-y0+5); + gr.drawRect(x0-2,y0-2,x1-x0+3,y1-y0+3); + + if (toolPreview.fillColor != null) { + gr.setColor(toolPreview.fillColor); + gr.fillRect(x0,y0,x1-x0,y1-y0); + } + } + } + + static class ToolPreview + { + Rectangle rect; + Color borderColor; + Color fillColor; + } + + public void setToolPreview(Rectangle newRect, Color toolColor) + { + ToolPreview tp = new ToolPreview(); + tp.rect = newRect; + tp.borderColor = toolColor; + setToolPreview(tp); + } + + public void setToolPreview(ToolPreview newPreview) + { + if (toolPreview == newPreview) + return; + if (toolPreview != null && toolPreview.equals(newPreview)) + return; + + if (toolPreview != null) + { + repaint(new Rectangle( + toolPreview.rect.x*TILE_WIDTH - 4, + toolPreview.rect.y*TILE_HEIGHT - 4, + toolPreview.rect.width*TILE_WIDTH + 8, + toolPreview.rect.height*TILE_HEIGHT + 8 + )); + } + toolPreview = newPreview; + if (toolPreview != null) + { + repaint(new Rectangle( + toolPreview.rect.x*TILE_WIDTH - 4, + toolPreview.rect.y*TILE_HEIGHT - 4, + toolPreview.rect.width*TILE_WIDTH + 8, + toolPreview.rect.height*TILE_HEIGHT + 8 + )); + } + } + + //implements Scrollable + public Dimension getPreferredScrollableViewportSize() + { + return PREFERRED_VIEWPORT_SIZE; + } + + //implements Scrollable + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) + { + if (orientation == SwingConstants.VERTICAL) + return visibleRect.height; + else + return visibleRect.width; + } + + //implements Scrollable + public boolean getScrollableTracksViewportWidth() + { + return false; + } + + //implements Scrollable + public boolean getScrollableTracksViewportHeight() + { + return false; + } + + //implements Scrollable + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) + { + if (orientation == SwingConstants.VERTICAL) + return TILE_HEIGHT * 3; + else + return TILE_WIDTH * 3; + } + + private Rectangle getSpriteBounds(Sprite sprite, int x, int y) + { + return new Rectangle(x+sprite.offx, y+sprite.offy, sprite.width, sprite.height); + } + + public Rectangle getTileBounds(int xpos, int ypos) + { + return new Rectangle(xpos*TILE_WIDTH, ypos * TILE_HEIGHT, + TILE_WIDTH, TILE_HEIGHT); + } + + //implements MapListener + public void mapOverlayDataChanged(MapState overlayDataType) + { + } + + //implements MapListener + public void spriteMoved(Sprite sprite) + { + repaint(getSpriteBounds(sprite, sprite.lastX, sprite.lastY)); + repaint(getSpriteBounds(sprite, sprite.x, sprite.y)); + } + + //implements MapListener + public void tileChanged(int xpos, int ypos) + { + repaint(getTileBounds(xpos, ypos)); + } + + //implements MapListener + public void wholeMapChanged() + { + repaint(); + } + + void doBlink() + { + if (!unpoweredZones.isEmpty()) + { + blink = !blink; + for (Point loc : unpoweredZones) + { + repaint(getTileBounds(loc.x, loc.y)); + } + unpoweredZones.clear(); + } + } + + void startBlinkTimer() + { + assert blinkTimer == null; + + ActionListener callback = new ActionListener() { + public void actionPerformed(ActionEvent evt) + { + doBlink(); + } + }; + + blinkTimer = new Timer(500, callback); + blinkTimer.start(); + } + + void stopBlinkTimer() + { + if (blinkTimer != null) { + blinkTimer.stop(); + blinkTimer = null; + } + } + + void shake(int i) + { + shakeStep = i; + repaint(); + } + + static final int SHAKE_STEPS = 40; + int getShakeModifier(int row) + { + return (int)Math.round(4.0 * Math.sin((double)(shakeStep+row/2)/2.0)); + } +} diff --git a/src/micropolisj/gui/NewCityDialog.java b/src/micropolisj/gui/NewCityDialog.java new file mode 100644 index 0000000..6778ce1 --- /dev/null +++ b/src/micropolisj/gui/NewCityDialog.java @@ -0,0 +1,225 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.util.*; +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; + +import micropolisj.engine.*; +import static micropolisj.gui.MainWindow.EXTENSION; + +public class NewCityDialog extends JDialog +{ + Micropolis engine; + JButton previousMapBtn; + Stack previousMaps = new Stack(); + Stack nextMaps = new Stack(); + OverlayMapView mapPane; + HashMap levelBtns = new HashMap(); + + static final ResourceBundle strings = MainWindow.strings; + + public NewCityDialog(MainWindow owner, boolean showCancelOption) + { + super(owner); + setTitle(strings.getString("welcome.caption")); + setModal(true); + + assert owner != null; + + JPanel p1 = new JPanel(new BorderLayout()); + p1.setBorder(BorderFactory.createEmptyBorder(10,20,10,20)); + getContentPane().add(p1, BorderLayout.CENTER); + + engine = new Micropolis(); + new MapGenerator(engine).generateNewCity(); + + mapPane = new OverlayMapView(engine); + mapPane.setBorder(BorderFactory.createLoweredBevelBorder()); + p1.add(mapPane, BorderLayout.CENTER); + + JPanel p2 = new JPanel(new BorderLayout()); + p1.add(p2, BorderLayout.EAST); + + Box levelBox = new Box(BoxLayout.Y_AXIS); + levelBox.setBorder(BorderFactory.createEmptyBorder(0,10,0,10)); + p2.add(levelBox, BorderLayout.CENTER); + + levelBox.add(Box.createVerticalGlue()); + JRadioButton radioBtn; + for (int lev = GameLevel.MIN_LEVEL; lev <= GameLevel.MAX_LEVEL; lev++) + { + final int x = lev; + radioBtn = new JRadioButton(strings.getString("menu.difficulty."+lev)); + radioBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + setGameLevel(x); + }}); + levelBox.add(radioBtn); + levelBtns.put(lev, radioBtn); + } + levelBox.add(Box.createVerticalGlue()); + setGameLevel(GameLevel.MIN_LEVEL); + + JPanel buttonPane = new JPanel(); + getContentPane().add(buttonPane, BorderLayout.SOUTH); + + JButton btn; + btn = new JButton(strings.getString("welcome.previous_map")); + btn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onPreviousMapClicked(); + }}); + btn.setEnabled(false); + buttonPane.add(btn); + previousMapBtn = btn; + + btn = new JButton(strings.getString("welcome.play_this_map")); + btn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onPlayClicked(); + }}); + buttonPane.add(btn); + getRootPane().setDefaultButton(btn); + + btn = new JButton(strings.getString("welcome.next_map")); + btn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onNextMapClicked(); + }}); + buttonPane.add(btn); + + btn = new JButton(strings.getString("welcome.load_city")); + btn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onLoadCityClicked(); + }}); + buttonPane.add(btn); + + if (showCancelOption) { + btn = new JButton(strings.getString("welcome.cancel")); + btn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onCancelClicked(); + }}); + buttonPane.add(btn); + } + else { + btn = new JButton(strings.getString("welcome.quit")); + btn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onQuitClicked(); + }}); + buttonPane.add(btn); + } + + pack(); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setLocationRelativeTo(owner); + } + + private void onPreviousMapClicked() + { + if (previousMaps.isEmpty()) + return; + + nextMaps.push(engine); + engine = previousMaps.pop(); + mapPane.setEngine(engine); + + previousMapBtn.setEnabled(!previousMaps.isEmpty()); + } + + private void onNextMapClicked() + { + if (nextMaps.isEmpty()) + { + Micropolis m = new Micropolis(); + new MapGenerator(m).generateNewCity(); + nextMaps.add(m); + } + + previousMaps.push(engine); + engine = nextMaps.pop(); + mapPane.setEngine(engine); + + previousMapBtn.setEnabled(true); + } + + private void onLoadCityClicked() + { + try + { + JFileChooser fc = new JFileChooser(); + FileNameExtensionFilter filter1 = new FileNameExtensionFilter(strings.getString("cty_file"), EXTENSION); + fc.setFileFilter(filter1); + + int rv = fc.showOpenDialog(this); + if (rv == JFileChooser.APPROVE_OPTION) { + File file = fc.getSelectedFile(); + Micropolis newEngine = new Micropolis(); + newEngine.load(file); + startPlaying(newEngine, file); + } + } + catch (Exception e) + { + e.printStackTrace(System.err); + JOptionPane.showMessageDialog(this, e, strings.getString("main.error_caption"), + JOptionPane.ERROR_MESSAGE); + } + } + + void startPlaying(Micropolis newEngine, File file) + { + MainWindow win = (MainWindow) getOwner(); + win.setEngine(newEngine); + win.currentFile = file; + dispose(); + } + + private void onPlayClicked() + { + engine.setGameLevel(getSelectedGameLevel()); + startPlaying(engine, null); + } + + private void onCancelClicked() + { + dispose(); + } + + private void onQuitClicked() + { + System.exit(0); + } + + private int getSelectedGameLevel() + { + for (int lev : levelBtns.keySet()) + { + if (levelBtns.get(lev).isSelected()) { + return lev; + } + } + return GameLevel.MIN_LEVEL; + } + + private void setGameLevel(int level) + { + for (int lev : levelBtns.keySet()) + { + levelBtns.get(lev).setSelected(lev == level); + } + } +} diff --git a/src/micropolisj/gui/NotificationPane.java b/src/micropolisj/gui/NotificationPane.java new file mode 100644 index 0000000..38d69f3 --- /dev/null +++ b/src/micropolisj/gui/NotificationPane.java @@ -0,0 +1,176 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import javax.swing.*; + +import micropolisj.engine.*; +import static micropolisj.gui.ColorParser.parseColor; + +public class NotificationPane extends JPanel +{ + JLabel headerLbl; + JViewport mapViewport; + MicropolisDrawingArea mapView; + JPanel mainPane; + JComponent infoPane; + + static final Dimension VIEWPORT_SIZE = new Dimension(160,160); + static final Color QUERY_COLOR = new Color(255,165,0); + static final ResourceBundle strings = MainWindow.strings; + static final ResourceBundle mstrings = ResourceBundle.getBundle("micropolisj.CityMessages"); + static final ResourceBundle s_strings = ResourceBundle.getBundle("micropolisj.StatusMessages"); + + public NotificationPane(Micropolis engine) + { + super(new BorderLayout()); + setVisible(false); + + headerLbl = new JLabel(); + headerLbl.setOpaque(true); + headerLbl.setHorizontalAlignment(SwingConstants.CENTER); + headerLbl.setBorder(BorderFactory.createRaisedBevelBorder()); + add(headerLbl, BorderLayout.NORTH); + + JButton dismissBtn = new JButton(strings.getString("notification.dismiss")); + dismissBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + onDismissClicked(); + }}); + add(dismissBtn, BorderLayout.SOUTH); + + mainPane = new JPanel(new BorderLayout()); + add(mainPane, BorderLayout.CENTER); + + JPanel viewportContainer = new JPanel(new BorderLayout()); + viewportContainer.setBorder( + BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(8,4,8,4), + BorderFactory.createLineBorder(Color.BLACK) + )); + mainPane.add(viewportContainer, BorderLayout.WEST); + + mapViewport = new JViewport(); + mapViewport.setPreferredSize(VIEWPORT_SIZE); + mapViewport.setMaximumSize(VIEWPORT_SIZE); + mapViewport.setMinimumSize(VIEWPORT_SIZE); + viewportContainer.add(mapViewport, BorderLayout.CENTER); + + mapView = new MicropolisDrawingArea(engine); + mapViewport.setView(mapView); + } + + private void onDismissClicked() + { + setVisible(false); + } + + void setPicture(Micropolis engine, int xpos, int ypos) + { + Dimension sz = VIEWPORT_SIZE; + + mapView.setEngine(engine); + Rectangle r = mapView.getTileBounds(xpos,ypos); + + mapViewport.setViewPosition(new Point( + r.x + r.width/2 - sz.width/2, + r.y + r.height/2 - sz.height/2 + )); + } + + public void showMessage(Micropolis engine, MicropolisMessage msg, int xpos, int ypos) + { + setPicture(engine, xpos, ypos); + + if (infoPane != null) { + mainPane.remove(infoPane); + infoPane = null; + } + + headerLbl.setText(mstrings.getString(msg.name()+".title")); + headerLbl.setBackground(parseColor(mstrings.getString(msg.name()+".color"))); + + JLabel myLabel = new JLabel("

"+ + mstrings.getString(msg.name()+".detail") + "

"); + myLabel.setPreferredSize(new Dimension(1,1)); + + infoPane = myLabel; + mainPane.add(myLabel, BorderLayout.CENTER); + + setVisible(true); + } + + public void showZoneStatus(Micropolis engine, int xpos, int ypos, ZoneStatus zone) + { + headerLbl.setText(strings.getString("notification.query_hdr")); + headerLbl.setBackground(QUERY_COLOR); + + String buildingStr = s_strings.getString("zone."+zone.building); + String popDensityStr = s_strings.getString("status."+zone.popDensity); + String landValueStr = s_strings.getString("status."+zone.landValue); + String crimeLevelStr = s_strings.getString("status."+zone.crimeLevel); + String pollutionStr = s_strings.getString("status."+zone.pollution); + String growthRateStr = s_strings.getString("status."+zone.growthRate); + + setPicture(engine, xpos, ypos); + + if (infoPane != null) { + mainPane.remove(infoPane); + infoPane = null; + } + + JPanel p = new JPanel(new GridBagLayout()); + mainPane.add(p, BorderLayout.CENTER); + infoPane = p; + + GridBagConstraints c1 = new GridBagConstraints(); + GridBagConstraints c2 = new GridBagConstraints(); + + c1.gridx = 0; + c2.gridx = 1; + c1.gridy = c2.gridy = 0; + c1.anchor = GridBagConstraints.WEST; + c2.anchor = GridBagConstraints.WEST; + c1.insets = new Insets(0,0,0,8); + c2.weightx = 1.0; + + p.add(new JLabel(strings.getString("notification.zone_lbl")), c1); + p.add(new JLabel(buildingStr), c2); + + c1.gridy = ++c2.gridy; + p.add(new JLabel(strings.getString("notification.density_lbl")), c1); + p.add(new JLabel(popDensityStr), c2); + + c1.gridy = ++c2.gridy; + p.add(new JLabel(strings.getString("notification.value_lbl")), c1); + p.add(new JLabel(landValueStr), c2); + + c1.gridy = ++c2.gridy; + p.add(new JLabel(strings.getString("notification.crime_lbl")), c1); + p.add(new JLabel(crimeLevelStr), c2); + + c1.gridy = ++c2.gridy; + p.add(new JLabel(strings.getString("notification.pollution_lbl")), c1); + p.add(new JLabel(pollutionStr), c2); + + c1.gridy = ++c2.gridy; + p.add(new JLabel(strings.getString("notification.growth_lbl")), c1); + p.add(new JLabel(growthRateStr), c2); + + c1.gridy++; + c1.gridwidth = 2; + c1.weighty = 1.0; + p.add(new JLabel(), c1); + + setVisible(true); + } +} diff --git a/src/micropolisj/gui/OverlayMapView.java b/src/micropolisj/gui/OverlayMapView.java new file mode 100644 index 0000000..461ccb6 --- /dev/null +++ b/src/micropolisj/gui/OverlayMapView.java @@ -0,0 +1,509 @@ +// This file is part of MicropolisJ. +// Copyright (C) 2013 Jason Long +// Portions Copyright (C) 1989-2007 Electronic Arts Inc. +// +// MicropolisJ is free software; you can redistribute it and/or modify +// it under the terms of the GNU GPLv3, with additional terms. +// See the README file, included in this distribution, for details. + +package micropolisj.gui; + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import java.net.URL; +import java.util.*; +import javax.swing.*; +import javax.swing.event.*; + +import micropolisj.engine.*; +import static micropolisj.engine.TileConstants.*; + +public class OverlayMapView extends JComponent + implements Scrollable, MapListener +{ + Micropolis engine; + ArrayList views = new ArrayList<>(); + MapState mapState = MapState.ALL; + + public OverlayMapView(Micropolis _engine) + { + assert _engine != null; + + MouseAdapter mouse = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent ev) + { + onMousePressed(ev); + } + @Override + public void mouseDragged(MouseEvent ev) + { + onMouseDragged(ev); + } + }; + addMouseListener(mouse); + addMouseMotionListener(mouse); + + setEngine(_engine); + } + + public void setEngine(Micropolis newEngine) + { + assert newEngine != null; + + if (engine != null) { //old engine + engine.removeMapListener(this); + } + engine = newEngine; + if (engine != null) { //new engine + engine.addMapListener(this); + } + + invalidate(); //map size may have changed + repaint(); + engine.calculateCenterMass(); + dragViewToCityCenter(); + } + + public MapState getMapState() + { + return mapState; + } + + @Override + public Dimension getPreferredSize() + { + return new Dimension( + getInsets().left + getInsets().right + TILE_WIDTH*engine.getWidth(), + getInsets().top + getInsets().bottom + TILE_HEIGHT*engine.getHeight() + ); + } + + public void setMapState(MapState newState) + { + if (mapState == newState) + return; + + mapState = newState; + repaint(); + } + + static BufferedImage tileArrayImage = loadImage("/tilessm.png"); + static final int TILE_WIDTH = 3; + static final int TILE_HEIGHT = 3; + static final int TILE_OFFSET_Y = 3; + + static BufferedImage loadImage(String resourceName) + { + URL iconUrl = MicropolisDrawingArea.class.getResource(resourceName); + Image refImage = new ImageIcon(iconUrl).getImage(); + + BufferedImage bi = new BufferedImage(refImage.getWidth(null), refImage.getHeight(null), + BufferedImage.TYPE_INT_RGB); + Graphics2D gr = bi.createGraphics(); + gr.drawImage(refImage, 0, 0, null); + + return bi; + } + + static final Color VAL_LOW = new Color(0xbfbfbf); + static final Color VAL_MEDIUM = new Color(0xffff00); + static final Color VAL_HIGH = new Color(0xff7f00); + static final Color VAL_VERYHIGH = new Color(0xff0000); + static final Color VAL_PLUS = new Color(0x007f00); + static final Color VAL_VERYPLUS = new Color(0x00e600); + static final Color VAL_MINUS = new Color(0xff7f00); + static final Color VAL_VERYMINUS = new Color(0xffff00); + + private Color getCI(int x) + { + if (x < 50) + return null; + else if (x < 100) + return VAL_LOW; + else if (x < 150) + return VAL_MEDIUM; + else if (x < 200) + return VAL_HIGH; + else + return VAL_VERYHIGH; + } + + private Color getCI_rog(int x) + { + if (x > 100) + return VAL_VERYPLUS; + else if (x > 20) + return VAL_PLUS; + else if (x < -100) + return VAL_VERYMINUS; + else if (x < -20) + return VAL_MINUS; + else + return null; + } + + private void drawLandMap(Graphics gr) + { + int [][] A = engine.landValueMem; + + for (int y = 0; y < A.length; y++) { + for (int x = 0; x < A[y].length; x++) { + maybeDrawRect(gr, getCI(A[y][x]),x*6,y*6,6,6); + } + } + } + + private void drawPollutionMap(Graphics gr) + { + int [][] A = engine.pollutionMem; + + for (int y = 0; y < A.length; y++) { + for (int x = 0; x < A[y].length; x++) { + maybeDrawRect(gr, getCI(10 + A[y][x]),x*6,y*6,6,6); + } + } + } + + private void drawCrimeMap(Graphics gr) + { + int [][] A = engine.crimeMem; + + for (int y = 0; y < A.length; y++) { + for (int x = 0; x < A[y].length; x++) { + maybeDrawRect(gr, getCI(A[y][x]),x*6,y*6,6,6); + } + } + } + + private void drawTrafficMap(Graphics gr) + { + int [][] A = engine.trfDensity; + + for (int y = 0; y < A.length; y++) { + for (int x = 0; x < A[y].length; x++) { + maybeDrawRect(gr, getCI(A[y][x]),x*6,y*6,6,6); + } + } + } + + private void drawPopDensity(Graphics gr) + { + int [][] A = engine.popDensity; + + for (int y = 0; y < A.length; y++) { + for (int x = 0; x < A[y].length; x++) { + maybeDrawRect(gr, getCI(A[y][x]),x*6,y*6,6,6); + } + } + } + + private void drawRateOfGrowth(Graphics gr) + { + int [][] A = engine.rateOGMem; + + for (int y = 0; y < A.length; y++) { + for (int x = 0; x < A[y].length; x++) { + maybeDrawRect(gr, getCI_rog(A[y][x]),x*24,y*24,24,24); + } + } + } + + private void drawFireRadius(Graphics gr) + { + int [][] A = engine.fireRate; + + for (int y = 0; y < A.length; y++) { + for (int x = 0; x < A[y].length; x++) { + maybeDrawRect(gr, getCI(A[y][x]),x*24,y*24,24,24); + } + } + } + + private void drawPoliceRadius(Graphics gr) + { + int [][] A = engine.policeMapEffect; + + for (int y = 0; y < A.length; y++) { + for (int x = 0; x < A[y].length; x++) { + maybeDrawRect(gr, getCI(A[y][x]),x*24,y*24,24,24); + } + } + } + + private void maybeDrawRect(Graphics gr, Color col, int x, int y, int width, int height) + { + if (col != null) { + gr.setColor(col); + gr.fillRect(x,y,width,height); + } + } + + static final int UNPOWERED = 0x6666e6; //lightblue + static final int POWERED = 0xff0000; //red + static final int CONDUCTIVE = 0xbfbfbf; //lightgray + + private int checkPower(BufferedImage img, int x, int y, int rawTile) + { + int pix; + + if ((rawTile & LOMASK) <= 63) { + return rawTile & LOMASK; + } + else if ((rawTile & ZONEBIT) != 0) { + // zone + pix = ((rawTile & PWRBIT) != 0) ? POWERED : UNPOWERED; + } + else if ((rawTile & CONDBIT) != 0) { + pix = CONDUCTIVE; + } + else { + return DIRT; + } + + for (int yy = 0; yy < TILE_HEIGHT; yy++) + { + for (int xx = 0; xx < TILE_WIDTH; xx++) + { + img.setRGB(x*TILE_WIDTH+xx,y*TILE_HEIGHT+yy, pix); + } + } + return -1; //this special value tells caller to skip the tile bitblt, + //since it was performed here + } + + @Override + public void paintComponent(Graphics gr) + { + final int width = engine.getWidth(); + final int height = engine.getHeight(); + + BufferedImage img = new BufferedImage(width*TILE_WIDTH, height*TILE_HEIGHT, + BufferedImage.TYPE_INT_RGB); + + final Insets INSETS = getInsets(); + Rectangle clipRect = gr.getClipBounds(); + int minX = Math.max(0, (clipRect.x - INSETS.left) / TILE_WIDTH); + int minY = Math.max(0, (clipRect.y - INSETS.top) / TILE_HEIGHT); + int maxX = Math.min(width, 1 + (clipRect.x - INSETS.left + clipRect.width-1) / TILE_WIDTH); + int maxY = Math.min(height, 1 + (clipRect.y - INSETS.top + clipRect.height-1) / TILE_HEIGHT); + + for (int y = minY; y < maxY; y++) + { + for (int x = minX; x < maxX; x++) + { + int tile = engine.getTile(x,y) & LOMASK; + switch (mapState) { + case RESIDENTIAL: + if (tile >= COMBASE) { tile = DIRT; } + break; + case COMMERCIAL: + if (tile > COMLAST || (tile >= RESBASE && tile < COMBASE)) { tile = DIRT; } + break; + case INDUSTRIAL: + if ((tile >= RESBASE && tile < INDBASE) || + (tile >= PORTBASE && tile < SMOKEBASE) || + (tile >= TINYEXP && tile < 884) || + tile >= FOOTBALLGAME1) + { tile = DIRT; } + break; + case POWER_OVERLAY: + tile = checkPower(img, x, y, engine.getTile(x,y)); + break; + case TRANSPORT: + case TRAFFIC_OVERLAY: + if (tile >= RESBASE || + (tile >= 207 && tile <= LVPOWER10) || + tile == 223) { tile = DIRT; } + break; + default: + } + + if (tile != -1) { + for (int yy = 0; yy < TILE_HEIGHT; yy++) + { + for (int xx = 0; xx < TILE_WIDTH; xx++) + { + img.setRGB(x*TILE_WIDTH+xx,y*TILE_HEIGHT+yy, + tileArrayImage.getRGB(xx,tile*TILE_OFFSET_Y+yy)); + } + } + } + } + } + + gr.drawImage(img, INSETS.left, INSETS.top, null); + + gr = gr.create(); + gr.translate(INSETS.left, INSETS.top); + + switch (mapState) { + case POLICE_OVERLAY: + drawPoliceRadius(gr); break; + case FIRE_OVERLAY: + drawFireRadius(gr); break; + case LANDVALUE_OVERLAY: + drawLandMap(gr); break; + case CRIME_OVERLAY: + drawCrimeMap(gr); break; + case POLLUTE_OVERLAY: + drawPollutionMap(gr); break; + case TRAFFIC_OVERLAY: + drawTrafficMap(gr); break; + case GROWTHRATE_OVERLAY: + drawRateOfGrowth(gr); break; + case POPDEN_OVERLAY: + drawPopDensity(gr); break; + default: + } + + for (ConnectedView cv : views) + { + Rectangle rect = getViewRect(cv); + gr.setColor(Color.WHITE); + gr.drawRect(rect.x-2,rect.y-2,rect.width+2,rect.height+2); + + gr.setColor(Color.BLACK); + gr.drawRect(rect.x-0,rect.y-0,rect.width+2,rect.height+2); + + gr.setColor(Color.YELLOW); + gr.drawRect(rect.x-1,rect.y-1,rect.width+2,rect.height+2); + } + } + + Rectangle getViewRect(ConnectedView cv) + { + Rectangle rawRect = cv.scrollPane.getViewport().getViewRect(); + return new Rectangle( + rawRect.x * 3 / 16, + rawRect.y * 3 / 16, + rawRect.width * 3 / 16, + rawRect.height * 3 / 16 + ); + } + + private void dragViewTo(Point p) + { + if (views.isEmpty()) + return; + + ConnectedView cv = views.get(0); + Dimension d = cv.scrollPane.getViewport().getExtentSize(); + Dimension mapSize = cv.scrollPane.getViewport().getViewSize(); + + Point np = new Point( + p.x * 16 / 3 - d.width / 2, + p.y * 16 / 3 - d.height / 2 + ); + np.x = Math.max(0, Math.min(np.x, mapSize.width - d.width)); + np.y = Math.max(0, Math.min(np.y, mapSize.height - d.height)); + + cv.scrollPane.getViewport().setViewPosition(np); + } + + //implements Scrollable + public Dimension getPreferredScrollableViewportSize() + { + return new Dimension(120,120); + } + + //implements Scrollable + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) + { + if (orientation == SwingConstants.VERTICAL) + return visibleRect.height; + else + return visibleRect.width; + } + + //implements Scrollable + public boolean getScrollableTracksViewportWidth() + { + return false; + } + + //implements Scrollable + public boolean getScrollableTracksViewportHeight() + { + return false; + } + + //implements Scrollable + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) + { + if (orientation == SwingConstants.VERTICAL) + return TILE_HEIGHT; + else + return TILE_WIDTH; + } + + //implements MapListener + public void mapOverlayDataChanged(MapState overlayDataType) + { + repaint(); + } + + //implements MapListener + public void spriteMoved(Sprite sprite) + { + } + + //implements MapListener + public void tileChanged(int xpos, int ypos) + { + Rectangle r = new Rectangle(xpos*TILE_WIDTH, ypos * TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT); + repaint(r); + } + + //implements MapListener + public void wholeMapChanged() + { + repaint(); + engine.calculateCenterMass(); + dragViewToCityCenter(); + } + + public void dragViewToCityCenter() + { + dragViewTo(new Point(TILE_WIDTH * engine.centerMassX + 1, + TILE_HEIGHT * engine.centerMassY + 1)); + } + + class ConnectedView implements ChangeListener + { + JScrollPane scrollPane; + + ConnectedView(MicropolisDrawingArea view, JScrollPane scrollPane) + { + this.scrollPane = scrollPane; + scrollPane.getViewport().addChangeListener(this); + } + + public void stateChanged(ChangeEvent ev) + { + repaint(); + } + } + + public void connectView(MicropolisDrawingArea view, JScrollPane scrollPane) + { + ConnectedView cv = new ConnectedView(view, scrollPane); + views.add(cv); + repaint(); + } + + private void onMousePressed(MouseEvent ev) + { + if (ev.getButton() == MouseEvent.BUTTON1) + dragViewTo(ev.getPoint()); + } + + private void onMouseDragged(MouseEvent ev) + { + if ((ev.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0) + return; + + dragViewTo(ev.getPoint()); + } +} diff --git a/src/micropolisj/gui/package.html b/src/micropolisj/gui/package.html new file mode 100644 index 0000000..b6c461b --- /dev/null +++ b/src/micropolisj/gui/package.html @@ -0,0 +1,9 @@ + + +

Contains the front-end user interface that drives the game.

+

+Most of the funtionality is tied in by the MainWindow class. +The MicropolisDrawingArea class provides the city renderer. +The OverlapMapView class provides the mini-map. +

+ diff --git a/strings/CityMessages.properties b/strings/CityMessages.properties new file mode 100644 index 0000000..3a1f1c8 --- /dev/null +++ b/strings/CityMessages.properties @@ -0,0 +1,138 @@ +!! This file is part of MicropolisJ. +!! Copyright (C) 2013 Jason Long +!! Portions Copyright (C) 1989-2007 Electronic Arts Inc. +!! +!! MicropolisJ is free software; you can redistribute it and/or modify +!! it under the terms of the GNU GPLv3, with additional terms. +!! See the README file, included in this distribution, for details. + +NEED_RES = More residential zones needed. +NEED_COM = More commercial zones needed. +NEED_IND = More industrial zones needed. +NEED_ROADS = More roads required. +NEED_RAILS = Inadequate rail system. +NEED_POWER = Build a Power Plant. +NEED_STADIUM = Residents demand a Stadium. +NEED_SEAPORT = Industry requires a Sea Port. +NEED_AIRPORT = Commerce requires an Airport. +HIGH_POLLUTION = Pollution very high. +HIGH_CRIME = Crime very high. +HIGH_TRAFFIC = Frequent traffic jams reported. +NEED_FIRESTATION = Citizens demand a Fire Department. +NEED_POLICE = Citizens demand a Police Department. +BLACKOUTS = Blackouts reported. Check power map. +HIGH_TAXES = Citizens upset. The tax rate is too high. +ROADS_NEED_FUNDING = Roads deteriorating, due to lack of funds. +FIRE_NEED_FUNDING = Fire departments need funding. +POLICE_NEED_FUNDING = Police departments need funding. +FIRE_REPORT = Fire reported ! +MONSTER_REPORT = A Monster has been sighted !! +TORNADO_REPORT = Tornado reported !! +EARTHQUAKE_REPORT = Major earthquake reported !! +PLANECRASH_REPORT = A plane has crashed ! +SHIPWRECK_REPORT = Shipwreck reported ! +TRAIN_CRASH_REPORT = A train crashed ! +COPTER_CRASH_REPORT = A helicopter crashed ! +HIGH_UNEMPLOYMENT = Unemployment rate is high. +OUT_OF_FUNDS_REPORT = YOUR CITY HAS GONE BROKE! +FIREBOMBING_REPORT = Firebombing reported ! +NEED_PARKS = Need more parks. +EXPLOSION_REPORT = Explosion detected ! +INSUFFICIENT_FUNDS = Insufficient funds to build that. +BULLDOZE_FIRST = Area must be bulldozed first. +POP_2K_REACHED = Population has reached 2,000. +POP_10K_REACHED = Population has reached 10,000. +POP_50K_REACHED = Population has reached 50,000. +POP_100K_REACHED = Population has reached 100,000. +POP_500K_REACHED = Population has reached 500,000. +BROWNOUTS_REPORT = Brownouts, build another Power Plant. +HEAVY_TRAFFIC_REPORT = Heavy Traffic reported. +FLOOD_REPORT = Flooding reported !! +MELTDOWN_REPORT = A Nuclear Meltdown has occurred !!! +RIOTING_REPORT = They're rioting in the streets !! + +NO_NUCLEAR_PLANTS = Cannot meltdown. Build a nuclear power plant first. + +HIGH_POLLUTION.title = POLLUTION ALERT! +HIGH_POLLUTION.color = #ff4f4f +HIGH_POLLUTION.detail = Pollution in your city has exceeded the maximum allowable amounts established by the Micropolis Pollution Agency. You are running the risk of grave ecological consequences.

Either clean up your act or open a gas mask concession at city hall. + +HIGH_CRIME.title = CRIME ALERT! +HIGH_CRIME.color = #ff4f4f +HIGH_CRIME.detail = Crime in your city is out of hand. Angry mobs are looting and vandalizing the central city. The president will send in the national guard soon if you cannot control the problem. + +HIGH_TRAFFIC.title = TRAFFIC WARNING! +HIGH_TRAFFIC.color = #ff4f4f +HIGH_TRAFFIC.detail = Traffic in this city is horrible. The city gridlock is expanding. The commuters are getting militant.

Either build more roads and rails or get a bulletproof limo. + +FIRE_REPORT.title = FIRE REPORTED! +FIRE_REPORT.color = #ff4f4f +FIRE_REPORT.detail = A fire has been reported! + +MONSTER_REPORT.title = MONSTER ATTACK! +MONSTER_REPORT.color = #ff4f4f +MONSTER_REPORT.detail = A large reptilian creature has been spotted in the water. It seems to be attracted to areas of high pollution. There is a trail of destruction wherever it goes.
All you can do is wait till he leaves, then rebuild from the rubble. + +TORNADO_REPORT.title = TORNADO ALERT! +TORNADO_REPORT.color = #ff4f4f +TORNADO_REPORT.detail = A tornado has been reported! There's nothing you can do to stop it, so you'd better prepare to clean up after the disaster! + +EARTHQUAKE_REPORT.title = EARTHQUAKE! +EARTHQUAKE_REPORT.color = #ff4f4f +EARTHQUAKE_REPORT.detail = A major earthquake has occurred! Put out the fires as quickly as possible, before they spread, then reconnect the power grid and rebuild the city. + +PLANECRASH_REPORT.title = PLANE CRASH! +PLANECRASH_REPORT.color = #ff4f4f +PLANECRASH_REPORT.detail = A plane has crashed! + +SHIPWRECK_REPORT.title = SHIPWRECK! +SHIPWRECK_REPORT.color = #ff4f4f +SHIPWRECK_REPORT.detail = A ship has wrecked! + +TRAIN_CRASH_REPORT.title = TRAIN CRASH! +TRAIN_CRASH_REPORT.color = #ff4f4f +TRAIN_CRASH_REPORT.detail = A train has crashed! + +COPTER_CRASH_REPORT.title = HELICOPTER CRASH! +COPTER_CRASH_REPORT.color = #ff4f4f +COPTER_CRASH_REPORT.detail = A helicopter has crashed! + +FIREBOMBING_REPORT.title = FIREBOMBING REPORTED! +FIREBOMBING_REPORT.color = #ff4f4f +FIREBOMBING_REPORT.detail = Firebombs are falling!! + +POP_2K_REACHED.title = TOWN +POP_2K_REACHED.color = #7fff7f +POP_2K_REACHED.detail = Congratulations, your village has grown to town status. You now have 2,000 citizens. + +POP_10K_REACHED.title = CITY +POP_10K_REACHED.color = #7fff7f +POP_10K_REACHED.detail = Your town has grown into a full sized city, with a current population of 10,000. Keep up the good work! + +POP_50K_REACHED.title = CAPITAL +POP_50K_REACHED.color = #7fff7f +POP_50K_REACHED.detail =Your city has become a capital. The current population here is 50,000. Your political future looks bright. + +POP_100K_REACHED.title = METROPOLIS +POP_100K_REACHED.color = #7fff7f +POP_100K_REACHED.detail = Your capital city has now achieved the status of metropolis. The current population is 100,000. With your management skills, you should seriously consider running for governor. + +POP_500K_REACHED.title = MEGALOPOLIS +POP_500K_REACHED.color = #7fff7f +POP_500K_REACHED.detail = Congratulation, you have reached the highest category of urban development, the megalopolis. + +HEAVY_TRAFFIC_REPORT.title = HEAVY TRAFFIC! +HEAVY_TRAFFIC_REPORT.color = #ff4f4f +HEAVY_TRAFFIC_REPORT.detail = Sky Watch One
reporting heavy traffic! + +FLOOD_REPORT.title = FLOODING REPORTED! +FLOOD_REPORT.color = #ff4f4f +FLOOD_REPORT.detail = Flooding has been been reported along the water's edge! + +MELTDOWN_REPORT.title = NUCLEAR MELTDOWN! +MELTDOWN_REPORT.color = #ff4f4f +MELTDOWN_REPORT.detail = A nuclear meltdown has occurred at your power plant. You are advised to avoid the area until the radioactive isotopes decay.

Many generations will confront this problem before it goes away, so don't hold your breath. + +RIOTING_REPORT.title = RIOTS! +RIOTING_REPORT.color = #ff4f4f +RIOTING_REPORT.detail = The citizens are rioting in the streets, setting cars and houses on fire, and bombing government buildings and businesses!

All media coverage is blacked out, while the fascist pigs beat the poor citizens into submission. diff --git a/strings/CityStrings.properties b/strings/CityStrings.properties new file mode 100644 index 0000000..df42366 --- /dev/null +++ b/strings/CityStrings.properties @@ -0,0 +1,26 @@ +!! This file is part of MicropolisJ. +!! Copyright (C) 2013 Jason Long +!! Portions Copyright (C) 1989-2007 Electronic Arts Inc. +!! +!! MicropolisJ is free software; you can redistribute it and/or modify +!! it under the terms of the GNU GPLv3, with additional terms. +!! See the README file, included in this distribution, for details. + +problem.CRIME = CRIME +problem.POLLUTION = POLLUTION +problem.HOUSING = HOUSING COSTS +problem.TAXES = TAXES +problem.TRAFFIC = TRAFFIC +problem.UNEMPLOYMENT = UNEMPLOYMENT +problem.FIRE = FIRES + +class.0 = VILLAGE +class.1 = TOWN +class.2 = CITY +class.3 = CAPITAL +class.4 = METROPOLIS +class.5 = MEGALOPOLIS + +level.0 = Easy +level.1 = Medium +level.2 = Hard diff --git a/strings/GuiStrings.properties b/strings/GuiStrings.properties new file mode 100644 index 0000000..0318a7d --- /dev/null +++ b/strings/GuiStrings.properties @@ -0,0 +1,306 @@ +!! This file is part of MicropolisJ. +!! Copyright (C) 2013 Jason Long +!! Portions Copyright (C) 1989-2007 Electronic Arts Inc. +!! +!! MicropolisJ is free software; you can redistribute it and/or modify +!! it under the terms of the GNU GPLv3, with additional terms. +!! See the README file, included in this distribution, for details. + +! +! Main window +! +PRODUCT = MicropolisJ +main.save_query = Do you want to save this city? +main.date_label = Date: +main.funds_label = Funds: +main.population_label = Population: +main.error_caption = Error +main.tools_caption = Tools +main.about_caption = About MicropolisJ +main.version_string = Version {0} +main.caption_unnamed_city = MicropolisJ +main.caption_named_city = {0} - MicropolisJ +main.about_text = \ +

Copyright 2013 Jason Long
\ + Portions Copyright 1989-2007 Electronic Arts Inc.

\ +

This is free software; you can redistribute it and/or modify it
\ + under the terms of the GNU GPLv3; see the README file for details.

\ +

There is no warranty, to the extent permitted by law.

\ + +cty_file = CTY file +funds = ${0,number,integer} +citytime = {0,date,MMM yyyy} + +! +! Welcome screen +! +welcome.caption = Welcome to MicropolisJ +welcome.previous_map = Previous Map +welcome.next_map = Next Map +welcome.play_this_map = Play This Map +welcome.load_city = Load City +welcome.cancel = Cancel +welcome.quit = Quit + +! +! Menus +! +menu.zones = Zones +menu.zones.ALL = All +menu.zones.RESIDENTIAL = Residential +menu.zones.COMMERCIAL = Commercial +menu.zones.INDUSTRIAL = Industrial +menu.zones.TRANSPORT = Transportation +menu.overlays = Overlays +menu.overlays.POPDEN_OVERLAY = Population Density +menu.overlays.GROWTHRATE_OVERLAY = Rate of Growth +menu.overlays.LANDVALUE_OVERLAY = Land Value +menu.overlays.CRIME_OVERLAY = Crime Rate +menu.overlays.POLLUTE_OVERLAY = Pollution +menu.overlays.TRAFFIC_OVERLAY = Traffic Density +menu.overlays.POWER_OVERLAY = Power Grid +menu.overlays.FIRE_OVERLAY = Fire Coverage +menu.overlays.POLICE_OVERLAY = Police Coverage + +menu.game = Game +menu.game.new = New City... +menu.game.load = Load City... +menu.game.save = Save City +menu.game.save_as = Save City as... +menu.game.exit = Exit + +menu.options = Options +menu.options.auto_budget = Auto Budget +menu.options.auto_bulldoze = Auto Bulldoze +menu.options.disasters = Disasters +menu.options.sound = Sound + +menu.difficulty = Difficulty +menu.difficulty.0 = Easy +menu.difficulty.1 = Medium +menu.difficulty.2 = Hard + +menu.disasters = Disasters +menu.disasters.MONSTER = Monster +menu.disasters.FIRE = Fire +menu.disasters.FLOOD = Flood +menu.disasters.MELTDOWN = Meltdown +menu.disasters.TORNADO = Tornado +menu.disasters.EARTHQUAKE = Earthquake + +menu.speed = Speed +menu.speed.SUPER_FAST = Super Fast +menu.speed.FAST = Fast +menu.speed.NORMAL = Normal +menu.speed.SLOW = Slow +menu.speed.PAUSED = Paused + +menu.windows = Windows +menu.windows.budget = Budget +menu.windows.evaluation = Evaluation +menu.windows.graph = Graph + +menu.help = Help +menu.help.about = About + +! +! Tools +! +tool.BULLDOZER.name = BULLDOZER +tool.BULLDOZER.icon = /icdozr.png +tool.BULLDOZER.selected_icon = /icdozrhi.png +tool.BULLDOZER.tip = Bulldozer +tool.WIRE.name = WIRE +tool.WIRE.icon = /icwire.png +tool.WIRE.selected_icon = /icwirehi.png +tool.WIRE.tip = Build Powerlines +tool.PARK.name = PARK +tool.PARK.icon = /icpark.png +tool.PARK.selected_icon = /icparkhi.png +tool.PARK.tip = Build Parks +tool.ROADS.name = ROADS +tool.ROADS.icon = /icroad.png +tool.ROADS.selected_icon = /icroadhi.png +tool.ROADS.tip = Build Roads +tool.RAIL.name = RAIL +tool.RAIL.icon = /icrail.png +tool.RAIL.selected_icon = /icrailhi.png +tool.RAIL.tip = Build Tracks +tool.RESIDENTIAL.name = RESIDENTIAL +tool.RESIDENTIAL.icon = /icres.png +tool.RESIDENTIAL.selected_icon = /icreshi.png +tool.RESIDENTIAL.tip = Zone Residential +tool.COMMERCIAL.name = COMMERCIAL +tool.COMMERCIAL.icon = /iccom.png +tool.COMMERCIAL.selected_icon = /iccomhi.png +tool.COMMERCIAL.tip = Zone Commercial +tool.INDUSTRIAL.name = INDUSTRIAL +tool.INDUSTRIAL.icon = /icind.png +tool.INDUSTRIAL.selected_icon = /icindhi.png +tool.INDUSTRIAL.tip = Zone Industrial +tool.FIRE.name = FIRE +tool.FIRE.icon = /icfire.png +tool.FIRE.selected_icon = /icfirehi.png +tool.FIRE.tip = Build Fire Station +tool.POLICE.name = POLICE +tool.POLICE.icon = /icpol.png +tool.POLICE.selected_icon = /icpolhi.png +tool.POLICE.tip = Build Police Station +tool.POWERPLANT.name = POWERPLANT +tool.POWERPLANT.icon = /iccoal.png +tool.POWERPLANT.selected_icon = /iccoalhi.png +tool.POWERPLANT.tip = Build Coal Powerplant +tool.NUCLEAR.name = NUCLEAR +tool.NUCLEAR.icon = /icnuc.png +tool.NUCLEAR.selected_icon = /icnuchi.png +tool.NUCLEAR.tip = Build Nuclear Powerplant +tool.STADIUM.name = STADIUM +tool.STADIUM.icon = /icstad.png +tool.STADIUM.selected_icon = /icstadhi.png +tool.STADIUM.tip = Build Stadium +tool.SEAPORT.name = SEAPORT +tool.SEAPORT.icon = /icseap.png +tool.SEAPORT.selected_icon = /icseaphi.png +tool.SEAPORT.tip = Build Port +tool.AIRPORT.name = AIRPORT +tool.AIRPORT.icon = /icairp.png +tool.AIRPORT.selected_icon = /icairphi.png +tool.AIRPORT.tip = Build Airport +tool.QUERY.name = QUERY +tool.QUERY.icon = /icqry.png +tool.QUERY.selected_icon = /icqryhi.png +tool.QUERY.tip = Query Zone Status + +tool.BULLDOZER.border = #bf7900 +tool.WIRE.border = #ffff00 +tool.ROADS.border = #5d5d5d +tool.RAIL.border = #5d5d5d +tool.RESIDENTIAL.border = #00ff00 +tool.COMMERCIAL.border = #0000ff +tool.INDUSTRIAL.border = #ffff00 +tool.FIRE.border = #ff0000 +tool.POLICE.border = #0000ff +tool.STADIUM.border = #00ff00 +tool.PARK.border = #bf7900 +tool.SEAPORT.border = #0000ff +tool.POWERPLANT.border = #ffff00 +tool.NUCLEAR.border = #ffff00 +tool.AIRPORT.border = #bf7900 + +tool.BULLDOZER.bgcolor = rgba(0,0,0,0) +tool.WIRE.bgcolor = rgba(0,0,0,0.375) +tool.ROADS.bgcolor = rgba(255,255,255,0.375) +tool.RAIL.bgcolor = rgba(127,127,0,0.375) +tool.RESIDENTIAL.bgcolor = rgba(0,255,0,0.375) +tool.COMMERCIAL.bgcolor = rgba(0,0,255,0.375) +tool.INDUSTRIAL.bgcolor = rgba(255,255,0,0.375) +tool.FIRE.bgcolor = rgba(0,255,0,0.375) +tool.POLICE.bgcolor = rgba(0,255,0,0.375) +tool.POWERPLANT.bgcolor = rgba(93,93,93,0.375) +tool.NUCLEAR.bgcolor = rgba(93,93,93,0.375) +tool.STADIUM.bgcolor = rgba(93,93,93,0.375) +tool.SEAPORT.bgcolor = rgba(93,93,93,0.375) +tool.AIRPORT.bgcolor = rgba(93,93,93,0.375) +tool.PARK.bgcolor = rgba(0,255,0,0.375) + +! +! The Graphs pane (accessible through Window -> Graphs) +! +dismiss_graph = Dismiss Graph +ten_years = 10 YRS +onetwenty_years = 120 YRS + +graph_button.RESPOP = grres.png +graph_button.COMPOP = grcom.png +graph_button.INDPOP = grind.png +graph_button.MONEY = grmony.png +graph_button.CRIME = grcrim.png +graph_button.POLLUTION = grpoll.png + +graph_button.RESPOP.selected = grreshi.png +graph_button.COMPOP.selected = grcomhi.png +graph_button.INDPOP.selected = grindhi.png +graph_button.MONEY.selected = grmonyhi.png +graph_button.CRIME.selected = grcrimhi.png +graph_button.POLLUTION.selected = grpollhi.png + +graph_color.RESPOP = #00e600 +graph_color.COMPOP = #0000e6 +graph_color.INDPOP = #ffff00 +graph_color.MONEY = #007f00 +graph_color.CRIME = #7f0000 +graph_color.POLLUTION = #997f4c + +graph_label.RESPOP = Residential +graph_label.COMPOP = Commercial +graph_label.INDPOP = Industrial +graph_label.MONEY = Cash Flow +graph_label.CRIME = Crime +graph_label.POLLUTION = Pollution + +! +! The Evaluation Pane (accessible through Windows -> Evaluation) +! +dismiss-evaluation = Dismiss Evaluation +public-opinion = Public Opinion +public-opinion-1 = Is the mayor doing a good job? +public-opinion-2 = What are the worst problems? +public-opinion-yes = YES +public-opinion-no = NO +statistics-head = Statistics +city-score-head = Overall City Score (0 - 1000) +stats-population = Population: +stats-net-migration = Net Migration: +stats-last-year = (last year) +stats-assessed-value = Assessed Value: +stats-category = Category: +stats-game-level = Game Level: +city-score-current = Current Score: +city-score-change = Annual Change: + +! +! The mini-map, overlay legends +! +legend_image.POPDEN_OVERLAY = /legendmm.png +legend_image.GROWTHRATE_OVERLAY = /legendpm.png +legend_image.LANDVALUE_OVERLAY = /legendmm.png +legend_image.CRIME_OVERLAY = /legendmm.png +legend_image.POLLUTE_OVERLAY = /legendmm.png +legend_image.TRAFFIC_OVERLAY = /legendmm.png +legend_image.FIRE_OVERLAY = /legendmm.png +legend_image.POLICE_OVERLAY = /legendmm.png + +! +! Budget Dialog box, accessable through Windows -> Budget +! +budgetdlg.title = Budget +budgetdlg.funding_level_hdr = Funding Level +budgetdlg.allocation_hdr = Allocation +budgetdlg.road_fund = Trans. Fund +budgetdlg.police_fund = Police Fund +budgetdlg.fire_fund = Fire Fund +budgetdlg.continue = Continue With These Figures +budgetdlg.reset = Reset to Original Figures +budgetdlg.tax_rate_hdr = Tax Rate +budgetdlg.annual_receipts_hdr = Annual Receipts +budgetdlg.tax_revenue = Tax Revenue +budgetdlg.period_ending = Period Ending +budgetdlg.cash_begin = Cash, beginning of year +budgetdlg.taxes_collected = Taxes Collected +budgetdlg.capital_expenses = Capital Expenditures +budgetdlg.operating_expenses = Operating Expenses +budgetdlg.cash_end = Cash, end of year +budgetdlg.auto_budget = Auto Budget +budgetdlg.pause_game = Pause Game + +! +! Notification pane +! +notification.dismiss = Dismiss +notification.query_hdr = Query Zone Status +notification.zone_lbl = Zone: +notification.density_lbl = Density: +notification.value_lbl = Value: +notification.crime_lbl = Crime: +notification.pollution_lbl = Pollution: +notification.growth_lbl = Growth: diff --git a/strings/StatusMessages.properties b/strings/StatusMessages.properties new file mode 100644 index 0000000..29eaa55 --- /dev/null +++ b/strings/StatusMessages.properties @@ -0,0 +1,62 @@ +!! This file is part of MicropolisJ. +!! Copyright (C) 2013 Jason Long +!! Portions Copyright (C) 1989-2007 Electronic Arts Inc. +!! +!! MicropolisJ is free software; you can redistribute it and/or modify +!! it under the terms of the GNU GPLv3, with additional terms. +!! See the README file, included in this distribution, for details. + +zone.0 = Clear +zone.1 = Water +zone.2 = Trees +zone.3 = Rubble +zone.4 = Flood +zone.5 = Radioactive Waste +zone.6 = Fire +zone.7 = Road +zone.8 = Power +zone.9 = Rail +zone.10 = Residential +zone.11 = Commercial +zone.12 = Industrial +zone.13 = Seaport +zone.14 = Airport +zone.15 = Coal Power +zone.16 = Fire Department +zone.17 = Police Department +zone.18 = Stadium +zone.19 = Nuclear Power +zone.20 = Draw Bridge +zone.21 = Radar Dish +zone.22 = Fountain +zone.23 = Industrial +zone.24 = Steelers 38 Bears 3 +zone.25 = Draw Bridge +zone.26 = Ur 238 +zone.27 = + +! population density +status.1 = Low +status.2 = Medium +status.3 = High +status.4 = Very High +! land value +status.5 = Slum +status.6 = Lower Class +status.7 = Middle Class +status.8 = High +! crime level +status.9 = Safe +status.10 = Light +status.11 = Moderate +status.12 = Dangerous +! pollution +status.13 = None +status.14 = Moderate +status.15 = Heavy +status.16 = Very Heavy +! growth rate +status.17 = Declining +status.18 = Stable +status.19 = Slow Growth +status.20 = Fast Growth