diff --git a/.gitattributes b/.gitattributes index d899f655..61b4b3c3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,4 @@ *.wav filter=lfs diff=lfs merge=lfs -text +*.webm filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.psd filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 230ccaf3..b5949b54 100644 --- a/.gitignore +++ b/.gitignore @@ -107,5 +107,6 @@ dist # Buildfiles build +tmp_standalone_files + -res_built diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 00000000..d6233b1c --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,5 @@ +dengr1065 +- Contributed various fixes to the dark mode + +thelockj +- Contributed the design and initial implementation for the storage building \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e72bfdda --- /dev/null +++ b/LICENSE @@ -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 +. \ No newline at end of file diff --git a/README.md b/README.md index 88b65b4a..7104404d 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,43 @@ # shapez.io -This is the source code for shapez.io, an open source base building game inspired by factorio. +shapez.io Logo + +This is the source code for shapez.io, an open source base building game inspired by Factorio. Your goal is to produce shapes by cutting, rotating, merging and painting parts of shapes. ## Playing -You can already play it on https://beta.shapez.io +You can already play it [here](https://shapez.io). ## Building -- Make sure ffmpeg is on your path -- Install yarn and node 10 +- Make sure `ffmpeg` is on your path +- Install Yarn and Node.js 10 - Run `yarn` in the root folder, then run `yarn` in the `gulp/` folder -- Cd into `gulp` and run `yarn gulp`: It should now open in your browser +- Cd into `gulp` and run `yarn gulp` - it should now open in your browser -**Notice**: This will give you a debug build with several debugging flags enabled. If you want to disable them, check `config.js` +**Notice**: This will produce a debug build with several debugging flags enabled. If you want to disable them, modify `config.js`. ## Contributing -Since this game is in the more or less early development, I will only accept high-quality pull requests which add an immediate benefit. Please understand that low quality PR's might be closed by me with a short comment explaining why. +Since this game is in the more or less early development, I will only accept pull requests which add an immediate benefit. Please understand that low quality PR's might be closed by me with a short comment explaining why. + +If you want to add a new feature or in generally contribute I recommend to get in touch with me on Discord: + + +discord logo + ### Code -The game is based on a custom engine which itself is based on the YORG.io 3 game egine (Actually its almost the same core). -Most code in the engine is clean with some code for the actual game on top being hacky. +The game is based on a custom engine which itself is based on the YORG.io 3 game egine (Actually it shares almost the same core). +The code within the engine is relatively clean with some code for the actual game on top being hacky. + +This project is based on ES5. Some ES2015 features are used but most of them are too slow, especially when polyfilled. For example, `Array.prototype.forEach` is only used within non-critical loops since its slower than a plain for loop. ### Assets -You will need a texture packer license in order to regenerate the atlas. If you don't have one but you want to contribute assets, let me know and I might compile it for you. +For most assets I use Adobe Photoshop, you can find them in `assets/`. + +You will need a Texture Packer license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. diff --git a/artwork/buildings/RGB_Icon.psd b/artwork/buildings/RGB_Icon.psd new file mode 100644 index 00000000..ded296c4 --- /dev/null +++ b/artwork/buildings/RGB_Icon.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c424731dff98885b13a360f42933bc0f43635e9b69b62950c4befbfbd21663e +size 230312 diff --git a/artwork/buildings/belt.psd b/artwork/buildings/belt.psd new file mode 100644 index 00000000..980c52d9 --- /dev/null +++ b/artwork/buildings/belt.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:377f61c6947953c3df4f944397e51c87d318f80bb91ba4913de4a1da7f94a7cc +size 379425 diff --git a/artwork/buildings/belt_minimap_shapes.psd b/artwork/buildings/belt_minimap_shapes.psd new file mode 100644 index 00000000..6da7cd72 --- /dev/null +++ b/artwork/buildings/belt_minimap_shapes.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98d30b113681fcf248ec6c81b53d89fb8006e414ec68862b452a23c216ebfe85 +size 22652 diff --git a/artwork/buildings/buildings_1x1.psd b/artwork/buildings/buildings_1x1.psd new file mode 100644 index 00000000..c4b1abc3 --- /dev/null +++ b/artwork/buildings/buildings_1x1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a83f85517a44fa590a7a5e72b90736ba52065712e537f9eaa0d53aefd6a42457 +size 765795 diff --git a/artwork/buildings/buildings_2x1.psd b/artwork/buildings/buildings_2x1.psd new file mode 100644 index 00000000..21889669 --- /dev/null +++ b/artwork/buildings/buildings_2x1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7697c34997a719bed9ddf9c16c19c672a0fdf9641edf0a9761aea9c2c7e17c6b +size 632609 diff --git a/artwork/buildings/buildings_2x2.psd b/artwork/buildings/buildings_2x2.psd new file mode 100644 index 00000000..7017b4d9 --- /dev/null +++ b/artwork/buildings/buildings_2x2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7931a4ba2104bbf7de3a1332617d5a056e34c55bab751821a168a83e0b91793 +size 683910 diff --git a/artwork/buildings/buildings_3x3.psd b/artwork/buildings/buildings_3x3.psd new file mode 100644 index 00000000..73ad2e91 --- /dev/null +++ b/artwork/buildings/buildings_3x3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:573954846ee7adff355373ed22ce4cef90c5a1d4b7af928944c2110f6b5120f5 +size 595603 diff --git a/artwork/buildings/buildings_4x1.psd b/artwork/buildings/buildings_4x1.psd new file mode 100644 index 00000000..ffd3decb --- /dev/null +++ b/artwork/buildings/buildings_4x1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc99dc14248306295ce17761c0cadf369a713ed65e864554e374d20ea9d571af +size 778507 diff --git a/artwork/buildings/extractor.psd b/artwork/buildings/extractor.psd new file mode 100644 index 00000000..c0dfd420 --- /dev/null +++ b/artwork/buildings/extractor.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a4dd431e7ff12f72eb54545c10ba1d5e6dd16119fe1430864a4e449391adebb +size 216421 diff --git a/artwork/buildings/hub.psd b/artwork/buildings/hub.psd new file mode 100644 index 00000000..8964402a --- /dev/null +++ b/artwork/buildings/hub.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87ff03f1c77d8c245e4e2fe716b6243aecca174425ae24cfd19ffb5bd1df52f6 +size 1191627 diff --git a/artwork/gamedistribution/1280x550.jpg b/artwork/gamedistribution/1280x550.jpg new file mode 100644 index 00000000..9f5c0f44 Binary files /dev/null and b/artwork/gamedistribution/1280x550.jpg differ diff --git a/artwork/gamedistribution/1280x550.png b/artwork/gamedistribution/1280x550.png new file mode 100644 index 00000000..5086af18 Binary files /dev/null and b/artwork/gamedistribution/1280x550.png differ diff --git a/artwork/gamedistribution/1280x720.jpg b/artwork/gamedistribution/1280x720.jpg new file mode 100644 index 00000000..ea2bb545 Binary files /dev/null and b/artwork/gamedistribution/1280x720.jpg differ diff --git a/artwork/gamedistribution/1280x720.png b/artwork/gamedistribution/1280x720.png new file mode 100644 index 00000000..191fe91a Binary files /dev/null and b/artwork/gamedistribution/1280x720.png differ diff --git a/artwork/gamedistribution/1280x720.psd b/artwork/gamedistribution/1280x720.psd new file mode 100644 index 00000000..f6bae3e1 --- /dev/null +++ b/artwork/gamedistribution/1280x720.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccae5926d00eee9dd0686b1d415f30e302c53e25d8ca1c9d5517347510cea2b5 +size 11758220 diff --git a/artwork/gamedistribution/512x340.jpg b/artwork/gamedistribution/512x340.jpg new file mode 100644 index 00000000..d34f34af Binary files /dev/null and b/artwork/gamedistribution/512x340.jpg differ diff --git a/artwork/gamedistribution/512x340.png b/artwork/gamedistribution/512x340.png new file mode 100644 index 00000000..33eb371c Binary files /dev/null and b/artwork/gamedistribution/512x340.png differ diff --git a/artwork/gamedistribution/512x384.jpg b/artwork/gamedistribution/512x384.jpg new file mode 100644 index 00000000..77d3b3ad Binary files /dev/null and b/artwork/gamedistribution/512x384.jpg differ diff --git a/artwork/gamedistribution/512x384.png b/artwork/gamedistribution/512x384.png new file mode 100644 index 00000000..65fd95c4 Binary files /dev/null and b/artwork/gamedistribution/512x384.png differ diff --git a/artwork/gamedistribution/512x384.psd b/artwork/gamedistribution/512x384.psd new file mode 100644 index 00000000..4c4429de --- /dev/null +++ b/artwork/gamedistribution/512x384.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ac7dc426f618215bf8e62526dfe38d8076c439849ef341f5545561c551625d4 +size 10266632 diff --git a/artwork/gamedistribution/512x512.jpg b/artwork/gamedistribution/512x512.jpg new file mode 100644 index 00000000..db293db4 Binary files /dev/null and b/artwork/gamedistribution/512x512.jpg differ diff --git a/artwork/gamedistribution/512x512.png b/artwork/gamedistribution/512x512.png new file mode 100644 index 00000000..81c8b737 Binary files /dev/null and b/artwork/gamedistribution/512x512.png differ diff --git a/artwork/gamedistribution/iframe/index.html b/artwork/gamedistribution/iframe/index.html new file mode 100644 index 00000000..ac308b68 --- /dev/null +++ b/artwork/gamedistribution/iframe/index.html @@ -0,0 +1,85 @@ + + + + shapez.io + + + + + + + + + diff --git a/artwork/gamedistribution/iframe/manifest.json b/artwork/gamedistribution/iframe/manifest.json new file mode 100644 index 00000000..539af604 --- /dev/null +++ b/artwork/gamedistribution/iframe/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "shapez.io", + "short_name": "shapez.io", + "start_url": "index.html", + "display": "standalone", + "background_color": "#222428", + "description": "shapez.io" +} diff --git a/artwork/icon-standalone.ico b/artwork/icon-standalone.ico new file mode 100644 index 00000000..54721ebf Binary files /dev/null and b/artwork/icon-standalone.ico differ diff --git a/artwork/icon-standalone.png b/artwork/icon-standalone.png new file mode 100644 index 00000000..44994cc9 Binary files /dev/null and b/artwork/icon-standalone.png differ diff --git a/artwork/icon-standalone.psd b/artwork/icon-standalone.psd new file mode 100644 index 00000000..a972d472 --- /dev/null +++ b/artwork/icon-standalone.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a0062684305ff8ac875812f39e565fe00f27b537212b2ad70d68c27fb2ae6f8 +size 224962 diff --git a/artwork/icon.ico b/artwork/icon.ico new file mode 100644 index 00000000..4ec3676d Binary files /dev/null and b/artwork/icon.ico differ diff --git a/artwork/icon.png b/artwork/icon.png new file mode 100644 index 00000000..64c36eb8 Binary files /dev/null and b/artwork/icon.png differ diff --git a/artwork/icon.psd b/artwork/icon.psd new file mode 100644 index 00000000..1e9a7eae --- /dev/null +++ b/artwork/icon.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22b15510bbe17ebe46b899952fe7cb260271c103e66759544aef0a7fa963d319 +size 209192 diff --git a/artwork/installer/installer.ifp b/artwork/installer/installer.ifp new file mode 100644 index 00000000..98c09a1e Binary files /dev/null and b/artwork/installer/installer.ifp differ diff --git a/artwork/installer/wizard.bmp b/artwork/installer/wizard.bmp new file mode 100644 index 00000000..ae9c43b6 Binary files /dev/null and b/artwork/installer/wizard.bmp differ diff --git a/artwork/installer/wizard.psd b/artwork/installer/wizard.psd new file mode 100644 index 00000000..9e74b727 --- /dev/null +++ b/artwork/installer/wizard.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3fa43e6a02c92ac66717aaedde15c41b6694c390165c52a43e5b794b73bc895 +size 1091078 diff --git a/artwork/intial_concept/README.md b/artwork/intial_concept/README.md new file mode 100644 index 00000000..012abdee --- /dev/null +++ b/artwork/intial_concept/README.md @@ -0,0 +1 @@ +This is the initial concept which I made and was the birth of shapez.io diff --git a/artwork/intial_concept/concept-v2.png b/artwork/intial_concept/concept-v2.png new file mode 100644 index 00000000..2072fc47 Binary files /dev/null and b/artwork/intial_concept/concept-v2.png differ diff --git a/artwork/intial_concept/concept.png b/artwork/intial_concept/concept.png new file mode 100644 index 00000000..3c8468df Binary files /dev/null and b/artwork/intial_concept/concept.png differ diff --git a/artwork/intial_concept/concept.psd b/artwork/intial_concept/concept.psd new file mode 100644 index 00000000..b1f2218d --- /dev/null +++ b/artwork/intial_concept/concept.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c5b380e08ed27a1d7c47f2e7c686963a1837bfebce29cce180a741674eb4800 +size 1533043 diff --git a/artwork/itch.io/background.jpg b/artwork/itch.io/background.jpg new file mode 100644 index 00000000..619876a2 Binary files /dev/null and b/artwork/itch.io/background.jpg differ diff --git a/artwork/itch.io/banner.png b/artwork/itch.io/banner.png new file mode 100644 index 00000000..17488ee2 Binary files /dev/null and b/artwork/itch.io/banner.png differ diff --git a/artwork/itch.io/banner.psd b/artwork/itch.io/banner.psd new file mode 100644 index 00000000..c4203db4 --- /dev/null +++ b/artwork/itch.io/banner.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c334d77dabdc40fef4ece56ccef9eaa59b5b6b9381817c234f9bf07d00e676e5 +size 12721033 diff --git a/artwork/itch.io/full-page.psd b/artwork/itch.io/full-page.psd new file mode 100644 index 00000000..e33a1b91 --- /dev/null +++ b/artwork/itch.io/full-page.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:965ea97e2390927132f2463118ec7b8dd874b838fc770dc7f0ee76bb48ef0527 +size 21376906 diff --git a/artwork/itch.io/screenshots/1.png b/artwork/itch.io/screenshots/1.png new file mode 100644 index 00000000..75214ff7 Binary files /dev/null and b/artwork/itch.io/screenshots/1.png differ diff --git a/artwork/itch.io/screenshots/2.png b/artwork/itch.io/screenshots/2.png new file mode 100644 index 00000000..9dd0fc6f Binary files /dev/null and b/artwork/itch.io/screenshots/2.png differ diff --git a/artwork/itch.io/screenshots/3.png b/artwork/itch.io/screenshots/3.png new file mode 100644 index 00000000..af0d0ab3 Binary files /dev/null and b/artwork/itch.io/screenshots/3.png differ diff --git a/artwork/itch.io/screenshots/4.png b/artwork/itch.io/screenshots/4.png new file mode 100644 index 00000000..f4e34386 Binary files /dev/null and b/artwork/itch.io/screenshots/4.png differ diff --git a/artwork/itch.io/screenshots/5.png b/artwork/itch.io/screenshots/5.png new file mode 100644 index 00000000..b456d816 Binary files /dev/null and b/artwork/itch.io/screenshots/5.png differ diff --git a/artwork/itch.io/screenshots/6.png b/artwork/itch.io/screenshots/6.png new file mode 100644 index 00000000..bdbbaccf Binary files /dev/null and b/artwork/itch.io/screenshots/6.png differ diff --git a/artwork/itch.io/screenshots/7.png b/artwork/itch.io/screenshots/7.png new file mode 100644 index 00000000..0df86bcc Binary files /dev/null and b/artwork/itch.io/screenshots/7.png differ diff --git a/artwork/itch.io/screenshots/8.png b/artwork/itch.io/screenshots/8.png new file mode 100644 index 00000000..1b3bbdc2 Binary files /dev/null and b/artwork/itch.io/screenshots/8.png differ diff --git a/artwork/logo.png b/artwork/logo.png new file mode 100644 index 00000000..75dd3a35 Binary files /dev/null and b/artwork/logo.png differ diff --git a/artwork/logo.psd b/artwork/logo.psd new file mode 100644 index 00000000..04575aa1 --- /dev/null +++ b/artwork/logo.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16f131c43e555211929bfd90468c554416acf9a54f2172dd377bbe4f02b43227 +size 228133 diff --git a/artwork/promo/yorgio-promo.png b/artwork/promo/yorgio-promo.png new file mode 100644 index 00000000..60ebd785 Binary files /dev/null and b/artwork/promo/yorgio-promo.png differ diff --git a/artwork/promo/yorgio-promo.psd b/artwork/promo/yorgio-promo.psd new file mode 100644 index 00000000..589bdaa4 --- /dev/null +++ b/artwork/promo/yorgio-promo.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab0287f69a54c57e725e239a1a6ef35c79d60f4217184fcde7b27ab4d1c0ef60 +size 8866812 diff --git a/artwork/steam/184x69.png b/artwork/steam/184x69.png new file mode 100644 index 00000000..6e0a3c0a Binary files /dev/null and b/artwork/steam/184x69.png differ diff --git a/artwork/steam/1920x620.png b/artwork/steam/1920x620.png new file mode 100644 index 00000000..7f659383 Binary files /dev/null and b/artwork/steam/1920x620.png differ diff --git a/artwork/steam/1920x620.psd b/artwork/steam/1920x620.psd new file mode 100644 index 00000000..fd66178b --- /dev/null +++ b/artwork/steam/1920x620.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b909ed70673b45c3f651a6325f5d0c27d1bfd982bfbc41f964075135c6fede76 +size 23705912 diff --git a/artwork/steam/231x87.png b/artwork/steam/231x87.png new file mode 100644 index 00000000..73d17031 Binary files /dev/null and b/artwork/steam/231x87.png differ diff --git a/artwork/steam/32x32.png b/artwork/steam/32x32.png new file mode 100644 index 00000000..4c1cca09 Binary files /dev/null and b/artwork/steam/32x32.png differ diff --git a/artwork/steam/32x32.psd b/artwork/steam/32x32.psd new file mode 100644 index 00000000..9a3c3914 --- /dev/null +++ b/artwork/steam/32x32.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f09b39f525b2d1ed094ceec73b792ce75d593e769254154cf32f4df2a429f2ba +size 52046 diff --git a/artwork/steam/444x208.png b/artwork/steam/444x208.png new file mode 100644 index 00000000..e0e7216d Binary files /dev/null and b/artwork/steam/444x208.png differ diff --git a/artwork/steam/460x215.png b/artwork/steam/460x215.png new file mode 100644 index 00000000..69f58a94 Binary files /dev/null and b/artwork/steam/460x215.png differ diff --git a/artwork/steam/460x215.psd b/artwork/steam/460x215.psd new file mode 100644 index 00000000..4833668b --- /dev/null +++ b/artwork/steam/460x215.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12df556b43bb303aa0318a6adff70ff3acd9ac68693ebabf3fab094efecca810 +size 6757896 diff --git a/artwork/steam/600x900.png b/artwork/steam/600x900.png new file mode 100644 index 00000000..3064b240 Binary files /dev/null and b/artwork/steam/600x900.png differ diff --git a/artwork/steam/600x900.psd b/artwork/steam/600x900.psd new file mode 100644 index 00000000..b0bffc9e --- /dev/null +++ b/artwork/steam/600x900.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42015852b1abec0541c10f71acdde226170b60de7b7737282485b738022d8982 +size 11018053 diff --git a/artwork/steam/616x353.png b/artwork/steam/616x353.png new file mode 100644 index 00000000..58f9352e Binary files /dev/null and b/artwork/steam/616x353.png differ diff --git a/artwork/steam/616x353.psd b/artwork/steam/616x353.psd new file mode 100644 index 00000000..ba232882 --- /dev/null +++ b/artwork/steam/616x353.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a5a83129016b75d11af487f42b4bee3d85a48d2894b681d076724c9512aeb31 +size 10334692 diff --git a/artwork/steam/640x360.png b/artwork/steam/640x360.png new file mode 100644 index 00000000..5f70fb36 Binary files /dev/null and b/artwork/steam/640x360.png differ diff --git a/artwork/steam/640x360.psd b/artwork/steam/640x360.psd new file mode 100644 index 00000000..958e9e03 --- /dev/null +++ b/artwork/steam/640x360.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20ca95838254a46d4c23f4c24b7877eaad60aa1aeed3df087b242f389e428774 +size 156876 diff --git a/artwork/steam/screenshots/1.png b/artwork/steam/screenshots/1.png new file mode 100644 index 00000000..ff85b1dc Binary files /dev/null and b/artwork/steam/screenshots/1.png differ diff --git a/artwork/steam/screenshots/10.png b/artwork/steam/screenshots/10.png new file mode 100644 index 00000000..f7afe0a8 Binary files /dev/null and b/artwork/steam/screenshots/10.png differ diff --git a/artwork/steam/screenshots/11.png b/artwork/steam/screenshots/11.png new file mode 100644 index 00000000..6ef87b38 Binary files /dev/null and b/artwork/steam/screenshots/11.png differ diff --git a/artwork/steam/screenshots/2.png b/artwork/steam/screenshots/2.png new file mode 100644 index 00000000..fd3b5d1c Binary files /dev/null and b/artwork/steam/screenshots/2.png differ diff --git a/artwork/steam/screenshots/3.png b/artwork/steam/screenshots/3.png new file mode 100644 index 00000000..4b6e5d6a Binary files /dev/null and b/artwork/steam/screenshots/3.png differ diff --git a/artwork/steam/screenshots/4.png b/artwork/steam/screenshots/4.png new file mode 100644 index 00000000..4ee5bfdc Binary files /dev/null and b/artwork/steam/screenshots/4.png differ diff --git a/artwork/steam/screenshots/5.png b/artwork/steam/screenshots/5.png new file mode 100644 index 00000000..cb2fa743 Binary files /dev/null and b/artwork/steam/screenshots/5.png differ diff --git a/artwork/steam/screenshots/6.png b/artwork/steam/screenshots/6.png new file mode 100644 index 00000000..e0182e70 Binary files /dev/null and b/artwork/steam/screenshots/6.png differ diff --git a/artwork/steam/screenshots/7.png b/artwork/steam/screenshots/7.png new file mode 100644 index 00000000..9da8d817 Binary files /dev/null and b/artwork/steam/screenshots/7.png differ diff --git a/artwork/steam/screenshots/8.png b/artwork/steam/screenshots/8.png new file mode 100644 index 00000000..f09363f7 Binary files /dev/null and b/artwork/steam/screenshots/8.png differ diff --git a/artwork/steam/screenshots/9.png b/artwork/steam/screenshots/9.png new file mode 100644 index 00000000..9bf9534f Binary files /dev/null and b/artwork/steam/screenshots/9.png differ diff --git a/artwork/thirdparty/armorgames/162x100.png b/artwork/thirdparty/armorgames/162x100.png new file mode 100644 index 00000000..aede09f3 Binary files /dev/null and b/artwork/thirdparty/armorgames/162x100.png differ diff --git a/artwork/thirdparty/armorgames/162x100.psd b/artwork/thirdparty/armorgames/162x100.psd new file mode 100644 index 00000000..c0c4160f --- /dev/null +++ b/artwork/thirdparty/armorgames/162x100.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f119ae5afd23eb038b9cb55699ea02b5908c8c4d67a0b06e91934ca41eb123fa +size 143528 diff --git a/artwork/thirdparty/armorgames/620x320.png b/artwork/thirdparty/armorgames/620x320.png new file mode 100644 index 00000000..8f0ff84d Binary files /dev/null and b/artwork/thirdparty/armorgames/620x320.png differ diff --git a/artwork/thirdparty/armorgames/620x320.psd b/artwork/thirdparty/armorgames/620x320.psd new file mode 100644 index 00000000..b0e3df6b --- /dev/null +++ b/artwork/thirdparty/armorgames/620x320.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c1b98562af8362919fe67e90fbf18033ef48ca3b394072f08ab2b3e2711a594 +size 1440735 diff --git a/artwork/thirdparty/armorgames/icon.png b/artwork/thirdparty/armorgames/icon.png new file mode 100644 index 00000000..4d78ff80 Binary files /dev/null and b/artwork/thirdparty/armorgames/icon.png differ diff --git a/artwork/thirdparty/armorgames/icon.psd b/artwork/thirdparty/armorgames/icon.psd new file mode 100644 index 00000000..1cca09a5 --- /dev/null +++ b/artwork/thirdparty/armorgames/icon.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d523d3aff76713cbce90986d86e33d1b937ccefadbd6958e2d90b25cd6ed47b +size 40609 diff --git a/artwork/thirdparty/armorgames/index.html b/artwork/thirdparty/armorgames/index.html new file mode 100644 index 00000000..2b2da26d --- /dev/null +++ b/artwork/thirdparty/armorgames/index.html @@ -0,0 +1,14 @@ + + + + shapez.io + + + + + + You will be redirected soon + + diff --git a/artwork/thirdparty/armorgames/index.zip b/artwork/thirdparty/armorgames/index.zip new file mode 100644 index 00000000..02aa81f0 Binary files /dev/null and b/artwork/thirdparty/armorgames/index.zip differ diff --git a/artwork/thirdparty/iogames.space/thumb.png b/artwork/thirdparty/iogames.space/thumb.png new file mode 100644 index 00000000..9f36f7b2 Binary files /dev/null and b/artwork/thirdparty/iogames.space/thumb.png differ diff --git a/artwork/thirdparty/iogames.space/thumb.psd b/artwork/thirdparty/iogames.space/thumb.psd new file mode 100644 index 00000000..93d392df --- /dev/null +++ b/artwork/thirdparty/iogames.space/thumb.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb09261c4d8f26cbf3c79f9e283a59d2bf9b54c8f360794a4696644edbcbcaa9 +size 8895432 diff --git a/artwork/thirdparty/kongregate/icon.png b/artwork/thirdparty/kongregate/icon.png new file mode 100644 index 00000000..dc742197 Binary files /dev/null and b/artwork/thirdparty/kongregate/icon.png differ diff --git a/artwork/thirdparty/kongregate/icon.psd b/artwork/thirdparty/kongregate/icon.psd new file mode 100644 index 00000000..25835670 --- /dev/null +++ b/artwork/thirdparty/kongregate/icon.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406ddccda44fd026d5a435ff80c418fa007f0252fb5ac13165514d5da9186065 +size 10271381 diff --git a/artwork/thirdparty/kongregate/iframe.html b/artwork/thirdparty/kongregate/iframe.html new file mode 100644 index 00000000..6b741420 --- /dev/null +++ b/artwork/thirdparty/kongregate/iframe.html @@ -0,0 +1,17 @@ + + + + Iframe test + + + + + diff --git a/artwork/thirdparty/kongregate/index.html b/artwork/thirdparty/kongregate/index.html new file mode 100644 index 00000000..ce19f275 --- /dev/null +++ b/artwork/thirdparty/kongregate/index.html @@ -0,0 +1,12 @@ + + + + Redirecting to shapez.io + + + + + Redirecting you to + shapez.io + + diff --git a/artwork/thirdparty/miniclip/150x110.psd b/artwork/thirdparty/miniclip/150x110.psd new file mode 100644 index 00000000..e4677214 --- /dev/null +++ b/artwork/thirdparty/miniclip/150x110.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:525a1f60529723c8e534233adbbbd8ff8c6d5fec25360f35c77f787166fb0526 +size 7666025 diff --git a/artwork/thirdparty/miniclip/216x287.psd b/artwork/thirdparty/miniclip/216x287.psd new file mode 100644 index 00000000..1e535a14 --- /dev/null +++ b/artwork/thirdparty/miniclip/216x287.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:064c6e73648537b2c6225784dd56ed37bdf46c5e3b1106e05278243f1d86cb8d +size 1136861 diff --git a/artwork/thirdparty/miniclip/444x287.psd b/artwork/thirdparty/miniclip/444x287.psd new file mode 100644 index 00000000..ced5937b --- /dev/null +++ b/artwork/thirdparty/miniclip/444x287.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0989c0e5d438247599f37e6c5bda458ff4bd0dcda5b7a0e5c392c2a98ac14c80 +size 9068311 diff --git a/artwork/thirdparty/miniclip/512x512.psd b/artwork/thirdparty/miniclip/512x512.psd new file mode 100644 index 00000000..b4efeec9 --- /dev/null +++ b/artwork/thirdparty/miniclip/512x512.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aad36634a8b25092fab68560668c56abeac1ef4f5f0f52e805cc5e2a6c4ca6bb +size 2535755 diff --git a/artwork/thirdparty/miniclip/70x59.psd b/artwork/thirdparty/miniclip/70x59.psd new file mode 100644 index 00000000..fa13ab2d --- /dev/null +++ b/artwork/thirdparty/miniclip/70x59.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23c3cb1a03dd1986d5dc7385762bbbf000b1b5adc5c1b5f202d79651aaae8fc7 +size 801189 diff --git a/artwork/thirdparty/miniclip/shapezio-miniclip-assets.zip b/artwork/thirdparty/miniclip/shapezio-miniclip-assets.zip new file mode 100644 index 00000000..53b2b380 Binary files /dev/null and b/artwork/thirdparty/miniclip/shapezio-miniclip-assets.zip differ diff --git a/artwork/thirdparty/miniclip/shapezio__150x110.png b/artwork/thirdparty/miniclip/shapezio__150x110.png new file mode 100644 index 00000000..c4696af6 Binary files /dev/null and b/artwork/thirdparty/miniclip/shapezio__150x110.png differ diff --git a/artwork/thirdparty/miniclip/shapezio__216x287.png b/artwork/thirdparty/miniclip/shapezio__216x287.png new file mode 100644 index 00000000..eba64e2c Binary files /dev/null and b/artwork/thirdparty/miniclip/shapezio__216x287.png differ diff --git a/artwork/thirdparty/miniclip/shapezio__444x287.png b/artwork/thirdparty/miniclip/shapezio__444x287.png new file mode 100644 index 00000000..335fd664 Binary files /dev/null and b/artwork/thirdparty/miniclip/shapezio__444x287.png differ diff --git a/artwork/thirdparty/miniclip/shapezio__512x512.png b/artwork/thirdparty/miniclip/shapezio__512x512.png new file mode 100644 index 00000000..81c8b737 Binary files /dev/null and b/artwork/thirdparty/miniclip/shapezio__512x512.png differ diff --git a/artwork/thirdparty/miniclip/shapezio__70x59.png b/artwork/thirdparty/miniclip/shapezio__70x59.png new file mode 100644 index 00000000..a982eaf3 Binary files /dev/null and b/artwork/thirdparty/miniclip/shapezio__70x59.png differ diff --git a/artwork/tutorial_videos/level_1.webm b/artwork/tutorial_videos/level_1.webm new file mode 100644 index 00000000..c14a3918 --- /dev/null +++ b/artwork/tutorial_videos/level_1.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc60f1f38c775874047967fea3b81350b8b62496fd276e89227ce3b3bd8f593b +size 1038054 diff --git a/artwork/tutorial_videos/level_10.webm b/artwork/tutorial_videos/level_10.webm new file mode 100644 index 00000000..aa1124a3 --- /dev/null +++ b/artwork/tutorial_videos/level_10.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db8026c2a132c9d2f016986dc7a66127254c725c068eaae60909ddff6d50a210 +size 2397175 diff --git a/artwork/tutorial_videos/level_11.webm b/artwork/tutorial_videos/level_11.webm new file mode 100644 index 00000000..29a782a7 --- /dev/null +++ b/artwork/tutorial_videos/level_11.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce02cec04d080b2114822e9241a9baa073a56f38dd0c52e235373fd8ae2f856d +size 1105340 diff --git a/artwork/tutorial_videos/level_2.webm b/artwork/tutorial_videos/level_2.webm new file mode 100644 index 00000000..775c8629 --- /dev/null +++ b/artwork/tutorial_videos/level_2.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68e0987d534972b90a40b814180c19eb69100a8661295388c4237cbb3ee3b907 +size 2029655 diff --git a/artwork/tutorial_videos/level_3.webm b/artwork/tutorial_videos/level_3.webm new file mode 100644 index 00000000..c20b650a --- /dev/null +++ b/artwork/tutorial_videos/level_3.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2afc6b142575631260ef546c4e47e444cb47bf20668635d69babe77d7771e71f +size 1651886 diff --git a/artwork/tutorial_videos/level_4.webm b/artwork/tutorial_videos/level_4.webm new file mode 100644 index 00000000..a0fe2602 --- /dev/null +++ b/artwork/tutorial_videos/level_4.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0790736c943db421ec09cef70df722dbd7c3ff25dbad8cfd711a68c4b4806dcd +size 1467335 diff --git a/artwork/tutorial_videos/level_5.webm b/artwork/tutorial_videos/level_5.webm new file mode 100644 index 00000000..60292038 --- /dev/null +++ b/artwork/tutorial_videos/level_5.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c8e8f9ebb7523c50d7a8797ec1830c1669bd4dbe233295274b17c08ba52cdf5 +size 2063527 diff --git a/artwork/tutorial_videos/level_6.webm b/artwork/tutorial_videos/level_6.webm new file mode 100644 index 00000000..6ba82c0c --- /dev/null +++ b/artwork/tutorial_videos/level_6.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95a9f337551e9c9a0064f28ac20cf51f6a76f9f44e0163b458db9dacc36feaa4 +size 3892951 diff --git a/artwork/tutorial_videos/level_7.webm b/artwork/tutorial_videos/level_7.webm new file mode 100644 index 00000000..55b5f70b --- /dev/null +++ b/artwork/tutorial_videos/level_7.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25d07f2a8facbd7faf8710ed86e882743bfc46dece51d80ee959be239dd0f482 +size 3188174 diff --git a/artwork/tutorial_videos/level_9.webm b/artwork/tutorial_videos/level_9.webm new file mode 100644 index 00000000..40e240e2 --- /dev/null +++ b/artwork/tutorial_videos/level_9.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a996fc3605f4ef180a7985f2a74621510489cc88b2b8a97a9dc1bd19566383b7 +size 1727787 diff --git a/artwork/ui/building_tutorials.psd b/artwork/ui/building_tutorials.psd new file mode 100644 index 00000000..5d35a9f4 --- /dev/null +++ b/artwork/ui/building_tutorials.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e519ee86412cc34446e2315e6c4ffa9aa93aaa034e511dc6d80d92e5bb953d3f +size 14461756 diff --git a/artwork/ui/debug_arrows.psd b/artwork/ui/debug_arrows.psd new file mode 100644 index 00000000..e9421289 --- /dev/null +++ b/artwork/ui/debug_arrows.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5502e112c14310aa90e0137b8153b7acecf91e9c0311191b903d87bb10b99a1 +size 26245 diff --git a/artwork/ui/ejector_arrow.psd b/artwork/ui/ejector_arrow.psd new file mode 100644 index 00000000..c6b201ac --- /dev/null +++ b/artwork/ui/ejector_arrow.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2628ddad3e861d403ceee1d8f736f11d8c79d2ab597ae9e7939fed6f9dc5fc69 +size 65914 diff --git a/artwork/ui/get_on_steam.psd b/artwork/ui/get_on_steam.psd new file mode 100644 index 00000000..36a11a89 --- /dev/null +++ b/artwork/ui/get_on_steam.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27d142e1f200f474af2cb622774248f42225c45836ce5d0b0badd5c17b78ae86 +size 198569 diff --git a/artwork/ui/storage_overlay.psd b/artwork/ui/storage_overlay.psd new file mode 100644 index 00000000..78052848 --- /dev/null +++ b/artwork/ui/storage_overlay.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c289cd61980aaf502fe4eda8591b2a2684137c85ca37d927598f916449f70de4 +size 38930 diff --git a/artwork/ui/toolbar-icons.psd b/artwork/ui/toolbar-icons.psd new file mode 100644 index 00000000..e8efcfa3 --- /dev/null +++ b/artwork/ui/toolbar-icons.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:037d27409bf78b828470e553ee0f29304eda24c46598151bb4b9c69c66a6c6c1 +size 146363 diff --git a/artwork/ui/toolbar_background.psd b/artwork/ui/toolbar_background.psd new file mode 100644 index 00000000..d4dc44b0 --- /dev/null +++ b/artwork/ui/toolbar_background.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcde4f1a9e2a7b1ec77f904fe248a9cabfeed9eec9aea31d25627a46da1f8b7f +size 48317 diff --git a/artwork/ui/vignette.psd b/artwork/ui/vignette.psd new file mode 100644 index 00000000..6769736c --- /dev/null +++ b/artwork/ui/vignette.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9b171450a2713faaa0eb41ea052da8ee67d4ded246b01ec92f322a25b989a51 +size 113869 diff --git a/electron/electron.code-workspace b/electron/electron.code-workspace new file mode 100644 index 00000000..fc9ab864 --- /dev/null +++ b/electron/electron.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.exclude": { + "**/node_modules": true, + "**/typedefs_gen": true + } + } +} \ No newline at end of file diff --git a/electron/favicon.ico b/electron/favicon.ico new file mode 100644 index 00000000..54721ebf Binary files /dev/null and b/electron/favicon.ico differ diff --git a/electron/favicon.png b/electron/favicon.png new file mode 100644 index 00000000..44994cc9 Binary files /dev/null and b/electron/favicon.png differ diff --git a/electron/index.js b/electron/index.js new file mode 100644 index 00000000..7b9377df --- /dev/null +++ b/electron/index.js @@ -0,0 +1,228 @@ +/* eslint-disable quotes,no-undef */ + +const { app, BrowserWindow, Menu, MenuItem, session } = require("electron"); +const path = require("path"); +const url = require("url"); +const childProcess = require("child_process"); +const { ipcMain } = require("electron"); +const fs = require("fs"); +const isDev = process.argv.indexOf("--dev") >= 0; +const isLocal = process.argv.indexOf("--local") >= 0; + +const roamingFolder = + process.env.APPDATA || + (process.platform == "darwin" + ? process.env.HOME + "/Library/Preferences" + : process.env.HOME + "/.local/share"); +let storePath = path.join(roamingFolder, "shapez.io", "saves"); + +if (!fs.existsSync(storePath)) { + // No try-catch by design + fs.mkdirSync(storePath, { recursive: true }); +} + +/** @type {BrowserWindow} */ +let win = null; +let menu = null; + +function createWindow() { + let faviconExtension = ".png"; + if (process.platform === "win32") { + faviconExtension = ".ico"; + } + + win = new BrowserWindow({ + width: 1280, + height: 800, + show: false, + backgroundColor: "#222428", + useContentSize: true, + minWidth: 800, + minHeight: 600, + title: "shapez.io Standalone", + transparent: false, + icon: path.join(__dirname, "favicon" + faviconExtension), + // fullscreen: true, + autoHideMenuBar: true, + webPreferences: { + nodeIntegration: true, + webSecurity: false, + }, + allowRunningInsecureContent: false, + }); + + if (isLocal) { + win.loadURL("http://localhost:3005"); + } else { + win.loadURL( + url.format({ + pathname: path.join(__dirname, "index.html"), + protocol: "file:", + slashes: true, + }) + ); + } + win.webContents.session.clearCache(); + win.webContents.session.clearStorageData(); + + win.webContents.on("new-window", (event, pth) => { + event.preventDefault(); + if (process.platform == "win32") { + childProcess.execSync("start " + pth); + } else if (process.platform == "linux") { + childProcess.execSync("xdg-open " + pth); + } + }); + + win.on("closed", () => { + console.log("Window closed"); + win = null; + app.quit(); + }); + + function handleWindowBeforeunload(event) { + const confirmed = dialog.showMessageBox(remote.getCurrentWindow(), options) === 1; + if (confirmed) { + remote.getCurrentWindow().close(); + } else { + event.returnValue = false; + } + } + + win.on("", handleWindowBeforeunload); + + if (isDev) { + menu = new Menu(); + + const mainItem = new MenuItem({ + label: "Toggle Dev Tools", + click: () => win.toggleDevTools(), + accelerator: "F12", + }); + menu.append(mainItem); + + const reloadItem = new MenuItem({ + label: "Restart", + click: () => win.reload(), + accelerator: "F5", + }); + menu.append(reloadItem); + + const fullscreenItem = new MenuItem({ + label: "Fullscreen", + click: () => win.setFullScreen(!win.isFullScreen()), + accelerator: "F11", + }); + menu.append(fullscreenItem); + + Menu.setApplicationMenu(menu); + } else { + Menu.setApplicationMenu(null); + } + + win.once("ready-to-show", () => { + win.show(); + win.focus(); + }); +} + +if (!app.requestSingleInstanceLock()) { + app.exit(0); +} else { + app.on("second-instance", (event, commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus + if (win) { + if (win.isMinimized()) { + win.restore(); + } + win.focus(); + } + }); +} + +app.on("ready", createWindow); + +app.on("window-all-closed", () => { + console.log("All windows closed"); + app.quit(); +}); + +ipcMain.on("set-fullscreen", (event, flag) => { + win.setFullScreen(flag); +}); + +ipcMain.on("exit-app", (event, flag) => { + win.close(); + app.quit(); +}); + +function performFsJob(job) { + const fname = path.join(storePath, job.filename); + + switch (job.type) { + case "read": { + if (!fs.existsSync(fname)) { + return { + // Special FILE_NOT_FOUND error code + error: "file_not_found", + }; + } + + let contents = ""; + try { + contents = fs.readFileSync(fname, { encoding: "utf8" }); + } catch (ex) { + return { + error: ex, + }; + } + + return { + success: true, + data: contents, + }; + } + case "write": { + try { + fs.writeFileSync(fname, job.contents); + } catch (ex) { + return { + error: ex, + }; + } + + return { + success: true, + data: job.contents, + }; + } + + case "delete": { + try { + fs.unlinkSync(fname); + } catch (ex) { + return { + error: ex, + }; + } + + return { + success: true, + data: null, + }; + } + + default: + throw new Error("Unkown fs job: " + job.type); + } +} + +ipcMain.on("fs-job", (event, arg) => { + const result = performFsJob(arg); + event.reply("fs-response", { id: arg.id, result }); +}); + +ipcMain.on("fs-sync-job", (event, arg) => { + const result = performFsJob(arg); + event.returnValue = result; +}); diff --git a/electron/package.json b/electron/package.json new file mode 100644 index 00000000..a67249a0 --- /dev/null +++ b/electron/package.json @@ -0,0 +1,16 @@ +{ + "name": "electron", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "private": true, + "scripts": { + "startDev": "electron --disable-direct-composition --in-process-gpu . --dev --local", + "startDevGpu": "electron --enable-gpu-rasterization --enable-accelerated-2d-canvas --num-raster-threads=8 --enable-zero-copy . --dev --local", + "start": "electron --disable-direct-composition --in-process-gpu ." + }, + "devDependencies": { + "electron": "^6.1.12" + }, + "dependencies": {} +} diff --git a/electron/steam_appid.txt b/electron/steam_appid.txt new file mode 100644 index 00000000..b3a09531 --- /dev/null +++ b/electron/steam_appid.txt @@ -0,0 +1 @@ +1134480 \ No newline at end of file diff --git a/electron/yarn.lock b/electron/yarn.lock new file mode 100644 index 00000000..01214c92 --- /dev/null +++ b/electron/yarn.lock @@ -0,0 +1,961 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@^10.12.18": + version "10.17.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944" + integrity sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA== + +ajv@^6.5.5: + version "6.12.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" + integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +concat-stream@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@2.6.9, debug@^2.1.3, debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.0.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +electron-download@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8" + integrity sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg== + dependencies: + debug "^3.0.0" + env-paths "^1.0.0" + fs-extra "^4.0.1" + minimist "^1.2.0" + nugget "^2.0.1" + path-exists "^3.0.0" + rc "^1.2.1" + semver "^5.4.1" + sumchecker "^2.0.2" + +electron@^6.1.12: + version "6.1.12" + resolved "https://registry.yarnpkg.com/electron/-/electron-6.1.12.tgz#a7aee6dfa75b57f32b3645ef8e14dcef6d5f31a9" + integrity sha512-RUPM8xJfTcm53V9EKMBhvpLu1+CQkmuvWDmVCypR5XbUG1OOrOLiKl0CqUZ9+tEDuOmC+DmzmJP2MZXScBU5IA== + dependencies: + "@types/node" "^10.12.18" + electron-download "^4.1.0" + extract-zip "^1.0.3" + +env-paths@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" + integrity sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA= + +error-ex@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extract-zip@^1.0.3: + version "1.6.7" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" + integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= + dependencies: + concat-stream "1.6.2" + debug "2.6.9" + mkdirp "0.5.1" + yauzl "2.4.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= + dependencies: + pend "~1.2.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-extra@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +graceful-fs@^4.1.2: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +graceful-fs@^4.1.6: + version "4.2.2" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" + integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + +inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-finite@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +meow@^3.1.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +nugget@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0" + integrity sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA= + dependencies: + debug "^2.1.3" + minimist "^1.1.0" + pretty-bytes "^1.0.2" + progress-stream "^1.1.0" + request "^2.45.0" + single-line-log "^1.1.2" + throttleit "0.0.2" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-keys@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" + integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pretty-bytes@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" + integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ= + dependencies: + get-stdin "^4.0.1" + meow "^3.1.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress-stream@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" + integrity sha1-LNPP6jO6OonJwSHsM0er6asSX3c= + dependencies: + speedometer "~0.1.2" + through2 "~0.2.3" + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +rc@^1.2.1: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +readable-stream@^2.2.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +request@^2.45.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +resolve@^1.10.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +"semver@2 || 3 || 4 || 5", semver@^5.4.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +signal-exit@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +single-line-log@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364" + integrity sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q= + dependencies: + string-width "^1.0.1" + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +speedometer@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" + integrity sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +sumchecker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e" + integrity sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4= + dependencies: + debug "^2.2.0" + +throttleit@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" + integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8= + +through2@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f" + integrity sha1-6zKE2k6jEbbMis42U3SKUqvyWj8= + dependencies: + readable-stream "~1.1.9" + xtend "~2.1.1" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +xtend@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" + integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os= + dependencies: + object-keys "~0.4.0" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= + dependencies: + fd-slicer "~1.0.1" diff --git a/gulp/.babelrc b/gulp/.babelrc new file mode 100644 index 00000000..a0765e18 --- /dev/null +++ b/gulp/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/gulp/babel-es6.config.js b/gulp/babel-es6.config.js index e687f070..b3f088f9 100644 --- a/gulp/babel-es6.config.js +++ b/gulp/babel-es6.config.js @@ -10,7 +10,6 @@ module.exports = function (api) { loose: true, spec: false, modules: "auto", - // debug: true }, ], ]; diff --git a/gulp/babel.config.js b/gulp/babel.config.js index 0552a94b..44d47d17 100644 --- a/gulp/babel.config.js +++ b/gulp/babel.config.js @@ -4,18 +4,24 @@ module.exports = function (api) { [ "@babel/preset-env", { - targets: "android >= 4.4.4", + targets: "cover 99.5%", useBuiltIns: "usage", corejs: 3, loose: true, spec: false, modules: "auto", - // debug: true }, ], ]; const plugins = [ "closure-elimination", + // var is faster than let and const! + [ + "@babel/plugin-transform-block-scoping", + { + throwIfClosureRequired: false, + }, + ], [ "@babel/plugin-transform-classes", { diff --git a/gulp/ftp.js b/gulp/ftp.js index 2ab905e2..4d328a6e 100644 --- a/gulp/ftp.js +++ b/gulp/ftp.js @@ -6,6 +6,27 @@ const buildUtils = require("./buildutils"); function gulptasksFTP($, gulp, buildFolder) { const commitHash = buildUtils.getRevision(); + const additionalFolder = path.join(__dirname, "additional_build_files"); + + const additionalFiles = [ + path.join(additionalFolder, "*"), + path.join(additionalFolder, "*.*"), + path.join(additionalFolder, ".*"), + ]; + + const credentials = { + staging: { + host: process.env.SHAPEZ_CLI_SERVER_HOST, + user: process.env.SHAPEZ_CLI_STAGING_FTP_USER, + pass: process.env.SHAPEZ_CLI_STAGING_FTP_PW, + }, + prod: { + host: process.env.SHAPEZ_CLI_SERVER_HOST, + user: process.env.SHAPEZ_CLI_LIVE_FTP_USER, + pass: process.env.SHAPEZ_CLI_LIVE_FTP_PW, + }, + }; + // Write the "commit.txt" file gulp.task("ftp.writeVersion", () => { fs.writeFileSync( @@ -22,13 +43,6 @@ function gulptasksFTP($, gulp, buildFolder) { ); }); - // Copies additional files (like .htaccess) which should be deployed when running - // on the ftp server - // gulp.task("ftp.copyServerFiles", () => { - // return gulp.src(["../ftp_upload/*.*", "../ftp_upload/.*", "../ftp_upload/*"]) - // .pipe(gulp.dest(buildFolder)); - // }); - const gameSrcGlobs = [ path.join(buildFolder, "**/*.*"), path.join(buildFolder, "**/.*"), @@ -36,67 +50,43 @@ function gulptasksFTP($, gulp, buildFolder) { path.join(buildFolder, "!**/index.html"), ]; - gulp.task("ftp.upload.staging.game", () => { - return gulp - .src(gameSrcGlobs, { base: buildFolder }) - .pipe( - $.rename(pth => { - pth.dirname = path.join("v", commitHash, pth.dirname); + for (const deployEnv of ["prod", "staging"]) { + const deployCredentials = credentials[deployEnv]; + + gulp.task(`ftp.upload.${deployEnv}.game`, () => { + return gulp + .src(gameSrcGlobs, { base: buildFolder }) + .pipe( + $.rename(pth => { + pth.dirname = path.join("v", commitHash, pth.dirname); + }) + ) + .pipe($.sftp(deployCredentials)); + }); + + gulp.task(`ftp.upload.${deployEnv}.indexHtml`, () => { + return gulp + .src([path.join(buildFolder, "index.html"), path.join(buildFolder, "version.json")], { + base: buildFolder, }) - ) - .pipe( - $.sftp({ - host: process.env.SHAPEZ_CLI_SERVER_HOST, - user: process.env.SHAPEZ_CLI_STAGING_FTP_USER, - pass: process.env.SHAPEZ_CLI_STAGING_FTP_PW, - }) - ); - }); + .pipe($.sftp(deployCredentials)); + }); - gulp.task("ftp.upload.staging.indexHtml", () => { - return gulp.src(path.join(buildFolder, "index.html"), { base: buildFolder }).pipe( - $.sftp({ - host: process.env.SHAPEZ_CLI_SERVER_HOST, - user: process.env.SHAPEZ_CLI_STAGING_FTP_USER, - pass: process.env.SHAPEZ_CLI_STAGING_FTP_PW, - }) - ); - }); + gulp.task(`ftp.upload.${deployEnv}.additionalFiles`, () => { + return gulp + .src(additionalFiles, { base: additionalFolder }) // + .pipe($.sftp(deployCredentials)); + }); - gulp.task("ftp.upload.staging", cb => { - $.sequence("ftp.writeVersion", "ftp.upload.staging.game", "ftp.upload.staging.indexHtml")(cb); - }); - - gulp.task("ftp.upload.prod.game", () => { - return gulp - .src(gameSrcGlobs, { base: buildFolder }) - .pipe( - $.rename(pth => { - pth.dirname = path.join("v", commitHash, pth.dirname); - }) - ) - .pipe( - $.sftp({ - host: process.env.SHAPEZ_CLI_SERVER_HOST, - user: process.env.SHAPEZ_CLI_LIVE_FTP_USER, - pass: process.env.SHAPEZ_CLI_LIVE_FTP_PW, - }) - ); - }); - - gulp.task("ftp.upload.prod.indexHtml", () => { - return gulp.src(path.join(buildFolder, "index.html"), { base: buildFolder }).pipe( - $.sftp({ - host: process.env.SHAPEZ_CLI_SERVER_HOST, - user: process.env.SHAPEZ_CLI_LIVE_FTP_USER, - pass: process.env.SHAPEZ_CLI_LIVE_FTP_PW, - }) - ); - }); - - gulp.task("ftp.upload.prod", cb => { - $.sequence("ftp.writeVersion", "ftp.upload.prod.game", "ftp.upload.prod.indexHtml")(cb); - }); + gulp.task(`ftp.upload.${deployEnv}`, cb => { + $.sequence( + "ftp.writeVersion", + `ftp.upload.${deployEnv}.game`, + `ftp.upload.${deployEnv}.indexHtml`, + `ftp.upload.${deployEnv}.additionalFiles` + )(cb); + }); + } } module.exports = { diff --git a/gulp/gulpfile.js b/gulp/gulpfile.js index 72d5b699..d87ba792 100644 --- a/gulp/gulpfile.js +++ b/gulp/gulpfile.js @@ -1,5 +1,11 @@ /* eslint-disable */ +const nodeVersion = process.versions.node.split(".")[0]; +if (nodeVersion !== "10") { + console.error("This cli requires exactly Node.js 10. You are using Node.js " + nodeVersion); + process.exit(1); +} + require("colors"); const gulp = require("gulp"); @@ -8,6 +14,23 @@ const path = require("path"); const deleteEmpty = require("delete-empty"); const execSync = require("child_process").execSync; +const lfsOutput = execSync("git lfs install", { encoding: "utf-8" }); +if (!lfsOutput.toLowerCase().includes("git lfs initialized")) { + console.error(` + Git LFS is not installed, unable to build. + + To install Git LFS on Linux: + - Arch: + sudo pacman -S git-lfs + - Debian/Ubuntu: + sudo apt install git-lfs + + For other systems, see: + https://github.com/git-lfs/git-lfs/wiki/Installation + `); + process.exit(1); +} + // Load other plugins dynamically const $ = require("gulp-load-plugins")({ scope: ["devDependencies"], @@ -61,6 +84,9 @@ docs.gulptasksDocs($, gulp, buildFolder); const standalone = require("./standalone"); standalone.gulptasksStandalone($, gulp, buildFolder); +const translations = require("./translations"); +translations.gulptasksTranslations($, gulp, buildFolder); + // FIXME // const cordova = require("./cordova"); // cordova.gulptasksCordova($, gulp, buildFolder); @@ -68,9 +94,16 @@ standalone.gulptasksStandalone($, gulp, buildFolder); ///////////////////// BUILD TASKS ///////////////////// // Cleans up everything -gulp.task("utils.cleanup", () => { +gulp.task("utils.cleanBuildFolder", () => { return gulp.src(buildFolder, { read: false }).pipe($.clean({ force: true })); }); +gulp.task("utils.cleanBuildTempFolder", () => { + return gulp + .src(path.join(__dirname, "..", "src", "js", "built-temp"), { read: false }) + .pipe($.clean({ force: true })); +}); + +gulp.task("utils.cleanup", $.sequence("utils.cleanBuildFolder", "utils.cleanBuildTempFolder")); // Requires no uncomitted files gulp.task("utils.requireCleanWorkingTree", cb => { @@ -136,17 +169,16 @@ function serve({ standalone }) { // Watch sound files // gulp.watch(["../res_raw/sounds/**/*.mp3", "../res_raw/sounds/**/*.wav"], ["sounds.dev"]); + // Watch translations + gulp.watch("../translations/**/*.yaml", ["translations.convertToJson"]); + gulp.watch( - ["../res_raw/sounds/ui/*.mp3", "../res_raw/sounds/ui/*.wav"], - $.sequence("sounds.encodeUi", "sounds.copy") - ); - gulp.watch( - ["../res_raw/sounds/game/*.mp3", "../res_raw/sounds/game/*.wav"], - $.sequence("sounds.encodeGame", "sounds.copy") + ["../res_raw/sounds/sfx/*.mp3", "../res_raw/sounds/sfx/*.wav"], + $.sequence("sounds.sfx", "sounds.copy") ); gulp.watch( ["../res_raw/sounds/music/*.mp3", "../res_raw/sounds/music/*.wav"], - $.sequence("sounds.encodeMusic", "sounds.copy") + $.sequence("sounds.music", "sounds.copy") ); // Watch resource files and copy them on change @@ -157,11 +189,15 @@ function serve({ standalone }) { gulp.watch("../res_built/atlas/*.json", ["imgres.atlas"]); // Watch the build folder and reload when anything changed - const extensions = ["html", "js", "png", "jpg", "svg", "mp3", "ico", "woff2"]; + const extensions = ["html", "js", "png", "jpg", "svg", "mp3", "ico", "woff2", "json"]; gulp.watch(extensions.map(ext => path.join(buildFolder, "**", "*." + ext))).on("change", function (e) { return gulp.src(e.path).pipe(browserSync.reload({ stream: true })); }); + gulp.watch("../src/js/built-temp/*.json").on("change", function (e) { + return gulp.src(e.path).pipe(browserSync.reload({ stream: true })); + }); + // Start the webpack watching server (Will never return) if (standalone) { $.sequence("js.standalone-dev.watch")(() => true); @@ -179,7 +215,7 @@ gulp.task("default", ["main.serveDev"]); ///////////////////// RUNNABLE TASKS ///////////////////// // Pre and postbuild -gulp.task("step.baseResources", cb => $.multiProcess(["sounds.fullbuild", "imgres.allOptimized"], cb, false)); +gulp.task("step.baseResources", cb => $.sequence("imgres.allOptimized")(cb)); gulp.task("step.deleteEmpty", cb => { deleteEmpty.sync(buildFolder); cb(); @@ -196,7 +232,7 @@ gulp.task("build.dev", cb => { "sounds.dev", "imgres.copyImageResources", "imgres.copyNonImageResources", - "js.dev", + "translations.fullBuild", "css.dev", "html.dev" )(cb); @@ -210,6 +246,7 @@ gulp.task("build.standalone.dev", cb => { "sounds.dev", "imgres.copyImageResources", "imgres.copyNonImageResources", + "translations.fullBuild", "js.standalone-dev", "css.dev", "html.standalone-dev" @@ -217,7 +254,7 @@ gulp.task("build.standalone.dev", cb => { }); // Builds everything (staging) -gulp.task("step.staging.code", $.sequence("js.staging")); +gulp.task("step.staging.code", $.sequence("sounds.fullbuild", "translations.fullBuild", "js.staging")); gulp.task("step.staging.mainbuild", cb => $.multiProcess(["utils.copyAdditionalBuildFiles", "step.baseResources", "step.staging.code"], cb, false) ); @@ -225,7 +262,7 @@ gulp.task("step.staging.all", $.sequence("step.staging.mainbuild", "css.prod", " gulp.task("build.staging", $.sequence("utils.cleanup", "step.staging.all", "step.postbuild")); // Builds everything (prod) -gulp.task("step.prod.code", $.sequence("js.prod")); +gulp.task("step.prod.code", $.sequence("sounds.fullbuild", "translations.fullBuild", "js.prod")); gulp.task("step.prod.mainbuild", cb => $.multiProcess(["utils.copyAdditionalBuildFiles", "step.baseResources", "step.prod.code"], cb, false) ); @@ -233,7 +270,10 @@ gulp.task("step.prod.all", $.sequence("step.prod.mainbuild", "css.prod", "html.p gulp.task("build.prod", $.sequence("utils.cleanup", "step.prod.all", "step.postbuild")); // Builds everything (standalone-beta) -gulp.task("step.standalone-beta.code", $.sequence("js.standalone-beta")); +gulp.task( + "step.standalone-beta.code", + $.sequence("sounds.fullbuild", "translations.fullBuild", "js.standalone-beta") +); gulp.task("step.standalone-beta.mainbuild", cb => $.multiProcess( ["utils.copyAdditionalBuildFiles", "step.baseResources", "step.standalone-beta.code"], @@ -248,7 +288,10 @@ gulp.task( gulp.task("build.standalone-beta", $.sequence("utils.cleanup", "step.standalone-beta.all", "step.postbuild")); // Builds everything (standalone-prod) -gulp.task("step.standalone-prod.code", $.sequence("js.standalone-prod")); +gulp.task( + "step.standalone-prod.code", + $.sequence("sounds.fullbuild", "translations.fullBuild", "js.standalone-prod") +); gulp.task("step.standalone-prod.mainbuild", cb => $.multiProcess( ["utils.copyAdditionalBuildFiles", "step.baseResources", "step.standalone-prod.code"], @@ -269,6 +312,4 @@ gulp.task( ); gulp.task("main.deploy.prod", $.sequence("utils.requireCleanWorkingTree", "build.prod", "ftp.upload.prod")); gulp.task("main.deploy.all", $.sequence("main.deploy.staging", "main.deploy.prod")); - -// gulp.task("main.standalone.beta", $.sequence("build.standalone-beta", "standalone.package.beta")); gulp.task("main.standalone", $.sequence("build.standalone-prod", "standalone.package.prod")); diff --git a/gulp/html.js b/gulp/html.js index 48c5547a..816b4401 100644 --- a/gulp/html.js +++ b/gulp/html.js @@ -13,15 +13,7 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) { const commitHash = buildUtils.getRevision(); async function buildHtml( apiUrl, - { - analytics = false, - standalone = false, - app = false, - integrity = true, - enableCachebust = true, - gameAnalyticsKey = null, - gameAnalyticsSecret = null, - } + { analytics = false, standalone = false, app = false, integrity = true, enableCachebust = true } ) { function cachebust(url) { if (enableCachebust) { @@ -87,21 +79,6 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) { // document.head.appendChild(logrocketInit); } - if (gameAnalyticsKey && gameAnalyticsSecret) { - const gaLoader = document.createElement("script"); - gaLoader.textContent = ` - window.GameAnalytics=window.GameAnalytics||function(){(GameAnalytics.q=GameAnalytics.q||[]).push(arguments)}; - window.ga_comKey = "${gameAnalyticsKey}"; - window.ga_comToken = "${gameAnalyticsSecret}"; - `; - document.head.appendChild(gaLoader); - - const gaScript = document.createElement("script"); - gaScript.src = "https://download.gameanalytics.com/js/GameAnalytics-4.0.10.min.js"; - gaScript.setAttribute("async", ""); - document.head.appendChild(gaScript); - } - if (app) { // Append cordova link const cdv = document.createElement("script"); @@ -133,7 +110,7 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) { const images = buildUtils.getAllResourceImages(); // Preload essentials - const preloads = ["fonts/LouisGeorgeCafe.woff2"]; + const preloads = ["fonts/GameFont.woff2"]; // for (let i = 0; i < images.length; ++i) { // if (preloads.indexOf(images[i]) < 0) { @@ -180,7 +157,7 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) { font-style: normal; font-weight: normal; font-display: swap; - src: url('${cachebust("res/fonts/LouisGeorgeCafe.woff2")}') format('woff2'); + src: url('${cachebust("res/fonts/GameFont.woff2")}') format('woff2'); } #ll_fp { @@ -305,27 +282,18 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) { analytics: false, integrity: false, enableCachebust: false, - gameAnalyticsKey: "c8d77921633d5c32a7134e5d5cfcdf12", - // Not an actual "secret" since its built into the JS code - gameAnalyticsSecret: "6d23b40a70199bff0e7a7d8a073543772cf07097", }); }); gulp.task("html.staging", () => { return buildHtml("https://api-staging.shapez.io", { analytics: true, - gameAnalyticsKey: "903fa0dd2d2e23b07e66ea96ddc4c10c", - // Not an actual "secret" since its built into the JS code - gameAnalyticsSecret: "9417fc391d7142b9d73a3861ba6046cafa9df6cb", }); }); gulp.task("html.prod", () => { return buildHtml("https://api.shapez.io", { analytics: true, - gameAnalyticsKey: "16c7f9d352e40c92f6a750fc1a4f0443", - // Not an actual "secret" since its built into the JS code - gameAnalyticsSecret: "4202d7adf154c325ff91731e8be6912e6c0d10e5", }); }); @@ -340,7 +308,7 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) { gulp.task("html.standalone-beta", () => { return buildHtml("https://api-staging.shapez.io", { - analytics: true, + analytics: false, standalone: true, enableCachebust: false, }); @@ -348,7 +316,7 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) { gulp.task("html.standalone-prod", () => { return buildHtml("https://api.shapez.io", { - analytics: true, + analytics: false, standalone: true, enableCachebust: false, }); diff --git a/gulp/image-resources.js b/gulp/image-resources.js index 09511bc3..b01ca400 100644 --- a/gulp/image-resources.js +++ b/gulp/image-resources.js @@ -2,7 +2,7 @@ const path = require("path"); // Globs for non-ui resources -const nonImageResourcesGlobs = ["../res/**/*.woff2", "../res/*.ico"]; +const nonImageResourcesGlobs = ["../res/**/*.woff2", "../res/*.ico", "../res/**/*.webm"]; // Globs for ui resources const imageResourcesGlobs = ["../res/**/*.png", "../res/**/*.svg", "../res/**/*.jpg"]; diff --git a/gulp/package.json b/gulp/package.json index 3cb8ae15..c4ab10c4 100644 --- a/gulp/package.json +++ b/gulp/package.json @@ -18,6 +18,7 @@ "@types/filesystem": "^0.0.29", "@types/node": "^12.7.5", "ajv": "^6.10.2", + "audiosprite": "^0.7.2", "babel-loader": "^8.1.0", "browser-sync": "^2.24.6", "circular-dependency-plugin": "^5.0.2", @@ -32,6 +33,7 @@ "fastdom": "^1.0.9", "flatted": "^2.0.1", "fs-extra": "^8.1.0", + "gulp-audiosprite": "^1.1.0", "howler": "^2.1.2", "html-loader": "^0.5.5", "ignore-loader": "^0.1.2", @@ -48,6 +50,7 @@ "strictdom": "^1.0.1", "string-replace-webpack-plugin": "^0.1.3", "terser-webpack-plugin": "^1.1.0", + "through2": "^3.0.1", "uglify-template-string-loader": "^1.1.0", "unused-files-webpack-plugin": "^3.4.0", "webpack": "^4.31.0", @@ -97,6 +100,7 @@ "gulp-sftp": "^0.1.5", "gulp-terser": "^1.2.0", "gulp-webserver": "^0.9.1", + "gulp-yaml": "^2.0.4", "imagemin-mozjpeg": "^8.0.0", "imagemin-pngquant": "^8.0.0", "jimp": "^0.6.1", @@ -110,6 +114,7 @@ "speed-measure-webpack-plugin": "^1.3.1", "strip-json-comments": "^3.0.1", "trim": "^0.0.1", - "webpack-stream": "^5.1.0" + "webpack-stream": "^5.1.0", + "yaml-loader": "^0.6.0" } } diff --git a/gulp/sounds.js b/gulp/sounds.js index 3233162b..042cb5a6 100644 --- a/gulp/sounds.js +++ b/gulp/sounds.js @@ -1,4 +1,5 @@ const path = require("path"); +const audiosprite = require("gulp-audiosprite"); function gulptasksSounds($, gulp, buildFolder) { // Gather some basic infos @@ -9,16 +10,17 @@ function gulptasksSounds($, gulp, buildFolder) { return gulp.src(builtSoundsDir).pipe($.clean({ force: true })); }); - const filters = ["loudnorm", "volume=0.2"]; + const filters = ["volume=0.2"]; const fileCache = new $.cache.Cache({ cacheDirName: "shapezio-precompiled-sounds", }); // Encodes the game music - gulp.task("sounds.encodeMusic", () => { + gulp.task("sounds.music", () => { return gulp .src([path.join(soundsDir, "music", "**", "*.wav"), path.join(soundsDir, "music", "**", "*.mp3")]) + .pipe($.plumber()) .pipe( $.cache( $.fluentFfmpeg("mp3", function (cmd) { @@ -26,8 +28,8 @@ function gulptasksSounds($, gulp, buildFolder) { .audioBitrate(48) .audioChannels(1) .audioFrequency(22050) - .audioCodec("libmp3lame"); - // .audioFilters(["volume=0.25"]) + .audioCodec("libmp3lame") + .audioFilters(["volume=0.3"]); }), { name: "music", @@ -39,67 +41,59 @@ function gulptasksSounds($, gulp, buildFolder) { }); // Encodes the ui sounds - gulp.task("sounds.encodeUi", () => { + gulp.task("sounds.sfxGenerateSprites", () => { return gulp - .src([path.join(soundsDir, "ui", "**", "*.wav"), path.join(soundsDir, "ui", "**", "*.mp3")]) + .src([path.join(soundsDir, "sfx", "**", "*.wav"), path.join(soundsDir, "sfx", "**", "*.mp3")]) + .pipe($.plumber()) .pipe( - $.cache( - $.fluentFfmpeg("mp3", function (cmd) { - return cmd - .audioBitrate(128) - .audioChannels(1) - .audioFrequency(22050) - .audioCodec("libmp3lame") - .audioFilters(filters); - }) - ), - { - name: "uisounds", - fileCache, - } + audiosprite({ + format: "howler", + output: "sfx", + gap: 0.1, + export: "mp3", + }) ) - .pipe(gulp.dest(path.join(builtSoundsDir, "ui"))); + .pipe(gulp.dest(path.join(builtSoundsDir))); + }); + gulp.task("sounds.sfxOptimize", () => { + return gulp + .src([path.join(builtSoundsDir, "sfx.mp3")]) + .pipe($.plumber()) + .pipe( + $.fluentFfmpeg("mp3", function (cmd) { + return cmd + .audioBitrate(128) + .audioChannels(1) + .audioFrequency(22050) + .audioCodec("libmp3lame") + .audioFilters(filters); + }) + ) + .pipe(gulp.dest(path.join(builtSoundsDir))); + }); + gulp.task("sounds.sfxCopyAtlas", () => { + return gulp + .src([path.join(builtSoundsDir, "sfx.json")]) + .pipe(gulp.dest(path.join(__dirname, "..", "src", "js", "built-temp"))); }); - // Encodes the game sounds - gulp.task("sounds.encodeGame", () => { - return gulp - .src([path.join(soundsDir, "game", "**", "*.wav"), path.join(soundsDir, "game", "**", "*.mp3")]) - .pipe( - $.cache( - $.fluentFfmpeg("mp3", function (cmd) { - return cmd - .audioBitrate(128) - .audioChannels(1) - .audioFrequency(22050) - .audioCodec("libmp3lame") - .audioFilters(filters); - }), - { - nane: "gamesounds", - fileCache, - } - ) - ) - .pipe(gulp.dest(path.join(builtSoundsDir, "game"))); - }); + gulp.task( + "sounds.sfx", + $.sequence("sounds.sfxGenerateSprites", "sounds.sfxOptimize", "sounds.sfxCopyAtlas") + ); gulp.task("sounds.copy", () => { return gulp .src(path.join(builtSoundsDir, "**", "*.mp3")) + .pipe($.plumber()) .pipe($.cached("sounds.copy")) .pipe(gulp.dest(path.join(buildFolder, "res", "sounds"))); }); - gulp.task("sounds.buildall", cb => - $.multiProcess(["sounds.encodeMusic", "sounds.encodeUi", "sounds.encodeGame"], cb, true) - ); + gulp.task("sounds.buildall", cb => $.multiProcess(["sounds.music", "sounds.sfx"], cb, true)); gulp.task("sounds.fullbuild", cb => $.sequence("sounds.clear", "sounds.buildall", "sounds.copy")(cb)); - - gulp.task("sounds.dev", cb => { - return $.sequence("sounds.buildall", "sounds.copy")(cb); - }); + gulp.task("sounds.dev", cb => $.sequence("sounds.buildall", "sounds.copy")(cb)); } module.exports = { diff --git a/gulp/standalone.js b/gulp/standalone.js index 26bc0043..00d2b685 100644 --- a/gulp/standalone.js +++ b/gulp/standalone.js @@ -33,7 +33,7 @@ function gulptasksStandalone($, gulp, buildFolder) { JSON.stringify( { devDependencies: { - electron: "6.0.10", + electron: "6.1.12", }, }, null, @@ -87,7 +87,7 @@ function gulptasksStandalone($, gulp, buildFolder) { }); gulp.task("standalone.prepare.copyGamefiles", () => { - return gulp.src("../../www/**/*.*", { base: "../../www" }).pipe(gulp.dest(tempDestBuildDir)); + return gulp.src("../build/**/*.*", { base: "../build" }).pipe(gulp.dest(tempDestBuildDir)); }); gulp.task("standalone.killRunningInstances", () => { @@ -118,18 +118,9 @@ function gulptasksStandalone($, gulp, buildFolder) { * @param {boolean=} isRelease */ function packageStandalone(platform, arch, cb, isRelease = false) { - const libDirName = (platform === "win32" ? "win" : platform) + (arch === "x64" ? "64" : "32"); - - const libDir = path.join(electronBaseDir, "lib", libDirName); - if (!fs.existsSync(libDir)) { - console.error("FATAL ERROR: LIB DIR does not exist:", libDir); - cb(); - return; - } - packager({ dir: tempDestBuildDir, - appCopyright: "Tobias Springer IT Solutions", + appCopyright: "Tobias Springer", appVersion: buildutils.getVersion(), buildVersion: "1.0.0", arch, @@ -137,7 +128,7 @@ function gulptasksStandalone($, gulp, buildFolder) { asar: true, executableName: "shapezio", icon: path.join(electronBaseDir, "favicon"), - name: "Shapez.io Standalone", + name: "shapez.io-standalone", out: tempDestDir, overwrite: true, appBundleId: "io.shapez.standalone", @@ -151,14 +142,7 @@ function gulptasksStandalone($, gulp, buildFolder) { return; } - console.log("Copying lib files to", appPath); - const libFiles = $.glob.sync(path.join("**", "*.+(dylib|so|dll|lib)"), { cwd: libDir }); - libFiles.forEach(f => { - console.log(" -> Copying", f); - fs.copyFileSync(path.join(libDir, f), path.join(appPath, f)); - }); - - const playablePath = appPath + "_PLAYABLE"; + const playablePath = appPath + "_playable"; fse.copySync(appPath, playablePath); fs.writeFileSync(path.join(playablePath, "steam_appid.txt"), "1134480"); fs.writeFileSync( @@ -180,20 +164,6 @@ function gulptasksStandalone($, gulp, buildFolder) { ); } - // gulp.task("standalone.package.beta.win64", (cb) => packageStandalone("win32", "x64", cb)); - // gulp.task("standalone.package.beta.win32", (cb) => packageStandalone("win32", "ia32", cb)); - // gulp.task("standalone.package.beta.linux64", (cb) => packageStandalone("linux", "x64", cb)); - // gulp.task("standalone.package.beta.linux32", (cb) => packageStandalone("linux", "ia32", cb)); - // gulp.task("standalone.package.beta.darwin64", (cb) => packageStandalone("darwin", "x64", cb)); - - // gulp.task("standalone.package.beta", $.sequence("standalone.prepare", [ - // "standalone.package.beta.win64", - // "standalone.package.beta.win32", - // "standalone.package.beta.linux64", - // "standalone.package.beta.linux32", - // "standalone.package.beta.darwin64" - // ])); - gulp.task("standalone.package.prod.win64", cb => packageStandalone("win32", "x64", cb, true)); gulp.task("standalone.package.prod.win32", cb => packageStandalone("win32", "ia32", cb, true)); gulp.task("standalone.package.prod.linux64", cb => packageStandalone("linux", "x64", cb, true)); diff --git a/gulp/translations.js b/gulp/translations.js new file mode 100644 index 00000000..98f0ce95 --- /dev/null +++ b/gulp/translations.js @@ -0,0 +1,22 @@ +const path = require("path"); + +const yaml = require("gulp-yaml"); + +const translationsSourceDir = path.join(__dirname, "..", "translations"); +const translationsJsonDir = path.join(__dirname, "..", "src", "js", "built-temp"); + +function gulptasksTranslations($, gulp, buildFolder) { + gulp.task("translations.convertToJson", () => { + return gulp + .src(path.join(translationsSourceDir, "*.yaml")) + .pipe($.plumber()) + .pipe(yaml({ space: 2, safe: true })) + .pipe(gulp.dest(translationsJsonDir)); + }); + + gulp.task("translations.fullBuild", $.sequence("translations.convertToJson")); +} + +module.exports = { + gulptasksTranslations, +}; diff --git a/gulp/webpack.config.js b/gulp/webpack.config.js index 31d2d7f5..026ec55e 100644 --- a/gulp/webpack.config.js +++ b/gulp/webpack.config.js @@ -1,3 +1,5 @@ +// @ts-nocheck + const path = require("path"); const webpack = require("webpack"); const utils = require("./buildutils"); @@ -106,6 +108,11 @@ module.exports = ({ watch = false, standalone = false }) => { }, }, }, + { + test: /\.ya?ml$/, + type: "json", // Required by Webpack v4 + use: "yaml-loader", + }, ], }, output: { diff --git a/gulp/webpack.production.config.js b/gulp/webpack.production.config.js index 420d4aba..c8dce19d 100644 --- a/gulp/webpack.production.config.js +++ b/gulp/webpack.production.config.js @@ -1,3 +1,5 @@ +// @ts-nocheck + const path = require("path"); const webpack = require("webpack"); const utils = require("./buildutils"); @@ -59,7 +61,8 @@ module.exports = ({ // Display bailout reasons optimizationBailout: true, }, - devtool: "source-map", + // devtool: "source-map", + devtool: false, resolve: { alias: { "global-compression": path.resolve(__dirname, "..", "src", "js", "core", "lzstring.js"), @@ -83,7 +86,7 @@ module.exports = ({ minimizer: [ new TerserPlugin({ parallel: true, - sourceMap: true, + sourceMap: false, cache: false, terserOptions: { ecma: es6 ? 6 : 5, @@ -176,6 +179,10 @@ module.exports = ({ patterns: ["../src/js/**/*.js"], }), + // new webpack.SourceMapDevToolPlugin({ + // filename: "[name].map", + // publicPath: "/v/" + utils.getRevision() + "/", + // }), // new ReplaceCompressBlocks() // new webpack.optimize.ModuleConcatenationPlugin() // new WebpackDeepScopeAnalysisPlugin() @@ -230,7 +237,7 @@ module.exports = ({ { pattern: /globalConfig\.halfTileSize/g, replacement: () => "16" }, { pattern: /globalConfig\.beltSpeedItemsPerSecond/g, - replacement: () => "1.0", + replacement: () => "2.0", }, { pattern: /globalConfig\.itemSpacingOnBelts/g, replacement: () => "0.8" }, { pattern: /globalConfig\.debug/g, replacement: () => "''" }, @@ -240,18 +247,33 @@ module.exports = ({ }, { test: /\.worker\.js$/, - use: { - loader: "worker-loader", - options: { - fallback: false, - inline: true, + use: [ + { + loader: "worker-loader", + options: { + fallback: false, + inline: true, + }, }, - }, + { + loader: "babel-loader?cacheDirectory", + options: { + configFile: require.resolve( + es6 ? "./babel-es6.config.js" : "./babel.config.js" + ), + }, + }, + ], }, { test: /\.md$/, use: ["html-loader", "markdown-loader"], }, + { + test: /\.ya?ml$/, + type: "json", // Required by Webpack v4 + use: "yaml-loader", + }, ], }, }; diff --git a/gulp/yarn.lock b/gulp/yarn.lock index 702d54b9..7389e868 100644 --- a/gulp/yarn.lock +++ b/gulp/yarn.lock @@ -1854,6 +1854,11 @@ async@~0.9.0: resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= +async@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + integrity sha1-+PwEyjoTeErenhZBr5hXjPvWR6k= + async@~2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" @@ -1871,6 +1876,18 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +audiosprite@*, audiosprite@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/audiosprite/-/audiosprite-0.7.2.tgz#ac431a6c30c127bbb6ed743e5d178862ddf9e31e" + integrity sha512-9Z6UwUuv4To5nUQNRIw5/Q3qA7HYm0ANzoW5EDGPEsU2oIRVgmIlLlm9YZfpPKoeUxt54vMStl2/762189VmJw== + dependencies: + async "~0.9.0" + glob "^6.0.4" + mkdirp "^0.5.0" + optimist "~0.6.1" + underscore "~1.8.3" + winston "~1.0.0" + author-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/author-regex/-/author-regex-1.0.0.tgz#d08885be6b9bbf9439fe087c76287245f0a81450" @@ -2477,6 +2494,13 @@ buffer@^5.2.0, buffer@^5.2.1: base64-js "^1.0.2" ieee754 "^1.1.4" +bufferstreams@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bufferstreams/-/bufferstreams-2.0.1.tgz#441b267c2fc3fee02bb1d929289da113903bd5ef" + integrity sha512-ZswyIoBfFb3cVDsnZLLj2IDJ/0ppYdil/v2EGlZXvoefO689FokEmFEldhN5dV7R2QBxFneqTJOMIpfqhj+n0g== + dependencies: + readable-stream "^2.3.6" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -3058,7 +3082,7 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" -colors@1.0.3: +colors@1.0.3, colors@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= @@ -3654,6 +3678,11 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI= + cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -5031,6 +5060,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +eyes@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= + fancy-log@^1.1.0, fancy-log@^1.2.0, fancy-log@^1.3.2, fancy-log@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" @@ -5830,6 +5864,17 @@ glob@^5.0.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" @@ -6073,6 +6118,15 @@ graceful-fs@~1.2.0: resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= +gulp-audiosprite@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/gulp-audiosprite/-/gulp-audiosprite-1.1.0.tgz#1762d7fb9a669f372b33c1511e3402d79b624892" + integrity sha512-CwSfZjmNPlTyzcAFaE8RiKzh1dQFDLPPZMHshKwvGqNeTB86s30K8hMXGrrjFqHNF9xb0SUnXfbYT32MO4aNog== + dependencies: + audiosprite "*" + through2 "*" + vinyl "*" + gulp-cache@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/gulp-cache/-/gulp-cache-1.1.3.tgz#7c427670aad4d25364c3cc9c53e492b348190d27" @@ -6452,6 +6506,18 @@ gulp-webserver@^0.9.1: tiny-lr "0.1.4" watch "^0.11.0" +gulp-yaml@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/gulp-yaml/-/gulp-yaml-2.0.4.tgz#86569e2becc9f5dfc95dc92db5a71a237f4b6ab4" + integrity sha512-S/9Ib8PO+jGkCvWDwBUkmFkeW7QM0pp4PO8NNrMEfWo5Sk30P+KqpyXc4055L/vOX326T/b9MhM4nw5EenyX9g== + dependencies: + bufferstreams "^2.0.1" + js-yaml "^3.13.1" + object-assign "^4.1.1" + plugin-error "^1.0.1" + replace-ext "^1.0.0" + through2 "^3.0.0" + gulp@^3.9.1: version "3.9.1" resolved "https://registry.yarnpkg.com/gulp/-/gulp-3.9.1.tgz#571ce45928dd40af6514fc4011866016c13845b4" @@ -7608,7 +7674,7 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -isstream@~0.1.2: +isstream@0.1.x, isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= @@ -8831,6 +8897,11 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + minipass@^2.2.1, minipass@^2.6.0, minipass@^2.6.4: version "2.7.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.7.0.tgz#c01093a82287c8331f08f1075499fef124888796" @@ -9504,6 +9575,14 @@ opn@5.3.0: dependencies: is-wsl "^1.1.0" +optimist@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + optionator@^0.8.1, optionator@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" @@ -10050,6 +10129,11 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" +pkginfo@0.3.x: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" + integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE= + plist@^3.0.0, plist@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.1.tgz#a9b931d17c304e8912ef0ba3bdd6182baf2e1f8c" @@ -12311,6 +12395,11 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + stat-mode@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-0.2.2.tgz#e6c80b623123d7d80cf132ce538f346289072502" @@ -12873,6 +12962,13 @@ through2-filter@^3.0.0: through2 "~2.0.0" xtend "~4.0.0" +through2@*, through2@3.0.1, through2@^3.0.0, through2@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" + integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww== + dependencies: + readable-stream "2 || 3" + through2@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9" @@ -12889,13 +12985,6 @@ through2@2.0.3: readable-stream "^2.1.5" xtend "~4.0.1" -through2@3.0.1, through2@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" - integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww== - dependencies: - readable-stream "2 || 3" - through2@^0.4.1, through2@~0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b" @@ -13260,6 +13349,11 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= +underscore@~1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -13625,6 +13719,18 @@ vinyl-sourcemaps-apply@^0.2.0, vinyl-sourcemaps-apply@^0.2.1: dependencies: source-map "^0.5.1" +vinyl@*, vinyl@^2.1.0, vinyl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" + integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg== + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" + vinyl@^0.2.1, vinyl@~0.2.2: version "0.2.3" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.2.3.tgz#bca938209582ec5a49ad538a00fa1f125e513252" @@ -13658,18 +13764,6 @@ vinyl@^1.0.0: clone-stats "^0.0.1" replace-ext "0.0.1" -vinyl@^2.1.0, vinyl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" - integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg== - dependencies: - clone "^2.1.1" - clone-buffer "^1.0.0" - clone-stats "^1.0.0" - cloneable-readable "^1.0.0" - remove-trailing-separator "^1.0.1" - replace-ext "^1.0.0" - vm-browserify@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" @@ -13891,6 +13985,24 @@ window-size@^0.2.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" integrity sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU= +winston@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/winston/-/winston-1.0.2.tgz#351c58e2323f8a4ca29a45195aa9aa3b4c35d76f" + integrity sha1-NRxY4jI/ikyimkUZWqmqO0w1128= + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + pkginfo "0.3.x" + stack-trace "0.0.x" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -14068,6 +14180,19 @@ yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== +yaml-loader@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/yaml-loader/-/yaml-loader-0.6.0.tgz#fe1c48b9f4803dace55a59a1474e790ba6ab1b48" + integrity sha512-1bNiLelumURyj+zvVHOv8Y3dpCri0F2S+DCcmps0pA1zWRLjS+FhZQg4o3aUUDYESh73+pKZNI18bj7stpReow== + dependencies: + loader-utils "^1.4.0" + yaml "^1.8.3" + +yaml@^1.8.3: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== + yargs-parser@^13.0.0, yargs-parser@^13.1.0: version "13.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" diff --git a/package.json b/package.json index b978337a..2e90c019 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "private": true, "scripts": { "dev": "./gulp/gulp main.serveDev", - "prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*" + "tslint": "cd src/js && tsc", + "prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*", + "publishOnItch": "butler push tmp_standalone_files/shapez.io-standalone-win32-x64 tobspr/shapezio:windows --userversion-file version" }, "dependencies": { "@babel/core": "^7.5.4", diff --git a/res/bg_render.webm b/res/bg_render.webm new file mode 100644 index 00000000..bda0db6b --- /dev/null +++ b/res/bg_render.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c78021dc8093dd92987b7117541e4a378d16bf89c489b7376a44b92f905da878 +size 864380 diff --git a/res/fonts/GameFont.woff2 b/res/fonts/GameFont.woff2 new file mode 100644 index 00000000..b8ca0868 Binary files /dev/null and b/res/fonts/GameFont.woff2 differ diff --git a/res/fonts/LouisGeorgeCafe.woff2 b/res/fonts/LouisGeorgeCafe.woff2 deleted file mode 100644 index 7e4ebe03..00000000 Binary files a/res/fonts/LouisGeorgeCafe.woff2 and /dev/null differ diff --git a/res/logo.png b/res/logo.png index 4edea174..75dd3a35 100644 Binary files a/res/logo.png and b/res/logo.png differ diff --git a/res/ui/building_icons/belt.png b/res/ui/building_icons/belt.png new file mode 100644 index 00000000..f9ed2542 Binary files /dev/null and b/res/ui/building_icons/belt.png differ diff --git a/res/ui/building_icons/cutter.png b/res/ui/building_icons/cutter.png new file mode 100644 index 00000000..ab69cfbd Binary files /dev/null and b/res/ui/building_icons/cutter.png differ diff --git a/res/ui/building_icons/miner.png b/res/ui/building_icons/miner.png new file mode 100644 index 00000000..6bf727ec Binary files /dev/null and b/res/ui/building_icons/miner.png differ diff --git a/res/ui/building_icons/mixer.png b/res/ui/building_icons/mixer.png new file mode 100644 index 00000000..62e15780 Binary files /dev/null and b/res/ui/building_icons/mixer.png differ diff --git a/res/ui/building_icons/painter.png b/res/ui/building_icons/painter.png new file mode 100644 index 00000000..60a1c784 Binary files /dev/null and b/res/ui/building_icons/painter.png differ diff --git a/res/ui/building_icons/rotater.png b/res/ui/building_icons/rotater.png new file mode 100644 index 00000000..fe0b2186 Binary files /dev/null and b/res/ui/building_icons/rotater.png differ diff --git a/res/ui/building_icons/splitter.png b/res/ui/building_icons/splitter.png new file mode 100644 index 00000000..29ea7123 Binary files /dev/null and b/res/ui/building_icons/splitter.png differ diff --git a/res/ui/building_icons/stacker.png b/res/ui/building_icons/stacker.png new file mode 100644 index 00000000..3098ae3e Binary files /dev/null and b/res/ui/building_icons/stacker.png differ diff --git a/res/ui/building_icons/trash.png b/res/ui/building_icons/trash.png new file mode 100644 index 00000000..8f094f2e Binary files /dev/null and b/res/ui/building_icons/trash.png differ diff --git a/res/ui/building_icons/underground_belt.png b/res/ui/building_icons/underground_belt.png new file mode 100644 index 00000000..240e08a6 Binary files /dev/null and b/res/ui/building_icons/underground_belt.png differ diff --git a/res/ui/building_tutorials/belt.png b/res/ui/building_tutorials/belt.png index d36cc813..58209c6f 100644 Binary files a/res/ui/building_tutorials/belt.png and b/res/ui/building_tutorials/belt.png differ diff --git a/res/ui/building_tutorials/cutter-quad.png b/res/ui/building_tutorials/cutter-quad.png new file mode 100644 index 00000000..22137353 Binary files /dev/null and b/res/ui/building_tutorials/cutter-quad.png differ diff --git a/res/ui/building_tutorials/cutter.png b/res/ui/building_tutorials/cutter.png index 6b9daca8..cbbb2fac 100644 Binary files a/res/ui/building_tutorials/cutter.png and b/res/ui/building_tutorials/cutter.png differ diff --git a/res/ui/building_tutorials/miner-chainable.png b/res/ui/building_tutorials/miner-chainable.png new file mode 100644 index 00000000..565abe75 Binary files /dev/null and b/res/ui/building_tutorials/miner-chainable.png differ diff --git a/res/ui/building_tutorials/miner.png b/res/ui/building_tutorials/miner.png index 2bc95bc3..8f8978e8 100644 Binary files a/res/ui/building_tutorials/miner.png and b/res/ui/building_tutorials/miner.png differ diff --git a/res/ui/building_tutorials/painter-double.png b/res/ui/building_tutorials/painter-double.png new file mode 100644 index 00000000..3833c53c Binary files /dev/null and b/res/ui/building_tutorials/painter-double.png differ diff --git a/res/ui/building_tutorials/painter-quad.png b/res/ui/building_tutorials/painter-quad.png new file mode 100644 index 00000000..57536440 Binary files /dev/null and b/res/ui/building_tutorials/painter-quad.png differ diff --git a/res/ui/building_tutorials/painter.png b/res/ui/building_tutorials/painter.png index 16fb4850..8791082f 100644 Binary files a/res/ui/building_tutorials/painter.png and b/res/ui/building_tutorials/painter.png differ diff --git a/res/ui/building_tutorials/rotater-ccw.png b/res/ui/building_tutorials/rotater-ccw.png new file mode 100644 index 00000000..a76b7179 Binary files /dev/null and b/res/ui/building_tutorials/rotater-ccw.png differ diff --git a/res/ui/building_tutorials/rotater.png b/res/ui/building_tutorials/rotater.png index ed1ef55e..c8ecb8b0 100644 Binary files a/res/ui/building_tutorials/rotater.png and b/res/ui/building_tutorials/rotater.png differ diff --git a/res/ui/building_tutorials/splitter-compact-inverse.png b/res/ui/building_tutorials/splitter-compact-inverse.png new file mode 100644 index 00000000..38965f4d Binary files /dev/null and b/res/ui/building_tutorials/splitter-compact-inverse.png differ diff --git a/res/ui/building_tutorials/splitter-compact.png b/res/ui/building_tutorials/splitter-compact.png new file mode 100644 index 00000000..17367926 Binary files /dev/null and b/res/ui/building_tutorials/splitter-compact.png differ diff --git a/res/ui/building_tutorials/splitter.png b/res/ui/building_tutorials/splitter.png index ece30fb5..25f54f33 100644 Binary files a/res/ui/building_tutorials/splitter.png and b/res/ui/building_tutorials/splitter.png differ diff --git a/res/ui/building_tutorials/trash-storage.png b/res/ui/building_tutorials/trash-storage.png new file mode 100644 index 00000000..50c77719 Binary files /dev/null and b/res/ui/building_tutorials/trash-storage.png differ diff --git a/res/ui/building_tutorials/trash.png b/res/ui/building_tutorials/trash.png index b33d1980..3d69bb53 100644 Binary files a/res/ui/building_tutorials/trash.png and b/res/ui/building_tutorials/trash.png differ diff --git a/res/ui/building_tutorials/underground_belt-tier2.png b/res/ui/building_tutorials/underground_belt-tier2.png new file mode 100644 index 00000000..476ad4da Binary files /dev/null and b/res/ui/building_tutorials/underground_belt-tier2.png differ diff --git a/res/ui/building_tutorials/underground_belt.png b/res/ui/building_tutorials/underground_belt.png index 693882e4..ed2916f6 100644 Binary files a/res/ui/building_tutorials/underground_belt.png and b/res/ui/building_tutorials/underground_belt.png differ diff --git a/res/ui/demo_badge.png b/res/ui/demo_badge.png new file mode 100644 index 00000000..6c80db7d Binary files /dev/null and b/res/ui/demo_badge.png differ diff --git a/res/ui/get_on_itch_io.svg b/res/ui/get_on_itch_io.svg new file mode 100644 index 00000000..f6dde21e --- /dev/null +++ b/res/ui/get_on_itch_io.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ui/get_on_steam.png b/res/ui/get_on_steam.png new file mode 100644 index 00000000..f8503229 Binary files /dev/null and b/res/ui/get_on_steam.png differ diff --git a/res/ui/icons/close.png b/res/ui/icons/close.png index a03a2976..e4108d12 100644 Binary files a/res/ui/icons/close.png and b/res/ui/icons/close.png differ diff --git a/res/ui/icons/current_goal_marker.png b/res/ui/icons/current_goal_marker.png new file mode 100644 index 00000000..0ebd6c0d Binary files /dev/null and b/res/ui/icons/current_goal_marker.png differ diff --git a/res/ui/icons/delete.png b/res/ui/icons/delete.png new file mode 100644 index 00000000..db1c86f1 Binary files /dev/null and b/res/ui/icons/delete.png differ diff --git a/res/ui/icons/display_icons.png b/res/ui/icons/display_icons.png new file mode 100644 index 00000000..68fd2838 Binary files /dev/null and b/res/ui/icons/display_icons.png differ diff --git a/res/ui/icons/display_list.png b/res/ui/icons/display_list.png new file mode 100644 index 00000000..665f5b8e Binary files /dev/null and b/res/ui/icons/display_list.png differ diff --git a/res/ui/icons/download.png b/res/ui/icons/download.png new file mode 100644 index 00000000..bb28bf81 Binary files /dev/null and b/res/ui/icons/download.png differ diff --git a/res/ui/icons/edit_key.png b/res/ui/icons/edit_key.png new file mode 100644 index 00000000..544342d5 Binary files /dev/null and b/res/ui/icons/edit_key.png differ diff --git a/res/ui/icons/enum_selector.png b/res/ui/icons/enum_selector.png new file mode 100644 index 00000000..0d54ca59 Binary files /dev/null and b/res/ui/icons/enum_selector.png differ diff --git a/res/ui/icons/help.png b/res/ui/icons/help.png new file mode 100644 index 00000000..07ec30d1 Binary files /dev/null and b/res/ui/icons/help.png differ diff --git a/res/ui/icons/main_menu_exit.png b/res/ui/icons/main_menu_exit.png new file mode 100644 index 00000000..c1d3fb46 Binary files /dev/null and b/res/ui/icons/main_menu_exit.png differ diff --git a/res/ui/icons/main_menu_settings.png b/res/ui/icons/main_menu_settings.png new file mode 100644 index 00000000..8ce3aff4 Binary files /dev/null and b/res/ui/icons/main_menu_settings.png differ diff --git a/res/ui/icons/music_off.png b/res/ui/icons/music_off.png new file mode 100644 index 00000000..efe37dc4 Binary files /dev/null and b/res/ui/icons/music_off.png differ diff --git a/res/ui/icons/music_on.png b/res/ui/icons/music_on.png new file mode 100644 index 00000000..e1a679ca Binary files /dev/null and b/res/ui/icons/music_on.png differ diff --git a/res/ui/icons/notification_saved.png b/res/ui/icons/notification_saved.png new file mode 100644 index 00000000..de7096a4 Binary files /dev/null and b/res/ui/icons/notification_saved.png differ diff --git a/res/ui/icons/notification_success.png b/res/ui/icons/notification_success.png new file mode 100644 index 00000000..123619d5 Binary files /dev/null and b/res/ui/icons/notification_success.png differ diff --git a/res/ui/icons/notification_upgrade.png b/res/ui/icons/notification_upgrade.png new file mode 100644 index 00000000..9d98cd90 Binary files /dev/null and b/res/ui/icons/notification_upgrade.png differ diff --git a/res/ui/icons/pin.png b/res/ui/icons/pin.png new file mode 100644 index 00000000..004d1e72 Binary files /dev/null and b/res/ui/icons/pin.png differ diff --git a/res/ui/icons/play.png b/res/ui/icons/play.png new file mode 100644 index 00000000..cd7f610c Binary files /dev/null and b/res/ui/icons/play.png differ diff --git a/res/ui/icons/reset_key.png b/res/ui/icons/reset_key.png new file mode 100644 index 00000000..ee457b5d Binary files /dev/null and b/res/ui/icons/reset_key.png differ diff --git a/res/ui/icons/save.png b/res/ui/icons/save.png new file mode 100644 index 00000000..d48274bc Binary files /dev/null and b/res/ui/icons/save.png differ diff --git a/res/ui/icons/settings.png b/res/ui/icons/settings.png new file mode 100644 index 00000000..14a16b06 Binary files /dev/null and b/res/ui/icons/settings.png differ diff --git a/res/ui/icons/shop.png b/res/ui/icons/shop.png index 6e5d9c82..8d065ae1 100644 Binary files a/res/ui/icons/shop.png and b/res/ui/icons/shop.png differ diff --git a/res/ui/icons/sound_off.png b/res/ui/icons/sound_off.png new file mode 100644 index 00000000..d2275d1b Binary files /dev/null and b/res/ui/icons/sound_off.png differ diff --git a/res/ui/icons/sound_on.png b/res/ui/icons/sound_on.png new file mode 100644 index 00000000..ac0f88ce Binary files /dev/null and b/res/ui/icons/sound_on.png differ diff --git a/res/ui/icons/state_back_button.png b/res/ui/icons/state_back_button.png new file mode 100644 index 00000000..78685f5e Binary files /dev/null and b/res/ui/icons/state_back_button.png differ diff --git a/res/ui/icons/statistics.png b/res/ui/icons/statistics.png index f9c548ea..c6b8e68a 100644 Binary files a/res/ui/icons/statistics.png and b/res/ui/icons/statistics.png differ diff --git a/res/ui/icons/unpin.png b/res/ui/icons/unpin.png new file mode 100644 index 00000000..da3492e6 Binary files /dev/null and b/res/ui/icons/unpin.png differ diff --git a/res/ui/loading.svg b/res/ui/loading.svg index e8f0d271..ea7dcbbd 100644 --- a/res/ui/loading.svg +++ b/res/ui/loading.svg @@ -1,6 +1,25 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/ui/main_menu/bg_pattern.png b/res/ui/main_menu/bg_pattern.png new file mode 100644 index 00000000..00291768 Binary files /dev/null and b/res/ui/main_menu/bg_pattern.png differ diff --git a/res/ui/main_menu/discord.png b/res/ui/main_menu/discord.png new file mode 100644 index 00000000..db0e70d5 Binary files /dev/null and b/res/ui/main_menu/discord.png differ diff --git a/res/ui/main_menu/github.png b/res/ui/main_menu/github.png new file mode 100644 index 00000000..ea6ff545 Binary files /dev/null and b/res/ui/main_menu/github.png differ diff --git a/res/ui/menu_bg.noinline.jpg b/res/ui/menu_bg.noinline.jpg deleted file mode 100644 index a9e5c689..00000000 Binary files a/res/ui/menu_bg.noinline.jpg and /dev/null differ diff --git a/res/ui/toolbar_bg.lossless.png b/res/ui/toolbar_bg.lossless.png new file mode 100644 index 00000000..93967a91 Binary files /dev/null and b/res/ui/toolbar_bg.lossless.png differ diff --git a/res/ui/upgrades/belt.png b/res/ui/upgrades/belt.png deleted file mode 100644 index a1a9b716..00000000 Binary files a/res/ui/upgrades/belt.png and /dev/null differ diff --git a/res/ui/upgrades/miner.png b/res/ui/upgrades/miner.png deleted file mode 100644 index 535edc06..00000000 Binary files a/res/ui/upgrades/miner.png and /dev/null differ diff --git a/res/ui/upgrades/painting.png b/res/ui/upgrades/painting.png deleted file mode 100644 index 03749952..00000000 Binary files a/res/ui/upgrades/painting.png and /dev/null differ diff --git a/res/ui/upgrades/processors.png b/res/ui/upgrades/processors.png deleted file mode 100644 index 5d785baa..00000000 Binary files a/res/ui/upgrades/processors.png and /dev/null differ diff --git a/res/ui/vignette.lossless.png b/res/ui/vignette.lossless.png new file mode 100644 index 00000000..b7bb65ac Binary files /dev/null and b/res/ui/vignette.lossless.png differ diff --git a/res_built/.gitignore b/res_built/.gitignore new file mode 100644 index 00000000..060e04d9 --- /dev/null +++ b/res_built/.gitignore @@ -0,0 +1,2 @@ +# Ignore built sounds +sounds diff --git a/res_built/atlas/atlas0_10.json b/res_built/atlas/atlas0_10.json new file mode 100644 index 00000000..15caddac --- /dev/null +++ b/res_built/atlas/atlas0_10.json @@ -0,0 +1,604 @@ +{"frames": { + +"sprites/belt/forward_0.png": +{ + "frame": {"x":160,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/forward_1.png": +{ + "frame": {"x":177,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/forward_2.png": +{ + "frame": {"x":194,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/forward_3.png": +{ + "frame": {"x":211,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/forward_4.png": +{ + "frame": {"x":228,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/forward_5.png": +{ + "frame": {"x":245,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/left_0.png": +{ + "frame": {"x":167,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/left_1.png": +{ + "frame": {"x":184,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/left_2.png": +{ + "frame": {"x":201,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/left_3.png": +{ + "frame": {"x":262,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/left_4.png": +{ + "frame": {"x":279,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/left_5.png": +{ + "frame": {"x":218,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/right_0.png": +{ + "frame": {"x":235,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/right_1.png": +{ + "frame": {"x":296,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/right_2.png": +{ + "frame": {"x":252,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/right_3.png": +{ + "frame": {"x":269,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/right_4.png": +{ + "frame": {"x":313,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/belt/right_5.png": +{ + "frame": {"x":330,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/blueprints/belt_left.png": +{ + "frame": {"x":286,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/blueprints/belt_right.png": +{ + "frame": {"x":347,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/blueprints/belt_top.png": +{ + "frame": {"x":303,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/blueprints/cutter-quad.png": +{ + "frame": {"x":87,"y":82,"w":76,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":76,"h":19}, + "sourceSize": {"w":77,"h":19} +}, +"sprites/blueprints/cutter.png": +{ + "frame": {"x":209,"y":66,"w":36,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":36,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/blueprints/miner-chainable.png": +{ + "frame": {"x":286,"y":43,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/miner.png": +{ + "frame": {"x":249,"y":89,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/mixer.png": +{ + "frame": {"x":205,"y":43,"w":37,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":37,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/blueprints/painter-double.png": +{ + "frame": {"x":3,"y":83,"w":38,"h":38}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":38,"h":38}, + "sourceSize": {"w":38,"h":38} +}, +"sprites/blueprints/painter-quad.png": +{ + "frame": {"x":82,"y":45,"w":77,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":77,"h":19}, + "sourceSize": {"w":77,"h":19} +}, +"sprites/blueprints/painter.png": +{ + "frame": {"x":163,"y":43,"w":38,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":38,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/blueprints/rotater-ccw.png": +{ + "frame": {"x":272,"y":89,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/rotater.png": +{ + "frame": {"x":289,"y":66,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/splitter-compact-inverse.png": +{ + "frame": {"x":309,"y":43,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/splitter-compact.png": +{ + "frame": {"x":322,"y":3,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/splitter.png": +{ + "frame": {"x":246,"y":43,"w":36,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":36,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/blueprints/stacker.png": +{ + "frame": {"x":241,"y":3,"w":37,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":37,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/blueprints/trash-storage.png": +{ + "frame": {"x":82,"y":3,"w":35,"h":38}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":35,"h":38}, + "sourceSize": {"w":38,"h":38} +}, +"sprites/blueprints/trash.png": +{ + "frame": {"x":345,"y":3,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/underground_belt_entry-tier2.png": +{ + "frame": {"x":378,"y":26,"w":19,"h":18}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":1,"w":19,"h":18}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/underground_belt_entry.png": +{ + "frame": {"x":341,"y":89,"w":19,"h":16}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":3,"w":19,"h":16}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/underground_belt_exit-tier2.png": +{ + "frame": {"x":341,"y":109,"w":19,"h":16}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":16}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/blueprints/underground_belt_exit.png": +{ + "frame": {"x":364,"y":70,"w":19,"h":16}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":16}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/belt_left.png": +{ + "frame": {"x":167,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/buildings/belt_right.png": +{ + "frame": {"x":235,"y":112,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/buildings/belt_top.png": +{ + "frame": {"x":160,"y":26,"w":13,"h":13}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, + "sourceSize": {"w":13,"h":13} +}, +"sprites/buildings/cutter-quad.png": +{ + "frame": {"x":87,"y":105,"w":76,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":76,"h":19}, + "sourceSize": {"w":77,"h":19} +}, +"sprites/buildings/cutter.png": +{ + "frame": {"x":282,"y":3,"w":36,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":36,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/buildings/hub.png": +{ + "frame": {"x":3,"y":3,"w":75,"h":76}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":1,"w":75,"h":76}, + "sourceSize": {"w":77,"h":77} +}, +"sprites/buildings/miner-chainable.png": +{ + "frame": {"x":295,"y":89,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/miner.png": +{ + "frame": {"x":312,"y":66,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/mixer.png": +{ + "frame": {"x":167,"y":89,"w":37,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":37,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/buildings/painter-double.png": +{ + "frame": {"x":45,"y":83,"w":38,"h":38}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":38,"h":38}, + "sourceSize": {"w":38,"h":38} +}, +"sprites/buildings/painter-quad.png": +{ + "frame": {"x":160,"y":3,"w":77,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":77,"h":19}, + "sourceSize": {"w":77,"h":19} +}, +"sprites/buildings/painter.png": +{ + "frame": {"x":167,"y":66,"w":38,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":38,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/buildings/rotater-ccw.png": +{ + "frame": {"x":332,"y":43,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/rotater.png": +{ + "frame": {"x":318,"y":89,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/splitter-compact-inverse.png": +{ + "frame": {"x":335,"y":66,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/splitter-compact.png": +{ + "frame": {"x":355,"y":43,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/splitter.png": +{ + "frame": {"x":249,"y":66,"w":36,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":36,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/buildings/stacker.png": +{ + "frame": {"x":208,"y":89,"w":37,"h":19}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":37,"h":19}, + "sourceSize": {"w":38,"h":19} +}, +"sprites/buildings/trash-storage.png": +{ + "frame": {"x":121,"y":3,"w":35,"h":38}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":35,"h":38}, + "sourceSize": {"w":38,"h":38} +}, +"sprites/buildings/trash.png": +{ + "frame": {"x":378,"y":3,"w":19,"h":19}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":19}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/underground_belt_entry-tier2.png": +{ + "frame": {"x":378,"y":48,"w":19,"h":18}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":1,"w":19,"h":18}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/underground_belt_entry.png": +{ + "frame": {"x":387,"y":70,"w":19,"h":16}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":3,"w":19,"h":16}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/underground_belt_exit-tier2.png": +{ + "frame": {"x":364,"y":90,"w":19,"h":16}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":16}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/buildings/underground_belt_exit.png": +{ + "frame": {"x":387,"y":90,"w":19,"h":16}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":16}, + "sourceSize": {"w":19,"h":19} +}, +"sprites/debug/acceptor_slot.png": +{ + "frame": {"x":132,"y":68,"w":6,"h":6}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":6,"h":6}, + "sourceSize": {"w":6,"h":6} +}, +"sprites/debug/ejector_slot.png": +{ + "frame": {"x":142,"y":68,"w":6,"h":6}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":6,"h":6}, + "sourceSize": {"w":6,"h":6} +}, +"sprites/map_overview/belt_forward.png": +{ + "frame": {"x":152,"y":68,"w":3,"h":3}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":3,"h":3}, + "sourceSize": {"w":3,"h":3} +}, +"sprites/map_overview/belt_left.png": +{ + "frame": {"x":152,"y":75,"w":3,"h":3}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":3,"h":3}, + "sourceSize": {"w":3,"h":3} +}, +"sprites/map_overview/belt_right.png": +{ + "frame": {"x":159,"y":68,"w":3,"h":3}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":3,"h":3}, + "sourceSize": {"w":3,"h":3} +}, +"sprites/misc/deletion_marker.png": +{ + "frame": {"x":82,"y":68,"w":10,"h":10}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":10,"h":10}, + "sourceSize": {"w":10,"h":10} +}, +"sprites/misc/slot_bad_arrow.png": +{ + "frame": {"x":82,"y":68,"w":10,"h":10}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":10,"h":10}, + "sourceSize": {"w":10,"h":10} +}, +"sprites/misc/slot_good_arrow.png": +{ + "frame": {"x":96,"y":68,"w":10,"h":10}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":10,"h":10}, + "sourceSize": {"w":10,"h":10} +}, +"sprites/misc/storage_overlay.png": +{ + "frame": {"x":110,"y":68,"w":18,"h":9}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":9}, + "sourceSize": {"w":18,"h":9} +}}, +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "atlas0_10.png", + "format": "RGBA8888", + "size": {"w":409,"h":128}, + "scale": "0.1", + "smartupdate": "$TexturePacker:SmartUpdate:c2a63b817240ea11b013f390be5c690a:0c1b9b304a864f030526d8b9e8ae3352:f159918d23e5952766c6d23ab52278c6$" +} +} diff --git a/res_built/atlas/atlas0_10.png b/res_built/atlas/atlas0_10.png new file mode 100644 index 00000000..c674ee9c Binary files /dev/null and b/res_built/atlas/atlas0_10.png differ diff --git a/res_built/atlas/atlas0_100.json b/res_built/atlas/atlas0_100.json new file mode 100644 index 00000000..4a92f8a0 --- /dev/null +++ b/res_built/atlas/atlas0_100.json @@ -0,0 +1,604 @@ +{"frames": { + +"sprites/belt/forward_0.png": +{ + "frame": {"x":1871,"y":1504,"w":100,"h":126}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":0,"w":100,"h":126}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/forward_1.png": +{ + "frame": {"x":1871,"y":240,"w":100,"h":126}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":0,"w":100,"h":126}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/forward_2.png": +{ + "frame": {"x":1844,"y":394,"w":100,"h":126}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":0,"w":100,"h":126}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/forward_3.png": +{ + "frame": {"x":1871,"y":1634,"w":100,"h":126}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":0,"w":100,"h":126}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/forward_4.png": +{ + "frame": {"x":1433,"y":785,"w":100,"h":126}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":0,"w":100,"h":126}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/forward_5.png": +{ + "frame": {"x":917,"y":1564,"w":100,"h":126}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":0,"w":100,"h":126}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/left_0.png": +{ + "frame": {"x":1021,"y":1563,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/left_1.png": +{ + "frame": {"x":1138,"y":1563,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/left_2.png": +{ + "frame": {"x":1255,"y":1563,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/left_3.png": +{ + "frame": {"x":1372,"y":1562,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/left_4.png": +{ + "frame": {"x":1489,"y":1562,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/left_5.png": +{ + "frame": {"x":1021,"y":1680,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/right_0.png": +{ + "frame": {"x":1138,"y":1680,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/right_1.png": +{ + "frame": {"x":1255,"y":1680,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/right_2.png": +{ + "frame": {"x":1372,"y":1679,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/right_3.png": +{ + "frame": {"x":1489,"y":1679,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/right_4.png": +{ + "frame": {"x":1606,"y":1676,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/belt/right_5.png": +{ + "frame": {"x":1723,"y":1676,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/blueprints/belt_left.png": +{ + "frame": {"x":1873,"y":122,"w":114,"h":114}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":12,"w":114,"h":114}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/blueprints/belt_right.png": +{ + "frame": {"x":1873,"y":3,"w":114,"h":115}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":12,"y":11,"w":114,"h":115}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/blueprints/belt_top.png": +{ + "frame": {"x":1871,"y":1374,"w":102,"h":126}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":12,"y":0,"w":102,"h":126}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/blueprints/cutter-quad.png": +{ + "frame": {"x":735,"y":395,"w":730,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":23,"y":0,"w":730,"h":191}, + "sourceSize": {"w":768,"h":192} +}, +"sprites/blueprints/cutter.png": +{ + "frame": {"x":726,"y":979,"w":341,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":23,"y":0,"w":341,"h":191}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/blueprints/miner-chainable.png": +{ + "frame": {"x":1500,"y":1368,"w":182,"h":190}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":182,"h":190}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/miner.png": +{ + "frame": {"x":1437,"y":590,"w":182,"h":190}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":182,"h":190}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/mixer.png": +{ + "frame": {"x":735,"y":590,"w":347,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":18,"y":0,"w":347,"h":191}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/blueprints/painter-double.png": +{ + "frame": {"x":3,"y":931,"w":384,"h":382}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":384,"h":382}, + "sourceSize": {"w":384,"h":384} +}, +"sprites/blueprints/painter-quad.png": +{ + "frame": {"x":735,"y":3,"w":746,"h":192}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":0,"w":746,"h":192}, + "sourceSize": {"w":768,"h":192} +}, +"sprites/blueprints/painter.png": +{ + "frame": {"x":1485,"y":3,"w":384,"h":192}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":384,"h":192}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/blueprints/rotater-ccw.png": +{ + "frame": {"x":1116,"y":1368,"w":189,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":189,"h":191}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/rotater.png": +{ + "frame": {"x":724,"y":1564,"w":189,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":189,"h":191}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/splitter-compact-inverse.png": +{ + "frame": {"x":1652,"y":394,"w":188,"h":182}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":188,"h":182}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/splitter-compact.png": +{ + "frame": {"x":1623,"y":587,"w":185,"h":182}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":4,"w":185,"h":182}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/splitter.png": +{ + "frame": {"x":1071,"y":979,"w":340,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":23,"y":0,"w":340,"h":191}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/blueprints/stacker.png": +{ + "frame": {"x":1086,"y":590,"w":347,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":18,"y":0,"w":347,"h":191}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/blueprints/trash-storage.png": +{ + "frame": {"x":391,"y":931,"w":331,"h":384}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":29,"y":0,"w":331,"h":384}, + "sourceSize": {"w":384,"h":384} +}, +"sprites/blueprints/trash.png": +{ + "frame": {"x":724,"y":1368,"w":192,"h":192}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":192,"h":192}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/underground_belt_entry-tier2.png": +{ + "frame": {"x":1791,"y":1035,"w":183,"h":166}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":26,"w":183,"h":166}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/underground_belt_entry.png": +{ + "frame": {"x":1812,"y":580,"w":182,"h":148}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":44,"w":182,"h":148}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/underground_belt_exit-tier2.png": +{ + "frame": {"x":1623,"y":773,"w":185,"h":148}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":185,"h":148}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/underground_belt_exit.png": +{ + "frame": {"x":1812,"y":732,"w":182,"h":148}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":182,"h":148}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/belt_left.png": +{ + "frame": {"x":1021,"y":1563,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/buildings/belt_right.png": +{ + "frame": {"x":1138,"y":1680,"w":113,"h":113}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":13,"w":113,"h":113}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/buildings/belt_top.png": +{ + "frame": {"x":1871,"y":1504,"w":100,"h":126}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":0,"w":100,"h":126}, + "sourceSize": {"w":126,"h":126} +}, +"sprites/buildings/cutter-quad.png": +{ + "frame": {"x":3,"y":737,"w":728,"h":190}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":24,"y":0,"w":728,"h":190}, + "sourceSize": {"w":768,"h":192} +}, +"sprites/buildings/cutter.png": +{ + "frame": {"x":726,"y":1174,"w":339,"h":190}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":24,"y":0,"w":339,"h":190}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/buildings/hub.png": +{ + "frame": {"x":3,"y":3,"w":728,"h":730}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":20,"y":22,"w":728,"h":730}, + "sourceSize": {"w":768,"h":768} +}, +"sprites/buildings/miner-chainable.png": +{ + "frame": {"x":1469,"y":395,"w":179,"h":188}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":8,"y":1,"w":179,"h":188}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/miner.png": +{ + "frame": {"x":1415,"y":979,"w":179,"h":189}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":8,"y":0,"w":179,"h":189}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/mixer.png": +{ + "frame": {"x":735,"y":785,"w":345,"h":190}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":19,"y":0,"w":345,"h":190}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/buildings/painter-double.png": +{ + "frame": {"x":3,"y":1317,"w":384,"h":381}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":384,"h":381}, + "sourceSize": {"w":384,"h":384} +}, +"sprites/buildings/painter-quad.png": +{ + "frame": {"x":735,"y":199,"w":744,"h":192}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":8,"y":0,"w":744,"h":192}, + "sourceSize": {"w":768,"h":192} +}, +"sprites/buildings/painter.png": +{ + "frame": {"x":1483,"y":199,"w":384,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":384,"h":191}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/buildings/rotater-ccw.png": +{ + "frame": {"x":1309,"y":1368,"w":187,"h":190}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":187,"h":190}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/rotater.png": +{ + "frame": {"x":1412,"y":1174,"w":187,"h":190}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":187,"h":190}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/splitter-compact-inverse.png": +{ + "frame": {"x":1598,"y":925,"w":187,"h":180}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":5,"w":187,"h":180}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/splitter-compact.png": +{ + "frame": {"x":1603,"y":1109,"w":184,"h":180}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":8,"y":5,"w":184,"h":180}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/splitter.png": +{ + "frame": {"x":1069,"y":1174,"w":339,"h":190}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":24,"y":0,"w":339,"h":190}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/buildings/stacker.png": +{ + "frame": {"x":1084,"y":785,"w":345,"h":190}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":19,"y":0,"w":345,"h":190}, + "sourceSize": {"w":384,"h":192} +}, +"sprites/buildings/trash-storage.png": +{ + "frame": {"x":391,"y":1319,"w":329,"h":384}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":30,"y":0,"w":329,"h":384}, + "sourceSize": {"w":384,"h":384} +}, +"sprites/buildings/trash.png": +{ + "frame": {"x":920,"y":1368,"w":192,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":1,"w":192,"h":191}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/underground_belt_entry-tier2.png": +{ + "frame": {"x":1791,"y":1205,"w":181,"h":165}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":27,"w":181,"h":165}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/underground_belt_entry.png": +{ + "frame": {"x":1686,"y":1374,"w":181,"h":147}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":45,"w":181,"h":147}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/underground_belt_exit-tier2.png": +{ + "frame": {"x":1812,"y":884,"w":182,"h":147}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":0,"w":182,"h":147}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/underground_belt_exit.png": +{ + "frame": {"x":1686,"y":1525,"w":181,"h":147}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":0,"w":181,"h":147}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/debug/acceptor_slot.png": +{ + "frame": {"x":1603,"y":1293,"w":50,"h":64}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":0,"w":50,"h":64}, + "sourceSize": {"w":64,"h":64} +}, +"sprites/debug/ejector_slot.png": +{ + "frame": {"x":1606,"y":1562,"w":50,"h":64}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":0,"w":50,"h":64}, + "sourceSize": {"w":64,"h":64} +}, +"sprites/map_overview/belt_forward.png": +{ + "frame": {"x":354,"y":1702,"w":24,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":0,"w":24,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/map_overview/belt_left.png": +{ + "frame": {"x":1844,"y":524,"w":28,"h":28}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":28,"h":28}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/map_overview/belt_right.png": +{ + "frame": {"x":1433,"y":915,"w":28,"h":28}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":4,"w":28,"h":28}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/misc/deletion_marker.png": +{ + "frame": {"x":268,"y":1702,"w":82,"h":82}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":7,"w":82,"h":82}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/misc/slot_bad_arrow.png": +{ + "frame": {"x":268,"y":1702,"w":82,"h":82}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":7,"w":82,"h":82}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/misc/slot_good_arrow.png": +{ + "frame": {"x":184,"y":1702,"w":80,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":8,"y":0,"w":80,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/misc/storage_overlay.png": +{ + "frame": {"x":3,"y":1702,"w":177,"h":86}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":177,"h":86}, + "sourceSize": {"w":177,"h":86} +}}, +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "atlas0_100.png", + "format": "RGBA8888", + "size": {"w":1997,"h":1801}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:c2a63b817240ea11b013f390be5c690a:0c1b9b304a864f030526d8b9e8ae3352:f159918d23e5952766c6d23ab52278c6$" +} +} diff --git a/res_built/atlas/atlas0_100.png b/res_built/atlas/atlas0_100.png new file mode 100644 index 00000000..5c833849 Binary files /dev/null and b/res_built/atlas/atlas0_100.png differ diff --git a/res_built/atlas/atlas0_25.json b/res_built/atlas/atlas0_25.json new file mode 100644 index 00000000..86aa0060 --- /dev/null +++ b/res_built/atlas/atlas0_25.json @@ -0,0 +1,604 @@ +{"frames": { + +"sprites/belt/forward_0.png": +{ + "frame": {"x":493,"y":55,"w":28,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":28,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/forward_1.png": +{ + "frame": {"x":525,"y":55,"w":28,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":28,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/forward_2.png": +{ + "frame": {"x":557,"y":55,"w":28,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":28,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/forward_3.png": +{ + "frame": {"x":589,"y":55,"w":28,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":28,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/forward_4.png": +{ + "frame": {"x":621,"y":55,"w":28,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":28,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/forward_5.png": +{ + "frame": {"x":653,"y":55,"w":28,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":28,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/left_0.png": +{ + "frame": {"x":717,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/left_1.png": +{ + "frame": {"x":751,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/left_2.png": +{ + "frame": {"x":785,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/left_3.png": +{ + "frame": {"x":819,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/left_4.png": +{ + "frame": {"x":853,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/left_5.png": +{ + "frame": {"x":887,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/right_0.png": +{ + "frame": {"x":921,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/right_1.png": +{ + "frame": {"x":955,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/right_2.png": +{ + "frame": {"x":989,"y":54,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/right_3.png": +{ + "frame": {"x":989,"y":88,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/right_4.png": +{ + "frame": {"x":986,"y":122,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/belt/right_5.png": +{ + "frame": {"x":976,"y":182,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/blueprints/belt_left.png": +{ + "frame": {"x":929,"y":216,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/blueprints/belt_right.png": +{ + "frame": {"x":963,"y":216,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/blueprints/belt_top.png": +{ + "frame": {"x":685,"y":55,"w":28,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":28,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/blueprints/cutter-quad.png": +{ + "frame": {"x":373,"y":155,"w":184,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":184,"h":48}, + "sourceSize": {"w":192,"h":48} +}, +"sprites/blueprints/cutter.png": +{ + "frame": {"x":661,"y":143,"w":87,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/blueprints/miner-chainable.png": +{ + "frame": {"x":856,"y":141,"w":47,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/miner.png": +{ + "frame": {"x":858,"y":89,"w":47,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/mixer.png": +{ + "frame": {"x":491,"y":3,"w":89,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":89,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/blueprints/painter-double.png": +{ + "frame": {"x":191,"y":3,"w":96,"h":96}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/painter-quad.png": +{ + "frame": {"x":3,"y":192,"w":188,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":188,"h":48}, + "sourceSize": {"w":192,"h":48} +}, +"sprites/blueprints/painter.png": +{ + "frame": {"x":561,"y":155,"w":96,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":96,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/blueprints/rotater-ccw.png": +{ + "frame": {"x":752,"y":143,"w":48,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/rotater.png": +{ + "frame": {"x":754,"y":89,"w":48,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/splitter-compact-inverse.png": +{ + "frame": {"x":774,"y":195,"w":48,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/splitter-compact.png": +{ + "frame": {"x":907,"y":141,"w":47,"h":47}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/splitter.png": +{ + "frame": {"x":663,"y":91,"w":87,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/blueprints/stacker.png": +{ + "frame": {"x":584,"y":3,"w":89,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":89,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/blueprints/trash-storage.png": +{ + "frame": {"x":195,"y":155,"w":85,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":85,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/trash.png": +{ + "frame": {"x":804,"y":141,"w":48,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/underground_belt_entry-tier2.png": +{ + "frame": {"x":373,"y":207,"w":48,"h":43}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":5,"w":48,"h":43}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/underground_belt_entry.png": +{ + "frame": {"x":476,"y":207,"w":48,"h":38}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":10,"w":48,"h":38}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/underground_belt_exit-tier2.png": +{ + "frame": {"x":528,"y":207,"w":48,"h":38}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":38}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/underground_belt_exit.png": +{ + "frame": {"x":580,"y":207,"w":48,"h":38}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":38}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/belt_left.png": +{ + "frame": {"x":717,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/buildings/belt_right.png": +{ + "frame": {"x":921,"y":55,"w":30,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":2,"w":30,"h":30}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/buildings/belt_top.png": +{ + "frame": {"x":493,"y":55,"w":28,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":28,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/buildings/cutter-quad.png": +{ + "frame": {"x":383,"y":103,"w":184,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":184,"h":48}, + "sourceSize": {"w":192,"h":48} +}, +"sprites/buildings/cutter.png": +{ + "frame": {"x":769,"y":3,"w":87,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/buildings/hub.png": +{ + "frame": {"x":3,"y":3,"w":184,"h":185}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":4,"w":184,"h":185}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/miner-chainable.png": +{ + "frame": {"x":912,"y":3,"w":47,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/miner.png": +{ + "frame": {"x":878,"y":193,"w":47,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/mixer.png": +{ + "frame": {"x":571,"y":91,"w":88,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":0,"w":88,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/buildings/painter-double.png": +{ + "frame": {"x":291,"y":3,"w":96,"h":96}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/painter-quad.png": +{ + "frame": {"x":191,"y":103,"w":188,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":188,"h":48}, + "sourceSize": {"w":192,"h":48} +}, +"sprites/buildings/painter.png": +{ + "frame": {"x":391,"y":3,"w":96,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":96,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/buildings/rotater-ccw.png": +{ + "frame": {"x":806,"y":89,"w":48,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/rotater.png": +{ + "frame": {"x":860,"y":3,"w":48,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/splitter-compact-inverse.png": +{ + "frame": {"x":963,"y":3,"w":48,"h":47}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":47}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/splitter-compact.png": +{ + "frame": {"x":909,"y":89,"w":47,"h":47}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/splitter.png": +{ + "frame": {"x":683,"y":195,"w":87,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/buildings/stacker.png": +{ + "frame": {"x":677,"y":3,"w":88,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":0,"w":88,"h":48}, + "sourceSize": {"w":96,"h":48} +}, +"sprites/buildings/trash-storage.png": +{ + "frame": {"x":284,"y":155,"w":85,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":85,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/trash.png": +{ + "frame": {"x":826,"y":193,"w":48,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/underground_belt_entry-tier2.png": +{ + "frame": {"x":425,"y":207,"w":47,"h":42}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":6,"w":47,"h":42}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/underground_belt_entry.png": +{ + "frame": {"x":632,"y":207,"w":47,"h":38}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":10,"w":47,"h":38}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/underground_belt_exit-tier2.png": +{ + "frame": {"x":391,"y":55,"w":47,"h":38}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":38}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/underground_belt_exit.png": +{ + "frame": {"x":442,"y":55,"w":47,"h":38}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":38}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/debug/acceptor_slot.png": +{ + "frame": {"x":958,"y":143,"w":14,"h":16}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":14,"h":16}, + "sourceSize": {"w":16,"h":16} +}, +"sprites/debug/ejector_slot.png": +{ + "frame": {"x":958,"y":163,"w":14,"h":16}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":14,"h":16}, + "sourceSize": {"w":16,"h":16} +}, +"sprites/map_overview/belt_forward.png": +{ + "frame": {"x":493,"y":91,"w":8,"h":8}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, + "sourceSize": {"w":8,"h":8} +}, +"sprites/map_overview/belt_left.png": +{ + "frame": {"x":505,"y":91,"w":8,"h":8}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, + "sourceSize": {"w":8,"h":8} +}, +"sprites/map_overview/belt_right.png": +{ + "frame": {"x":517,"y":91,"w":8,"h":8}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, + "sourceSize": {"w":8,"h":8} +}, +"sprites/misc/deletion_marker.png": +{ + "frame": {"x":960,"y":117,"w":22,"h":22}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":1,"w":22,"h":22}, + "sourceSize": {"w":24,"h":24} +}, +"sprites/misc/slot_bad_arrow.png": +{ + "frame": {"x":960,"y":117,"w":22,"h":22}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":1,"w":22,"h":22}, + "sourceSize": {"w":24,"h":24} +}, +"sprites/misc/slot_good_arrow.png": +{ + "frame": {"x":960,"y":89,"w":22,"h":24}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":22,"h":24}, + "sourceSize": {"w":24,"h":24} +}, +"sprites/misc/storage_overlay.png": +{ + "frame": {"x":976,"y":156,"w":44,"h":22}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":44,"h":22}, + "sourceSize": {"w":44,"h":22} +}}, +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "atlas0_25.png", + "format": "RGBA8888", + "size": {"w":1023,"h":254}, + "scale": "0.25", + "smartupdate": "$TexturePacker:SmartUpdate:c2a63b817240ea11b013f390be5c690a:0c1b9b304a864f030526d8b9e8ae3352:f159918d23e5952766c6d23ab52278c6$" +} +} diff --git a/res_built/atlas/atlas0_25.png b/res_built/atlas/atlas0_25.png new file mode 100644 index 00000000..ae736848 Binary files /dev/null and b/res_built/atlas/atlas0_25.png differ diff --git a/res_built/atlas/atlas0_50.json b/res_built/atlas/atlas0_50.json new file mode 100644 index 00000000..931f5155 --- /dev/null +++ b/res_built/atlas/atlas0_50.json @@ -0,0 +1,604 @@ +{"frames": { + +"sprites/belt/forward_0.png": +{ + "frame": {"x":1854,"y":231,"w":51,"h":63}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":51,"h":63}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/forward_1.png": +{ + "frame": {"x":1909,"y":231,"w":51,"h":63}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":51,"h":63}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/forward_2.png": +{ + "frame": {"x":1842,"y":300,"w":51,"h":63}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":51,"h":63}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/forward_3.png": +{ + "frame": {"x":1897,"y":298,"w":51,"h":63}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":51,"h":63}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/forward_4.png": +{ + "frame": {"x":1902,"y":365,"w":51,"h":63}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":51,"h":63}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/forward_5.png": +{ + "frame": {"x":1648,"y":391,"w":51,"h":63}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":51,"h":63}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/left_0.png": +{ + "frame": {"x":953,"y":399,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/left_1.png": +{ + "frame": {"x":1014,"y":399,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/left_2.png": +{ + "frame": {"x":1874,"y":170,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/left_3.png": +{ + "frame": {"x":1780,"y":362,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/left_4.png": +{ + "frame": {"x":1265,"y":403,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/left_5.png": +{ + "frame": {"x":1326,"y":403,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/right_0.png": +{ + "frame": {"x":1841,"y":367,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/right_1.png": +{ + "frame": {"x":1587,"y":391,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/right_2.png": +{ + "frame": {"x":1703,"y":387,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/right_3.png": +{ + "frame": {"x":1387,"y":403,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/right_4.png": +{ + "frame": {"x":1448,"y":402,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/belt/right_5.png": +{ + "frame": {"x":1509,"y":402,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/blueprints/belt_left.png": +{ + "frame": {"x":1203,"y":398,"w":58,"h":58}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":5,"w":58,"h":58}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/blueprints/belt_right.png": +{ + "frame": {"x":1780,"y":300,"w":58,"h":58}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":5,"w":58,"h":58}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/blueprints/belt_top.png": +{ + "frame": {"x":1874,"y":103,"w":53,"h":63}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":53,"h":63}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/blueprints/cutter-quad.png": +{ + "frame": {"x":3,"y":374,"w":366,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":11,"y":0,"w":366,"h":96}, + "sourceSize": {"w":384,"h":96} +}, +"sprites/blueprints/cutter.png": +{ + "frame": {"x":1496,"y":3,"w":172,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":11,"y":0,"w":172,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/blueprints/miner-chainable.png": +{ + "frame": {"x":1778,"y":103,"w":92,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":92,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/miner.png": +{ + "frame": {"x":1301,"y":303,"w":92,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":92,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/mixer.png": +{ + "frame": {"x":947,"y":103,"w":175,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":8,"y":0,"w":175,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/blueprints/painter-double.png": +{ + "frame": {"x":373,"y":203,"w":192,"h":192}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":192,"h":192}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/painter-quad.png": +{ + "frame": {"x":373,"y":3,"w":374,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":374,"h":96}, + "sourceSize": {"w":384,"h":96} +}, +"sprites/blueprints/painter.png": +{ + "frame": {"x":751,"y":103,"w":192,"h":96}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":192,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/blueprints/rotater-ccw.png": +{ + "frame": {"x":1479,"y":103,"w":96,"h":96}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/rotater.png": +{ + "frame": {"x":1459,"y":203,"w":96,"h":96}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/splitter-compact-inverse.png": +{ + "frame": {"x":1658,"y":203,"w":95,"h":93}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":1,"w":95,"h":93}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/splitter-compact.png": +{ + "frame": {"x":1757,"y":203,"w":93,"h":93}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":1,"w":93,"h":93}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/splitter.png": +{ + "frame": {"x":1304,"y":103,"w":171,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":11,"y":0,"w":171,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/blueprints/stacker.png": +{ + "frame": {"x":1317,"y":3,"w":175,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":8,"y":0,"w":175,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/blueprints/trash-storage.png": +{ + "frame": {"x":765,"y":203,"w":167,"h":192}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":14,"y":0,"w":167,"h":192}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/blueprints/trash.png": +{ + "frame": {"x":1847,"y":3,"w":96,"h":96}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/underground_belt_entry-tier2.png": +{ + "frame": {"x":1587,"y":303,"w":93,"h":84}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":12,"w":93,"h":84}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/underground_belt_entry.png": +{ + "frame": {"x":667,"y":398,"w":93,"h":75}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":21,"w":93,"h":75}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/underground_belt_exit-tier2.png": +{ + "frame": {"x":569,"y":398,"w":94,"h":75}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":94,"h":75}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/underground_belt_exit.png": +{ + "frame": {"x":1106,"y":398,"w":93,"h":75}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":93,"h":75}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/belt_left.png": +{ + "frame": {"x":953,"y":399,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/buildings/belt_right.png": +{ + "frame": {"x":1841,"y":367,"w":57,"h":57}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":6,"w":57,"h":57}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/buildings/belt_top.png": +{ + "frame": {"x":1854,"y":231,"w":51,"h":63}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":6,"y":0,"w":51,"h":63}, + "sourceSize": {"w":63,"h":63} +}, +"sprites/buildings/cutter-quad.png": +{ + "frame": {"x":751,"y":3,"w":366,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":11,"y":0,"w":366,"h":96}, + "sourceSize": {"w":384,"h":96} +}, +"sprites/buildings/cutter.png": +{ + "frame": {"x":1284,"y":203,"w":171,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":11,"y":0,"w":171,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/buildings/hub.png": +{ + "frame": {"x":3,"y":3,"w":366,"h":367}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":10,"w":366,"h":367}, + "sourceSize": {"w":384,"h":384} +}, +"sprites/buildings/miner-chainable.png": +{ + "frame": {"x":1397,"y":303,"w":91,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":91,"h":95}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/miner.png": +{ + "frame": {"x":1492,"y":303,"w":91,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":91,"h":95}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/mixer.png": +{ + "frame": {"x":1126,"y":103,"w":174,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":174,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/buildings/painter-double.png": +{ + "frame": {"x":569,"y":203,"w":192,"h":191}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":192,"h":191}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/painter-quad.png": +{ + "frame": {"x":373,"y":103,"w":374,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":374,"h":96}, + "sourceSize": {"w":384,"h":96} +}, +"sprites/buildings/painter.png": +{ + "frame": {"x":1121,"y":3,"w":192,"h":96}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":192,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/buildings/rotater-ccw.png": +{ + "frame": {"x":1559,"y":203,"w":95,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/rotater.png": +{ + "frame": {"x":1679,"y":103,"w":95,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/splitter-compact-inverse.png": +{ + "frame": {"x":1106,"y":303,"w":94,"h":91}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":94,"h":91}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/splitter-compact.png": +{ + "frame": {"x":1204,"y":303,"w":93,"h":91}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":2,"w":93,"h":91}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/splitter.png": +{ + "frame": {"x":1672,"y":3,"w":171,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":11,"y":0,"w":171,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/buildings/stacker.png": +{ + "frame": {"x":1106,"y":203,"w":174,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":174,"h":96}, + "sourceSize": {"w":192,"h":96} +}, +"sprites/buildings/trash-storage.png": +{ + "frame": {"x":936,"y":203,"w":166,"h":192}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":14,"y":0,"w":166,"h":192}, + "sourceSize": {"w":192,"h":192} +}, +"sprites/buildings/trash.png": +{ + "frame": {"x":1579,"y":103,"w":96,"h":96}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/underground_belt_entry-tier2.png": +{ + "frame": {"x":1684,"y":300,"w":92,"h":83}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":13,"w":92,"h":83}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/underground_belt_entry.png": +{ + "frame": {"x":373,"y":399,"w":92,"h":74}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":22,"w":92,"h":74}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/underground_belt_exit-tier2.png": +{ + "frame": {"x":469,"y":399,"w":92,"h":74}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":92,"h":74}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/underground_belt_exit.png": +{ + "frame": {"x":764,"y":399,"w":92,"h":74}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":92,"h":74}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/debug/acceptor_slot.png": +{ + "frame": {"x":1075,"y":399,"w":26,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":26,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/debug/ejector_slot.png": +{ + "frame": {"x":1810,"y":423,"w":26,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":26,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"sprites/map_overview/belt_forward.png": +{ + "frame": {"x":1854,"y":203,"w":14,"h":16}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":14,"h":16}, + "sourceSize": {"w":16,"h":16} +}, +"sprites/map_overview/belt_left.png": +{ + "frame": {"x":860,"y":446,"w":15,"h":15}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":1,"w":15,"h":15}, + "sourceSize": {"w":16,"h":16} +}, +"sprites/map_overview/belt_right.png": +{ + "frame": {"x":1075,"y":435,"w":15,"h":15}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":1,"w":15,"h":15}, + "sourceSize": {"w":16,"h":16} +}, +"sprites/misc/deletion_marker.png": +{ + "frame": {"x":1840,"y":428,"w":42,"h":42}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":3,"w":42,"h":42}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/misc/slot_bad_arrow.png": +{ + "frame": {"x":1840,"y":428,"w":42,"h":42}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":3,"w":42,"h":42}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/misc/slot_good_arrow.png": +{ + "frame": {"x":1764,"y":423,"w":42,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":42,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/misc/storage_overlay.png": +{ + "frame": {"x":860,"y":399,"w":89,"h":43}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":89,"h":43}, + "sourceSize": {"w":89,"h":43} +}}, +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "atlas0_50.png", + "format": "RGBA8888", + "size": {"w":1963,"h":476}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:c2a63b817240ea11b013f390be5c690a:0c1b9b304a864f030526d8b9e8ae3352:f159918d23e5952766c6d23ab52278c6$" +} +} diff --git a/res_built/atlas/atlas0_50.png b/res_built/atlas/atlas0_50.png new file mode 100644 index 00000000..8d1a3fde Binary files /dev/null and b/res_built/atlas/atlas0_50.png differ diff --git a/res_built/atlas/atlas0_75.json b/res_built/atlas/atlas0_75.json new file mode 100644 index 00000000..ee646a0d --- /dev/null +++ b/res_built/atlas/atlas0_75.json @@ -0,0 +1,604 @@ +{"frames": { + +"sprites/belt/forward_0.png": +{ + "frame": {"x":1936,"y":151,"w":77,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":77,"h":95}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/forward_1.png": +{ + "frame": {"x":1936,"y":250,"w":77,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":77,"h":95}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/forward_2.png": +{ + "frame": {"x":1600,"y":708,"w":77,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":77,"h":95}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/forward_3.png": +{ + "frame": {"x":1544,"y":808,"w":77,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":77,"h":95}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/forward_4.png": +{ + "frame": {"x":1625,"y":807,"w":77,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":77,"h":95}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/forward_5.png": +{ + "frame": {"x":1544,"y":907,"w":77,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":77,"h":95}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/left_0.png": +{ + "frame": {"x":1680,"y":593,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/left_1.png": +{ + "frame": {"x":1770,"y":592,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/left_2.png": +{ + "frame": {"x":1860,"y":592,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/left_3.png": +{ + "frame": {"x":1681,"y":683,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/left_4.png": +{ + "frame": {"x":1771,"y":682,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/left_5.png": +{ + "frame": {"x":1861,"y":682,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/right_0.png": +{ + "frame": {"x":1706,"y":773,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/right_1.png": +{ + "frame": {"x":1796,"y":772,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/right_2.png": +{ + "frame": {"x":1886,"y":772,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/right_3.png": +{ + "frame": {"x":1716,"y":863,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/right_4.png": +{ + "frame": {"x":1806,"y":862,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/belt/right_5.png": +{ + "frame": {"x":1896,"y":862,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/blueprints/belt_left.png": +{ + "frame": {"x":1924,"y":349,"w":87,"h":87}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":8,"w":87,"h":87}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/blueprints/belt_right.png": +{ + "frame": {"x":1625,"y":906,"w":87,"h":87}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":8,"y":8,"w":87,"h":87}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/blueprints/belt_top.png": +{ + "frame": {"x":1517,"y":709,"w":79,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":8,"y":0,"w":79,"h":95}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/blueprints/cutter-quad.png": +{ + "frame": {"x":3,"y":556,"w":548,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":17,"y":0,"w":548,"h":144}, + "sourceSize": {"w":576,"h":144} +}, +"sprites/blueprints/cutter.png": +{ + "frame": {"x":1411,"y":150,"w":256,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":17,"y":0,"w":256,"h":144}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/blueprints/miner-chainable.png": +{ + "frame": {"x":1784,"y":299,"w":136,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":136,"h":143}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/miner.png": +{ + "frame": {"x":1544,"y":446,"w":136,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":136,"h":143}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/mixer.png": +{ + "frame": {"x":1671,"y":151,"w":261,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":0,"w":261,"h":144}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/blueprints/painter-double.png": +{ + "frame": {"x":555,"y":299,"w":288,"h":287}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":288,"h":287}, + "sourceSize": {"w":288,"h":288} +}, +"sprites/blueprints/painter-quad.png": +{ + "frame": {"x":555,"y":3,"w":560,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":560,"h":144}, + "sourceSize": {"w":576,"h":144} +}, +"sprites/blueprints/painter.png": +{ + "frame": {"x":1119,"y":150,"w":288,"h":144}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":288,"h":144}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/blueprints/rotater-ccw.png": +{ + "frame": {"x":1397,"y":445,"w":143,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":143,"h":144}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/rotater.png": +{ + "frame": {"x":1084,"y":594,"w":143,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":143,"h":144}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/splitter-compact-inverse.png": +{ + "frame": {"x":1071,"y":742,"w":142,"h":138}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":142,"h":138}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/splitter-compact.png": +{ + "frame": {"x":1217,"y":742,"w":139,"h":138}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":2,"w":139,"h":138}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/splitter.png": +{ + "frame": {"x":1119,"y":298,"w":256,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":17,"y":0,"w":256,"h":144}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/blueprints/stacker.png": +{ + "frame": {"x":555,"y":590,"w":261,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":13,"y":0,"w":261,"h":144}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/blueprints/trash-storage.png": +{ + "frame": {"x":847,"y":299,"w":250,"h":288}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":21,"y":0,"w":250,"h":288}, + "sourceSize": {"w":288,"h":288} +}, +"sprites/blueprints/trash.png": +{ + "frame": {"x":1101,"y":446,"w":144,"h":144}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/underground_belt_entry-tier2.png": +{ + "frame": {"x":835,"y":885,"w":138,"h":125}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":19,"w":138,"h":125}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/underground_belt_entry.png": +{ + "frame": {"x":1261,"y":884,"w":138,"h":112}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":32,"w":138,"h":112}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/underground_belt_exit-tier2.png": +{ + "frame": {"x":1118,"y":884,"w":139,"h":112}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":0,"w":139,"h":112}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/underground_belt_exit.png": +{ + "frame": {"x":1397,"y":593,"w":138,"h":112}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":0,"w":138,"h":112}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/belt_left.png": +{ + "frame": {"x":1680,"y":593,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/buildings/belt_right.png": +{ + "frame": {"x":1706,"y":773,"w":86,"h":86}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":9,"w":86,"h":86}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/buildings/belt_top.png": +{ + "frame": {"x":1936,"y":151,"w":77,"h":95}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":77,"h":95}, + "sourceSize": {"w":95,"h":95} +}, +"sprites/buildings/cutter-quad.png": +{ + "frame": {"x":1119,"y":3,"w":548,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":17,"y":0,"w":548,"h":143}, + "sourceSize": {"w":576,"h":144} +}, +"sprites/buildings/cutter.png": +{ + "frame": {"x":811,"y":738,"w":256,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":17,"y":0,"w":256,"h":143}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/buildings/hub.png": +{ + "frame": {"x":3,"y":3,"w":548,"h":549}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":14,"y":16,"w":548,"h":549}, + "sourceSize": {"w":576,"h":576} +}, +"sprites/buildings/miner-chainable.png": +{ + "frame": {"x":1684,"y":446,"w":136,"h":142}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":136,"h":142}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/miner.png": +{ + "frame": {"x":1824,"y":446,"w":136,"h":142}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":136,"h":142}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/mixer.png": +{ + "frame": {"x":547,"y":738,"w":260,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":14,"y":0,"w":260,"h":143}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/buildings/painter-double.png": +{ + "frame": {"x":3,"y":704,"w":288,"h":286}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":288,"h":286}, + "sourceSize": {"w":288,"h":288} +}, +"sprites/buildings/painter-quad.png": +{ + "frame": {"x":555,"y":151,"w":560,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":560,"h":144}, + "sourceSize": {"w":576,"h":144} +}, +"sprites/buildings/painter.png": +{ + "frame": {"x":1671,"y":3,"w":288,"h":144}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":288,"h":144}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/buildings/rotater-ccw.png": +{ + "frame": {"x":1231,"y":594,"w":141,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":141,"h":143}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/rotater.png": +{ + "frame": {"x":1639,"y":299,"w":141,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":141,"h":143}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/splitter-compact-inverse.png": +{ + "frame": {"x":547,"y":885,"w":141,"h":136}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":3,"w":141,"h":136}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/splitter-compact.png": +{ + "frame": {"x":692,"y":885,"w":139,"h":136}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":3,"w":139,"h":136}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/splitter.png": +{ + "frame": {"x":1379,"y":298,"w":256,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":17,"y":0,"w":256,"h":143}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/buildings/stacker.png": +{ + "frame": {"x":820,"y":591,"w":260,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":14,"y":0,"w":260,"h":143}, + "sourceSize": {"w":288,"h":144} +}, +"sprites/buildings/trash-storage.png": +{ + "frame": {"x":295,"y":704,"w":248,"h":288}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":22,"y":0,"w":248,"h":288}, + "sourceSize": {"w":288,"h":288} +}, +"sprites/buildings/trash.png": +{ + "frame": {"x":1249,"y":446,"w":144,"h":144}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/underground_belt_entry-tier2.png": +{ + "frame": {"x":977,"y":885,"w":137,"h":124}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":20,"w":137,"h":124}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/underground_belt_entry.png": +{ + "frame": {"x":1539,"y":593,"w":137,"h":111}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":33,"w":137,"h":111}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/underground_belt_exit-tier2.png": +{ + "frame": {"x":1376,"y":709,"w":137,"h":111}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":137,"h":111}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/underground_belt_exit.png": +{ + "frame": {"x":1403,"y":824,"w":137,"h":111}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":137,"h":111}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/debug/acceptor_slot.png": +{ + "frame": {"x":1963,"y":3,"w":38,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":38,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/debug/ejector_slot.png": +{ + "frame": {"x":1963,"y":55,"w":38,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":38,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/map_overview/belt_forward.png": +{ + "frame": {"x":1625,"y":997,"w":20,"h":24}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":0,"w":20,"h":24}, + "sourceSize": {"w":24,"h":24} +}, +"sprites/map_overview/belt_left.png": +{ + "frame": {"x":3,"y":994,"w":22,"h":22}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":22,"h":22}, + "sourceSize": {"w":24,"h":24} +}, +"sprites/map_overview/belt_right.png": +{ + "frame": {"x":1963,"y":107,"w":22,"h":22}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":2,"y":2,"w":22,"h":22}, + "sourceSize": {"w":24,"h":24} +}, +"sprites/misc/deletion_marker.png": +{ + "frame": {"x":1716,"y":953,"w":62,"h":62}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":5,"w":62,"h":62}, + "sourceSize": {"w":72,"h":72} +}, +"sprites/misc/slot_bad_arrow.png": +{ + "frame": {"x":1716,"y":953,"w":62,"h":62}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":5,"w":62,"h":62}, + "sourceSize": {"w":72,"h":72} +}, +"sprites/misc/slot_good_arrow.png": +{ + "frame": {"x":1950,"y":592,"w":62,"h":72}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":62,"h":72}, + "sourceSize": {"w":72,"h":72} +}, +"sprites/misc/storage_overlay.png": +{ + "frame": {"x":1403,"y":939,"w":133,"h":65}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":133,"h":65}, + "sourceSize": {"w":133,"h":65} +}}, +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "atlas0_75.png", + "format": "RGBA8888", + "size": {"w":2016,"h":1024}, + "scale": "0.75", + "smartupdate": "$TexturePacker:SmartUpdate:c2a63b817240ea11b013f390be5c690a:0c1b9b304a864f030526d8b9e8ae3352:f159918d23e5952766c6d23ab52278c6$" +} +} diff --git a/res_built/atlas/atlas0_75.png b/res_built/atlas/atlas0_75.png new file mode 100644 index 00000000..1a1b069b Binary files /dev/null and b/res_built/atlas/atlas0_75.png differ diff --git a/res_raw/atlas.tps b/res_raw/atlas.tps index 211d1659..883a6932 100644 --- a/res_raw/atlas.tps +++ b/res_raw/atlas.tps @@ -15,13 +15,13 @@ spriteFilter acceptFractionalValues - + maxTextureSize width - -1 + 2048 height - -1 + 2048 @@ -32,13 +32,13 @@ spriteFilter acceptFractionalValues - + maxTextureSize width - -1 + 2048 height - -1 + 2048 @@ -49,7 +49,7 @@ spriteFilter acceptFractionalValues - + maxTextureSize width @@ -66,7 +66,7 @@ spriteFilter acceptFractionalValues - + maxTextureSize width @@ -83,7 +83,7 @@ spriteFilter acceptFractionalValues - + maxTextureSize width @@ -156,9 +156,9 @@ maxTextureSize width - 4096 + 2048 height - 4096 + 2048 fixedTextureSize @@ -284,6 +284,9 @@ sprites/belt/right_3.png sprites/belt/right_4.png sprites/belt/right_5.png + sprites/blueprints/belt_left.png + sprites/blueprints/belt_right.png + sprites/blueprints/belt_top.png sprites/buildings/belt_left.png sprites/buildings/belt_right.png sprites/buildings/belt_top.png @@ -301,6 +304,93 @@ scale9FromFile + sprites/blueprints/cutter-quad.png + sprites/blueprints/painter-quad.png + sprites/buildings/cutter-quad.png + sprites/buildings/painter-quad.png + + pivotPoint + 0.5,0.5 + spriteScale + 1 + scale9Enabled + + scale9Borders + 192,48,384,96 + scale9Paddings + 192,48,384,96 + scale9FromFile + + + sprites/blueprints/cutter.png + sprites/blueprints/mixer.png + sprites/blueprints/painter.png + sprites/blueprints/splitter.png + sprites/blueprints/stacker.png + + pivotPoint + 0.5,0.5 + spriteScale + 1 + scale9Enabled + + scale9Borders + 96,48,192,96 + scale9Paddings + 96,48,192,96 + scale9FromFile + + + sprites/blueprints/miner-chainable.png + sprites/blueprints/miner.png + sprites/blueprints/rotater-ccw.png + sprites/blueprints/rotater.png + sprites/blueprints/splitter-compact-inverse.png + sprites/blueprints/splitter-compact.png + sprites/blueprints/trash.png + sprites/blueprints/underground_belt_entry-tier2.png + sprites/blueprints/underground_belt_entry.png + sprites/blueprints/underground_belt_exit-tier2.png + sprites/blueprints/underground_belt_exit.png + sprites/buildings/miner-chainable.png + sprites/buildings/rotater-ccw.png + sprites/buildings/splitter-compact-inverse.png + sprites/buildings/splitter-compact.png + sprites/buildings/underground_belt_entry-tier2.png + sprites/buildings/underground_belt_entry.png + sprites/buildings/underground_belt_exit-tier2.png + sprites/buildings/underground_belt_exit.png + + pivotPoint + 0.5,0.5 + spriteScale + 1 + scale9Enabled + + scale9Borders + 48,48,96,96 + scale9Paddings + 48,48,96,96 + scale9FromFile + + + sprites/blueprints/painter-double.png + sprites/blueprints/trash-storage.png + sprites/buildings/painter-double.png + + pivotPoint + 0.5,0.5 + spriteScale + 1 + scale9Enabled + + scale9Borders + 96,96,192,192 + scale9Paddings + 96,96,192,192 + scale9FromFile + + sprites/buildings/cutter.png sprites/buildings/mixer.png sprites/buildings/painter.png @@ -335,8 +425,7 @@ scale9FromFile - sprites/buildings/underground_belt_entry.png - sprites/buildings/underground_belt_exit.png + sprites/buildings/trash-storage.png pivotPoint 0.5,0.5 @@ -345,9 +434,9 @@ scale9Enabled scale9Borders - 48,48,96,96 + 144,144,288,288 scale9Paddings - 48,48,96,96 + 144,144,288,288 scale9FromFile @@ -370,6 +459,7 @@ scale9FromFile + sprites/misc/deletion_marker.png sprites/misc/slot_bad_arrow.png sprites/misc/slot_good_arrow.png @@ -386,6 +476,21 @@ scale9FromFile + sprites/misc/storage_overlay.png + + pivotPoint + 0.5,0.5 + spriteScale + 1 + scale9Enabled + + scale9Borders + 44,22,89,43 + scale9Paddings + 44,22,89,43 + scale9FromFile + + fileList diff --git a/res_raw/sounds/music/main_menu.wav b/res_raw/sounds/music/main_menu.wav deleted file mode 100644 index 488479a8..00000000 --- a/res_raw/sounds/music/main_menu.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:811a57acb5a16af8079744e048c3809799211f1d7c6e79eaa5247ed852b5d4eb -size 23990546 diff --git a/res_raw/sounds/music/menu.mp3 b/res_raw/sounds/music/menu.mp3 new file mode 100644 index 00000000..87864f71 --- /dev/null +++ b/res_raw/sounds/music/menu.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e94039ba13b6af3a9e59d8675ed2f7373aad20fc9bb0c12e35b1a901d906efd +size 1463494 diff --git a/res_raw/sounds/music/theme.mp3 b/res_raw/sounds/music/theme.mp3 new file mode 100644 index 00000000..66b78e55 --- /dev/null +++ b/res_raw/sounds/music/theme.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f61e851402fbc92d6909912fb203c63258fedbdea83e81f7ececb5091f4bee03 +size 3732550 diff --git a/res_raw/sounds/music/theme_full.wav b/res_raw/sounds/music/theme_full.wav deleted file mode 100644 index 68a7a043..00000000 --- a/res_raw/sounds/music/theme_full.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b4cbf863591aba38d1729fb1cbaa792323f045722279f0deca2492acaaf19b5 -size 123844300 diff --git a/res_raw/sounds/sfx/badge_notification.wav b/res_raw/sounds/sfx/badge_notification.wav new file mode 100644 index 00000000..0af30494 --- /dev/null +++ b/res_raw/sounds/sfx/badge_notification.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dba7e859fb98713943890281a543e209d57c6633496090bd70d23dc58bed4405 +size 743136 diff --git a/res_raw/sounds/ui/dialog_error.wav b/res_raw/sounds/sfx/dialog_error.wav similarity index 100% rename from res_raw/sounds/ui/dialog_error.wav rename to res_raw/sounds/sfx/dialog_error.wav diff --git a/res_raw/sounds/ui/dialog_ok.wav b/res_raw/sounds/sfx/dialog_ok.wav similarity index 100% rename from res_raw/sounds/ui/dialog_ok.wav rename to res_raw/sounds/sfx/dialog_ok.wav diff --git a/res_raw/sounds/sfx/level_complete.wav b/res_raw/sounds/sfx/level_complete.wav new file mode 100644 index 00000000..71444cd8 --- /dev/null +++ b/res_raw/sounds/sfx/level_complete.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4f810fcfbfcea08cfb48a8d58f9b65d1fd14cd20ff3cabf94a03c39b5250547 +size 1765124 diff --git a/res_raw/sounds/sfx/place_belt.wav b/res_raw/sounds/sfx/place_belt.wav new file mode 100644 index 00000000..087104bc --- /dev/null +++ b/res_raw/sounds/sfx/place_belt.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d802fdcef7ae2d9486b362bca260e64383c690e14d7246e632324e49bed76abe +size 24580 diff --git a/res_raw/sounds/sfx/place_building.wav b/res_raw/sounds/sfx/place_building.wav new file mode 100644 index 00000000..3bb89df5 --- /dev/null +++ b/res_raw/sounds/sfx/place_building.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecd3d0d02bcc16363cd977b70cd94dddc774e809e2670d903b6dc62a0e2ff291 +size 238210 diff --git a/res_raw/sounds/sfx/ui_click.wav b/res_raw/sounds/sfx/ui_click.wav new file mode 100644 index 00000000..673b8904 --- /dev/null +++ b/res_raw/sounds/sfx/ui_click.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75b848d50cd6c02ee7ba53424ce0e1d8432b35bc044aa195b0b3e35b5021c92d +size 43630 diff --git a/res_raw/sounds/ui/ui_error.wav b/res_raw/sounds/sfx/ui_error.wav similarity index 100% rename from res_raw/sounds/ui/ui_error.wav rename to res_raw/sounds/sfx/ui_error.wav diff --git a/res_raw/sounds/ui/ui_swish_hide.wav b/res_raw/sounds/sfx/ui_swish_hide.wav similarity index 100% rename from res_raw/sounds/ui/ui_swish_hide.wav rename to res_raw/sounds/sfx/ui_swish_hide.wav diff --git a/res_raw/sounds/ui/ui_swish_show.wav b/res_raw/sounds/sfx/ui_swish_show.wav similarity index 100% rename from res_raw/sounds/ui/ui_swish_show.wav rename to res_raw/sounds/sfx/ui_swish_show.wav diff --git a/res_raw/sounds/ui/ui_click.wav b/res_raw/sounds/ui/ui_click.wav deleted file mode 100644 index 3118481d..00000000 --- a/res_raw/sounds/ui/ui_click.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f4c56728534d030fca2d75f297027a2774d6f765f5be84a27a3705569f878f2 -size 20158 diff --git a/res_raw/sprites/blueprints/belt_left.png b/res_raw/sprites/blueprints/belt_left.png new file mode 100644 index 00000000..476a17e2 Binary files /dev/null and b/res_raw/sprites/blueprints/belt_left.png differ diff --git a/res_raw/sprites/blueprints/belt_right.png b/res_raw/sprites/blueprints/belt_right.png new file mode 100644 index 00000000..77ba6a33 Binary files /dev/null and b/res_raw/sprites/blueprints/belt_right.png differ diff --git a/res_raw/sprites/blueprints/belt_top.png b/res_raw/sprites/blueprints/belt_top.png new file mode 100644 index 00000000..d8532b64 Binary files /dev/null and b/res_raw/sprites/blueprints/belt_top.png differ diff --git a/res_raw/sprites/blueprints/cutter-quad.png b/res_raw/sprites/blueprints/cutter-quad.png new file mode 100644 index 00000000..5ae8989e Binary files /dev/null and b/res_raw/sprites/blueprints/cutter-quad.png differ diff --git a/res_raw/sprites/blueprints/cutter.png b/res_raw/sprites/blueprints/cutter.png new file mode 100644 index 00000000..43d56cb2 Binary files /dev/null and b/res_raw/sprites/blueprints/cutter.png differ diff --git a/res_raw/sprites/blueprints/miner-chainable.png b/res_raw/sprites/blueprints/miner-chainable.png new file mode 100644 index 00000000..11b633ed Binary files /dev/null and b/res_raw/sprites/blueprints/miner-chainable.png differ diff --git a/res_raw/sprites/blueprints/miner.png b/res_raw/sprites/blueprints/miner.png new file mode 100644 index 00000000..f07d0276 Binary files /dev/null and b/res_raw/sprites/blueprints/miner.png differ diff --git a/res_raw/sprites/blueprints/mixer.png b/res_raw/sprites/blueprints/mixer.png new file mode 100644 index 00000000..2937c38d Binary files /dev/null and b/res_raw/sprites/blueprints/mixer.png differ diff --git a/res_raw/sprites/blueprints/painter-double.png b/res_raw/sprites/blueprints/painter-double.png new file mode 100644 index 00000000..0fe1d913 Binary files /dev/null and b/res_raw/sprites/blueprints/painter-double.png differ diff --git a/res_raw/sprites/blueprints/painter-quad.png b/res_raw/sprites/blueprints/painter-quad.png new file mode 100644 index 00000000..c3c755f2 Binary files /dev/null and b/res_raw/sprites/blueprints/painter-quad.png differ diff --git a/res_raw/sprites/blueprints/painter.png b/res_raw/sprites/blueprints/painter.png new file mode 100644 index 00000000..c2326268 Binary files /dev/null and b/res_raw/sprites/blueprints/painter.png differ diff --git a/res_raw/sprites/blueprints/rotater-ccw.png b/res_raw/sprites/blueprints/rotater-ccw.png new file mode 100644 index 00000000..2dd7e2db Binary files /dev/null and b/res_raw/sprites/blueprints/rotater-ccw.png differ diff --git a/res_raw/sprites/blueprints/rotater.png b/res_raw/sprites/blueprints/rotater.png new file mode 100644 index 00000000..c986d8df Binary files /dev/null and b/res_raw/sprites/blueprints/rotater.png differ diff --git a/res_raw/sprites/blueprints/splitter-compact-inverse.png b/res_raw/sprites/blueprints/splitter-compact-inverse.png new file mode 100644 index 00000000..db528f32 Binary files /dev/null and b/res_raw/sprites/blueprints/splitter-compact-inverse.png differ diff --git a/res_raw/sprites/blueprints/splitter-compact.png b/res_raw/sprites/blueprints/splitter-compact.png new file mode 100644 index 00000000..3e3c81f7 Binary files /dev/null and b/res_raw/sprites/blueprints/splitter-compact.png differ diff --git a/res_raw/sprites/blueprints/splitter.png b/res_raw/sprites/blueprints/splitter.png new file mode 100644 index 00000000..984a99a8 Binary files /dev/null and b/res_raw/sprites/blueprints/splitter.png differ diff --git a/res_raw/sprites/blueprints/stacker.png b/res_raw/sprites/blueprints/stacker.png new file mode 100644 index 00000000..f11116b4 Binary files /dev/null and b/res_raw/sprites/blueprints/stacker.png differ diff --git a/res_raw/sprites/blueprints/trash-storage.png b/res_raw/sprites/blueprints/trash-storage.png new file mode 100644 index 00000000..a4e6122f Binary files /dev/null and b/res_raw/sprites/blueprints/trash-storage.png differ diff --git a/res_raw/sprites/blueprints/trash.png b/res_raw/sprites/blueprints/trash.png new file mode 100644 index 00000000..99ca2340 Binary files /dev/null and b/res_raw/sprites/blueprints/trash.png differ diff --git a/res_raw/sprites/blueprints/underground_belt_entry-tier2.png b/res_raw/sprites/blueprints/underground_belt_entry-tier2.png new file mode 100644 index 00000000..9a4beb66 Binary files /dev/null and b/res_raw/sprites/blueprints/underground_belt_entry-tier2.png differ diff --git a/res_raw/sprites/blueprints/underground_belt_entry.png b/res_raw/sprites/blueprints/underground_belt_entry.png new file mode 100644 index 00000000..aa237b6d Binary files /dev/null and b/res_raw/sprites/blueprints/underground_belt_entry.png differ diff --git a/res_raw/sprites/blueprints/underground_belt_exit-tier2.png b/res_raw/sprites/blueprints/underground_belt_exit-tier2.png new file mode 100644 index 00000000..b9c97b75 Binary files /dev/null and b/res_raw/sprites/blueprints/underground_belt_exit-tier2.png differ diff --git a/res_raw/sprites/blueprints/underground_belt_exit.png b/res_raw/sprites/blueprints/underground_belt_exit.png new file mode 100644 index 00000000..760f63a9 Binary files /dev/null and b/res_raw/sprites/blueprints/underground_belt_exit.png differ diff --git a/res_raw/sprites/buildings/cutter-quad.png b/res_raw/sprites/buildings/cutter-quad.png new file mode 100644 index 00000000..e67628f2 Binary files /dev/null and b/res_raw/sprites/buildings/cutter-quad.png differ diff --git a/res_raw/sprites/buildings/hub.png b/res_raw/sprites/buildings/hub.png index d7953ee2..56a92ae7 100644 Binary files a/res_raw/sprites/buildings/hub.png and b/res_raw/sprites/buildings/hub.png differ diff --git a/res_raw/sprites/buildings/miner-chainable.png b/res_raw/sprites/buildings/miner-chainable.png new file mode 100644 index 00000000..d14d0299 Binary files /dev/null and b/res_raw/sprites/buildings/miner-chainable.png differ diff --git a/res_raw/sprites/buildings/painter-double.png b/res_raw/sprites/buildings/painter-double.png new file mode 100644 index 00000000..0d909786 Binary files /dev/null and b/res_raw/sprites/buildings/painter-double.png differ diff --git a/res_raw/sprites/buildings/painter-quad.png b/res_raw/sprites/buildings/painter-quad.png new file mode 100644 index 00000000..18790a78 Binary files /dev/null and b/res_raw/sprites/buildings/painter-quad.png differ diff --git a/res_raw/sprites/buildings/painter.png b/res_raw/sprites/buildings/painter.png index 2eb883d3..1ebca21f 100644 Binary files a/res_raw/sprites/buildings/painter.png and b/res_raw/sprites/buildings/painter.png differ diff --git a/res_raw/sprites/buildings/rotater-ccw.png b/res_raw/sprites/buildings/rotater-ccw.png new file mode 100644 index 00000000..a64caad3 Binary files /dev/null and b/res_raw/sprites/buildings/rotater-ccw.png differ diff --git a/res_raw/sprites/buildings/splitter-compact-inverse.png b/res_raw/sprites/buildings/splitter-compact-inverse.png new file mode 100644 index 00000000..1d9bd4c9 Binary files /dev/null and b/res_raw/sprites/buildings/splitter-compact-inverse.png differ diff --git a/res_raw/sprites/buildings/splitter-compact.png b/res_raw/sprites/buildings/splitter-compact.png new file mode 100644 index 00000000..f0f80a0b Binary files /dev/null and b/res_raw/sprites/buildings/splitter-compact.png differ diff --git a/res_raw/sprites/buildings/trash-storage.png b/res_raw/sprites/buildings/trash-storage.png new file mode 100644 index 00000000..57b1519e Binary files /dev/null and b/res_raw/sprites/buildings/trash-storage.png differ diff --git a/res_raw/sprites/buildings/underground_belt_entry-tier2.png b/res_raw/sprites/buildings/underground_belt_entry-tier2.png new file mode 100644 index 00000000..3a30f4b5 Binary files /dev/null and b/res_raw/sprites/buildings/underground_belt_entry-tier2.png differ diff --git a/res_raw/sprites/buildings/underground_belt_exit-tier2.png b/res_raw/sprites/buildings/underground_belt_exit-tier2.png new file mode 100644 index 00000000..87c59bf3 Binary files /dev/null and b/res_raw/sprites/buildings/underground_belt_exit-tier2.png differ diff --git a/res_raw/sprites/create_blueprint_previews.py b/res_raw/sprites/create_blueprint_previews.py new file mode 100644 index 00000000..4dd44445 --- /dev/null +++ b/res_raw/sprites/create_blueprint_previews.py @@ -0,0 +1,96 @@ +# Requirements: numpy, scipy, Pillow, + +import sys +import numpy as np +from scipy import ndimage +from PIL import Image, ImageFilter, ImageChops +import math +from os import listdir +from os.path import isdir, isfile + +roberts_cross_v = np.array([[0, 0, 0], + [0, 1, 0], + [0, 0, -1]]) + +roberts_cross_h = np.array([[0, 0, 0], + [0, 0, 1], + [0, -1, 0]]) + + +def rgb2gray(rgb): + return np.dot(rgb[..., :3], [0.2989, 0.5870, 0.1140]) + + +def save_image(data, outfilename, src_image): + img = Image.fromarray(np.asarray( + np.clip(data, 0, 255), dtype="uint8"), "L") + dest = Image.new("RGBA", (img.width, img.height)) + src = img.load() + dst = dest.load() + + realSrc = src_image.load() + mask = src_image.filter(ImageFilter.GaussianBlur(10)).load() + orig = src_image.load() + + for x in range(img.width): + for y in range(img.height): + realpixl = realSrc[x, y] + greyval = float(src[x, y]) + greyval = min(255.0, greyval) + greyval = math.pow( + min(1, float(greyval / 255.0 * 1)), 1.5) * 255.0 * 1 + greyval = max(0, greyval) + alpha = mask[x, y][3] / 255.0 * 1 + + edgeFactor = src[x, y] / 255.0 + noEdge = 1 - edgeFactor + + shadow = min(1, 1 - realpixl[3] / 255.0 - edgeFactor) + noShadow = 1 - shadow + + dst[x, y] = ( + min(255, int((realpixl[0] / 255.0 * 0.4 + 0.6) * 104 * 1.1)), + min(255, int((realpixl[1] / 255.0 * 0.4 + 0.6) * 200 * 1.1)), + min(255, int((realpixl[2] / 255.0 * 0.4 + 0.6) * 255 * 1.1)), + min(255, int(float(realpixl[3]) * (0.6 + 5 * edgeFactor)))) + + + dest.save(outfilename) + + +def roberts_cross(infilename, outfilename): + print "Processing", infilename + img = Image.open(infilename) + img.load() + img = img.filter(ImageFilter.GaussianBlur(0.5)) + + image = rgb2gray(np.asarray(img, dtype="int32")) + vertical = ndimage.convolve(image, roberts_cross_v) + horizontal = ndimage.convolve(image, roberts_cross_h) + output_image = np.sqrt(np.square(horizontal) + np.square(vertical)) + save_image(output_image, outfilename, img) + + +def generateUiPreview(srcPath, buildingId): + print srcPath, buildingId + img = Image.open(srcPath) + img.load() + img.thumbnail((110, 110), Image.ANTIALIAS) + img.save("../res/ui/hud/building_previews/" + buildingId + ".png") + + img = img.convert("LA") + + data = img.load() + for x in range(img.width): + for y in range(img.height): + data[x, y] = (data[x, y][0], int(data[x, y][1] * 0.5)) + + img.save("../res/ui/hud/building_previews/" + buildingId + "_disabled.png") + + +buildings = listdir("buildings") + +for buildingId in buildings: + if "hub" in buildingId: + continue + roberts_cross("buildings/" + buildingId + "", "blueprints/" + buildingId + "") diff --git a/res_raw/sprites/misc/deletion_marker.png b/res_raw/sprites/misc/deletion_marker.png new file mode 100644 index 00000000..bb3a2b75 Binary files /dev/null and b/res_raw/sprites/misc/deletion_marker.png differ diff --git a/res_raw/sprites/misc/slot_good_arrow.png b/res_raw/sprites/misc/slot_good_arrow.png index 760ab171..3001e8b4 100644 Binary files a/res_raw/sprites/misc/slot_good_arrow.png and b/res_raw/sprites/misc/slot_good_arrow.png differ diff --git a/res_raw/sprites/misc/storage_overlay.png b/res_raw/sprites/misc/storage_overlay.png new file mode 100644 index 00000000..92b2ecf8 Binary files /dev/null and b/res_raw/sprites/misc/storage_overlay.png differ diff --git a/src/css/adinplay.scss b/src/css/adinplay.scss index 8b69bd84..c0deca58 100644 --- a/src/css/adinplay.scss +++ b/src/css/adinplay.scss @@ -40,7 +40,7 @@ &.waitingForFinish { .videoInner { - @include BorderRadius(4px); + @include S(border-radius, $globalBorderRadius); overflow: hidden; &::after { @@ -74,7 +74,7 @@ .adInner { @include BoxShadow3D(lighten($mainBgColor, 15)); - @include BorderRadius(4px); + @include S(border-radius, $globalBorderRadius); @include S(padding, 15px); // max-width: 960px; display: block !important; diff --git a/src/css/application_error.scss b/src/css/application_error.scss index a53e4aa2..b69a1cfe 100644 --- a/src/css/application_error.scss +++ b/src/css/application_error.scss @@ -53,7 +53,7 @@ font-family: monospace; text-align: left; @include S(padding, 6px); - @include BorderRadius(2px); + @include S(border-radius, $globalBorderRadius); @include BoxShadow3D(#eee); position: absolute; @include S(bottom, 25px); diff --git a/src/css/common.scss b/src/css/common.scss index 7b8bafd2..242bb9c7 100644 --- a/src/css/common.scss +++ b/src/css/common.scss @@ -72,6 +72,33 @@ body { overflow: hidden; @include Text; + &.externalAdOpen { + &::before { + text-transform: uppercase; + @include SuperSmallText; + content: "Loading Advertisement..."; + color: #333; + position: fixed; + top: 0; + pointer-events: all; + left: 0; + right: 0; + bottom: 0; + background: rgba(50, 60, 70, 0.8); + z-index: 9999; + display: flex; + justify-content: center; + align-items: center; + color: #fff; + + @include InlineAnimation(1s ease-in-out infinite) { + 50% { + transform: scale(1.05); + } + } + } + } + // For recording the bg video // filter: blur(5px); @@ -87,11 +114,6 @@ body { // } } -// Dirty hack -* { - @include TextShadow3DImpl; -} - img { -webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */ } @@ -174,20 +196,26 @@ button { box-sizing: content-box; @include S(padding, 3px, 10px); @include IncreasedClickArea(10px); - @include BorderRadius(4px); @include TextShadow3D(#fff, $borderColor: #28292a); @include ButtonText; - @include Button3D($accentColorBright); - border: #{D(1px)} solid rgba(0, 10, 20, 0.2); + border: 0; + background: $colorBlueBright; + color: #fff; + + // border: #{D(1px)} solid rgba(0, 10, 20, 0.2); @include S(border-bottom-width, 2px); - color: $accentColorDark; + // color: $accentColorDark; letter-spacing: 0.05em !important; - box-shadow: 0 #{D(1px)} #{D(2px)} 0 rgba(0, 10, 20, 0.2); + // box-shadow: 0 #{D(1px)} #{D(2px)} 0 rgba(0, 10, 20, 0.2); .keybinding { @include S(bottom, -2.5px); @include S(right, -2px); } + transition: opacity 0.12s ease-in-out; + &:hover { + opacity: 0.9; + } } ::selection { @@ -215,7 +243,7 @@ input[type="email"] { @include Text; @include IncreasedClickArea(15px); - @include BorderRadius(4px); + @include S(border-radius, $globalBorderRadius); &::placeholder { color: #fff; @@ -312,7 +340,7 @@ canvas { } ::-webkit-scrollbar-thumb { - border-radius: 4px; + // border-radius: 4px; background: #cdd0d4; } @@ -412,7 +440,7 @@ canvas { content: "Coming soon!"; z-index: 10000; background: rgba(lighten($mainBgColor, 0), 0.4); - @include BorderRadius(4px); + @include S(border-radius, $globalBorderRadius); position: absolute; display: flex; justify-content: center; @@ -485,7 +513,7 @@ canvas { bottom: 0; z-index: 1; @include BoxShadow3D($themeColor, $size: 1px); - @include BorderRadius(4px); + @include S(border-radius, $globalBorderRadius); transform-origin: 0% 50%; @@ -522,16 +550,16 @@ canvas { } .checkbox { - $bgColor: darken($mainBgColor, 0); + $bgColor: darken($mainBgColor, 3); background-color: $bgColor; - @include S(width, 45px); - @include S(height, 20px); + @include S(width, 35px); + @include S(height, 17px); display: flex; @include S(padding, 3px); box-sizing: content-box; cursor: pointer; pointer-events: all; - transition: opacity 0.2s ease-in-out, background-color 0.4s ease-in-out, box-shadow 0.4s ease-in-out !important; + transition: opacity 0.2s ease-in-out, background-color 0.3s ease-in-out, box-shadow 0.4s ease-in-out !important; position: relative; @include BorderRadius(20px); @include IncreasedClickArea(10px); @@ -540,9 +568,13 @@ canvas { opacity: 0.2; } + &:hover { + background-color: darken($bgColor, 5); + } + .knob { @include S(width, 20px); - @include S(height, 20px); + @include S(height, 17px); display: inline-block; transition: margin-left 0.4s ease-in-out !important; background: #fff; @@ -555,7 +587,11 @@ canvas { background-color: $themeColor; @include BoxShadow3D($themeColor, $size: 2px); .knob { - @include S(margin-left, 25px); + @include S(margin-left, 15px); + } + + &:hover { + background-color: lighten($themeColor, 15); } } } @@ -563,9 +599,9 @@ canvas { .keybinding { background: #fff; text-transform: uppercase; - @include S(padding, 2px, 1px, 2px); + @include S(padding, 1.5px, 3px, 2px); @include PlainText; - @include BorderRadius(2px); + @include S(border-radius, $globalBorderRadius); &, > span { @include S(font-size, 9px); @@ -589,7 +625,7 @@ canvas { box-sizing: border-box; @include S(height, 12px); overflow: hidden; - border: #{D(1px)} solid $accentColorDark; + border: #{D(0px)} solid $accentColorDark; .keybinding_space { @include S(font-size, 17px); diff --git a/src/css/icons.scss b/src/css/icons.scss index fa124f67..b2af1b64 100644 --- a/src/css/icons.scss +++ b/src/css/icons.scss @@ -1,22 +1,23 @@ -// $icons: ; - -// @each $icon in $icons { -// [data-icon="#{$icon}"] { -// background-image: uiResource("res/ui/#{$icon}"); -// } -// } - $buildings: belt, cutter, miner, mixer, painter, rotater, splitter, stacker, trash, underground_belt; @each $building in $buildings { + [data-icon="building_icons/#{$building}.png"] { + background-image: uiResource("res/ui/building_icons/#{$building}.png") !important; + } +} + +$buildingsAndVariants: belt, splitter, splitter-compact, splitter-compact-inverse, underground_belt, + underground_belt-tier2, miner, miner-chainable, cutter, cutter-quad, rotater, rotater-ccw, stacker, mixer, + painter, painter-double, painter-quad, trash, trash-storage; +@each $building in $buildingsAndVariants { [data-icon="building_tutorials/#{$building}.png"] { background-image: uiResource("res/ui/building_tutorials/#{$building}.png") !important; } } -$upgrades: belt, miner, painting, processors; -@each $upgrade in $upgrades { - [data-icon="upgrades/#{$upgrade}.png"] { - background-image: uiResource("res/ui/upgrades/#{$upgrade}.png") !important; +$icons: notification_saved, notification_success, notification_upgrade; +@each $icon in $icons { + [data-icon="icons/#{$icon}.png"] { + background-image: uiResource("res/ui/icons/#{$icon}.png") !important; } } diff --git a/src/css/ingame_hud/blur_overlay.scss b/src/css/ingame_hud/blur_overlay.scss deleted file mode 100644 index d821b6b8..00000000 --- a/src/css/ingame_hud/blur_overlay.scss +++ /dev/null @@ -1,8 +0,0 @@ -body.ingameDialogOpen { - #ingame_Canvas, - #ingame_HUD_GameMenu, - #ingame_HUD_KeybindingOverlay, - #ingame_HUD_buildings_toolbar { - filter: blur(5px); - } -} diff --git a/src/css/ingame_hud/building_placer.scss b/src/css/ingame_hud/building_placer.scss index f90f155f..bcfddc8a 100644 --- a/src/css/ingame_hud/building_placer.scss +++ b/src/css/ingame_hud/building_placer.scss @@ -1,35 +1,159 @@ -#ingame_HUD_building_placer { +#ingame_HUD_PlacementHints { position: fixed; - @include S(bottom, 60px); - left: 50%; - transform: translateX(-50%); + @include S(top, 60px); + @include S(right, 10px); - display: flex; - flex-direction: column; + display: grid; @include S(padding, 6px); - justify-content: center; - align-items: center; - background-color: $ingameHudBg; - @include S(border-radius, 4px); - background: #333; - @include S(width, 300px); + @include S(border-radius, $globalBorderRadius); + @include S(width, 240px); + @include S(grid-column-gap, 5px); + + background: rgba(#333438, 0.8); + grid-template-columns: 1fr auto; + grid-template-rows: auto 1fr; + + @include DarkThemeOverride { + background-color: #55585a; + } .buildingLabel { @include PlainText; + @include S(margin-bottom, 2px); color: #fff; text-transform: uppercase; - @include S(margin-bottom, 2px); + grid-column: 1 / 3; + grid-row: 1 / 2; } - .instructions, .description { - text-align: center; - color: mix($accentColorDark, $accentColorBright, 50%); + color: #bbb; @include SuperSmallText; + grid-column: 1 / 2; + grid-row: 2 / 3; + display: grid; + grid-template-rows: 1fr auto; + + strong { + color: #fff; + } + } + + .additionalInfo { + display: grid; + grid-template-columns: auto 1fr; + + label { + color: lighten($colorGreenBright, 10); + font-weight: bold; + @include S(margin-right, 5px); + } + } + + .hotkey { + color: lighten($colorGreenBright, 10); + font-weight: bold; + display: flex; + flex-direction: row; + align-items: center; + .keybinding { + position: relative; + @include S(margin-left, 5px); + } + } + + .buildingImage { + grid-column: 2 / 3; + grid-row: 1 / 3; + @include S(width, 100px); + @include S(height, 100px); + background: top left / 100% 100% no-repeat; + @include S(border-radius, $globalBorderRadius); } @include StyleBelowWidth(700px) { display: none !important; } } + +#ingame_HUD_PlacerVariants { + position: absolute; + @include S(right, 10px); + @include S(top, 200px); + display: flex; + @include S(grid-gap, 5px); + flex-direction: column; + align-items: flex-end; + + .explanation { + text-transform: uppercase; + grid-row: 1 / 2; + @include SuperSmallText; + text-align: right; + .keybinding { + position: relative; + } + + @include DarkThemeOverride { + color: rgba(#fff, 0.5); + } + } + + .variants { + display: grid; + @include S(grid-gap, 5px); + + .variant { + grid-row: 2 / 3; + @include S(border-radius, $globalBorderRadius); + background: rgba(0, 10, 20, 0.2); + display: inline-flex; + vertical-align: top; + position: relative; + align-items: center; + @include S(padding, 3px); + @include S(grid-gap, 10px); + + &.active { + background-color: rgba(74, 163, 223, 0.6); + } + + $iconSize: 25px; + + .iconWrap { + grid-column: 1 / 2; + grid-row: 1 / 2; + position: relative; + @include S(width, $iconSize); + @include S(height, $iconSize); + background: center center / contain no-repeat; + + &[data-tile-w="2"] { + @include S(width, 2 * $iconSize); + } + &[data-tile-h="2"] { + @include S(height, 2 * $iconSize); + } + &[data-tile-h="3"] { + @include S(height, 3 * $iconSize); + } + &[data-tile-w="3"] { + @include S(width, 3 * $iconSize); + } + &[data-tile-w="4"] { + @include S(width, 4 * $iconSize); + } + } + + .label { + display: none; + grid-column: 2 / 3; + text-transform: uppercase; + grid-row: 1 / 2; + @include SuperSmallText; + color: #fff; + } + } + } +} diff --git a/src/css/ingame_hud/buildings_toolbar.scss b/src/css/ingame_hud/buildings_toolbar.scss index 779333ba..d9f91fcc 100644 --- a/src/css/ingame_hud/buildings_toolbar.scss +++ b/src/css/ingame_hud/buildings_toolbar.scss @@ -7,14 +7,14 @@ $toolbarBg: rgba($accentColorBright, 0.9); display: flex; flex-direction: column; - background-color: $toolbarBg; - // border: $ingameHudBorder; + background-color: rgb(255, 255, 255); + background: transparent; border-bottom-width: 0; - - @include S(border-radius, 4px, 4px, 0, 0); - // box-shadow: 0 0 0 #{D(2px)} rgba(darken($toolbarBg, 20), 0.5); transition: transform 0.12s ease-in-out; + background: uiResource("toolbar_bg.lossless.png") center center / 100% 100% no-repeat; + @include S(padding, 20px, 100px, 0); + &:not(.visible) { transform: translateX(-50%) translateY(#{D(100px)}); } @@ -22,7 +22,7 @@ .buildings { display: grid; grid-auto-flow: column; - @include S(padding, 0, 5px); + @include S(margin-bottom, 2px); .building { color: $accentColorDark; @@ -32,20 +32,18 @@ align-items: center; justify-content: center; @include S(padding, 5px); - @include S(padding-bottom, 7px); - $buildingIconSize: 32px; + @include S(padding-bottom, 1px); + @include S(width, 35px); + @include S(height, 40px); + + background: center center / 70% no-repeat; &:not(.unlocked) { @include S(width, 30px); - .tooltip { - display: none !important; - } - .keybinding, - .iconWrap { - opacity: 0.01; - } + opacity: 0.8; + background-image: none !important; + &::before { - opacity: 0.5; content: " "; background: uiResource("locked_building.png") center center / #{D(20px)} #{D(20px)} no-repeat; @@ -58,124 +56,24 @@ } } - // &:first-child .tooltip { - // display: flex; - // } + @include S(border-radius, $globalBorderRadius); + + &.selected { + background-color: rgba($colorBlueBright, 0.3) !important; + transform: scale(1.05); + .keybinding { + color: #111; + } + } pointer-events: all; - transition: background-color 0.1s ease-in-out; + transition: all 0.05s ease-in-out; + transition-property: background-color, transform; + &.unlocked:hover { - background: rgba($accentColorDark, 0.1); + background-color: rgba($accentColorDark, 0.1); cursor: pointer; } - - .iconWrap { - position: relative; - @include S(width, $buildingIconSize); - @include S(height, $buildingIconSize); - @include S(margin-top, 3px); - @include S(margin-bottom, 6px); - } - - .label { - @include SuperSmallText; - display: none; - font-weight: bold; - text-transform: uppercase; - } - - .keybinding { - // position: relative; - right: 50%; - transform: translateX(50%); - background: transparent; - border: 0; - @include S(bottom, 2pxpx); - } - - &[data-tilewidth="2"] { - .iconWrap { - @include S(width, 2 * $buildingIconSize); - } - } - - &:last-child { - border: none; - } - - .tooltip { - position: absolute; - pointer-events: none; - background: #333; - @include S(padding, 7px); - bottom: calc(100% + #{D(10px)}); - left: 50%; - transform: translateX(-50%); - box-sizing: content-box; - @include SuperSmallText; - @include S(width, 200px); - @include S(border-radius, 4px); - - box-shadow: #{D(1px)} #{D(1px)} 0 0 rgba(0, 10, 25, 0.2); - - display: none; - z-index: 9999; - - flex-direction: column; - - .title { - color: #fff; - @include PlainText; - text-transform: uppercase; - margin-bottom: 5px; - } - - .desc { - color: #aaa; - @include SuperSmallText; - margin-bottom: 10px; - strong { - color: #fff; - } - } - - .tutorialImage { - display: inline-block; - @include S(width, 200px); - @include S(height, 200px); - @include S(border-radius, 4px); - background-size: contain; - background-repeat: no-repeat; - } - - &::after { - top: 100%; - left: 50%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - border-top-color: #333; - @include S(border-width, 5px); - transform: translateX(-50%); - } - } - - &:hover .tooltip { - display: flex; - - @include InlineAnimation(0.5s ease-in-out) { - 90% { - opacity: 0; - } - 0% { - transform: translate(-50%, 5%) scale(0.9); - opacity: 0; - } - } - } } } } diff --git a/src/css/ingame_hud/debug_info.scss b/src/css/ingame_hud/debug_info.scss new file mode 100644 index 00000000..37e3a07c --- /dev/null +++ b/src/css/ingame_hud/debug_info.scss @@ -0,0 +1,11 @@ +#ingame_HUD_DebugInfo { + position: absolute; + @include S(bottom, 5px); + @include S(left, 5px); + + font-size: 15px; + display: flex; + line-height: 15px; + flex-direction: column; + color: #fff; +} diff --git a/src/css/ingame_hud/dialogs.scss b/src/css/ingame_hud/dialogs.scss index b7aca1e5..868dac58 100644 --- a/src/css/ingame_hud/dialogs.scss +++ b/src/css/ingame_hud/dialogs.scss @@ -11,6 +11,29 @@ align-items: center; justify-content: center; + @include InlineAnimation(0.12s ease-in-out) { + 0% { + background-color: transparent; + opacity: 0.5; + } + 100% { + background-color: $modalDialogBg; + } + } + + @include DarkThemeOverride { + background: rgba(#33363d, 0.9); + @include InlineAnimation(0.12s ease-in-out) { + 0% { + background-color: transparent; + opacity: 0.5; + } + 100% { + background-color: rgba(#33363d, 0.9); + } + } + } + &.visible { .dialogInner { opacity: 1; @@ -22,30 +45,64 @@ opacity: 0; } + &.loadingDialog { + * { + color: #fff; + } + } + > .dialogInner { background: #fff; - @include S(min-width, 500px); - max-width: calc(100vw - #{D(50px)}); - max-height: calc(100vh - #{D(50px)}); - @include S(border-radius, 4px); + max-height: calc(100vh - #{D(40px)}); + @include S(border-radius, $globalBorderRadius); display: flex; flex-direction: column; - @include S(padding, 15px); + @include S(padding, 12px); pointer-events: all; + @include DarkThemeOverride { + background: #333438; + } + + &.optionChooserDialog { + .optionParent { + display: grid; + @include S(grid-gap, 5px); + grid-template-columns: 1fr 1fr; + .option { + pointer-events: all; + cursor: pointer; + @include S(padding, 10px); + background: #eee; + transition: background-color 0.12s ease-in-out; + + &:hover { + background-color: #e7e7e7; + } + + &.active { + background-color: $colorBlueBright; + color: #fff; + } + } + } + } + > .title { @include Heading; margin: 0; text-transform: uppercase; display: grid; + align-items: center; grid-template-columns: 1fr auto; @include S(margin-bottom, 10px); + @include DarkThemeInvert(); > .closeButton { opacity: 0.7; @include S(width, 20px); @include S(height, 20px); - background: uiResource("icons/close.png") center center / 60% no-repeat; + background: uiResource("icons/close.png") center center / 80% no-repeat; cursor: pointer; pointer-events: all; transition: opacity 0.2s ease-in-out; @@ -56,8 +113,67 @@ } > .content { + @include PlainText; overflow-y: auto; pointer-events: all; + @include S(width, 350px); + + > strong { + font-weight: bold; + } + } + + > .buttons { + @include S(margin-top, 15px); + display: flex; + justify-content: flex-end; + > button { + @include S(margin-left, 8px); + @include Text; + @include S(min-width, 60px); + @include S(padding, 5px, 15px); + + transition: opacity 0.12s ease-in-out; + &:hover { + opacity: 0.9; + } + + &.good { + background-color: $colorGreenBright; + color: #fff; + } + + &.bad { + background-color: $colorRedBright; + color: #fff; + } + + &.timedButton { + pointer-events: none; + cursor: default; + position: relative; + overflow: hidden; + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: unset; + z-index: 5; + content: " "; + display: inline-block; + background: rgba(#fff, 0.6); + @include InlineAnimation(5s linear) { + 0% { + width: 100%; + } + 100% { + width: 0%; + } + } + } + } + } } } } diff --git a/src/css/ingame_hud/entity_debugger.scss b/src/css/ingame_hud/entity_debugger.scss new file mode 100644 index 00000000..15f03848 --- /dev/null +++ b/src/css/ingame_hud/entity_debugger.scss @@ -0,0 +1,44 @@ +#ingame_HUD_EntityDebugger { + position: absolute; + @include S(right, 30px); + @include S(top, 200px); + + font-size: 14px; + line-height: 16px; + color: #fff; + background: rgba(0, 10, 20, 0.7); + padding: 10px; + &, + * { + pointer-events: all; + } + + .flag { + display: inline-block; + background: #333438; + @include S(padding, 2px); + @include S(margin-right, 2px); + + u { + opacity: 0.5; + } + } + + .components { + @include S(margin-top, 4px); + display: grid; + grid-template-columns: 1fr 1fr; + @include S(grid-gap, 3px); + .component { + @include S(padding, 2px); + background: #333; + display: flex; + flex-direction: column; + + .data { + @include S(width, 150px); + @include S(height, 130px); + } + } + } +} diff --git a/src/css/ingame_hud/game_menu.scss b/src/css/ingame_hud/game_menu.scss index 16ac3129..b191bc31 100644 --- a/src/css/ingame_hud/game_menu.scss +++ b/src/css/ingame_hud/game_menu.scss @@ -1,45 +1,110 @@ #ingame_HUD_GameMenu { position: absolute; top: 0; - left: 50%; - display: grid; - transform: translateX(-50%); - @include S(grid-gap, 3px); + right: 0; + display: flex; grid-auto-flow: column; - button { - background: $colorGreenBright; + > .menuButtons { + position: relative; + display: flex; + flex-grow: 1; + @include S(padding, 5px, 4px); + justify-content: flex-end; + @include S(margin-left, 20px); + + > .button { + @include S(width, 30px); + @include S(height, 30px); + display: inline-block; + background: center center / 60% no-repeat; + pointer-events: all; + cursor: pointer; + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; + opacity: 0.9; + @include S(margin-left, 5px); + position: relative; + + @include IncreasedClickArea(0px); + + @include DarkThemeInvert; + + &:hover { + opacity: 0.8; + } + &.music { + background-image: uiResource("icons/music_on.png"); + &.muted { + background-image: uiResource("icons/music_off.png"); + } + } + + &.sfx { + background-image: uiResource("icons/sound_on.png"); + &.muted { + background-image: uiResource("icons/sound_off.png"); + } + } + + &.save { + background-image: uiResource("icons/save.png"); + @include MakeAnimationWrappedEvenOdd(0.5s ease-in-out) { + 0% { + transform: scale(1, 1); + } + + 70% { + transform: scale(1.5, 1.5) rotate(20deg); + opacity: 0.2; + } + + 85% { + transform: scale(0.9, 0.9); + opacity: 1; + } + + 90% { + transform: scale(1.1, 1.1); + } + } + } + + &.settings { + background-image: uiResource("icons/settings.png"); + } + } + } + + > button { @include PlainText; color: #fff; border-color: rgba(0, 0, 0, 0.1); @include S(padding, 5px, 5px, 5px); + + @include S(padding-left, 30px); + @include S(margin-right, 3px); + @include IncreasedClickArea(0px); + @include ButtonText; + @include S(min-height, 30px); transition: all 0.12s ease-in-out; transition-property: opacity, transform; + display: inline-flex; + background: center #{D(13px)} / #{D(20px)} no-repeat; + background-color: $colorGreenBright; + + &[data-button-id="shop"] { + background-color: rgb(93, 103, 250); + background-image: uiResource("icons/shop.png"); + background-size: #{D(18px)}; + } + &[data-button-id="stats"] { + background-color: rgb(85, 199, 138); + background-image: uiResource("icons/statistics.png"); + } &:hover { opacity: 0.9; - transform: translateY(3px); - } - - @include IncreasedClickArea(10px); - @include ButtonText; - border: #{D(2px)} solid rgba(0, 10, 20, 0.2); - @include S(border-width, 2px); - border-radius: 0 0 #{D(4px)} #{D(4px)}; - @include S(border-top-width, 10px); - @include S(padding-left, 30px); - @include S(margin-top, -5px); - @include S(letter-spacing, 1px, $important: true); - background: center #{D(10px)} / #{D(20px)} no-repeat; - @include S(min-height, 47px); - - &[data-button-id="shop"] { - background-color: rgb(141, 70, 223); - background-image: uiResource("icons/shop.png"); - } - &[data-button-id="stats"] { - background-color: rgb(53, 235, 113); - background-image: uiResource("icons/statistics.png"); } .keybinding { @@ -48,12 +113,47 @@ border-top-left-radius: 0; border-top-right-radius: 0; bottom: unset; - // background: rgba(0, 10, 20, 0.5); background: transparent; - @include S(top, -5px); + @include S(top, 0px); right: unset; left: 50%; transform: translateX(-50%); } + + &:not(.hasBadge) .badge { + display: none; + } + + &.hasBadge { + transform-origin: 50% 0%; + @include InlineAnimation(1s ease-in-out infinite) { + 50% { + transform: scale(1.02); + } + } + + .badge { + position: absolute; + @include S(bottom, -8px); + left: 50%; + transform: translateX(-50%); + + background: #333; + @include PlainText; + display: flex; + justify-content: center; + align-items: center; + @include S(min-width, 5px); + @include S(height, 10px); + @include S(padding, 1px, 3px, 2px); + @include S(border-radius, $globalBorderRadius); + border: #{D(1px)} solid #fff; + @include InlineAnimation(1s ease-in-out infinite) { + 50% { + transform: translateX(-50%) scale(1.05); + } + } + } + } } } diff --git a/src/css/ingame_hud/keybindings_overlay.scss b/src/css/ingame_hud/keybindings_overlay.scss index db05aacf..960ee9fd 100644 --- a/src/css/ingame_hud/keybindings_overlay.scss +++ b/src/css/ingame_hud/keybindings_overlay.scss @@ -20,7 +20,7 @@ @include S(height, 10px); width: 1px; @include S(margin, 0, 3px); - background-color: #ccc; + background-color: #888; transform: rotate(10deg); // @include S(margin, 0, 3px); } @@ -38,10 +38,15 @@ background: #fff uiResource("icons/mouse_left.png") center center / 85% no-repeat; } } + label { color: $accentColorDark; @include SuperSmallText; text-transform: uppercase; + // font-weight: bold; + color: #fff; + text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.1); + @include S(margin-left, 5px); } } @@ -49,7 +54,8 @@ &:not(.placementActive) .binding.placementOnly { display: none; } - &.placementActive .binding.noPlacementOnly { + + &.placementActive .noPlacementOnly { display: none; } @@ -64,15 +70,15 @@ } } - .shift .keybinding { + .keybinding.builtinKey { transition: all 0.1s ease-in-out; transition-property: background-color, color, border-color; background: $colorRedBright; border-color: $colorRedBright; color: #fff; } - - &.shiftDown .shift .keybinding { - border-color: darken($colorRedBright, 40); - } +} + +body.uiHidden #ingame_HUD_KeybindingOverlay .binding:not(.hudToggle) { + display: none; } diff --git a/src/css/ingame_hud/mass_selector.scss b/src/css/ingame_hud/mass_selector.scss new file mode 100644 index 00000000..3c41893c --- /dev/null +++ b/src/css/ingame_hud/mass_selector.scss @@ -0,0 +1,23 @@ +#ingame_HUD_MassSelector { + position: absolute; + @include S(top, 50px); + left: 50%; + transform: translateX(-50%); + background: rgba(lighten(#f77, 5), 0.95); + @include S(border-radius, $globalBorderRadius); + @include S(padding, 6px, 10px); + @include SuperSmallText; + color: #fff; + // color: #f77; + + .keybinding { + vertical-align: middle; + @include S(margin, 0, 4px); + position: relative; + top: unset; + left: unset; + right: unset; + bottom: unset; + @include S(margin-top, -2px); + } +} diff --git a/src/css/ingame_hud/notifications.scss b/src/css/ingame_hud/notifications.scss new file mode 100644 index 00000000..4b8ee767 --- /dev/null +++ b/src/css/ingame_hud/notifications.scss @@ -0,0 +1,48 @@ +#ingame_HUD_Notifications { + position: absolute; + @include S(bottom, 60px); + @include S(right, 10px); + + .notification { + background: rgba(#333438, 0.8); + @include S(border-radius, $globalBorderRadius); + @include S(margin-top, 3px); + color: #fff; + @include SuperSmallText; + @include S(padding, 7px, 10px); + @include S(width, 150px); + + @include DarkThemeOverride { + background-color: rgba(#55595d, 0.8); + } + + &[data-icon] { + @include S(background-position-x, 8px); + background-position-y: center; + @include S(padding-left, 35px); + background-repeat: no-repeat; + @include S(background-size, 15px); + } + + transform-origin: 100% 50%; + opacity: 0; + @include InlineAnimation(3s ease-in-out) { + 0% { + opacity: 1; + } + + 87% { + opacity: 1; + transform: scale(1); + } + 95% { + transform: scale(1.05); + } + + 100% { + opacity: 0; + transform: scale(0.5); + } + } + } +} diff --git a/src/css/ingame_hud/pinned_shapes.scss b/src/css/ingame_hud/pinned_shapes.scss new file mode 100644 index 00000000..063711b4 --- /dev/null +++ b/src/css/ingame_hud/pinned_shapes.scss @@ -0,0 +1,83 @@ +#ingame_HUD_PinnedShapes { + position: absolute; + @include S(left, 9px); + @include S(top, 150px); + @include PlainText; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + > .shape { + position: relative; + display: grid; + align-items: center; + justify-content: center; + grid-template-columns: auto 1fr; + grid-template-rows: 1fr 1fr; + @include S(margin-bottom, 4px); + color: #fff; + text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2); + + &.unpinable { + > canvas { + cursor: pointer; + pointer-events: all; + } + } + + > canvas { + @include S(width, 25px); + @include S(height, 25px); + grid-column: 1 / 2; + grid-row: 1 / 3; + } + + > .amountLabel, + > .goalLabel { + @include S(margin-left, 5px); + @include SuperSmallText; + font-weight: bold; + display: inline-flex; + align-items: center; + flex-direction: row; + grid-column: 2 / 3; + @include S(height, 9px); + + @include DarkThemeOverride { + color: #eee; + } + } + + > .goalLabel { + @include S(font-size, 7px); + opacity: 0.5; + align-self: start; + justify-self: start; + font-weight: normal; + grid-row: 2 / 3; + } + + > .amountLabel { + align-self: end; + justify-self: start; + grid-row: 1 / 2; + } + + &.marked .amountLabel { + &::after { + content: " "; + position: absolute; + display: inline-block; + @include S(width, 9px); + @include S(height, 9px); + opacity: 0.8; + @include S(top, -4px); + @include S(left, -4px); + background: uiResource("icons/current_goal_marker.png") center center / contain no-repeat; + + @include DarkThemeInvert; + } + } + } +} diff --git a/src/css/ingame_hud/settings_menu.scss b/src/css/ingame_hud/settings_menu.scss new file mode 100644 index 00000000..e0cec1f6 --- /dev/null +++ b/src/css/ingame_hud/settings_menu.scss @@ -0,0 +1,41 @@ +#ingame_HUD_SettingsMenu { + .statsElement { + position: absolute; + @include S(left, 30px); + @include S(top, 30px); + color: #fff; + display: flex; + grid-template-rows: 1fr auto; + flex-direction: column; + + strong { + text-transform: uppercase; + @include PlainText; + opacity: 0.5; + } + + span { + @include S(margin-bottom, 25px); + @include Heading; + } + } + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .buttons { + display: grid; + grid-auto-flow: row; + @include S(grid-gap, 10px); + background: rgba(0, 10, 20, 0.1); + @include S(padding, 10px); + @include S(border-radius, $globalBorderRadius); + + button { + background-color: #eee; + color: #55585a; + } + } +} diff --git a/src/css/ingame_hud/shop.scss b/src/css/ingame_hud/shop.scss index c6ad0711..d27c98de 100644 --- a/src/css/ingame_hud/shop.scss +++ b/src/css/ingame_hud/shop.scss @@ -3,60 +3,72 @@ @include S(padding-right, 10px); display: flex; flex-direction: column; + @include S(width, 500px); + .upgrade { display: grid; grid-template-columns: auto 1fr auto; background: #eee; - @include S(border-radius, 3px); + @include S(border-radius, $globalBorderRadius); @include S(margin-bottom, 4px); @include S(padding, 5px, 10px); - @include S(grid-row-gap, 5px); - @include S(height, 95px); + @include S(grid-row-gap, 1px); + @include S(height, 85px); grid-template-rows: #{D(20px)} auto; &:last-child { margin-bottom: 0; } + @include DarkThemeOverride { + background: #55585a; + } + .title { - grid-column: 2 / 3; + grid-column: 1 / 3; grid-row: 1 / 2; - @include Heading; + @include PlainText; display: flex; align-items: center; + flex-direction: row-reverse; + justify-content: flex-end; + + @include DarkThemeOverride { + color: #fff; + } .tier { - @include S(margin-left, 5px); + @include S(margin-right, 9px); background: $colorGreenBright; - @include S(border-radius, 4px); + @include S(border-radius, $globalBorderRadius); text-transform: uppercase; @include PlainText; color: #fff; + text-align: center; font-weight: bold; - @include S(margin-top, 1px); - + @include S(width, 50px); @include S(padding, 0px, 5px); &[data-tier="0"] { background-color: rgb(73, 186, 190); } &[data-tier="1"] { - background-color: rgb(73, 94, 190); + background-color: rgb(88, 110, 207); } &[data-tier="2"] { - background-color: rgb(186, 73, 190); + background-color: rgb(189, 100, 192); } &[data-tier="3"] { - background-color: rgb(96, 190, 73); + background-color: rgb(117, 192, 98); } &[data-tier="4"] { - background-color: rgb(190, 91, 73); + background-color: rgb(243, 77, 48); } &[data-tier="5"] { - background-color: rgb(219, 184, 29); + background-color: rgb(255, 209, 6); } &[data-tier="6"] { - background-color: rgb(190, 73, 73); + background-color: rgb(44, 41, 46); } } } @@ -64,13 +76,15 @@ .icon { @include S(width, 40px); @include S(height, 40px); - background: center center / contain no-repeat; + background: center center / 80% no-repeat; align-self: center; justify-self: center; grid-column: 1 / 2; - grid-row: 1 / 4; - @include S(margin-right, 20px); - opacity: 0.2; + grid-row: 2 / 4; + @include S(margin-right, 30px); + @include S(margin-left, 10px); + opacity: 0.32; + display: none; } .description { @@ -87,7 +101,7 @@ grid-row: 3 / 4; display: grid; grid-auto-flow: column; - @include S(grid-gap, 15px); + @include S(grid-gap, 9px); justify-content: start; .requirement { @@ -95,6 +109,55 @@ display: flex; flex-direction: column; align-items: center; + @include S(width, 70px); + overflow: hidden; + + button.pin { + @include S(width, 12px); + @include S(height, 12px); + background: uiResource("icons/pin.png") center center / 95% no-repeat; + position: absolute; + @include S(top, 2px); + @include S(right, 2px); + opacity: 0.6; + cursor: pointer; + pointer-events: all; + @include IncreasedClickArea(5px); + transition: opacity 0.12s ease-in-out; + + @include DarkThemeInvert; + + &:hover { + opacity: 0.7; + } + + &.alreadyPinned { + opacity: 0.1 !important; + pointer-events: none; + cursor: default; + } + + &.pinned { + opacity: 0.1; + pointer-events: none; + cursor: default; + @include InlineAnimation(0.3s ease-in-out) { + 0% { + opacity: 1; + transform: scale(0.8); + } + + 30% { + opacity: 1; + transform: scale(1.2); + } + + 100% { + transform: scale(1); + } + } + } + } canvas { @include S(width, 40px); @@ -109,19 +172,26 @@ background: #e2e4e6; @include S(line-height, 13px); - @include S(border-radius, 2px); - @include S(padding, 0, 2px, 3px); + @include S(border-radius, $globalBorderRadius); + @include S(padding, 1px, 0px, 2px); position: relative; text-align: center; @include S(min-width, 50px); + // @include S(max-width, 100px); overflow: hidden; + width: 100%; + + @include DarkThemeOverride { + background: #333438; + color: #fff; + } .progressBar { bottom: 0; left: 0; right: 0; top: 0; - @include S(border-radius, 2px); + @include S(border-radius, $globalBorderRadius); position: absolute; display: inline-block; z-index: -1; @@ -129,6 +199,10 @@ transition-property: width, background-color; background: #bdbfca; + @include DarkThemeOverride { + background: #8c8d96; + } + &.complete { background-color: $colorGreenBright; } @@ -140,7 +214,7 @@ button.buy { grid-column: 3 / 4; grid-row: 3 / 4; - align-self: end; + align-self: center; justify-self: end; // @include S(padding, 4px, 5px); // @include PlainText; @@ -156,6 +230,19 @@ pointer-events: none; opacity: 0.3; } + + &.buyable { + @include InlineAnimation(1s ease-in-out infinite) { + 0% { + } + + 50% { + background-color: lighten($colorGreenBright, 10); + } + 100% { + } + } + } } &.maxLevel { diff --git a/src/css/ingame_hud/statistics.scss b/src/css/ingame_hud/statistics.scss new file mode 100644 index 00000000..fe56f393 --- /dev/null +++ b/src/css/ingame_hud/statistics.scss @@ -0,0 +1,173 @@ +#ingame_HUD_Statistics { + .content { + @include S(width, 500px); + } + + .filterHeader { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + justify-items: end; + + button { + @include S(height, 20px); + @include S(padding, 1px, 10px); + border: 0; + box-shadow: none; + border-radius: 0; + @include IncreasedClickArea(1px); + @include S(min-width, 30px); + color: #fff; + opacity: 0.25; + @include S(margin-left, 1px); + + &.displayIcons, + &.displayDetailed { + background: uiResource("icons/display_list.png") center center / #{D(15px)} no-repeat; + &.displayIcons { + background-image: uiResource("icons/display_icons.png"); + background-size: #{D(11.5px)}; + } + } + + background-color: #44484a !important; + transition: opacity 0.2s ease-in-out; + } + + .filtersDataSource, + .filtersDisplayMode { + display: flex; + padding: 0; + margin: 0; + + :first-child { + border-radius: #{D(2px)} 0 0 #{D(2px)} !important; + margin-left: 0 !important; + } + :last-child { + margin-right: 0 !important; + border-radius: 0 #{D(2px)} #{D(2px)} 0 !important; + } + } + } + + .sourceExplanation { + @include SuperSmallText(); + @include S(margin-top, 5px); + color: #aaa; + } + + .content { + @include S(margin-top, 10px); + @include S(height, 350px); + overflow-y: scroll; + display: flex; + flex-direction: column; + + justify-content: flex-start; + + @include S(padding-right, 4px); + + > .noEntries { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + @include PlainText; + color: #aaa; + } + + > div { + background: #f4f4f4; + @include S(border-radius, $globalBorderRadius); + @include S(margin-bottom, 4px); + display: grid; + + @include DarkThemeOverride { + background: #222428; + } + + grid-template-columns: 1fr auto; + @include S(padding, 5px); + &:last-child { + margin-bottom: 0; + } + + canvas.icon { + grid-column: 1 / 2; + grid-row: 1 / 2; + @include S(width, 40px); + @include S(height, 40px); + } + + .counter { + @include SuperSmallText; + + @include S(border-radius, $globalBorderRadius); + @include S(padding, 0, 3px); + } + } + } + + .dialogInner { + &[data-displaymode="detailed"] .displayDetailed, + &[data-displaymode="icons"] .displayIcons, + &[data-datasource="produced"] .modeProduced, + &[data-datasource="delivered"] .modeDelivered, + &[data-datasource="stored"] .modeStored { + opacity: 1; + } + + &[data-displaymode="icons"] .content.hasEntries { + display: grid; + grid-template-columns: repeat(6, 1fr); + grid-auto-rows: #{D(73px)}; + align-items: flex-start; + @include S(grid-column-gap, 3px); + > div { + @include S(grid-row-gap, 5px); + @include S(height, 60px); + grid-template-columns: 1fr; + grid-template-rows: 1fr auto; + justify-items: center; + align-items: center; + + .counter { + grid-column: 1 / 2; + grid-row: 2 / 3; + background: rgba(0, 10, 20, 0.05); + justify-self: end; + } + } + } + &[data-displaymode="detailed"] .content.hasEntries { + > div { + @include S(padding, 10px); + @include S(height, 40px); + grid-template-columns: auto 1fr auto; + @include S(grid-column-gap, 15px); + + .counter { + grid-column: 3 / 4; + grid-row: 1 / 2; + @include Heading; + align-self: center; + text-align: right; + color: #55595a; + } + + canvas.graph { + @include S(width, 270px); + @include S(height, 40px); + @include S(border-radius, 0, 0, 2px, 2px); + $color: rgba(0, 10, 20, 0.04); + // background: $color; + border: #{D(4px)} solid transparent; + // @include S(border-width, 1px, 0, 1px, 0); + @include S(margin-top, -3px); + } + } + } + } +} diff --git a/src/css/ingame_hud/tutorial_hints.scss b/src/css/ingame_hud/tutorial_hints.scss new file mode 100644 index 00000000..41cffacd --- /dev/null +++ b/src/css/ingame_hud/tutorial_hints.scss @@ -0,0 +1,111 @@ +#ingame_HUD_TutorialHints { + position: absolute; + @include S(left, 10px); + @include S(bottom, 50px); + display: flex; + flex-direction: column; + background: rgba(50, 60, 70, 0); + + transition: all 0.2s ease-in-out; + pointer-events: all; + + transition-property: background-color, transform, bottom, left; + + @include S(padding, 5px); + video { + transition: all 0.2s ease-in-out; + transition-property: opacity, width; + @include S(width, 0px); + opacity: 0; + z-index: 10; + position: relative; + } + + .header { + color: #333438; + display: grid; + align-items: center; + @include S(grid-gap, 2px); + grid-template-columns: 1fr; + @include S(margin-bottom, 3px); + z-index: 11; + position: relative; + + > span { + @include DarkThemeInvert; + + display: flex; + @include SuperSmallText; + justify-content: flex-start; + align-items: center; + &::before { + @include S(margin-right, 4px); + content: " "; + @include S(width, 12px); + @include S(height, 12px); + display: inline-block; + background: uiResource("icons/help.png") center center / 95% no-repeat; + } + } + + button.toggleHint { + @include PlainText; + } + } + + button.toggleHint { + .hide { + display: none; + } + } + + &.enlarged { + background: rgba(50, 60, 70, 0.9); + left: 50%; + bottom: 50%; + transform: translate(-50%, 50%); + + &::before { + pointer-events: all; + content: " "; + position: fixed; + top: -1000px; + left: -1000px; + right: -1000px; + bottom: -1000px; + z-index: 0; + + background: rgba(50, 60, 70, 0.3); + } + + .header { + grid-template-columns: 1fr auto; + > span { + display: none; + } + button.toggleHint { + grid-column: 2 / 3; + } + } + + video { + @include InlineAnimation(0.2s ease-in-out) { + 0% { + opacity: 0; + @include S(width, 0px); + } + } + + opacity: 1; + @include S(width, 500px); + } + button.toggleHint { + .hide { + display: block; + } + .show { + display: none; + } + } + } +} diff --git a/src/css/ingame_hud/unlock_notification.scss b/src/css/ingame_hud/unlock_notification.scss index f81232d0..5f72909d 100644 --- a/src/css/ingame_hud/unlock_notification.scss +++ b/src/css/ingame_hud/unlock_notification.scss @@ -4,11 +4,12 @@ left: 0; right: 0; bottom: 0; - background: rgba(#333538, 0.95) uiResource("dialog_bg_pattern.png") top left / #{D(10px)} repeat; + background: rgba(#333538, 0.98) uiResource("dialog_bg_pattern.png") top left / #{D(10px)} repeat; display: flex; justify-content: center; - align-items: flex-start; + align-items: center; pointer-events: all; + @include InlineAnimation(0.1s ease-in-out) { 0% { opacity: 0; @@ -16,7 +17,8 @@ } .dialog { - background: rgba(#333539, 0.5); + // background: rgba(#222428, 0.5); + @include S(border-radius, $globalBorderRadius); @include S(padding, 30px); @include InlineAnimation(0.5s ease-in-out) { @@ -31,7 +33,7 @@ .subTitle { @include SuperHeading; text-transform: uppercase; - @include S(font-size, 50px); + @include S(font-size, 40px); @include InlineAnimation(0.5s ease-in-out) { 0% { @@ -47,13 +49,12 @@ } .subTitle { - @include Heading; - background: $colorGreenBright; + @include PlainText; display: inline-block; - @include S(padding, 1px, 6px); - @include S(margin, 20px, 0, 20px); + @include S(margin, 0px, 0, 20px); + color: $colorGreenBright; - @include S(border-radius, 4px); + @include S(border-radius, $globalBorderRadius); @include InlineAnimation(0.5s ease-in-out) { 0% { transform: translateY(-60vh); @@ -81,14 +82,15 @@ transform: translateX(-2vw); } } - display: grid; - grid-template-columns: auto auto; + display: flex; + flex-direction: column; align-items: center; justify-content: center; @include S(grid-gap, 10px); - .reward { + .rewardName { grid-column: 1 / 3; + display: none; @include InlineAnimation(0.5s ease-in-out) { 0% { transform: translateX(200vw); @@ -103,32 +105,63 @@ } } - .buildingExplanation { - @include S(width, 200px); - @include S(height, 200px); - display: inline-block; - background-position: center center; - background-size: cover; - background-repeat: no-repeat; - @include S(border-radius, 4px); - box-shadow: #{D(2px)} #{D(3px)} 0 0 rgba(0, 0, 0, 0.15); + .rewardDesc { + grid-column: 1 / 3; + @include PlainText; + @include S(margin-bottom, 15px); + color: #aaacaf; + @include S(width, 400px); + text-align: left; + strong { + color: #fff; + } + } + + .images { + display: flex; + .buildingExplanation { + @include S(width, 200px); + @include S(height, 200px); + display: inline-block; + background-position: center center; + background-size: cover; + background-repeat: no-repeat; + @include S(border-radius, $globalBorderRadius); + box-shadow: #{D(2px)} #{D(3px)} 0 0 rgba(0, 0, 0, 0.15); + } } } button.close { border: 0; - @include InlineAnimation(0.5s ease-in-out) { - 0% { - transform: translateY(50vh); - } - 50% { - transform: translateY(-5vh); - } - 75% { - transform: translateY(2vh); + position: relative; + @include S(margin-top, 30px); + + &:not(.unlocked) { + pointer-events: none; + opacity: 0.8; + cursor: default; + } + + &::after { + content: " "; + display: inline-block; + position: absolute; + top: 0; + left: 100%; + right: 0; + bottom: 0; + background: rgba(0, 10, 20, 0.8); + + @include InlineAnimation(10s linear) { + 0% { + left: 0; + } + 100% { + left: 100%; + } } } - @include S(margin-top, 30px); } } } diff --git a/src/css/ingame_hud/vignette_overlay.scss b/src/css/ingame_hud/vignette_overlay.scss new file mode 100644 index 00000000..ce569fd4 --- /dev/null +++ b/src/css/ingame_hud/vignette_overlay.scss @@ -0,0 +1,14 @@ +#ingame_VignetteOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: uiResource("vignette.lossless.png") center center / cover no-repeat; + pointer-events: none; + + @include DarkThemeOverride { + // Too many artifacts + display: none; + } +} diff --git a/src/css/ingame_hud/watermark.scss b/src/css/ingame_hud/watermark.scss new file mode 100644 index 00000000..4d0e83a2 --- /dev/null +++ b/src/css/ingame_hud/watermark.scss @@ -0,0 +1,18 @@ +#ingame_HUD_Watermark { + position: absolute; + background: uiResource("get_on_itch_io.svg") center center / contain no-repeat; + @include S(width, 110px); + @include S(height, 40px); + @include S(top, 10px); + pointer-events: all; + cursor: pointer; + @include S(left, 160px); + + transition: all 0.12s ease-in; + transition-property: opacity, transform; + transform: skewX(-0.5deg); + &:hover { + transform: skewX(-1deg) scale(1.02); + opacity: 0.9; + } +} diff --git a/src/css/main.scss b/src/css/main.scss index 6d99f6a5..3f06853f 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -23,6 +23,11 @@ @import "states/preload"; @import "states/main_menu"; @import "states/ingame"; +@import "states/keybindings"; +@import "states/settings"; +@import "states/about"; +@import "states/mobile_warning"; +@import "states/changelog"; @import "ingame_hud/buildings_toolbar"; @import "ingame_hud/building_placer"; @@ -31,13 +36,49 @@ @import "ingame_hud/unlock_notification"; @import "ingame_hud/shop"; @import "ingame_hud/game_menu"; -@import "ingame_hud/blur_overlay"; @import "ingame_hud/dialogs"; +@import "ingame_hud/mass_selector"; +@import "ingame_hud/vignette_overlay"; +@import "ingame_hud/statistics"; +@import "ingame_hud/pinned_shapes"; +@import "ingame_hud/notifications"; +@import "ingame_hud/settings_menu"; +@import "ingame_hud/debug_info"; +@import "ingame_hud/entity_debugger"; +@import "ingame_hud/tutorial_hints"; +@import "ingame_hud/watermark"; -// Z-Index -$elements: ingame_Canvas, ingame_HUD_building_placer_overlay, ingame_HUD_building_placer, - ingame_HUD_buildings_toolbar, ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, ingame_HUD_Shop, - ingame_HUD_BetaOverlay, ingame_HUD_UnlockNotification; +// prettier-ignore +$elements: +// Base +ingame_Canvas, +ingame_VignetteOverlay, + +// Ingame overlays +ingame_HUD_PlacementHints, +ingame_HUD_PlacerVariants, + +// Regular hud +ingame_HUD_PinnedShapes, +ingame_HUD_GameMenu, +ingame_HUD_KeybindingOverlay, +ingame_HUD_Notifications, +ingame_HUD_MassSelector, +ingame_HUD_DebugInfo, +ingame_HUD_EntityDebugger, +ingame_HUD_TutorialHints, +ingame_HUD_buildings_toolbar, +ingame_HUD_Watermark, + +// Overlays +ingame_HUD_BetaOverlay, + +// Dialogs +ingame_HUD_UnlockNotification, +ingame_HUD_Shop, +ingame_HUD_Statistics, +ingame_HUD_SettingsMenu, +ingame_HUD_ModalDialogs; $zindex: 100; @@ -48,3 +89,23 @@ $zindex: 100; $zindex: $zindex + 10; } + +body.uiHidden { + #ingame_HUD_buildings_toolbar, + #ingame_HUD_PlacementHints, + #ingame_HUD_GameMenu, + #ingame_HUD_MassSelector, + #ingame_HUD_PinnedShapes, + #ingame_HUD_Notifications, + #ingame_HUD_TutorialHints { + display: none !important; + } +} + +body.modalDialogActive, +body.externalAdOpen, +body.ingameDialogOpen { + > *:not(.ingameDialog):not(.modalDialogParent):not(.loadingDialog):not(.gameLoadingOverlay):not(#ingame_HUD_ModalDialogs):not(.noBlur) { + filter: blur(5px) !important; + } +} diff --git a/src/css/mixins.scss b/src/css/mixins.scss index 9881ffea..dc5eeb86 100644 --- a/src/css/mixins.scss +++ b/src/css/mixins.scss @@ -253,38 +253,9 @@ button, } @mixin TextShadow3D($color: rgb(222, 234, 238), $borderColor: #000) { - // @if $borderColor != #000 { - // @include TextShadow3DImpl($color: $color, $borderColor: $borderColor); - // } color: $color; } -@mixin TextShadow3DImpl( - $color: rgb(222, 234, 238), - $scale: 1, - $additionalShadowAlpha: 1, - $borderColor: #222428 -) { - // color: $text3dColor; - - $borderColor: rgba(15, 18, 23, 0.9); - - // $shadowColor: darken($color, 40%); - - $border: 0.07em; - $borderMid: $border * 1.14; - - $drop1: $borderMid + 0.02; - $drop2: $borderMid + 0.06em; - - // text-shadow: #{$border} #{$border} 0 $borderColor, #{-$border} #{$border} 0 $borderColor, #{$border} #{-$border} 0 $borderColor, - // #{-$border} #{-$border} 0 $borderColor, 0 #{$borderMid} 0 $borderColor, 0 #{-$borderMid} 0 $borderColor, - // #{$borderMid} 0 0 $borderColor, #{-$borderMid} 0 0 $borderColor, 0 #{$drop1} 0 $borderColor, #{$borderMid} #{$drop1} 0 $borderColor, - // #{-$borderMid} #{$drop1} 0 $borderColor, 0 #{$drop2} 0 $borderColor, #{$borderMid} #{$drop2} 0 $borderColor, - // #{-$borderMid} #{$drop2} 0 $borderColor, -0.2em 0.13em 0 rgba(#111, 0.25); // 0px 0.07em 0px $shadowColor, - // 0px 0.15em 0.09em rgba(#333539, $additionalShadowAlpha);; -} - // ---------------------------------------- /* Shine animation prefab, useful for buttons etc. Adds a bright shine which moves over the button like a reflection. Performance heavy. */ @@ -377,3 +348,16 @@ button, @content; } } + +@mixin DarkThemeOverride { + @at-root body[data-theme="dark"] &, + &[data-theme="dark"] { + @content; + } +} + +@mixin DarkThemeInvert { + @include DarkThemeOverride { + filter: invert(1); + } +} diff --git a/src/css/states/about.scss b/src/css/states/about.scss new file mode 100644 index 00000000..aeac1e38 --- /dev/null +++ b/src/css/states/about.scss @@ -0,0 +1,9 @@ +#state_AboutState { + > .container .content { + @include PlainText; + } + + a { + @include S(margin, 0, 3px); + } +} diff --git a/src/css/states/changelog.scss b/src/css/states/changelog.scss new file mode 100644 index 00000000..69b7864c --- /dev/null +++ b/src/css/states/changelog.scss @@ -0,0 +1,31 @@ +#state_ChangelogState { + .content { + display: flex; + flex-direction: column; + } + .entry { + padding: 20px; + .version { + @include Heading; + } + .date { + @include PlainText; + &::before { + content: " | "; + } + color: #aaabaf; + } + + .changes { + @include SuperSmallText; + @include S(padding-left, 20px); + strong { + background: $colorBlueBright; + color: #fff; + text-transform: uppercase; + @include S(padding, 1px, 2px); + @include S(margin-right, 3px); + } + } + } +} diff --git a/src/css/states/ingame.scss b/src/css/states/ingame.scss index 04e9383a..8b0a614c 100644 --- a/src/css/states/ingame.scss +++ b/src/css/states/ingame.scss @@ -13,4 +13,19 @@ background: $mainBgColor; flex-direction: column; } + + #ingame_Canvas { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + #ingame_HUD_ModalDialogs { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } } diff --git a/src/css/states/keybindings.scss b/src/css/states/keybindings.scss new file mode 100644 index 00000000..f5ee3170 --- /dev/null +++ b/src/css/states/keybindings.scss @@ -0,0 +1,54 @@ +#state_KeybindingsState { + .content { + .topEntries { + display: grid; + grid-template-columns: 1fr auto; + @include S(grid-gap, 5px); + @include S(margin-bottom, 10px); + } + + .hint { + display: block; + background: #eee; + @include S(padding, 4px); + @include PlainText; + } + + .category { + .entry { + display: grid; + @include S(margin-top, 2px); + @include S(padding-top, 2px); + @include S(grid-gap, 4px); + grid-template-columns: 1fr #{D(100px)} auto auto; + border-bottom: #{D(1px)} dotted #eee; + color: #888c8f; + .mapping { + color: $colorBlueBright; + text-align: center; + } + + button { + @include S(height, 15px); + @include S(width, 15px); + @include IncreasedClickArea(0px); + background: transparent center center / 40% no-repeat; + opacity: 0.9; + &.editKeybinding { + background-image: uiResource("icons/edit_key.png"); + } + + &.resetKeybinding { + background-image: uiResource("icons/reset_key.png"); + } + + &.disabled { + pointer-events: none; + cursor: default; + opacity: 0.1 !important; + } + } + } + } + } +} diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index 4e027a4d..cb8b0b06 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -1,27 +1,348 @@ #state_MainMenuState { display: flex; - justify-content: center; align-items: center; + justify-content: center; flex-direction: column; - background: uiResource("menu_bg.noinline.jpg") center center / cover no-repeat !important; + // background: #aaacb4 center center / cover !important; + background: #bbc2cf center center / cover !important; + + .settingsButton, + .exitAppButton { + position: absolute; + @include S(top, 30px); + @include S(right, 30px); + @include S(width, 35px); + @include S(height, 35px); + pointer-events: all; + cursor: pointer; + background: uiResource("icons/main_menu_settings.png") center center / contain no-repeat; + transition: opacity 0.12s ease-in-out; + &:hover { + opacity: 0.9; + } + } + + .exitAppButton { + @include S(right, 100px); + background-image: uiResource("icons/main_menu_exit.png"); + } + + .fullscreenBackgroundVideo { + // display: none !important; + z-index: -1; + position: fixed; + right: 50%; + bottom: 50%; + min-width: 100%; + min-height: 100%; + + opacity: 0; + display: none; + transform: translate(50%, 50%); + filter: blur(10px); + + $opacity: 0.2; + &.loaded { + display: block; + opacity: $opacity; + + @include InlineAnimation(0.1s ease-in-out) { + 0% { + opacity: 0; + } + 100% { + opacity: $opacity; + } + } + } + } + + .mainWrapper { + @include S(padding, 0, 10px); + align-items: center; + justify-items: center; + + &.noDemo { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + &.demo { + @include S(grid-column-gap, 10px); + display: grid; + grid-template-columns: 1fr 1fr; + } + + .standaloneBanner { + background: rgb(255, 234, 245); + @include S(border-radius, $globalBorderRadius); + height: 100%; + box-sizing: border-box; + @include S(width, 300px); + @include S(padding, 15px); + + display: flex; + flex-direction: column; + + strong { + font-weight: bold; + @include S(margin, 0, 4px); + } + + h3 { + @include Heading; + font-weight: bold; + @include S(margin-bottom, 5px); + text-transform: uppercase; + color: $colorRedBright; + } + + p { + @include Text; + } + + ul { + @include S(margin-top, 5px); + @include S(padding-left, 20px); + li { + @include Text; + } + } + + .steamLink { + width: 100%; + @include S(height, 50px); + + background: uiResource("get_on_itch_io.svg") center center / contain no-repeat; + overflow: hidden; + display: block; + text-indent: -999em; + cursor: pointer; + @include S(margin-top, 20px); + pointer-events: all; + transition: all 0.12s ease-in; + transition-property: opacity, transform; + transform: skewX(-0.5deg); + &:hover { + transform: skewX(-1deg) scale(1.02); + opacity: 0.9; + } + } + } + } .logo { + display: flex; + flex-grow: 1; + align-items: center; + justify-content: center; + flex-direction: column; + @include S(padding-top, 20px); img { @include S(width, 350px); } + + .demoBadge { + @include S(margin, 10px, 0); + @include S(width, 100px); + @include S(height, 30px); + background: uiResource("demo_badge.png") center center / contain no-repeat; + display: inline-block; + } + } + + .betaWarning { + @include S(width, 400px); + @include PlainText; + background: $colorRedBright; + @include S(padding, 10px); + @include S(border-radius, $globalBorderRadius); + color: #fff; + @include S(margin-top, 10px); + border: #{D(2px)} solid rgba(0, 10, 20, 0.1); } .mainContainer { - @include S(margin-top, 40px); display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background: #fafafa; + @include S(padding, 20px); + @include S(border-radius, $globalBorderRadius); + // border: #{D(2px)} solid rgba(0, 10, 20, 0.1); + height: 100%; + width: 100%; + box-sizing: border-box; + + .browserWarning { + @include S(margin-bottom, 10px); + background-color: $colorRedBright; + @include PlainText; + color: #fff; + @include S(border-radius, $globalBorderRadius); + @include S(padding, 5px); + @include S(width, 300px); + } .playButton { @include SuperHeading; - @include S(width, 150px); + @include S(width, 130px); @include S(padding, 15px, 20px); + letter-spacing: 0.3em !important; + + font-weight: bold; color: #fff; - background-color: $accentColorDark; + background-color: $colorGreenBright; + transition: transform 0.12s ease-in-out; + &:hover { + transform: scale(1.02); + } + } + + .importButton { + @include S(margin-top, 15px); + } + + .savegames { + @include S(max-height, 105px); + overflow-y: auto; + @include S(width, 250px); + pointer-events: all; + @include S(padding-right, 5px); + display: grid; + grid-auto-flow: row; + @include S(grid-gap, 5px); + @include S(margin-top, 10px); + + .savegame { + background: #eee; + @include S(border-radius, $globalBorderRadius); + @include S(padding, 5px); + display: grid; + grid-template-columns: 1fr auto auto; + grid-template-rows: auto auto; + @include S(grid-column-gap, 5px); + @include S(grid-row-gap, 3px); + + .internalId { + grid-column: 1 / 2; + grid-row: 2 / 3; + @include SuperSmallText; + opacity: 0.5; + } + + .updateTime { + grid-column: 1 / 2; + grid-row: 1 / 2; + @include PlainText; + } + + button.resumeGame, + button.downloadGame, + button.deleteGame { + grid-column: 3 / 4; + grid-row: 1 / 3; + @include S(width, 30px); + @include S(height, 30px); + padding: 0; + align-self: center; + @include IncreasedClickArea(0px); + background: #44484a uiResource("icons/play.png") center center / 40% no-repeat; + } + + button.downloadGame { + grid-column: 2 / 3; + grid-row: 1 / 2; + background-image: uiResource("icons/download.png"); + @include S(width, 15px); + @include IncreasedClickArea(0px); + @include S(height, 15px); + align-self: end; + background-size: 60%; + } + + button.deleteGame { + grid-column: 2 / 3; + grid-row: 2 / 3; + background-color: $colorRedBright; + @include IncreasedClickArea(0px); + background-image: uiResource("icons/delete.png"); + @include S(width, 15px); + @include S(height, 15px); + align-self: end; + background-size: 60%; + } + } + } + } + + .footer { + display: flex; + flex-grow: 1; + justify-content: center; + align-items: flex-end; + width: 100%; + + .author { + flex-grow: 1; + text-align: right; + @include S(padding-right, 10px); + } + + @include S(padding, 15px); + > a { + display: grid; + align-items: center; + grid-template-columns: 1fr auto; + + justify-content: center; + background: #fafafa; + @include S(padding, 5px); + @include S(padding-left, 10px); + @include S(border-radius, $globalBorderRadius); + @include S(margin-left, 10px); + @include SuperSmallText(); + + font-weight: bold; + text-transform: uppercase; + color: #616266; + + transition: all 0.12s ease-in-out; + transition-property: background-color, transform; + pointer-events: all; + @include S(width, 120px); + @include S(height, 50px); + cursor: pointer; + &:hover { + background-color: #fff; + transform: scale(1.01); + } + + &:not(.boxLink) { + align-self: flex-end; + justify-self: flex-end; + height: unset; + width: unset; + @include S(padding, 3px); + } + + .thirdpartyLogo { + display: inline-block; + @include S(width, 50px); + @include S(height, 50px); + background: center center / 80% no-repeat; + &.githubLogo { + background-image: uiResource("main_menu/github.png"); + } + &.discordLogo { + background-image: uiResource("main_menu/discord.png"); + background-size: 95%; + } + } } } } diff --git a/src/css/states/mobile_warning.scss b/src/css/states/mobile_warning.scss new file mode 100644 index 00000000..2e68b56a --- /dev/null +++ b/src/css/states/mobile_warning.scss @@ -0,0 +1,48 @@ +#state_MobileWarningState { + display: flex; + align-items: center; + background: #333438 !important; + @include S(padding, 20px); + box-sizing: border-box; + justify-content: center; + flex-direction: column; + + .logo { + width: 80%; + max-width: 200px; + margin-bottom: 10px; + } + + p { + color: #aaacaf; + display: block; + margin-bottom: 13px; + font-size: 16px; + line-height: 20px; + max-width: 300px; + text-align: left; + a { + color: $colorBlueBright; + } + } + + .standaloneLink { + width: 200px; + height: 80px; + min-height: 40px; + background: uiResource("get_on_itch_io.svg") center center / contain no-repeat; + overflow: hidden; + display: block; + text-indent: -999em; + cursor: pointer; + margin-top: 10px; + pointer-events: all; + transition: all 0.12s ease-in; + transition-property: opacity, transform; + transform: skewX(-0.5deg); + &:hover { + transform: skewX(-1deg) scale(1.02); + opacity: 0.9; + } + } +} diff --git a/src/css/states/preload.scss b/src/css/states/preload.scss index 25b13ead..075a363f 100644 --- a/src/css/states/preload.scss +++ b/src/css/states/preload.scss @@ -6,6 +6,39 @@ } } + .changelogDialogEntry { + margin-top: 10px; + width: 100%; + flex-direction: column; + text-align: left; + padding: 10px; + box-sizing: border-box; + background: #eef1f4; + + .version { + @include Heading; + } + .date { + @include PlainText; + &::before { + content: " | "; + } + color: #aaabaf; + } + + .changes { + @include SuperSmallText; + @include S(padding-left, 15px); + strong { + background: $colorBlueBright; + color: #fff; + text-transform: uppercase; + @include S(padding, 1px, 2px); + @include S(margin-right, 3px); + } + } + } + .failureBox { .logo { img { @@ -32,7 +65,7 @@ @include BoxShadow3D(#fff); @include S(padding, 15px); - @include BorderRadius(4px); + @include S(border-radius, $globalBorderRadius); @include DropShadow; .errorHeader { @@ -79,6 +112,7 @@ @include Button3D($colorRedBright); @include PlainText; @include S(padding, 5px, 8px, 4px); + color: #fff; } } } diff --git a/src/css/states/settings.scss b/src/css/states/settings.scss new file mode 100644 index 00000000..e52152c3 --- /dev/null +++ b/src/css/states/settings.scss @@ -0,0 +1,91 @@ +#state_SettingsState { + .content { + .versionbar { + @include S(margin-top, 20px); + @include SuperSmallText; + display: grid; + align-items: center; + grid-template-columns: 1fr auto; + .buildVersion { + display: flex; + flex-direction: column; + color: #aaadaf; + } + } + + button.about { + background-color: $colorGreenBright; + } + + .setting { + @include S(padding, 10px); + background: #eeeff5; + @include S(border-radius, $globalBorderRadius); + @include S(margin-bottom, 5px); + + label { + text-transform: uppercase; + @include Text; + } + + .desc { + @include S(margin-top, 5px); + @include SuperSmallText; + color: #aaadb2; + } + + > .row { + display: grid; + align-items: center; + grid-template-columns: 1fr auto; + } + + &.disabled { + // opacity: 0.3; + pointer-events: none; + * { + pointer-events: none !important; + cursor: default !important; + } + position: relative; + .standaloneOnlyHint { + @include PlainText; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: all; + display: flex; + align-items: center; + justify-content: center; + background: rgba(#fff, 0.5); + text-transform: uppercase; + color: $colorRedBright; + } + } + + .value.enum { + background: #fff; + @include PlainText; + display: flex; + align-items: flex-start; + pointer-events: all; + cursor: pointer; + justify-content: center; + @include S(min-width, 100px); + @include S(border-radius, $globalBorderRadius); + @include S(padding, 4px); + @include S(padding-right, 15px); + + background: #fff uiResource("icons/enum_selector.png") calc(100% - #{D(5px)}) + calc(50% + #{D(1px)}) / #{D(15px)} no-repeat; + + transition: background-color 0.12s ease-in-out; + &:hover { + background-color: #fafafa; + } + } + } + } +} diff --git a/src/css/textual_game_state.scss b/src/css/textual_game_state.scss index 27cd27ba..897c379a 100644 --- a/src/css/textual_game_state.scss +++ b/src/css/textual_game_state.scss @@ -5,263 +5,60 @@ align-items: center; $padding: 15px; - .bottomPoppingInNotification { - position: absolute; - left: 50%; - text-align: center; - @include BoxShadow3D(mix(lighten($mainBgColor, 12), $colorRedBright, 50%)); - @include S(padding, 10px); - max-width: #{D(280px)}; - @include S(bottom, 30px); - box-sizing: border-box; - width: 100%; - @include PlainText; - @include BorderRadius(4px); - - $baseTransform: translateX(-50%); - transform-origin: 0% 100%; - transform: translateY(500%); - opacity: 0; - - display: block; - @include InlineAnimation(5s ease-in-out) { - 0% { - opacity: 0; - transform: scale(0) skew(5deg, 5deg) translateY(100%) $baseTransform; - } - 8% { - transform: scale(1.05) translateY(-2%) $baseTransform; - } - 12% { - transform: scale(1) $baseTransform; - opacity: 1; - } - 97% { - transform: scale(1) $baseTransform; - opacity: 1; - } - 100% { - opacity: 0; - transform: scale(0) skew(5deg, 5deg) translateY(100%) $baseTransform; - } - } + .headerBar, + > .container .content { + @include S(width, 500px); } - .widthKeeper { - width: 100%; - height: 100%; - + .headerBar { display: flex; - flex-direction: column; - overflow: hidden; + align-items: center; + justify-content: flex-start; - box-sizing: content-box; - @include S(max-width, 1000px); - - @include StyleAtHeight(800px) { - @include S(padding-top, 30px); - } - - .headerBar { + h1 { display: flex; - - @include VerticalStyle { - // margin-top: 1px; - } - - // margin-bottom: 15px; - padding: $padding; - - $h: 25px; - @include S(min-height, $h); - @include S(max-height, $h); - - align-items: center; - justify-content: center; - position: relative; - z-index: 50; - background: transparent; - - @include S(padding-top, $padding); - @include S(padding-left, $padding); - @include S(padding-right, $padding); - background-size: calc(100% - #{D(6px)}) 100%; - $paddingBottom: 20px; - - @include S(padding-bottom, $paddingBottom); - @include S(margin-bottom, -$h - $padding - $paddingBottom); - - h1 { - // text-align: center; - cursor: pointer; - // transform-origin: 0px 50%; - pointer-events: all; - @include S(padding, 5px, 0px, 5px, 30px); - @include S(left, -2px); - @include S(min-width, 100px); - position: relative; - @include IncreasedClickArea(25px); - text-transform: uppercase; - // background: uiResource("back_arrow.png") center center no-repeat; - @include S(background-position-x, -3px); - @include S(background-size, 25px, 25px); - - // Due to back button - color: $text3dColor; - @include TextShadow3D($borderColor: #18151d); - @include SuperHeading; - @include StyleBelowWidth(380px) { - @include Heading; - } - } - - .grow { - flex-grow: 1; - } - } - - .container { - text-align: left; - flex-direction: column; pointer-events: all; - box-sizing: border-box; - z-index: 25; + align-items: center; + cursor: pointer; + @include SuperHeading; + text-transform: uppercase; + color: #333438; position: relative; - - @include S(padding-left, 0px); - @include S(padding-right, 0px); - - height: 100%; - @include SupportsAndroidNotchQuery { - height: calc( - 100% - constant(safe-area-inset-top) - constant(safe-area-inset-bottom) - - var(--notch-inset-top) - var(--notch-inset-bottom) - ); - } - @include SupportsiOsNotchQuery { - height: calc( - 100% - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - - var(--notch-inset-top) - var(--notch-inset-bottom) - ); - } - - .loadingIndicator { - display: none; - } - - .errorIndicator { - display: none; - flex-direction: column; - text-align: center; - - .errorInner { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - @include S(max-width, 350px); - - strong { - $col: #ff4564; - @include TextShadow3D($col); - @include Heading; - } - - i { - @include PlainText; - color: #888; - @include S(margin-top, 10px); - display: inline-block; - } - } - } - - .loadingIndicator, - .errorIndicator { - box-sizing: border-box; - justify-content: center; - align-items: center; - height: 100%; - @include S(padding, 30px); - } - - // Loading state - &.loading { - .mainContent { - animation: none; - display: none !important; - } - .loadingIndicator { - display: flex; - } - } - - // Error state - &.errored { - .mainContent { - animation: none; - display: none !important; - } - - .errorIndicator { - animation: none; - display: flex; - } - } + @include IncreasedClickArea(10px); } - .mainContent { - overflow-y: auto !important; - overflow-x: hidden; - @include S(padding, $padding); - height: 100%; - width: 100%; - box-sizing: border-box; - - @include InlineAnimation(0.4s ease-in-out) { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } - } - - .category_label { - display: block; - @include S(margin-top, 40px); - - text-transform: uppercase; - &:first-child { - margin-top: 0 !important; - } - - @include S(margin-bottom, 16px); - @include Heading; - - @include TextShadow3D(#68a1bb, $borderColor: #141718); - } - - .cardbox { - @include S(padding, 20px, 15px); - $cardBg: lighten($mainBgColor, 9); - background: $cardBg; - margin-bottom: 15px; - @include S(margin-bottom, 15px); - @include BorderRadius(4px); - @include S(padding-bottom, 14px); - @include BoxShadow3D($cardBg); - - &:last-child { - border-bottom: 0; - } - } + .backButton { + @include S(width, 30px); + @include S(height, 30px); + @include S(margin-right, 10px); + @include S(margin-left, -5px); + background: uiResource("icons/state_back_button.png") center center / 70% no-repeat; } + + @include S(margin-bottom, 20px); } - &.hasTitle { - .mainContent { - @include S(padding-top, 70px, $important: true); + > .container { + > .content { + background: #fff; + @include S(border-radius, $globalBorderRadius); + @include S(padding, 10px); + height: calc(80vh - #{D(60px)}); + overflow-y: auto; + box-sizing: border-box; + pointer-events: all; + + a { + color: $colorBlueBright; + } + + .categoryLabel { + display: block; + text-transform: uppercase; + @include S(margin-top, 15px); + @include S(margin-bottom, 15px); + @include Heading; + } } } } diff --git a/src/css/variables.scss b/src/css/variables.scss index 392393d6..03213900 100644 --- a/src/css/variables.scss +++ b/src/css/variables.scss @@ -1,3 +1,5 @@ +$globalBorderRadius: 0px; + // When to reduce control elements size for small devices $layoutExpandMinWidth: 340px; @@ -18,7 +20,6 @@ $plainTextLineHeight: 17px; $supersmallTextFontSize: 10px; $supersmallTextLineHeight: 13px; - $buttonFontSize: 14px; $buttonLineHeight: 18px; @@ -30,15 +31,16 @@ $mainBgColor: #dee1ea; $accentColorBright: #e1e4ed; $accentColorDark: #7d808a; $colorGreenBright: #66bb6a; +$colorBlueBright: rgb(74, 163, 223); $colorRedBright: #ef5072; $themeColor: #393747; -$ingameHudBg: rgba($accentColorBright, 0.9); +$ingameHudBg: rgba($accentColorBright, 0.1); $ingameHudBorder: #{D(1.5px)} solid $accentColorDark; $text3dColor: #f4ffff; // Dialog properties -$modalDialogBg: rgba(#666a73, 0.8); +$modalDialogBg: rgba(160, 165, 180, 0.8); $dialogBgColor: lighten($mainBgColor, 10); $lightFontWeight: normal; diff --git a/src/html/index.html b/src/html/index.html index 1d4553c0..ffcab1d7 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -1,7 +1,7 @@ - shapez.io + shapez.io - Build your own shape factory! @@ -19,13 +19,16 @@ + - diff --git a/src/html/index.standalone.html b/src/html/index.standalone.html new file mode 100644 index 00000000..cde8e595 --- /dev/null +++ b/src/html/index.standalone.html @@ -0,0 +1,22 @@ + + + + shapez.io Standalone + + + + + + + + + + + + + + + diff --git a/src/js/.gitignore b/src/js/.gitignore new file mode 100644 index 00000000..d71c2f1e --- /dev/null +++ b/src/js/.gitignore @@ -0,0 +1 @@ +built-temp diff --git a/src/js/application.js b/src/js/application.js index 75d47ab0..27be4b1c 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -1,5 +1,5 @@ import { AnimationFrame } from "./core/animation_frame"; -import { performanceNow } from "./core/builtins"; +import { performanceNow, Math_min } from "./core/builtins"; import { GameState } from "./core/game_state"; import { GLOBAL_APP, setGlobalApp } from "./core/globals"; import { InputDistributor } from "./core/input_distributor"; @@ -27,7 +27,17 @@ import { AnalyticsInterface } from "./platform/analytics"; import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics"; import { Loader } from "./core/loader"; import { GameAnalyticsInterface } from "./platform/game_analytics"; -import { GameAnalyticsDotCom } from "./platform/browser/game_analytics"; +import { ShapezGameAnalytics } from "./platform/browser/game_analytics"; +import { queryParamOptions } from "./core/query_parameters"; +import { NoGameAnalytics } from "./platform/browser/no_game_analytics"; +import { StorageImplBrowserIndexedDB } from "./platform/browser/storage_indexed_db"; +import { SettingsState } from "./states/settings"; +import { KeybindingsState } from "./states/keybindings"; +import { AboutState } from "./states/about"; +import { PlatformWrapperImplElectron } from "./platform/electron/wrapper"; +import { StorageImplElectron } from "./platform/electron/storage"; +import { MobileWarningState } from "./states/mobile_warning"; +import { ChangelogState } from "./states/changelog"; const logger = createLogger("application"); @@ -117,11 +127,26 @@ export class Application { // Start with empty ad provider this.adProvider = new NoAdProvider(this); - this.storage = new StorageImplBrowser(this); + + if (G_IS_STANDALONE) { + this.storage = new StorageImplElectron(this); + } else { + if (window.indexedDB) { + this.storage = new StorageImplBrowserIndexedDB(this); + } else { + this.storage = new StorageImplBrowser(this); + } + } this.sound = new SoundImplBrowser(this); - this.platformWrapper = new PlatformWrapperImplBrowser(this); + + if (G_IS_STANDALONE) { + this.platformWrapper = new PlatformWrapperImplElectron(this); + } else { + this.platformWrapper = new PlatformWrapperImplBrowser(this); + } this.analytics = new GoogleAnalyticsImpl(this); - this.gameAnalytics = new GameAnalyticsDotCom(this); + + this.gameAnalytics = new ShapezGameAnalytics(this); } /** @@ -129,7 +154,16 @@ export class Application { */ registerStates() { /** @type {Array} */ - const states = [PreloadState, MainMenuState, InGameState]; + const states = [ + PreloadState, + MobileWarningState, + MainMenuState, + InGameState, + SettingsState, + KeybindingsState, + AboutState, + ChangelogState, + ]; for (let i = 0; i < states.length; ++i) { this.stateMgr.register(states[i]); @@ -194,6 +228,7 @@ export class Application { * @param {Event} event */ handleVisibilityChange(event) { + window.focus(); const pageVisible = !document[pageHiddenPropName]; if (pageVisible !== this.pageVisible) { this.pageVisible = pageVisible; @@ -233,6 +268,7 @@ export class Application { onAppRenderableStateChanged(renderable) { logger.log("Application renderable:", renderable); + window.focus(); if (!renderable) { this.stateMgr.getCurrentState().onAppPause(); } else { @@ -263,8 +299,7 @@ export class Application { logSection("BEFORE UNLOAD HANDLER", "#f77"); if (!G_IS_DEV && this.stateMgr.getCurrentState().getHasUnloadConfirmation()) { - if (G_IS_STANDALONE) { - } else { + if (!G_IS_STANDALONE) { // Need to show a "Are you sure you want to exit" event.preventDefault(); event.returnValue = "Are you sure you want to exit?"; @@ -276,17 +311,25 @@ export class Application { * Boots the application */ boot() { + console.log("Booting ..."); this.registerStates(); this.registerEventListeners(); Loader.linkAppAfterBoot(this); - this.stateMgr.moveToState("PreloadState"); + // Check for mobile + if (IS_MOBILE) { + this.stateMgr.moveToState("MobileWarningState"); + } else { + this.stateMgr.moveToState("PreloadState"); + } // Starting rendering this.ticker.frameEmitted.add(this.onFrameEmitted, this); this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this); this.ticker.start(); + + window.focus(); } /** diff --git a/src/js/changelog.js b/src/js/changelog.js new file mode 100644 index 00000000..f2744136 --- /dev/null +++ b/src/js/changelog.js @@ -0,0 +1,38 @@ +export const CHANGELOG = [ + { + version: "1.0.4", + date: "unreleased", + entries: [ + "Balancing Reduce cost of first painting upgrade, and change 'Shape Processing' to 'Cutting, Rotating & Stacking'", + "Tutorial Add dialog after completing level 2 to check out the upgrades tab.", + ], + }, + { + version: "1.0.3", + date: "24.05.2020", + entries: [ + "Balancing Reduced the amount of shapes required for the first 5 levels to make it easier to get into the game.", + ], + }, + { + version: "1.0.2", + date: "23.05.2020", + entries: [ + "Introduced changelog", + "Removed 'early access' label because the game isn't actually early access - its in a pretty good state already! (No worries, a lot more updates will follow!)", + "Added a 'Show hint' button which shows a small video for almost all levels to help out", + "Now showing proper descriptions when completing levels, with instructions on what the gained reward does.", + "Show a landing page on mobile devices about the game not being ready to be played on mobile yet", + "Fix painters and mixers being affected by the shape processors upgrade and not the painter one", + "Added 'multiplace' setting which is equivalent to holding SHIFT all the time", + "Added keybindings to zoom in / zoom out", + "Tunnels now also show connection lines to tunnel exits, instead of just tunnel entries", + "Lots of minor fixes and improvements", + ], + }, + { + version: "1.0.1", + date: "21.05.2020", + entries: ["Initial release!"], + }, +]; diff --git a/src/js/core/async_compression.js b/src/js/core/async_compression.js index 479ee80b..4afed1ea 100644 --- a/src/js/core/async_compression.js +++ b/src/js/core/async_compression.js @@ -84,39 +84,6 @@ class AsynCompression { }); } - /** - * Compresses regulary - * @param {string} text - */ - compressX64Async(text) { - if (text.length < 1024) { - // Ok so this is not worth it - return Promise.resolve(compressX64(text)); - } - return this.internalQueueJob("compressX64", text); - } - - /** - * Compresses with checksum - * @param {any} obj - */ - compressWithChecksum(obj) { - const stringified = JSON_stringify(obj); - return this.internalQueueJob("compressWithChecksum", stringified); - } - - /** - * Compresses with checksum - * @param {any} data The packets data - * @param {number} packetId The numeric packet id - */ - compressPacket(data, packetId) { - return this.internalQueueJob("compressPacket", { - data, - packetId, - }); - } - /** * Queues a new job * @param {string} job diff --git a/src/js/core/atlas_definitions.js b/src/js/core/atlas_definitions.js index 043b3996..42cd2bce 100644 --- a/src/js/core/atlas_definitions.js +++ b/src/js/core/atlas_definitions.js @@ -22,17 +22,10 @@ export class AtlasDefinition { } } -// @ts-ignore export const atlasFiles = require + // @ts-ignore .context("../../../res_built/atlas/", false, /.*\.json/i) .keys() .map(f => f.replace(/^\.\//gi, "")) .map(f => require("../../../res_built/atlas/" + f)) .map(data => new AtlasDefinition(data)); - -// export const atlasDefinitions = { -// qualityPreload: atlasFiles.filter((atlas) => atlas.meta.image.indexOf("_preload") >= 0), -// qualityLow: atlasFiles.filter((atlas) => atlas.meta.image.indexOf("_low") >= 0), -// qualityMedium: atlasFiles.filter((atlas) => atlas.meta.image.indexOf("_medium") >= 0), -// qualityHigh: atlasFiles.filter((atlas) => atlas.meta.image.indexOf("_high") >= 0), -// }; diff --git a/src/js/core/background_resources_loader.js b/src/js/core/background_resources_loader.js index 44bb598e..28d414f2 100644 --- a/src/js/core/background_resources_loader.js +++ b/src/js/core/background_resources_loader.js @@ -22,16 +22,11 @@ const essentialMainMenuSounds = [ const essentialBareGameAtlases = atlasFiles; const essentialBareGameSprites = G_ALL_UI_IMAGES; -const essentialBareGameSounds = [MUSIC.gameBg]; +const essentialBareGameSounds = [MUSIC.theme]; const additionalGameSprites = []; -const additionalGameSounds = []; -for (const key in SOUNDS) { - additionalGameSounds.push(SOUNDS[key]); -} -for (const key in MUSIC) { - additionalGameSounds.push(MUSIC[key]); -} +// @ts-ignore +const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)]; export class BackgroundResourcesLoader { /** diff --git a/src/js/core/buffer_maintainer.js b/src/js/core/buffer_maintainer.js index 3be1c08f..c92a92a5 100644 --- a/src/js/core/buffer_maintainer.js +++ b/src/js/core/buffer_maintainer.js @@ -1,13 +1,7 @@ import { GameRoot } from "../game/root"; -import { - makeOffscreenBuffer, - freeCanvas, - getBufferVramUsageBytes, - getBufferStats, - clearBufferBacklog, -} from "./buffer_utils"; +import { clearBufferBacklog, freeCanvas, getBufferStats, makeOffscreenBuffer } from "./buffer_utils"; import { createLogger } from "./logging"; -import { round2Digits, round1Digit } from "./utils"; +import { round1Digit } from "./utils"; /** * @typedef {{ @@ -19,7 +13,7 @@ import { round2Digits, round1Digit } from "./utils"; const logger = createLogger("buffers"); -const bufferGcDurationSeconds = 3; +const bufferGcDurationSeconds = 10; export class BufferMaintainer { /** diff --git a/src/js/core/click_detector.js b/src/js/core/click_detector.js index abe666da..557c1f28 100644 --- a/src/js/core/click_detector.js +++ b/src/js/core/click_detector.js @@ -4,6 +4,8 @@ import { Signal } from "../core/signal"; import { fastArrayDelete, fastArrayDeleteValueIfContained } from "./utils"; import { Vector } from "./vector"; import { IS_MOBILE } from "./config"; +import { SOUNDS } from "../platform/sound"; +import { GLOBAL_APP } from "./globals"; const logger = createLogger("click_detector"); @@ -36,6 +38,7 @@ export let clickDetectorGlobals = { * targetOnly?: boolean, * maxDistance?: number, * clickSound?: string, + * preventClick?: boolean, * }} ClickDetectorConstructorArgs */ @@ -53,6 +56,7 @@ export class ClickDetector { * @param {boolean=} param1.targetOnly Whether to also accept clicks on child elements (e.target !== element) * @param {number=} param1.maxDistance The maximum distance in pixels to accept clicks * @param {string=} param1.clickSound Sound key to play on touchdown + * @param {boolean=} param1.preventClick Whether to prevent click events */ constructor( element, @@ -63,7 +67,8 @@ export class ClickDetector { captureTouchmove = false, targetOnly = false, maxDistance = MAX_MOVE_DISTANCE_PX, - clickSound = null, + clickSound = SOUNDS.uiClick, + preventClick = false, } ) { assert(element, "No element given!"); @@ -76,6 +81,7 @@ export class ClickDetector { this.targetOnly = targetOnly; this.clickSound = clickSound; this.maxDistance = maxDistance; + this.preventClick = preventClick; // Signals this.click = new Signal(); @@ -126,6 +132,10 @@ export class ClickDetector { this.element.removeEventListener("mousemove", this.handlerTouchMove, options); } + if (this.preventClick) { + this.element.removeEventListener("click", this.handlerPreventClick, options); + } + this.click.removeAll(); this.touchstart.removeAll(); this.touchmove.removeAll(); @@ -140,6 +150,15 @@ export class ClickDetector { // INTERNAL METHODS + /** + * + * @param {Event} event + */ + internalPreventClick(event) { + window.focus(); + event.preventDefault(); + } + /** * Internal method to get the options to pass to an event listener */ @@ -162,6 +181,11 @@ export class ClickDetector { this.handlerTouchMove = this.internalOnPointerMove.bind(this); this.handlerTouchCancel = this.internalOnTouchCancel.bind(this); + if (this.preventClick) { + this.handlerPreventClick = this.internalPreventClick.bind(this); + element.addEventListener("click", this.handlerPreventClick, options); + } + element.addEventListener("touchstart", this.handlerTouchStart, options); element.addEventListener("touchend", this.handlerTouchEnd, options); element.addEventListener("touchcancel", this.handlerTouchCancel, options); @@ -278,6 +302,8 @@ export class ClickDetector { * @param {TouchEvent|MouseEvent} event */ internalOnPointerDown(event) { + window.focus(); + if (!this.internalEventPreHandler(event, 1)) { return false; } @@ -321,8 +347,7 @@ export class ClickDetector { // If we should play any sound, do this if (this.clickSound) { - throw new Error("TODO: Play sounds on click"); - // GLOBAL_APP.sound.playUiSound(this.clickSound); + GLOBAL_APP.sound.playUiSound(this.clickSound); } return false; @@ -347,6 +372,8 @@ export class ClickDetector { * @param {TouchEvent|MouseEvent} event */ internalOnPointerEnd(event) { + window.focus(); + if (!this.internalEventPreHandler(event, 0)) { return false; } diff --git a/src/js/core/config.js b/src/js/core/config.js index b9153cfa..922870d8 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -5,8 +5,20 @@ export const IS_DEBUG = (window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) && window.location.search.indexOf("nodebug") < 0; +export const IS_DEMO = + (G_IS_PROD && !G_IS_STANDALONE) || + (typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0); + const smoothCanvas = true; +export const THIRDPARTY_URLS = { + discord: "https://discord.gg/HN7EVzV", + github: "https://github.com/tobspr/shapez.io", + + // standaloneStorePage: "https://steam.shapez.io", + standaloneStorePage: "https://tobspr.itch.io/shapez.io", +}; + export const globalConfig = { // Size of a single tile in Pixels. // NOTICE: Update webpack.production.config too! @@ -18,32 +30,37 @@ export const globalConfig = { assetsSharpness: 1.2, shapesSharpness: 1.4, - // [Calculated] physics step size - physicsDeltaMs: 0, - physicsDeltaSeconds: 0, + // Production analytics + statisticsGraphDpi: 2.5, + statisticsGraphSlices: 100, + analyticsSliceDurationSeconds: 10, - // Update physics at N fps, independent of rendering - physicsUpdateRate: 60, + minimumTickRate: 25, + maximumTickRate: 500, // Map - mapChunkSize: 32, - mapChunkPrerenderMinZoom: 0.7, + mapChunkSize: 16, + mapChunkPrerenderMinZoom: 1.3, mapChunkOverviewMinZoom: 0.7, // Belt speeds // NOTICE: Update webpack.production.config too! - beltSpeedItemsPerSecond: 1, + beltSpeedItemsPerSecond: 2, itemSpacingOnBelts: 0.63, minerSpeedItemsPerSecond: 0, // COMPUTED - undergroundBeltMaxTiles: 5, + undergroundBeltMaxTilesByTier: [5, 8], buildingSpeeds: { - cutter: 1 / 6, - rotater: 1 / 2, - painter: 1 / 3, - mixer: 1 / 2, - stacker: 1 / 5, + cutter: 1 / 4, + cutterQuad: 1 / 4, + rotater: 1 / 1, + rotaterCCW: 1 / 1, + painter: 1 / 6, + painterDouble: 1 / 8, + painterQuad: 1 / 8, + mixer: 1 / 5, + stacker: 1 / 6, }, // Zooming @@ -66,20 +83,26 @@ export const globalConfig = { debug: { /* dev:start */ - fastGameEnter: true, - noArtificialDelays: true, - disableSavegameWrite: false, - showEntityBounds: false, - showAcceptorEjectors: false, - usePlainShapeIds: true, - disableMusic: true, - doNotRenderStatics: false, - disableZoomLimits: false, - showChunkBorders: false, - rewardsInstant: false, - allBuildingsUnlocked: true, + // fastGameEnter: true, + // noArtificialDelays: true, + // disableSavegameWrite: true, + // showEntityBounds: true, + // showAcceptorEjectors: true, + // disableMusic: true, + // doNotRenderStatics: true, + // disableZoomLimits: true, + // showChunkBorders: true, + // rewardsInstant: true, + // allBuildingsUnlocked: true, upgradesNoCost: true, - disableUnlockDialog: true, + // disableUnlockDialog: true, + // disableLogicTicks: true, + // testClipping: true, + // framePausesBetweenTicks: 40, + // testTranslations: true, + // enableEntityInspector: true, + // testAds: true, + disableMapOverview: true, /* dev:end */ }, @@ -90,6 +113,9 @@ export const globalConfig = { // Savegame salt sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF", + + // Analytics key + analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a", }, }; @@ -97,8 +123,9 @@ export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); // Automatic calculations -globalConfig.physicsDeltaMs = 1000.0 / globalConfig.physicsUpdateRate; -globalConfig.physicsDeltaSeconds = 1.0 / globalConfig.physicsUpdateRate; +globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5; -globalConfig.minerSpeedItemsPerSecond = - globalConfig.beltSpeedItemsPerSecond / globalConfig.itemSpacingOnBelts / 6; +if (globalConfig.debug.disableMapOverview) { + globalConfig.mapChunkOverviewMinZoom = 0; + globalConfig.mapChunkPrerenderMinZoom = 0; +} diff --git a/src/js/core/draw_parameters.js b/src/js/core/draw_parameters.js index 5e06c59f..dcdf6d13 100644 --- a/src/js/core/draw_parameters.js +++ b/src/js/core/draw_parameters.js @@ -1,4 +1,5 @@ import { Rectangle } from "./rectangle"; +import { globalConfig } from "./config"; /* typehints:start */ import { GameRoot } from "../game/root"; @@ -21,5 +22,9 @@ export class DrawParameters { // FIXME: Not really nice /** @type {GameRoot} */ this.root = root; + + if (G_IS_DEV && globalConfig.debug.testClipping) { + this.visibleRect = this.visibleRect.expandedInAllDirections(-100); + } } } diff --git a/src/js/core/game_state.js b/src/js/core/game_state.js index 81744089..cd3cb677 100644 --- a/src/js/core/game_state.js +++ b/src/js/core/game_state.js @@ -252,7 +252,7 @@ export class GameState { * @returns {string|null} */ getThemeMusic() { - return MUSIC.mainMenu; + return MUSIC.menu; } //////////////////// diff --git a/src/js/core/input_distributor.js b/src/js/core/input_distributor.js index 668cdcec..e0152774 100644 --- a/src/js/core/input_distributor.js +++ b/src/js/core/input_distributor.js @@ -25,6 +25,7 @@ export class InputDistributor { this.shiftIsDown = false; this.altIsDown = false; + this.ctrlIsDown = false; this.bindToEvents(); } @@ -175,7 +176,9 @@ export class InputDistributor { * Handles when the page got blurred */ handleBlur() { + this.ctrlIsDown = false; this.shiftIsDown = false; + this.altIsDown = false; this.forwardToReceiver("pageBlur", {}); this.forwardToReceiver("shiftUp", {}); } @@ -187,12 +190,18 @@ export class InputDistributor { if (event.keyCode === 16) { this.shiftIsDown = true; } + if (event.keyCode === 17) { + this.ctrlIsDown = true; + } + if (event.keyCode === 18) { + this.altIsDown = true; + } if ( // TAB event.keyCode === 9 || // F1 - F10 - (event.keyCode >= 112 && event.keyCode < 122 && !G_IS_DEV) + (event.keyCode >= 112 && event.keyCode < 122) ) { event.preventDefault(); } @@ -225,6 +234,14 @@ export class InputDistributor { this.shiftIsDown = false; this.forwardToReceiver("shiftUp", {}); } + if (event.keyCode === 17) { + this.ctrlIsDown = false; + this.forwardToReceiver("ctrlUp", {}); + } + if (event.keyCode === 18) { + this.altIsDown = false; + this.forwardToReceiver("altUp", {}); + } this.forwardToReceiver("keyup", { keyCode: event.keyCode, diff --git a/src/js/core/input_receiver.js b/src/js/core/input_receiver.js index f2fdf32f..1a373905 100644 --- a/src/js/core/input_receiver.js +++ b/src/js/core/input_receiver.js @@ -10,6 +10,8 @@ export class InputReceiver { this.keyup = new Signal(); this.pageBlur = new Signal(); this.shiftUp = new Signal(); + this.altUp = new Signal(); + this.ctrlUp = new Signal(); // Dispatched on destroy this.destroyed = new Signal(); diff --git a/src/js/core/loader.js b/src/js/core/loader.js index 5c8e17ec..8888ecbf 100644 --- a/src/js/core/loader.js +++ b/src/js/core/loader.js @@ -84,7 +84,7 @@ class LoaderImpl { return Promise.race([ new Promise((resolve, reject) => { - setTimeout(reject, G_IS_DEV ? 3000 : 60000); + setTimeout(reject, G_IS_DEV ? 500 : 10000); }), new Promise(resolve => { diff --git a/src/js/core/logging.js b/src/js/core/logging.js index e84d4159..20786241 100644 --- a/src/js/core/logging.js +++ b/src/js/core/logging.js @@ -240,10 +240,10 @@ function logInternal(handle, consoleMethod, args) { ...args ); } else { - if (G_IS_DEV && !globalConfig.debug.disableLoggingLogSources) { - consoleMethod.call(console, "%c" + context, "color: " + labelColor, ...args); - } else { - consoleMethod.call(console, ...args); - } + // if (G_IS_DEV && !globalConfig.debug.disableLoggingLogSources) { + consoleMethod.call(console, "%c" + context, "color: " + labelColor, ...args); + // } else { + // consoleMethod.call(console, ...args); + // } } } diff --git a/src/js/core/modal_dialog_elements.js b/src/js/core/modal_dialog_elements.js new file mode 100644 index 00000000..c6549131 --- /dev/null +++ b/src/js/core/modal_dialog_elements.js @@ -0,0 +1,430 @@ +/* typehints:start */ +import { Application } from "../application"; +/* typehints:end */ + +import { Signal, STOP_PROPAGATION } from "./signal"; +import { arrayDeleteValue, waitNextFrame } from "./utils"; +import { ClickDetector } from "./click_detector"; +import { SOUNDS } from "../platform/sound"; +import { InputReceiver } from "./input_receiver"; +import { FormElement } from "./modal_dialog_forms"; +import { globalConfig } from "./config"; +import { getStringForKeyCode } from "../game/key_action_mapper"; +import { createLogger } from "./logging"; +import { T } from "../translations"; + +const kbEnter = 13; +const kbCancel = 27; + +const logger = createLogger("dialogs"); + +/** + * Basic text based dialog + */ +export class Dialog { + /** + * + * Constructs a new dialog with the given options + * @param {object} param0 + * @param {Application} param0.app + * @param {string} param0.title Title of the dialog + * @param {string} param0.contentHTML Inner dialog html + * @param {Array} param0.buttons + * Button list, each button contains of up to 3 parts seperated by ':'. + * Part 0: The id, one of the one defined in dialog_buttons.yaml + * Part 1: The style, either good, bad or misc + * Part 2 (optional): Additional parameters seperated by '/', available are: + * timeout: This button is only available after some waiting time + * kb_enter: This button is triggered by the enter key + * kb_escape This button is triggered by the escape key + * @param {string=} param0.type The dialog type, either "info" or "warn" + * @param {boolean=} param0.closeButton Whether this dialog has a close button + */ + constructor({ app, title, contentHTML, buttons, type = "info", closeButton = false }) { + this.app = app; + this.title = title; + this.contentHTML = contentHTML; + this.type = type; + this.buttonIds = buttons; + this.closeButton = closeButton; + + this.closeRequested = new Signal(); + this.buttonSignals = {}; + + for (let i = 0; i < buttons.length; ++i) { + if (G_IS_DEV && globalConfig.debug.disableTimedButtons) { + this.buttonIds[i] = this.buttonIds[i].replace(":timeout", ""); + } + + const buttonId = this.buttonIds[i].split(":")[0]; + this.buttonSignals[buttonId] = new Signal(); + } + + this.timeouts = []; + this.clickDetectors = []; + + this.inputReciever = new InputReceiver("dialog-" + this.title); + + this.inputReciever.keydown.add(this.handleKeydown, this); + + this.enterHandler = null; + this.escapeHandler = null; + } + + /** + * Internal keydown handler + * @param {object} param0 + * @param {number} param0.keyCode + * @param {boolean} param0.shift + * @param {boolean} param0.alt + */ + handleKeydown({ keyCode, shift, alt }) { + if (keyCode === kbEnter && this.enterHandler) { + this.internalButtonHandler(this.enterHandler); + return STOP_PROPAGATION; + } + + if (keyCode === kbCancel && this.escapeHandler) { + this.internalButtonHandler(this.escapeHandler); + return STOP_PROPAGATION; + } + } + + internalButtonHandler(id, ...payload) { + this.app.inputMgr.popReciever(this.inputReciever); + + if (id !== "close-button") { + this.buttonSignals[id].dispatch(...payload); + } + this.closeRequested.dispatch(); + } + + createElement() { + const elem = document.createElement("div"); + elem.classList.add("ingameDialog"); + + this.dialogElem = document.createElement("div"); + this.dialogElem.classList.add("dialogInner"); + + if (this.type) { + this.dialogElem.classList.add(this.type); + } + elem.appendChild(this.dialogElem); + + const title = document.createElement("h1"); + title.innerText = this.title; + title.classList.add("title"); + this.dialogElem.appendChild(title); + + if (this.closeButton) { + this.dialogElem.classList.add("hasCloseButton"); + + const closeBtn = document.createElement("button"); + closeBtn.classList.add("closeButton"); + + this.trackClicks(closeBtn, () => this.internalButtonHandler("close-button"), { + applyCssClass: "pressedSmallElement", + }); + + title.appendChild(closeBtn); + this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button")); + } + + const content = document.createElement("div"); + content.classList.add("content"); + content.innerHTML = this.contentHTML; + this.dialogElem.appendChild(content); + + if (this.buttonIds.length > 0) { + const buttons = document.createElement("div"); + buttons.classList.add("buttons"); + + // Create buttons + for (let i = 0; i < this.buttonIds.length; ++i) { + const [buttonId, buttonStyle, rawParams] = this.buttonIds[i].split(":"); + + const button = document.createElement("button"); + button.classList.add("button"); + button.classList.add("styledButton"); + button.classList.add(buttonStyle); + button.innerText = T.dialogs.buttons[buttonId]; + + const params = (rawParams || "").split("/"); + const useTimeout = params.indexOf("timeout") >= 0; + + const isEnter = params.indexOf("enter") >= 0; + const isEscape = params.indexOf("escape") >= 0; + + if (isEscape && this.closeButton) { + logger.warn("Showing dialog with close button, and additional cancel button"); + } + + if (useTimeout) { + button.classList.add("timedButton"); + const timeout = setTimeout(() => { + button.classList.remove("timedButton"); + arrayDeleteValue(this.timeouts, timeout); + }, 5000); + this.timeouts.push(timeout); + } + if (isEnter || isEscape) { + // if (this.app.settings.getShowKeyboardShortcuts()) { + // Show keybinding + const spacer = document.createElement("code"); + spacer.classList.add("keybinding"); + spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel); + button.appendChild(spacer); + // } + + if (isEnter) { + this.enterHandler = buttonId; + } + if (isEscape) { + this.escapeHandler = buttonId; + } + } + + this.trackClicks(button, () => this.internalButtonHandler(buttonId)); + buttons.appendChild(button); + } + + this.dialogElem.appendChild(buttons); + } else { + this.dialogElem.classList.add("buttonless"); + } + + this.element = elem; + this.app.inputMgr.pushReciever(this.inputReciever); + + return this.element; + } + + setIndex(index) { + this.element.style.zIndex = index; + } + + destroy() { + if (!this.element) { + assert(false, "Tried to destroy dialog twice"); + return; + } + // We need to do this here, because if the backbutton event gets + // dispatched to the modal dialogs, it will not call the internalButtonHandler, + // and thus our receiver stays attached the whole time + this.app.inputMgr.destroyReceiver(this.inputReciever); + + for (let i = 0; i < this.clickDetectors.length; ++i) { + this.clickDetectors[i].cleanup(); + } + this.clickDetectors = []; + + this.element.remove(); + this.element = null; + + for (let i = 0; i < this.timeouts.length; ++i) { + clearTimeout(this.timeouts[i]); + } + this.timeouts = []; + } + + hide() { + this.element.classList.remove("visible"); + } + + show() { + this.element.classList.add("visible"); + } + + /** + * Helper method to track clicks on an element + * @param {Element} elem + * @param {function():void} handler + * @param {import("./click_detector").ClickDetectorConstructorArgs=} args + * @returns {ClickDetector} + */ + trackClicks(elem, handler, args = {}) { + const detector = new ClickDetector(elem, args); + detector.click.add(handler, this); + this.clickDetectors.push(detector); + return detector; + } +} + +/** + * Dialog which simply shows a loading spinner + */ +export class DialogLoading extends Dialog { + constructor(app) { + super({ + app, + title: "", + contentHTML: "", + buttons: [], + type: "loading", + }); + + // Loading dialog can not get closed with back button + this.inputReciever.backButton.removeAll(); + this.inputReciever.context = "dialog-loading"; + } + + createElement() { + const elem = document.createElement("div"); + elem.classList.add("ingameDialog"); + elem.classList.add("loadingDialog"); + this.element = elem; + + const loader = document.createElement("div"); + loader.classList.add("prefab_LoadingTextWithAnim"); + loader.classList.add("loadingIndicator"); + loader.innerText = T.global.loading; + elem.appendChild(loader); + + this.app.inputMgr.pushReciever(this.inputReciever); + + return elem; + } +} + +export class DialogOptionChooser extends Dialog { + constructor({ app, title, options }) { + let html = "
"; + + options.options.forEach(({ value, text, desc = null, iconPrefix = null }) => { + const descHtml = desc ? `${desc}` : ""; + let iconHtml = iconPrefix ? `` : ""; + html += ` +
+ ${iconHtml} + ${text} + ${descHtml} +
+ `; + }); + + html += "
"; + super({ + app, + title, + contentHTML: html, + buttons: [], + type: "info", + closeButton: true, + }); + + this.options = options; + this.initialOption = options.active; + + this.buttonSignals.optionSelected = new Signal(); + } + + createElement() { + const div = super.createElement(); + this.dialogElem.classList.add("optionChooserDialog"); + + div.querySelectorAll("[data-optionvalue]").forEach(handle => { + const value = handle.getAttribute("data-optionvalue"); + if (!handle) { + logger.error("Failed to bind option value in dialog:", value); + return; + } + // Need click detector here to forward elements, otherwise scrolling does not work + const detector = new ClickDetector(handle, { + consumeEvents: false, + preventDefault: false, + clickSound: null, + applyCssClass: "pressedOption", + targetOnly: true, + }); + this.clickDetectors.push(detector); + + if (value !== this.initialOption) { + detector.click.add(() => { + const selected = div.querySelector(".option.active"); + if (selected) { + selected.classList.remove("active"); + } else { + logger.warn("No selected option"); + } + handle.classList.add("active"); + this.app.sound.playUiSound(SOUNDS.uiClick); + this.internalButtonHandler("optionSelected", value); + }); + } + }); + return div; + } +} + +export class DialogWithForm extends Dialog { + /** + * + * @param {object} param0 + * @param {Application} param0.app + * @param {string} param0.title + * @param {string} param0.desc + * @param {string=} param0.confirmButton + * @param {Array} param0.formElements + */ + constructor({ app, title, desc, formElements, confirmButton = "ok:good" }) { + let html = ""; + html += desc + "
"; + for (let i = 0; i < formElements.length; ++i) { + html += formElements[i].getHtml(); + } + + super({ + app, + title: title, + contentHTML: html, + buttons: ["cancel:bad", confirmButton], + type: "info", + closeButton: true, + }); + this.confirmButtonId = confirmButton.split(":")[0]; + this.formElements = formElements; + } + + internalButtonHandler(id, ...payload) { + if (id === this.confirmButtonId) { + if (this.hasAnyInvalid()) { + this.dialogElem.classList.remove("errorShake"); + waitNextFrame().then(() => { + if (this.dialogElem) { + this.dialogElem.classList.add("errorShake"); + } + }); + this.app.sound.playUiSound(SOUNDS.uiError); + return; + } + } + + super.internalButtonHandler(id, payload); + } + + hasAnyInvalid() { + for (let i = 0; i < this.formElements.length; ++i) { + if (!this.formElements[i].isValid()) { + return true; + } + } + return false; + } + + createElement() { + const div = super.createElement(); + + for (let i = 0; i < this.formElements.length; ++i) { + const elem = this.formElements[i]; + elem.bindEvents(div, this.clickDetectors); + } + + waitNextFrame().then(() => { + this.formElements[0].focus(); + }); + + return div; + } +} diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js new file mode 100644 index 00000000..4d1c9f97 --- /dev/null +++ b/src/js/core/modal_dialog_forms.js @@ -0,0 +1,150 @@ +import { ClickDetector } from "./click_detector"; + +export class FormElement { + constructor(id, label) { + this.id = id; + this.label = label; + } + + getHtml() { + abstract; + return ""; + } + + getFormElement(parent) { + return parent.querySelector("[data-formId='" + this.id + "']"); + } + + bindEvents(parent, clickTrackers) { + abstract; + } + + focus(parent) {} + + isValid() { + return true; + } + + /** @returns {any} */ + getValue() { + abstract; + } +} + +export class FormElementInput extends FormElement { + constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) { + super(id, label); + this.placeholder = placeholder; + this.defaultValue = defaultValue; + this.inputType = inputType; + this.validator = validator; + + this.element = null; + } + + getHtml() { + let classes = []; + let inputType = "text"; + let maxlength = 256; + switch (this.inputType) { + case "text": { + classes.push("input-text"); + break; + } + + case "email": { + classes.push("input-email"); + inputType = "email"; + break; + } + + case "token": { + classes.push("input-token"); + inputType = "text"; + maxlength = 4; + break; + } + } + + return ` +
+ ${this.label ? `` : ""} + +
+ `; + } + + bindEvents(parent, clickTrackers) { + this.element = this.getFormElement(parent); + this.element.addEventListener("input", event => this.updateErrorState()); + this.updateErrorState(); + } + + updateErrorState() { + this.element.classList.toggle("errored", !this.isValid()); + } + + isValid() { + return !this.validator || this.validator(this.element.value); + } + + getValue() { + return this.element.value; + } + + focus() { + this.element.focus(); + } +} + +export class FormElementCheckbox extends FormElement { + constructor({ id, label, defaultValue = true }) { + super(id, label); + this.defaultValue = defaultValue; + this.value = this.defaultValue; + + this.element = null; + } + + getHtml() { + return ` +
+ ${this.label ? `` : ""} +
+ +
+
+ `; + } + + bindEvents(parent, clickTrackers) { + this.element = this.getFormElement(parent); + const detector = new ClickDetector(this.element, { + consumeEvents: false, + preventDefault: false, + }); + clickTrackers.push(detector); + detector.click.add(this.toggle, this); + } + + getValue() { + return this.value; + } + + toggle() { + this.value = !this.value; + this.element.classList.toggle("checked", this.value); + } + + focus(parent) {} +} diff --git a/src/js/core/perlin_noise.js b/src/js/core/perlin_noise.js deleted file mode 100644 index 0cce61ed..00000000 --- a/src/js/core/perlin_noise.js +++ /dev/null @@ -1,175 +0,0 @@ -import { perlinNoiseData } from "./perlin_noise_data"; -import { Math_sqrt } from "./builtins"; - -class Grad { - constructor(x, y, z) { - this.x = x; - this.y = y; - this.z = z; - } - - dot2(x, y) { - return this.x * x + this.y * y; - } - - dot3(x, y, z) { - return this.x * x + this.y * y + this.z * z; - } -} - -function fade(t) { - return t * t * t * (t * (t * 6 - 15) + 10); -} - -function lerp(a, b, t) { - return (1 - t) * a + t * b; -} - -const F2 = 0.5 * (Math_sqrt(3) - 1); -const G2 = (3 - Math_sqrt(3)) / 6; - -const F3 = 1 / 3; -const G3 = 1 / 6; - -export class PerlinNoise { - constructor(seed) { - this.perm = new Array(512); - this.gradP = new Array(512); - this.grad3 = [ - new Grad(1, 1, 0), - new Grad(-1, 1, 0), - new Grad(1, -1, 0), - new Grad(-1, -1, 0), - new Grad(1, 0, 1), - new Grad(-1, 0, 1), - new Grad(1, 0, -1), - new Grad(-1, 0, -1), - new Grad(0, 1, 1), - new Grad(0, -1, 1), - new Grad(0, 1, -1), - new Grad(0, -1, -1), - ]; - - this.seed = seed; - this.initializeFromSeed(seed); - } - - initializeFromSeed(seed) { - const P = perlinNoiseData; - - if (seed > 0 && seed < 1) { - // Scale the seed out - seed *= 65536; - } - - seed = Math.floor(seed); - if (seed < 256) { - seed |= seed << 8; - } - - for (let i = 0; i < 256; i++) { - let v; - if (i & 1) { - v = P[i] ^ (seed & 255); - } else { - v = P[i] ^ ((seed >> 8) & 255); - } - - this.perm[i] = this.perm[i + 256] = v; - this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12]; - } - } - - /** - * 2d Perlin Noise - * @param {number} x - * @param {number} y - * @returns {number} - */ - computePerlin2(x, y) { - // Find unit grid cell containing point - let X = Math.floor(x), - Y = Math.floor(y); - - // Get relative xy coordinates of point within that cell - x = x - X; - y = y - Y; - - // Wrap the integer cells at 255 (smaller integer period can be introduced here) - X = X & 255; - Y = Y & 255; - - // Calculate noise contributions from each of the four corners - let n00 = this.gradP[X + this.perm[Y]].dot2(x, y); - let n01 = this.gradP[X + this.perm[Y + 1]].dot2(x, y - 1); - let n10 = this.gradP[X + 1 + this.perm[Y]].dot2(x - 1, y); - let n11 = this.gradP[X + 1 + this.perm[Y + 1]].dot2(x - 1, y - 1); - - // Compute the fade curve value for x - let u = fade(x); - - // Interpolate the four results - return lerp(lerp(n00, n10, u), lerp(n01, n11, u), fade(y)); - } - - computeSimplex2(xin, yin) { - var n0, n1, n2; // Noise contributions from the three corners - // Skew the input space to determine which simplex cell we're in - var s = (xin + yin) * F2; // Hairy factor for 2D - var i = Math.floor(xin + s); - var j = Math.floor(yin + s); - var t = (i + j) * G2; - var x0 = xin - i + t; // The x,y distances from the cell origin, unskewed. - var y0 = yin - j + t; - // For the 2D case, the simplex shape is an equilateral triangle. - // Determine which simplex we are in. - var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords - if (x0 > y0) { - // lower triangle, XY order: (0,0)->(1,0)->(1,1) - i1 = 1; - j1 = 0; - } else { - // upper triangle, YX order: (0,0)->(0,1)->(1,1) - i1 = 0; - j1 = 1; - } - // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and - // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where - // c = (3-sqrt(3))/6 - var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords - var y1 = y0 - j1 + G2; - var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords - var y2 = y0 - 1 + 2 * G2; - // Work out the hashed gradient indices of the three simplex corners - i &= 255; - j &= 255; - var gi0 = this.gradP[i + this.perm[j]]; - var gi1 = this.gradP[i + i1 + this.perm[j + j1]]; - var gi2 = this.gradP[i + 1 + this.perm[j + 1]]; - // Calculate the contribution from the three corners - var t0 = 0.5 - x0 * x0 - y0 * y0; - if (t0 < 0) { - n0 = 0; - } else { - t0 *= t0; - n0 = t0 * t0 * gi0.dot2(x0, y0); // (x,y) of grad3 used for 2D gradient - } - var t1 = 0.5 - x1 * x1 - y1 * y1; - if (t1 < 0) { - n1 = 0; - } else { - t1 *= t1; - n1 = t1 * t1 * gi1.dot2(x1, y1); - } - var t2 = 0.5 - x2 * x2 - y2 * y2; - if (t2 < 0) { - n2 = 0; - } else { - t2 *= t2; - n2 = t2 * t2 * gi2.dot2(x2, y2); - } - // Add contributions from each corner to get the final noise value. - // The result is scaled to return values in the interval [-1,1]. - return 70 * (n0 + n1 + n2); - } -} diff --git a/src/js/core/perlin_noise_data.js b/src/js/core/perlin_noise_data.js deleted file mode 100644 index 14a25ba0..00000000 --- a/src/js/core/perlin_noise_data.js +++ /dev/null @@ -1,258 +0,0 @@ -export const perlinNoiseData = [ - 151, - 160, - 137, - 91, - 90, - 15, - 131, - 13, - 201, - 95, - 96, - 53, - 194, - 233, - 7, - 225, - 140, - 36, - 103, - 30, - 69, - 142, - 8, - 99, - 37, - 240, - 21, - 10, - 23, - 190, - 6, - 148, - 247, - 120, - 234, - 75, - 0, - 26, - 197, - 62, - 94, - 252, - 219, - 203, - 117, - 35, - 11, - 32, - 57, - 177, - 33, - 88, - 237, - 149, - 56, - 87, - 174, - 20, - 125, - 136, - 171, - 168, - 68, - 175, - 74, - 165, - 71, - 134, - 139, - 48, - 27, - 166, - 77, - 146, - 158, - 231, - 83, - 111, - 229, - 122, - 60, - 211, - 133, - 230, - 220, - 105, - 92, - 41, - 55, - 46, - 245, - 40, - 244, - 102, - 143, - 54, - 65, - 25, - 63, - 161, - 1, - 216, - 80, - 73, - 209, - 76, - 132, - 187, - 208, - 89, - 18, - 169, - 200, - 196, - 135, - 130, - 116, - 188, - 159, - 86, - 164, - 100, - 109, - 198, - 173, - 186, - 3, - 64, - 52, - 217, - 226, - 250, - 124, - 123, - 5, - 202, - 38, - 147, - 118, - 126, - 255, - 82, - 85, - 212, - 207, - 206, - 59, - 227, - 47, - 16, - 58, - 17, - 182, - 189, - 28, - 42, - 223, - 183, - 170, - 213, - 119, - 248, - 152, - 2, - 44, - 154, - 163, - 70, - 221, - 153, - 101, - 155, - 167, - 43, - 172, - 9, - 129, - 22, - 39, - 253, - 19, - 98, - 108, - 110, - 79, - 113, - 224, - 232, - 178, - 185, - 112, - 104, - 218, - 246, - 97, - 228, - 251, - 34, - 242, - 193, - 238, - 210, - 144, - 12, - 191, - 179, - 162, - 241, - 81, - 51, - 145, - 235, - 249, - 14, - 239, - 107, - 49, - 192, - 214, - 31, - 181, - 199, - 106, - 157, - 184, - 84, - 204, - 176, - 115, - 121, - 50, - 45, - 127, - 4, - 150, - 254, - 138, - 236, - 205, - 93, - 222, - 114, - 67, - 29, - 24, - 72, - 243, - 141, - 128, - 195, - 78, - 66, - 215, - 61, - 156, - 180, -]; diff --git a/src/js/core/polyfills.js b/src/js/core/polyfills.js index 22688836..64e6c6b9 100644 --- a/src/js/core/polyfills.js +++ b/src/js/core/polyfills.js @@ -48,9 +48,26 @@ function stringPolyfills() { } } +function objectPolyfills() { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries + // @ts-ignore + if (!Object.entries) { + // @ts-ignore + Object.entries = function (obj) { + var ownProps = Object.keys(obj), + i = ownProps.length, + resArray = new Array(i); // preallocate the Array + while (i--) resArray[i] = [ownProps[i], obj[ownProps[i]]]; + + return resArray; + }; + } +} + function initPolyfills() { mathPolyfills(); stringPolyfills(); + objectPolyfills(); } function initExtensions() { diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index 07bf5667..4a10f140 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -11,6 +11,7 @@ import { JSON_stringify, JSON_parse } from "./builtins"; import { ExplainedResult } from "./explained_result"; import { decompressX64, compressX64 } from ".//lzstring"; import { asyncCompressor, compressionPrefix } from "./async_compression"; +import { compressObject, decompressObject } from "../savegame/savegame_compressor"; const logger = createLogger("read_write_proxy"); @@ -78,6 +79,45 @@ export class ReadWriteProxy { return this.currentData; } + /** + * + * @param {object} obj + */ + static serializeObject(obj) { + const jsonString = JSON_stringify(compressObject(obj)); + const checksum = sha1(jsonString + salt); + return compressionPrefix + compressX64(checksum + jsonString); + } + + /** + * + * @param {object} text + */ + static deserializeObject(text) { + const decompressed = decompressX64(text.substr(compressionPrefix.length)); + if (!decompressed) { + // LZ string decompression failure + throw new Error("bad-content / decompression-failed"); + } + if (decompressed.length < 40) { + // String too short + throw new Error("bad-content / payload-too-small"); + } + + // Compare stored checksum with actual checksum + const checksum = decompressed.substring(0, 40); + const jsonString = decompressed.substr(40); + const desiredChecksum = sha1(jsonString + salt); + if (desiredChecksum !== checksum) { + // Checksum mismatch + throw new Error("bad-content / checksum-mismatch"); + } + + const parsed = JSON.parse(jsonString); + const decoded = decompressObject(parsed); + return decoded; + } + /** * Writes the data asychronously, fails if verify() fails * @returns {Promise} @@ -89,7 +129,7 @@ export class ReadWriteProxy { logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason); return Promise.reject(verifyResult.reason); } - const jsonString = JSON_stringify(this.currentData); + const jsonString = JSON_stringify(compressObject(this.currentData)); if (!this.app.pageVisible || this.app.unloaded) { logger.log("Saving file sync because in unload handler"); @@ -149,7 +189,7 @@ export class ReadWriteProxy { .then(rawData => { if (rawData == null) { // So, the file has not been found, use default data - return JSON_stringify(this.getDefaultData()); + return JSON_stringify(compressObject(this.getDefaultData())); } if (rawData.startsWith(compressionPrefix)) { @@ -198,6 +238,9 @@ export class ReadWriteProxy { } }) + // Decompress + .then(compressed => decompressObject(compressed)) + // Verify basic structure .then(contents => { const result = this.internalVerifyBasicStructure(contents); diff --git a/src/js/core/rectangle.js b/src/js/core/rectangle.js index e736f9bd..bf19a4ab 100644 --- a/src/js/core/rectangle.js +++ b/src/js/core/rectangle.js @@ -82,14 +82,11 @@ export class Rectangle { this.y = centerY - halfHeight; this.w = halfWidth * 2; this.h = halfHeight * 2; - // console.log("Assigned", this.x, this.y, this.w, this.h); } else { - // console.log("before", this.x, this.y, this.w, this.h); this.setLeft(Math_min(this.x, centerX - halfWidth)); this.setRight(Math_max(this.right(), centerX + halfWidth)); this.setTop(Math_min(this.y, centerY - halfHeight)); this.setBottom(Math_max(this.bottom(), centerY + halfHeight)); - // console.log("Extended", this.x, this.y, this.w, this.h); } } @@ -175,13 +172,14 @@ export class Rectangle { return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * factor); } - // Increases the rectangle in all directions - expandInAllDirections(amount) { - this.x -= amount; - this.y -= amount; - this.w += 2 * amount; - this.h += 2 * amount; - return this; + /** + * Expands the rectangle in all directions + * @param {number} amount + * @returns {Rectangle} new rectangle + */ + + expandedInAllDirections(amount) { + return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * amount); } // Culling helpers diff --git a/src/js/core/rng.js b/src/js/core/rng.js index 0653b95c..3322aafa 100644 --- a/src/js/core/rng.js +++ b/src/js/core/rng.js @@ -90,6 +90,15 @@ export class RandomNumberGenerator { return this.internalRng(); } + /** + * Random choice of an array + * @param {array} array + */ + choice(array) { + const index = this.nextIntRange(0, array.length); + return array[index]; + } + /** * @param {number} min * @param {number} max diff --git a/src/js/core/sensitive_utils.encrypt.js b/src/js/core/sensitive_utils.encrypt.js index 7434164e..710de478 100644 --- a/src/js/core/sensitive_utils.encrypt.js +++ b/src/js/core/sensitive_utils.encrypt.js @@ -17,46 +17,3 @@ export function sha1(str) { export function getNameOfProvider() { return window[decodeHashedString("DYewxghgLgliB2Q")][decodeHashedString("BYewzgLgdghgtgUyA")]; } - -export function compressWithChecksum(object) { - const stringified = JSON.stringify(object); - const checksum = Rusha.createHash() - .update(stringified + encryptKey) - .digest("hex"); - return compressX64(checksum + stringified); -} - -export function decompressWithChecksum(binary) { - let decompressed = null; - try { - decompressed = decompressX64(binary); - } catch (err) { - throw new Error("failed-to-decompress"); - } - - // Split into checksum and content - if (!decompressed || decompressed.length < 41) { - throw new Error("checksum-missing"); - } - - const checksum = decompressed.substr(0, 40); - const rawData = decompressed.substr(40); - - // Validate checksum - const computedChecksum = Rusha.createHash() - .update(rawData + encryptKey) - .digest("hex"); - if (computedChecksum !== checksum) { - throw new Error("checksum-mismatch"); - } - - // Try parsing the JSON - let data = null; - try { - data = JSON.parse(rawData); - } catch (err) { - throw new Error("failed-to-parse"); - } - - return data; -} diff --git a/src/js/core/signal.js b/src/js/core/signal.js index 57fe1fca..7daae4ea 100644 --- a/src/js/core/signal.js +++ b/src/js/core/signal.js @@ -8,7 +8,7 @@ export class Signal { /** * Adds a new signal listener - * @param {object} receiver + * @param {function} receiver * @param {object} scope */ add(receiver, scope = null) { @@ -40,7 +40,7 @@ export class Signal { /** * Removes a receiver - * @param {object} receiver + * @param {function} receiver */ remove(receiver) { let index = null; diff --git a/src/js/core/singleton_factory.js b/src/js/core/singleton_factory.js index 27d192c5..7fa38bd3 100644 --- a/src/js/core/singleton_factory.js +++ b/src/js/core/singleton_factory.js @@ -1,11 +1,21 @@ +import { createLogger } from "./logging"; + +const logger = createLogger("singleton_factory"); + // simple factory pattern export class SingletonFactory { - constructor() { + constructor(id) { + this.id = id; + // Store array as well as dictionary, to speed up lookups this.entries = []; this.idToEntry = {}; } + getId() { + return this.id; + } + register(classHandle) { // First, construct instance const instance = new classHandle(); @@ -39,6 +49,7 @@ export class SingletonFactory { findById(id) { const entry = this.idToEntry[id]; if (!entry) { + logger.error("Object with id", id, "is not registered!"); assert(false, "Factory: Object with id '" + id + "' is not registered!"); return null; } @@ -68,6 +79,14 @@ export class SingletonFactory { return this.entries; } + /** + * Returns all registered ids + * @returns {Array} + */ + getAllIds() { + return Object.keys(this.idToEntry); + } + /** * Returns amount of stored entries * @returns {number} diff --git a/src/js/core/textual_game_state.js b/src/js/core/textual_game_state.js new file mode 100644 index 00000000..52a1f946 --- /dev/null +++ b/src/js/core/textual_game_state.js @@ -0,0 +1,153 @@ +import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; +import { GameState } from "./game_state"; +import { T } from "../translations"; + +/** + * Baseclass for all game states which are structured similary: A header with back button + some + * scrollable content. + */ +export class TextualGameState extends GameState { + ///// INTERFACE //// + + /** + * Should return the states inner html. If not overriden, will create a scrollable container + * with the content of getMainContentHTML() + * @returns {string} + */ + getInnerHTML() { + return ` +
+ ${this.getMainContentHTML()} +
+ `; + } + + /** + * Should return the states HTML content. + */ + getMainContentHTML() { + return ""; + } + + /** + * Should return the title of the game state. If null, no title and back button will + * get created + * @returns {string|null} + */ + getStateHeaderTitle() { + return null; + } + + ///////////// + + /** + * Back button handler, can be overridden. Per default it goes back to the main menu, + * or if coming from the game it moves back to the game again. + */ + onBackButton() { + if (this.backToStateId) { + this.moveToState(this.backToStateId, this.backToStatePayload); + } else { + this.moveToState(this.getDefaultPreviousState()); + } + } + + /** + * Returns the default state to go back to + */ + getDefaultPreviousState() { + return "MainMenuState"; + } + + /** + * Goes to a new state, telling him to go back to this state later + * @param {string} stateId + */ + moveToStateAddGoBack(stateId) { + this.moveToState(stateId, { + backToStateId: this.key, + backToStatePayload: { + backToStateId: this.backToStateId, + backToStatePayload: this.backToStatePayload, + }, + }); + } + + /** + * Removes all click detectors, except the one on the back button. Useful when regenerating + * content. + */ + clearClickDetectorsExceptHeader() { + for (let i = 0; i < this.clickDetectors.length; ++i) { + const detector = this.clickDetectors[i]; + if (detector.element === this.headerElement) { + continue; + } + detector.cleanup(); + this.clickDetectors.splice(i, 1); + i -= 1; + } + } + + /** + * Overrides the GameState implementation to provide our own html + */ + internalGetFullHtml() { + let headerHtml = ""; + if (this.getStateHeaderTitle()) { + headerHtml = ` +
+ +

${this.getStateHeaderTitle()}

+
`; + } + + return ` + ${headerHtml} +
+ ${this.getInnerHTML()} + +
+ `; + } + + //// INTERNALS ///// + + /** + * Overrides the GameState leave callback to cleanup stuff + */ + internalLeaveCallback() { + super.internalLeaveCallback(); + this.dialogs.cleanup(); + } + + /** + * Overrides the GameState enter callback to setup required stuff + * @param {any} payload + */ + internalEnterCallback(payload) { + super.internalEnterCallback(payload, false); + if (payload.backToStateId) { + this.backToStateId = payload.backToStateId; + this.backToStatePayload = payload.backToStatePayload; + } + + this.htmlElement.classList.add("textualState"); + if (this.getStateHeaderTitle()) { + this.htmlElement.classList.add("hasTitle"); + } + + this.containerElement = this.htmlElement.querySelector(".widthKeeper .container"); + this.headerElement = this.htmlElement.querySelector(".headerBar > h1"); + + if (this.headerElement) { + this.trackClicks(this.headerElement, this.onBackButton); + } + + this.dialogs = new HUDModalDialogs(null, this.app); + const dialogsElement = document.body.querySelector(".modalDialogParent"); + this.dialogs.initializeToElement(dialogsElement); + + this.onEnter(payload); + } +} diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 2b7eca48..f756a651 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -15,6 +15,7 @@ import { performanceNow, } from "./builtins"; import { Vector } from "./vector"; +import { T } from "../translations"; // Constants export const TOP = new Vector(0, -1); @@ -421,7 +422,7 @@ export function formatBigNumber(num, divider = ".") { num = Math_abs(num); if (num > 1e54) { - return sign + "inf"; + return sign + T.global.infinite; } if (num < 10 && !Number.isInteger(num)) { @@ -435,26 +436,9 @@ export function formatBigNumber(num, divider = ".") { if (num < 1000) { return sign + "" + num; } - - // if (num > 1e51) return sign + T.common.number_format.sedecillion.replace("%amount%", "" + roundSmart(num / 1e51)); - // if (num > 1e48) - // return sign + T.common.number_format.quinquadecillion.replace("%amount%", "" + roundSmart(num / 1e48)); - // if (num > 1e45) - // return sign + T.common.number_format.quattuordecillion.replace("%amount%", "" + roundSmart(num / 1e45)); - // if (num > 1e42) return sign + T.common.number_format.tredecillion.replace("%amount%", "" + roundSmart(num / 1e42)); - // if (num > 1e39) return sign + T.common.number_format.duodecillions.replace("%amount%", "" + roundSmart(num / 1e39)); - // if (num > 1e36) return sign + T.common.number_format.undecillions.replace("%amount%", "" + roundSmart(num / 1e36)); - // if (num > 1e33) return sign + T.common.number_format.decillions.replace("%amount%", "" + roundSmart(num / 1e33)); - // if (num > 1e30) return sign + T.common.number_format.nonillions.replace("%amount%", "" + roundSmart(num / 1e30)); - // if (num > 1e27) return sign + T.common.number_format.octillions.replace("%amount%", "" + roundSmart(num / 1e27)); - // if (num >= 1e24) return sign + T.common.number_format.septillions.replace("%amount%", "" + roundSmart(num / 1e24)); - // if (num >= 1e21) return sign + T.common.number_format.sextillions.replace("%amount%", "" + roundSmart(num / 1e21)); - // if (num >= 1e18) return sign + T.common.number_format.quintillions.replace("%amount%", "" + roundSmart(num / 1e18)); - // if (num >= 1e15) return sign + T.common.number_format.quantillions.replace("%amount%", "" + roundSmart(num / 1e15)); - // if (num >= 1e12) return sign + T.common.number_format.trillions.replace("%amount%", "" + roundSmart(num / 1e12)); - // if (num >= 1e9) return sign + T.common.number_format.billions.replace("%amount%", "" + roundSmart(num / 1e9)); - // if (num >= 1e6) return sign + T.common.number_format.millions.replace("%amount%", "" + roundSmart(num / 1e6)); - // if (num > 99999) return sign + T.common.number_format.thousands.replace("%amount%", "" + roundSmart(num / 1e3)); + if (num > 10000) { + return Math_floor(num / 1000.0) + "k"; + } let rest = num; let out = ""; @@ -474,12 +458,12 @@ export function formatBigNumber(num, divider = ".") { * @param {string=} divider THe divider for numbers like 50,000 (divider=',') * @returns {string} */ -export function formatBigNumberFull(num, divider = T.common.number_format.divider_thousands || " ") { +export function formatBigNumberFull(num, divider = T.global.thousandsDivider) { if (num < 1000) { return num + ""; } if (num > 1e54) { - return "infinite"; + return T.global.infinite; } let rest = num; let out = ""; @@ -492,65 +476,6 @@ export function formatBigNumberFull(num, divider = T.common.number_format.divide return out.substring(0, out.length - 1); } -/** - * Formats an amount of seconds into something like "5s ago" - * @param {number} secs Seconds - * @returns {string} - */ -export function formatSecondsToTimeAgo(secs) { - const seconds = Math_floor(secs); - const minutes = Math_floor(seconds / 60); - const hours = Math_floor(minutes / 60); - const days = Math_floor(hours / 24); - - const trans = T.common.time; - - if (seconds <= 60) { - if (seconds <= 1) { - return trans.one_second_before; - } - return trans.seconds_before.replace("%amount%", "" + seconds); - } else if (minutes <= 60) { - if (minutes <= 1) { - return trans.one_minute_before; - } - return trans.minutes_before.replace("%amount%", "" + minutes); - } else if (hours <= 60) { - if (hours <= 1) { - return trans.one_hour_before; - } - return trans.hours_before.replace("%amount%", "" + hours); - } else { - if (days <= 1) { - return trans.one_day_before; - } - return trans.days_before.replace("%amount%", "" + days); - } -} - -/** - * Formats seconds into a readable string like "5h 23m" - * @param {number} secs Seconds - * @returns {string} - */ -export function formatSeconds(secs) { - const trans = T.common.time; - secs = Math_ceil(secs); - if (secs < 60) { - return trans.seconds_short.replace("%seconds%", "" + secs); - } else if (secs < 60 * 60) { - const minutes = Math_floor(secs / 60); - const seconds = secs % 60; - return trans.minutes_seconds_short - .replace("%seconds%", "" + seconds) - .replace("%minutes%", "" + minutes); - } else { - const hours = Math_floor(secs / 3600); - const minutes = Math_floor(secs / 60) % 60; - return trans.hours_minutes_short.replace("%minutes%", "" + minutes).replace("%hours%", "" + hours); - } -} - /** * Delayes a promise so that it will resolve after a *minimum* amount of time only * @param {Promise} promise The promise to delay @@ -725,14 +650,33 @@ export function makeDiv(parent, id = null, classes = [], innerHTML = "") { return div; } +/** + * Helper method to create a new button + * @param {Element} parent + * @param {Array=} classes + * @param {string=} innerHTML + */ +export function makeButton(parent, classes = [], innerHTML = "") { + const element = document.createElement("button"); + for (let i = 0; i < classes.length; ++i) { + element.classList.add(classes[i]); + } + element.classList.add("styledButton"); + element.innerHTML = innerHTML; + parent.appendChild(element); + return element; +} + /** * Removes all children of the given element * @param {Element} elem */ export function removeAllChildren(elem) { - var range = document.createRange(); - range.selectNodeContents(elem); - range.deleteContents(); + if (elem) { + var range = document.createRange(); + range.selectNodeContents(elem); + range.deleteContents(); + } } export function smartFadeNumber(current, newOne, minFade = 0.01, maxFade = 0.9) { @@ -810,15 +754,6 @@ export function checkTimerExpired(now, lastTick, tickRate) { * Returns if the game supports this browser */ export function isSupportedBrowser() { - if (navigator.userAgent.toLowerCase().indexOf("firefox") >= 0) { - return true; - } - - return isSupportedBrowserForMultiplayer(); -} - -// https://stackoverflow.com/questions/4565112/javascript-how-to-find-out-if-the-user-browser-is-chrome/13348618#13348618 -export function isSupportedBrowserForMultiplayer() { // please note, // that IE11 now returns undefined again for window.chrome // and new Opera 30 outputs true for window.chrome @@ -836,7 +771,6 @@ export function isSupportedBrowserForMultiplayer() { var winNav = window.navigator; var vendorName = winNav.vendor; // @ts-ignore - var isOpera = typeof window.opr !== "undefined"; var isIEedge = winNav.userAgent.indexOf("Edge") > -1; var isIOSChrome = winNav.userAgent.match("CriOS"); @@ -887,3 +821,97 @@ export function fastRotateMultipleOf90(x, y, deg) { } } } + +/** + * Formats an amount of seconds into something like "5s ago" + * @param {number} secs Seconds + * @returns {string} + */ +export function formatSecondsToTimeAgo(secs) { + const seconds = Math_floor(secs); + const minutes = Math_floor(seconds / 60); + const hours = Math_floor(minutes / 60); + const days = Math_floor(hours / 24); + + if (seconds <= 60) { + if (seconds <= 1) { + return T.global.time.oneSecondAgo; + } + return T.global.time.xSecondsAgo.replace("", "" + seconds); + } else if (minutes <= 60) { + if (minutes <= 1) { + return T.global.time.oneMinuteAgo; + } + return T.global.time.xMinutesAgo.replace("", "" + minutes); + } else if (hours <= 60) { + if (hours <= 1) { + return T.global.time.oneHourAgo; + } + return T.global.time.xHoursAgo.replace("", "" + hours); + } else { + if (days <= 1) { + return T.global.time.oneDayAgo; + } + return T.global.time.xDaysAgo.replace("", "" + days); + } +} + +/** + * Formats seconds into a readable string like "5h 23m" + * @param {number} secs Seconds + * @returns {string} + */ +export function formatSeconds(secs) { + const trans = T.global.time; + secs = Math_ceil(secs); + if (secs < 60) { + return trans.secondsShort.replace("", "" + secs); + } else if (secs < 60 * 60) { + const minutes = Math_floor(secs / 60); + const seconds = secs % 60; + return trans.minutesAndSecondsShort + .replace("", "" + seconds) + .replace("", "" + minutes); + } else { + const hours = Math_floor(secs / 3600); + const minutes = Math_floor(secs / 60) % 60; + return trans.hoursAndMinutesShort.replace("", "" + minutes).replace("", "" + hours); + } +} + +/** + * Generates a file download + * @param {string} filename + * @param {string} text + */ +export function generateFileDownload(filename, text) { + var element = document.createElement("a"); + element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); + element.setAttribute("download", filename); + + element.style.display = "none"; + document.body.appendChild(element); + + element.click(); + document.body.removeChild(element); +} + +/** + * Capitalizes the first letter + * @param {string} str + */ +export function capitalizeFirstLetter(str) { + return str.substr(0, 1).toUpperCase() + str.substr(1).toLowerCase(); +} + +/** + * Formats a number like 2.5 to "2.5 items / s" + * @param {number} speed + * @param {boolean=} double + */ +export function formatItemsPerSecond(speed, double = false) { + return speed === 1.0 + ? T.ingame.buildingPlacement.infoTexts.oneItemPerSecond + : T.ingame.buildingPlacement.infoTexts.itemsPerSecond.replace("", "" + round2Digits(speed)) + + (double ? " " + T.ingame.buildingPlacement.infoTexts.itemsPerSecondDouble : ""); +} diff --git a/src/js/game/automatic_save.js b/src/js/game/automatic_save.js index 03686603..6c80976f 100644 --- a/src/js/game/automatic_save.js +++ b/src/js/game/automatic_save.js @@ -1,6 +1,7 @@ import { GameRoot } from "./root"; import { globalConfig, IS_DEBUG } from "../core/config"; import { Math_max } from "../core/builtins"; +import { createLogger } from "../core/logging"; // How important it is that a savegame is created /** @@ -11,15 +12,10 @@ export const enumSavePriority = { asap: 100, }; -// Internals -let MIN_INTERVAL_SECS = 15; +const logger = createLogger("autosave"); -if (G_IS_DEV && IS_DEBUG) { - // // Testing - // MIN_INTERVAL_SECS = 1; - // MAX_INTERVAL_SECS = 1; - MIN_INTERVAL_SECS = 9999999; -} +// Internals +let MIN_INTERVAL_SECS = 60; export class AutomaticSave { constructor(root) { @@ -50,6 +46,7 @@ export class AutomaticSave { // Bad idea return; } + // Check when the last save was, but make sure that if it fails, we don't spam const lastSaveTime = Math_max(this.lastSaveAttempt, this.root.savegame.getRealLastUpdate()); @@ -72,7 +69,7 @@ export class AutomaticSave { break; } if (shouldSave) { - // log(this, "Saving automatically"); + logger.log("Saving automatically"); this.lastSaveAttempt = Date.now(); this.doSave(); } diff --git a/src/js/game/base_item.js b/src/js/game/base_item.js index a4bd5e68..e70263df 100644 --- a/src/js/game/base_item.js +++ b/src/js/game/base_item.js @@ -1,5 +1,6 @@ import { DrawParameters } from "../core/draw_parameters"; import { BasicSerializableObject, types } from "../savegame/serialization"; +import { THEME } from "./theme"; /** * Class for items on belts etc. Not an entity for performance reasons @@ -28,6 +29,7 @@ export class BaseItem extends BasicSerializableObject { draw(x, y, parameters, size) {} getBackgroundColorAsResource() { - return "#eaebec"; + abstract; + return ""; } } diff --git a/src/js/game/buildings/belt_base.js b/src/js/game/buildings/belt_base.js index f021f74e..b662ea77 100644 --- a/src/js/game/buildings/belt_base.js +++ b/src/js/game/buildings/belt_base.js @@ -1,5 +1,8 @@ import { Loader } from "../../core/loader"; +import { formatItemsPerSecond } from "../../core/utils"; import { enumAngleToDirection, enumDirection, Vector } from "../../core/vector"; +import { SOUNDS } from "../../platform/sound"; +import { T } from "../../translations"; import { BeltComponent } from "../components/belt"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; @@ -19,6 +22,63 @@ export class MetaBeltBaseBuilding extends MetaBuilding { return "#777"; } + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const beltSpeed = root.hubGoals.getBeltBaseSpeed(); + + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]]; + } + + getPreviewSprite(rotationVariant) { + switch (arrayBeltVariantToRotation[rotationVariant]) { + case enumDirection.top: { + return Loader.getSprite("sprites/buildings/belt_top.png"); + } + case enumDirection.left: { + return Loader.getSprite("sprites/buildings/belt_left.png"); + } + case enumDirection.right: { + return Loader.getSprite("sprites/buildings/belt_right.png"); + } + default: { + assertAlways(false, "Invalid belt rotation variant"); + } + } + } + + getBlueprintSprite(rotationVariant) { + switch (arrayBeltVariantToRotation[rotationVariant]) { + case enumDirection.top: { + return Loader.getSprite("sprites/blueprints/belt_top.png"); + } + case enumDirection.left: { + return Loader.getSprite("sprites/blueprints/belt_left.png"); + } + case enumDirection.right: { + return Loader.getSprite("sprites/blueprints/belt_right.png"); + } + default: { + assertAlways(false, "Invalid belt rotation variant"); + } + } + } + + getStayInPlacementMode() { + return true; + } + + getRotateAutomaticallyWhilePlacing() { + return true; + } + + getPlacementSound() { + return SOUNDS.placeBelt; + } + /** * Creates the entity at the given location * @param {Entity} entity @@ -38,6 +98,7 @@ export class MetaBeltBaseBuilding extends MetaBuilding { directions: [enumDirection.bottom], }, ], + animated: false, }) ); @@ -61,7 +122,7 @@ export class MetaBeltBaseBuilding extends MetaBuilding { * @param {Entity} entity * @param {number} rotationVariant */ - updateRotationVariant(entity, rotationVariant) { + updateVariants(entity, rotationVariant) { entity.components.Belt.direction = arrayBeltVariantToRotation[rotationVariant]; entity.components.ItemEjector.slots[0].direction = arrayBeltVariantToRotation[rotationVariant]; @@ -73,9 +134,10 @@ export class MetaBeltBaseBuilding extends MetaBuilding { * @param {GameRoot} root * @param {Vector} tile * @param {number} rotation + * @param {string} variant * @return {{ rotation: number, rotationVariant: number }} */ - computeOptimalDirectionAndRotationVariantAtTile(root, tile, rotation) { + computeOptimalDirectionAndRotationVariantAtTile(root, tile, rotation, variant) { const topDirection = enumAngleToDirection[rotation]; const rightDirection = enumAngleToDirection[(rotation + 90) % 360]; const bottomDirection = enumAngleToDirection[(rotation + 180) % 360]; @@ -84,8 +146,8 @@ export class MetaBeltBaseBuilding extends MetaBuilding { const { ejectors, acceptors } = root.logic.getEjectorsAndAcceptorsAtTile(tile); let hasBottomEjector = false; - let hasLeftEjector = false; let hasRightEjector = false; + let hasLeftEjector = false; let hasTopAcceptor = false; let hasLeftAcceptor = false; @@ -98,9 +160,9 @@ export class MetaBeltBaseBuilding extends MetaBuilding { if (ejector.toDirection === topDirection) { hasBottomEjector = true; } else if (ejector.toDirection === leftDirection) { - hasLeftEjector = true; - } else if (ejector.toDirection === rightDirection) { hasRightEjector = true; + } else if (ejector.toDirection === rightDirection) { + hasLeftEjector = true; } } @@ -121,7 +183,8 @@ export class MetaBeltBaseBuilding extends MetaBuilding { if (!hasBottomEjector) { // When something ejects to us from the left and nothing from the right, // do a curve from the left to the top - if (hasLeftEjector && !hasRightEjector) { + + if (hasRightEjector && !hasLeftEjector) { return { rotation: (rotation + 270) % 360, rotationVariant: 2, @@ -130,7 +193,7 @@ export class MetaBeltBaseBuilding extends MetaBuilding { // When something ejects to us from the right and nothing from the left, // do a curve from the right to the top - if (hasRightEjector && !hasLeftEjector) { + if (hasLeftEjector && !hasRightEjector) { return { rotation: (rotation + 90) % 360, rotationVariant: 1, @@ -140,65 +203,29 @@ export class MetaBeltBaseBuilding extends MetaBuilding { // When there is a top acceptor, ignore sides // NOTICE: This makes the belt prefer side turns *way* too much! - // if (!hasTopAcceptor) { - // // When there is an acceptor to the right but no acceptor to the left, - // // do a turn to the right - // if (hasRightAcceptor && !hasLeftAcceptor) { - // return { - // rotation, - // rotationVariant: 2, - // }; - // } + if (!hasTopAcceptor) { + // When there is an acceptor to the right but no acceptor to the left, + // do a turn to the right + if (hasRightAcceptor && !hasLeftAcceptor) { + return { + rotation, + rotationVariant: 2, + }; + } - // // When there is an acceptor to the left but no acceptor to the right, - // // do a turn to the left - // if (hasLeftAcceptor && !hasRightAcceptor) { - // return { - // rotation, - // rotationVariant: 1, - // }; - // } - // } + // When there is an acceptor to the left but no acceptor to the right, + // do a turn to the left + if (hasLeftAcceptor && !hasRightAcceptor) { + return { + rotation, + rotationVariant: 1, + }; + } + } return { rotation, rotationVariant: 0, }; } - - getName() { - return "Belt"; - } - - getDescription() { - return "Transports items, hold and drag to place multiple, press 'R' to rotate."; - } - - getPreviewSprite(rotationVariant) { - switch (arrayBeltVariantToRotation[rotationVariant]) { - case enumDirection.top: { - return Loader.getSprite("sprites/belt/forward_0.png"); - } - case enumDirection.left: { - return Loader.getSprite("sprites/belt/left_0.png"); - } - case enumDirection.right: { - return Loader.getSprite("sprites/belt/right_0.png"); - } - default: { - assertAlways(false, "Invalid belt rotation variant"); - } - } - } - - getStayInPlacementMode() { - return true; - } - - /** - * Can be overridden - */ - internalGetBeltDirection(rotationVariant) { - return enumDirection.top; - } } diff --git a/src/js/game/buildings/cutter.js b/src/js/game/buildings/cutter.js index 07a574f7..a9433390 100644 --- a/src/js/game/buildings/cutter.js +++ b/src/js/game/buildings/cutter.js @@ -4,9 +4,14 @@ import { enumItemAcceptorItemFilter, ItemAcceptorComponent } from "../components import { ItemEjectorComponent } from "../components/item_ejector"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { MetaBuilding, defaultBuildingVariant } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; +import { T } from "../../translations"; +import { formatItemsPerSecond } from "../../core/utils"; + +/** @enum {string} */ +export const enumCutterVariants = { quad: "quad" }; export class MetaCutterBuilding extends MetaBuilding { constructor() { @@ -17,16 +22,39 @@ export class MetaCutterBuilding extends MetaBuilding { return "#7dcda2"; } - getDimensions() { - return new Vector(2, 1); + getDimensions(variant) { + switch (variant) { + case defaultBuildingVariant: + return new Vector(2, 1); + case enumCutterVariants.quad: + return new Vector(4, 1); + default: + assertAlways(false, "Unknown splitter variant: " + variant); + } } - getName() { - return "Cut Half"; + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const speed = root.hubGoals.getProcessorBaseSpeed( + variant === enumCutterVariants.quad + ? enumItemProcessorTypes.cutterQuad + : enumItemProcessorTypes.cutter + ); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; } - getDescription() { - return "Cuts shapes from top to bottom and outputs both halfs. If you use only one part, be sure to destroy the other part or it will stall!"; + /** + * @param {GameRoot} root + */ + getAvailableVariants(root) { + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_quad)) { + return [defaultBuildingVariant, enumCutterVariants.quad]; + } + return super.getAvailableVariants(root); } /** @@ -47,15 +75,7 @@ export class MetaCutterBuilding extends MetaBuilding { processorType: enumItemProcessorTypes.cutter, }) ); - - entity.addComponent( - new ItemEjectorComponent({ - slots: [ - { pos: new Vector(0, 0), direction: enumDirection.top }, - { pos: new Vector(1, 0), direction: enumDirection.top }, - ], - }) - ); + entity.addComponent(new ItemEjectorComponent({})); entity.addComponent( new ItemAcceptorComponent({ slots: [ @@ -68,4 +88,36 @@ export class MetaCutterBuilding extends MetaBuilding { }) ); } + + /** + * + * @param {Entity} entity + * @param {number} rotationVariant + * @param {string} variant + */ + updateVariants(entity, rotationVariant, variant) { + switch (variant) { + case defaultBuildingVariant: { + entity.components.ItemEjector.setSlots([ + { pos: new Vector(0, 0), direction: enumDirection.top }, + { pos: new Vector(1, 0), direction: enumDirection.top }, + ]); + entity.components.ItemProcessor.type = enumItemProcessorTypes.cutter; + break; + } + case enumCutterVariants.quad: { + entity.components.ItemEjector.setSlots([ + { pos: new Vector(0, 0), direction: enumDirection.top }, + { pos: new Vector(1, 0), direction: enumDirection.top }, + { pos: new Vector(2, 0), direction: enumDirection.top }, + { pos: new Vector(3, 0), direction: enumDirection.top }, + ]); + entity.components.ItemProcessor.type = enumItemProcessorTypes.cutterQuad; + break; + } + + default: + assertAlways(false, "Unknown painter variant: " + variant); + } + } } diff --git a/src/js/game/buildings/hub.js b/src/js/game/buildings/hub.js index 6186597e..b7b960de 100644 --- a/src/js/game/buildings/hub.js +++ b/src/js/game/buildings/hub.js @@ -20,14 +20,6 @@ export class MetaHubBuilding extends MetaBuilding { return "#eb5555"; } - getName() { - return "Hub"; - } - - getDescription() { - return "Your central hub, deliver shapes to it to unlock new buildings."; - } - isRotateable() { return false; } @@ -44,6 +36,10 @@ export class MetaHubBuilding extends MetaBuilding { processorType: enumItemProcessorTypes.hub, }) ); + + // We render the sprite ourself + entity.components.StaticMapEntity.spriteKey = null; + entity.addComponent(new UnremovableComponent()); entity.addComponent( new ItemAcceptorComponent({ diff --git a/src/js/game/buildings/miner.js b/src/js/game/buildings/miner.js index 83791504..25788917 100644 --- a/src/js/game/buildings/miner.js +++ b/src/js/game/buildings/miner.js @@ -2,23 +2,60 @@ import { enumDirection, Vector } from "../../core/vector"; import { ItemEjectorComponent } from "../components/item_ejector"; import { MinerComponent } from "../components/miner"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { MetaBuilding, defaultBuildingVariant } from "../meta_building"; +import { GameRoot } from "../root"; +import { enumHubGoalRewards } from "../tutorial_goals"; +import { T } from "../../translations"; +import { round1Digit, round2Digits, formatItemsPerSecond } from "../../core/utils"; + +/** @enum {string} */ +export const enumMinerVariants = { chainable: "chainable" }; export class MetaMinerBuilding extends MetaBuilding { constructor() { super("miner"); } - getName() { - return "Extract"; - } - getSilhouetteColor() { return "#b37dcd"; } - getDescription() { - return "Place over a shape or color to extract it. Six extractors fill exactly one belt."; + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const speed = root.hubGoals.getMinerBaseSpeed(); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; + } + + /** + * + * @param {GameRoot} root + */ + getAvailableVariants(root) { + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_miner_chainable)) { + return [defaultBuildingVariant, enumMinerVariants.chainable]; + } + return super.getAvailableVariants(root); + } + + /** + * @param {GameRoot} root + * @param {object} param0 + * @param {Vector} param0.origin + * @param {number} param0.rotation + * @param {number} param0.rotationVariant + * @param {string} param0.variant + */ + performAdditionalPlacementChecks(root, { origin, rotation, rotationVariant, variant }) { + // Make sure its placed above a resource + const lowerLayer = root.map.getLowerLayerContentXY(origin.x, origin.y); + if (!lowerLayer) { + return false; + } + return true; } /** @@ -33,4 +70,14 @@ export class MetaMinerBuilding extends MetaBuilding { }) ); } + + /** + * + * @param {Entity} entity + * @param {number} rotationVariant + * @param {string} variant + */ + updateVariants(entity, rotationVariant, variant) { + entity.components.Miner.chainable = variant === enumMinerVariants.chainable; + } } diff --git a/src/js/game/buildings/mixer.js b/src/js/game/buildings/mixer.js index 15c7d699..a20bff82 100644 --- a/src/js/game/buildings/mixer.js +++ b/src/js/game/buildings/mixer.js @@ -7,6 +7,8 @@ import { Entity } from "../entity"; import { MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; +import { T } from "../../translations"; +import { formatItemsPerSecond } from "../../core/utils"; export class MetaMixerBuilding extends MetaBuilding { constructor() { @@ -17,14 +19,6 @@ export class MetaMixerBuilding extends MetaBuilding { return new Vector(2, 1); } - getName() { - return "Mix Colors"; - } - - getDescription() { - return "Mixes two colors using additive blending."; - } - getSilhouetteColor() { return "#cdbb7d"; } @@ -36,6 +30,16 @@ export class MetaMixerBuilding extends MetaBuilding { return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_mixer); } + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.mixer); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; + } + /** * Creates the entity at the given location * @param {Entity} entity diff --git a/src/js/game/buildings/painter.js b/src/js/game/buildings/painter.js index 746f9070..7fa8bb38 100644 --- a/src/js/game/buildings/painter.js +++ b/src/js/game/buildings/painter.js @@ -4,31 +4,73 @@ import { enumItemAcceptorItemFilter, ItemAcceptorComponent } from "../components import { ItemEjectorComponent } from "../components/item_ejector"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { MetaBuilding, defaultBuildingVariant } from "../meta_building"; import { enumHubGoalRewards } from "../tutorial_goals"; import { GameRoot } from "../root"; +import { T } from "../../translations"; +import { formatItemsPerSecond } from "../../core/utils"; + +/** @enum {string} */ +export const enumPainterVariants = { double: "double", quad: "quad" }; export class MetaPainterBuilding extends MetaBuilding { constructor() { super("painter"); } - getDimensions() { - return new Vector(2, 1); - } - - getName() { - return "Dye"; - } - - getDescription() { - return "Colors the whole shape on the left input with the color from the right input."; + getDimensions(variant) { + switch (variant) { + case defaultBuildingVariant: + return new Vector(2, 1); + case enumPainterVariants.double: + return new Vector(2, 2); + case enumPainterVariants.quad: + return new Vector(4, 1); + default: + assertAlways(false, "Unknown painter variant: " + variant); + } } getSilhouetteColor() { return "#cd9b7d"; } + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + switch (variant) { + case defaultBuildingVariant: { + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.painter); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; + } + case enumPainterVariants.double: { + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.painterDouble); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed, true)]]; + } + case enumPainterVariants.quad: { + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.painterQuad); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; + } + } + } + + /** + * @param {GameRoot} root + */ + getAvailableVariants(root) { + let variants = [defaultBuildingVariant]; + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) { + variants.push(enumPainterVariants.double); + } + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_quad)) { + variants.push(enumPainterVariants.quad); + } + return variants; + } + /** * @param {GameRoot} root */ @@ -41,16 +83,11 @@ export class MetaPainterBuilding extends MetaBuilding { * @param {Entity} entity */ setupEntityComponents(entity) { - entity.addComponent( - new ItemProcessorComponent({ - inputsPerCharge: 2, - processorType: enumItemProcessorTypes.painter, - }) - ); + entity.addComponent(new ItemProcessorComponent({})); entity.addComponent( new ItemEjectorComponent({ - slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }], + slots: [{ pos: new Vector(1, 0), direction: enumDirection.right }], }) ); entity.addComponent( @@ -58,16 +95,114 @@ export class MetaPainterBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + directions: [enumDirection.left], filter: enumItemAcceptorItemFilter.shape, }, { pos: new Vector(1, 0), - directions: [enumDirection.bottom], + directions: [enumDirection.top], filter: enumItemAcceptorItemFilter.color, }, ], }) ); } + + /** + * + * @param {Entity} entity + * @param {number} rotationVariant + * @param {string} variant + */ + updateVariants(entity, rotationVariant, variant) { + switch (variant) { + case defaultBuildingVariant: { + entity.components.ItemAcceptor.setSlots([ + { + pos: new Vector(0, 0), + directions: [enumDirection.left], + filter: enumItemAcceptorItemFilter.shape, + }, + { + pos: new Vector(1, 0), + directions: [enumDirection.top], + filter: enumItemAcceptorItemFilter.color, + }, + ]); + + entity.components.ItemProcessor.type = enumItemProcessorTypes.painter; + entity.components.ItemProcessor.inputsPerCharge = 2; + entity.components.ItemEjector.setSlots([ + { pos: new Vector(1, 0), direction: enumDirection.right }, + ]); + break; + } + case enumPainterVariants.double: { + entity.components.ItemAcceptor.setSlots([ + { + pos: new Vector(0, 0), + directions: [enumDirection.left], + filter: enumItemAcceptorItemFilter.shape, + }, + { + pos: new Vector(0, 1), + directions: [enumDirection.left], + filter: enumItemAcceptorItemFilter.shape, + }, + { + pos: new Vector(1, 0), + directions: [enumDirection.top], + filter: enumItemAcceptorItemFilter.color, + }, + ]); + + entity.components.ItemProcessor.type = enumItemProcessorTypes.painterDouble; + entity.components.ItemProcessor.inputsPerCharge = 3; + + entity.components.ItemEjector.setSlots([ + { pos: new Vector(1, 0), direction: enumDirection.right }, + ]); + break; + } + case enumPainterVariants.quad: { + entity.components.ItemAcceptor.setSlots([ + { + pos: new Vector(0, 0), + directions: [enumDirection.left], + filter: enumItemAcceptorItemFilter.shape, + }, + { + pos: new Vector(0, 0), + directions: [enumDirection.bottom], + filter: enumItemAcceptorItemFilter.color, + }, + { + pos: new Vector(1, 0), + directions: [enumDirection.bottom], + filter: enumItemAcceptorItemFilter.color, + }, + { + pos: new Vector(2, 0), + directions: [enumDirection.bottom], + filter: enumItemAcceptorItemFilter.color, + }, + { + pos: new Vector(3, 0), + directions: [enumDirection.bottom], + filter: enumItemAcceptorItemFilter.color, + }, + ]); + + entity.components.ItemProcessor.type = enumItemProcessorTypes.painterQuad; + entity.components.ItemProcessor.inputsPerCharge = 5; + + entity.components.ItemEjector.setSlots([ + { pos: new Vector(0, 0), direction: enumDirection.top }, + ]); + break; + } + default: + assertAlways(false, "Unknown painter variant: " + variant); + } + } } diff --git a/src/js/game/buildings/rotater.js b/src/js/game/buildings/rotater.js index 6c129115..abd3f2c2 100644 --- a/src/js/game/buildings/rotater.js +++ b/src/js/game/buildings/rotater.js @@ -4,27 +4,49 @@ import { ItemAcceptorComponent, enumItemAcceptorItemFilter } from "../components import { ItemEjectorComponent } from "../components/item_ejector"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { MetaBuilding, defaultBuildingVariant } from "../meta_building"; import { enumHubGoalRewards } from "../tutorial_goals"; import { GameRoot } from "../root"; +import { T } from "../../translations"; +import { formatItemsPerSecond } from "../../core/utils"; + +/** @enum {string} */ +export const enumRotaterVariants = { ccw: "ccw" }; export class MetaRotaterBuilding extends MetaBuilding { constructor() { super("rotater"); } - getName() { - return "Rotate"; - } - - getDescription() { - return "Rotates shapes clockwise by 90 degrees."; - } - getSilhouetteColor() { return "#7dc6cd"; } + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const speed = root.hubGoals.getProcessorBaseSpeed( + variant === enumRotaterVariants.ccw + ? enumItemProcessorTypes.rotaterCCW + : enumItemProcessorTypes.rotater + ); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; + } + + /** + * + * @param {GameRoot} root + */ + getAvailableVariants(root) { + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater_ccw)) { + return [defaultBuildingVariant, enumRotaterVariants.ccw]; + } + return super.getAvailableVariants(root); + } + /** * @param {GameRoot} root */ @@ -61,4 +83,25 @@ export class MetaRotaterBuilding extends MetaBuilding { }) ); } + + /** + * + * @param {Entity} entity + * @param {number} rotationVariant + * @param {string} variant + */ + updateVariants(entity, rotationVariant, variant) { + switch (variant) { + case defaultBuildingVariant: { + entity.components.ItemProcessor.type = enumItemProcessorTypes.rotater; + break; + } + case enumRotaterVariants.ccw: { + entity.components.ItemProcessor.type = enumItemProcessorTypes.rotaterCCW; + break; + } + default: + assertAlways(false, "Unknown rotater variant: " + variant); + } + } } diff --git a/src/js/game/buildings/splitter.js b/src/js/game/buildings/splitter.js index 4e6370e6..d3c125df 100644 --- a/src/js/game/buildings/splitter.js +++ b/src/js/game/buildings/splitter.js @@ -4,29 +4,58 @@ import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { MetaBuilding, defaultBuildingVariant } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; +import { T } from "../../translations"; +import { formatItemsPerSecond } from "../../core/utils"; + +/** @enum {string} */ +export const enumSplitterVariants = { compact: "compact", compactInverse: "compact-inverse" }; export class MetaSplitterBuilding extends MetaBuilding { constructor() { super("splitter"); } - getDimensions() { - return new Vector(2, 1); + getDimensions(variant) { + switch (variant) { + case defaultBuildingVariant: + return new Vector(2, 1); + case enumSplitterVariants.compact: + case enumSplitterVariants.compactInverse: + return new Vector(1, 1); + default: + assertAlways(false, "Unknown splitter variant: " + variant); + } } - getName() { - return "Distribute"; + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.splitter); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; } getSilhouetteColor() { return "#444"; } - getDescription() { - return "Accepts up to two inputs and evenly distributes them on the outputs. Can also be used to merge two inputs into one output."; + /** + * @param {GameRoot} root + */ + getAvailableVariants(root) { + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_splitter_compact)) { + return [ + defaultBuildingVariant, + enumSplitterVariants.compact, + enumSplitterVariants.compactInverse, + ]; + } + return super.getAvailableVariants(root); } /** @@ -60,11 +89,6 @@ export class MetaSplitterBuilding extends MetaBuilding { new ItemProcessorComponent({ inputsPerCharge: 1, processorType: enumItemProcessorTypes.splitter, - - beltUnderlays: [ - { pos: new Vector(0, 0), direction: enumDirection.top }, - { pos: new Vector(1, 0), direction: enumDirection.top }, - ], }) ); @@ -77,4 +101,68 @@ export class MetaSplitterBuilding extends MetaBuilding { }) ); } + + /** + * + * @param {Entity} entity + * @param {number} rotationVariant + * @param {string} variant + */ + updateVariants(entity, rotationVariant, variant) { + switch (variant) { + case defaultBuildingVariant: { + entity.components.ItemAcceptor.setSlots([ + { + pos: new Vector(0, 0), + directions: [enumDirection.bottom], + }, + { + pos: new Vector(1, 0), + directions: [enumDirection.bottom], + }, + ]); + + entity.components.ItemEjector.setSlots([ + { pos: new Vector(0, 0), direction: enumDirection.top }, + { pos: new Vector(1, 0), direction: enumDirection.top }, + ]); + + entity.components.ItemAcceptor.beltUnderlays = [ + { pos: new Vector(0, 0), direction: enumDirection.top }, + { pos: new Vector(1, 0), direction: enumDirection.top }, + ]; + + break; + } + case enumSplitterVariants.compact: + case enumSplitterVariants.compactInverse: { + entity.components.ItemAcceptor.setSlots([ + { + pos: new Vector(0, 0), + directions: [enumDirection.bottom], + }, + { + pos: new Vector(0, 0), + directions: [ + variant === enumSplitterVariants.compactInverse + ? enumDirection.left + : enumDirection.right, + ], + }, + ]); + + entity.components.ItemEjector.setSlots([ + { pos: new Vector(0, 0), direction: enumDirection.top }, + ]); + + entity.components.ItemAcceptor.beltUnderlays = [ + { pos: new Vector(0, 0), direction: enumDirection.top }, + ]; + + break; + } + default: + assertAlways(false, "Unknown painter variant: " + variant); + } + } } diff --git a/src/js/game/buildings/stacker.js b/src/js/game/buildings/stacker.js index 5edfb050..9b6f5ef3 100644 --- a/src/js/game/buildings/stacker.js +++ b/src/js/game/buildings/stacker.js @@ -7,28 +7,32 @@ import { Entity } from "../entity"; import { MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; +import { formatItemsPerSecond } from "../../core/utils"; +import { T } from "../../translations"; export class MetaStackerBuilding extends MetaBuilding { constructor() { super("stacker"); } - getName() { - return "Combine"; - } - getSilhouetteColor() { return "#9fcd7d"; } - getDescription() { - return "Combines both items. If they can not be merged, the right item is placed above the left item."; - } - getDimensions() { return new Vector(2, 1); } + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.stacker); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; + } + /** * @param {GameRoot} root */ diff --git a/src/js/game/buildings/trash.js b/src/js/game/buildings/trash.js index e630255c..8ae36b8d 100644 --- a/src/js/game/buildings/trash.js +++ b/src/js/game/buildings/trash.js @@ -3,31 +3,64 @@ import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { MetaBuilding, defaultBuildingVariant } from "../meta_building"; import { enumHubGoalRewards } from "../tutorial_goals"; import { GameRoot } from "../root"; +import { StorageComponent } from "../components/storage"; +import { T } from "../../translations"; +import { formatBigNumber } from "../../core/utils"; + +/** @enum {string} */ +export const enumTrashVariants = { storage: "storage" }; + +const trashSize = 5000; export class MetaTrashBuilding extends MetaBuilding { constructor() { super("trash"); } - getName() { - return "Destroyer"; - } - - getDescription() { - return "Accepts inputs from all sides and destroys them. Forever."; - } - - isRotateable() { - return false; + isRotateable(variant) { + return variant !== defaultBuildingVariant; } getSilhouetteColor() { return "#cd7d86"; } + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + if (variant === enumTrashVariants.storage) { + return [[T.ingame.buildingPlacement.infoTexts.storage, formatBigNumber(trashSize)]]; + } + return []; + } + + getDimensions(variant) { + switch (variant) { + case defaultBuildingVariant: + return new Vector(1, 1); + case enumTrashVariants.storage: + return new Vector(2, 2); + default: + assertAlways(false, "Unknown trash variant: " + variant); + } + } + + /** + * @param {GameRoot} root + */ + getAvailableVariants(root) { + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_storage)) { + return [defaultBuildingVariant, enumTrashVariants.storage]; + } + return super.getAvailableVariants(root); + } + /** * @param {GameRoot} root */ @@ -40,13 +73,6 @@ export class MetaTrashBuilding extends MetaBuilding { * @param {Entity} entity */ setupEntityComponents(entity) { - entity.addComponent( - new ItemProcessorComponent({ - inputsPerCharge: 1, - processorType: enumItemProcessorTypes.trash, - }) - ); - // Required, since the item processor needs this. entity.addComponent( new ItemEjectorComponent({ @@ -70,4 +96,77 @@ export class MetaTrashBuilding extends MetaBuilding { }) ); } + + /** + * + * @param {Entity} entity + * @param {number} rotationVariant + * @param {string} variant + */ + updateVariants(entity, rotationVariant, variant) { + switch (variant) { + case defaultBuildingVariant: { + if (!entity.components.ItemProcessor) { + entity.addComponent( + new ItemProcessorComponent({ + inputsPerCharge: 1, + processorType: enumItemProcessorTypes.trash, + }) + ); + } + if (entity.components.Storage) { + entity.removeComponent(StorageComponent); + } + + entity.components.ItemAcceptor.setSlots([ + { + pos: new Vector(0, 0), + directions: [ + enumDirection.top, + enumDirection.right, + enumDirection.bottom, + enumDirection.left, + ], + }, + ]); + entity.components.ItemEjector.setSlots([]); + entity.components.ItemProcessor.type = enumItemProcessorTypes.trash; + break; + } + case enumTrashVariants.storage: { + if (entity.components.ItemProcessor) { + entity.removeComponent(ItemProcessorComponent); + } + if (!entity.components.Storage) { + entity.addComponent(new StorageComponent({})); + } + + entity.components.Storage.maximumStorage = trashSize; + entity.components.ItemAcceptor.setSlots([ + { + pos: new Vector(0, 1), + directions: [enumDirection.bottom], + }, + { + pos: new Vector(1, 1), + directions: [enumDirection.bottom], + }, + ]); + + entity.components.ItemEjector.setSlots([ + { + pos: new Vector(0, 0), + direction: enumDirection.top, + }, + { + pos: new Vector(1, 0), + direction: enumDirection.top, + }, + ]); + break; + } + default: + assertAlways(false, "Unknown trash variant: " + variant); + } + } } diff --git a/src/js/game/buildings/underground_belt.js b/src/js/game/buildings/underground_belt.js index 31021c6e..0cfc0421 100644 --- a/src/js/game/buildings/underground_belt.js +++ b/src/js/game/buildings/underground_belt.js @@ -4,10 +4,12 @@ import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { MetaBuilding, defaultBuildingVariant } from "../meta_building"; import { GameRoot } from "../root"; import { globalConfig } from "../../core/config"; import { enumHubGoalRewards } from "../tutorial_goals"; +import { formatItemsPerSecond } from "../../core/utils"; +import { T } from "../../translations"; /** @enum {string} */ export const arrayUndergroundRotationVariantToMode = [ @@ -15,23 +17,23 @@ export const arrayUndergroundRotationVariantToMode = [ enumUndergroundBeltMode.receiver, ]; +/** @enum {string} */ +export const enumUndergroundBeltVariants = { tier2: "tier2" }; + +export const enumUndergroundBeltVariantToTier = { + [defaultBuildingVariant]: 0, + [enumUndergroundBeltVariants.tier2]: 1, +}; + export class MetaUndergroundBeltBuilding extends MetaBuilding { constructor() { super("underground_belt"); } - getName() { - return "Tunnel"; - } - getSilhouetteColor() { return "#555"; } - getDescription() { - return "Allows to tunnel resources under buildings and belts."; - } - getFlipOrientationAfterPlacement() { return true; } @@ -40,12 +42,62 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { return true; } - getPreviewSprite(rotationVariant) { + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const rangeTiles = + globalConfig.undergroundBeltMaxTilesByTier[enumUndergroundBeltVariantToTier[variant]]; + + const beltSpeed = root.hubGoals.getUndergroundBeltBaseSpeed(); + return [ + [ + T.ingame.buildingPlacement.infoTexts.range, + T.ingame.buildingPlacement.infoTexts.tiles.replace("", "" + rangeTiles), + ], + [T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)], + ]; + } + + /** + * @param {GameRoot} root + */ + getAvailableVariants(root) { + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_underground_belt_tier_2)) { + return [defaultBuildingVariant, enumUndergroundBeltVariants.tier2]; + } + return super.getAvailableVariants(root); + } + + getPreviewSprite(rotationVariant, variant) { + let suffix = ""; + if (variant !== defaultBuildingVariant) { + suffix = "-" + variant; + } + switch (arrayUndergroundRotationVariantToMode[rotationVariant]) { case enumUndergroundBeltMode.sender: - return Loader.getSprite("sprites/buildings/underground_belt_entry.png"); + return Loader.getSprite("sprites/buildings/underground_belt_entry" + suffix + ".png"); case enumUndergroundBeltMode.receiver: - return Loader.getSprite("sprites/buildings/underground_belt_exit.png"); + return Loader.getSprite("sprites/buildings/underground_belt_exit" + suffix + ".png"); + default: + assertAlways(false, "Invalid rotation variant"); + } + } + + getBlueprintSprite(rotationVariant, variant) { + let suffix = ""; + if (variant !== defaultBuildingVariant) { + suffix = "-" + variant; + } + + switch (arrayUndergroundRotationVariantToMode[rotationVariant]) { + case enumUndergroundBeltMode.sender: + return Loader.getSprite("sprites/blueprints/underground_belt_entry" + suffix + ".png"); + case enumUndergroundBeltMode.receiver: + return Loader.getSprite("sprites/blueprints/underground_belt_exit" + suffix + ".png"); default: assertAlways(false, "Invalid rotation variant"); } @@ -82,34 +134,48 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { * @param {GameRoot} root * @param {Vector} tile * @param {number} rotation + * @param {string} variant * @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array }} */ - computeOptimalDirectionAndRotationVariantAtTile(root, tile, rotation) { + computeOptimalDirectionAndRotationVariantAtTile(root, tile, rotation, variant) { const searchDirection = enumAngleToDirection[rotation]; const searchVector = enumDirectionToVector[searchDirection]; + const tier = enumUndergroundBeltVariantToTier[variant]; const targetRotation = (rotation + 180) % 360; + const targetSenderRotation = rotation; - for (let searchOffset = 1; searchOffset <= globalConfig.undergroundBeltMaxTiles; ++searchOffset) { + for ( + let searchOffset = 1; + searchOffset <= globalConfig.undergroundBeltMaxTilesByTier[tier]; + ++searchOffset + ) { tile = tile.addScalars(searchVector.x, searchVector.y); const contents = root.map.getTileContent(tile); if (contents) { const undergroundComp = contents.components.UndergroundBelt; - if (undergroundComp) { + if (undergroundComp && undergroundComp.tier === tier) { const staticComp = contents.components.StaticMapEntity; - if (staticComp.rotationDegrees === targetRotation) { + if (staticComp.rotation === targetRotation) { if (undergroundComp.mode !== enumUndergroundBeltMode.sender) { // If we encounter an underground receiver on our way which is also faced in our direction, we don't accept that break; } - // console.log("GOT IT! rotation is", rotation, "and target is", staticComp.rotationDegrees); - return { rotation: targetRotation, rotationVariant: 1, connectedEntities: [contents], }; + } else if (staticComp.rotation === targetSenderRotation) { + // Draw connections to receivers + if (undergroundComp.mode === enumUndergroundBeltMode.receiver) { + return { + rotation: rotation, + rotationVariant: 0, + connectedEntities: [contents], + }; + } } } } @@ -122,11 +188,17 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { } /** + * * @param {Entity} entity * @param {number} rotationVariant + * @param {string} variant */ - updateRotationVariant(entity, rotationVariant) { - entity.components.StaticMapEntity.spriteKey = this.getPreviewSprite(rotationVariant).spriteName; + updateVariants(entity, rotationVariant, variant) { + entity.components.UndergroundBelt.tier = enumUndergroundBeltVariantToTier[variant]; + entity.components.StaticMapEntity.spriteKey = this.getPreviewSprite( + rotationVariant, + variant + ).spriteName; switch (arrayUndergroundRotationVariantToMode[rotationVariant]) { case enumUndergroundBeltMode.sender: { diff --git a/src/js/game/camera.js b/src/js/game/camera.js index f302f543..7fb25b32 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -2,20 +2,22 @@ import { Math_abs, Math_ceil, Math_floor, + Math_max, Math_min, Math_random, performanceNow, - Math_max, } from "../core/builtins"; +import { clickDetectorGlobals } from "../core/click_detector"; +import { globalConfig } from "../core/config"; +import { createLogger } from "../core/logging"; +import { queryParamOptions } from "../core/query_parameters"; import { Rectangle } from "../core/rectangle"; import { Signal, STOP_PROPAGATION } from "../core/signal"; -import { clamp, lerp } from "../core/utils"; +import { clamp } from "../core/utils"; import { mixVector, Vector } from "../core/vector"; -import { globalConfig } from "../core/config"; -import { GameRoot } from "./root"; import { BasicSerializableObject, types } from "../savegame/serialization"; -import { clickDetectorGlobals } from "../core/click_detector"; -import { createLogger } from "../core/logging"; +import { GameRoot } from "./root"; +import { KEYMAPPINGS } from "./key_action_mapper"; const logger = createLogger("camera"); @@ -28,6 +30,15 @@ const velocityFade = 0.98; const velocityStrength = 0.4; const velocityMax = 20; +/** + * @enum {string} + */ +export const enumMouseButton = { + left: "left", + middle: "middle", + right: "right", +}; + export class Camera extends BasicSerializableObject { constructor(root) { super(); @@ -80,14 +91,21 @@ export class Camera extends BasicSerializableObject { this.touchPostMoveVelocity = new Vector(0, 0); // Handlers - this.downPreHandler = new Signal(/* pos */); - this.movePreHandler = new Signal(/* pos */); - this.pinchPreHandler = new Signal(/* pos */); - this.upPostHandler = new Signal(/* pos */); + this.downPreHandler = /** @type {TypedSignal<[Vector, enumMouseButton]>} */ (new Signal()); + this.movePreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); + // this.pinchPreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); + this.upPostHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); this.internalInitEvents(); this.clampZoomLevel(); this.bindKeys(); + if (G_IS_DEV) { + window.addEventListener("keydown", ev => { + if (ev.key === "l") { + this.zoomLevel = 3; + } + }); + } } // Serialization @@ -149,8 +167,7 @@ export class Camera extends BasicSerializableObject { * Finds a good initial zoom level */ findInitialZoom() { - return 3; - const desiredWorldSpaceWidth = 20 * globalConfig.tileSize; + const desiredWorldSpaceWidth = 15 * globalConfig.tileSize; const zoomLevelX = this.root.gameWidth / desiredWorldSpaceWidth; const zoomLevelY = this.root.gameHeight / desiredWorldSpaceWidth; @@ -320,13 +337,20 @@ export class Camera extends BasicSerializableObject { * Binds the arrow keys */ bindKeys() { - const mapper = this.root.gameState.keyActionMapper; - mapper.getBinding("map_move_up").add(() => (this.keyboardForce.y = -1)); - mapper.getBinding("map_move_down").add(() => (this.keyboardForce.y = 1)); - mapper.getBinding("map_move_right").add(() => (this.keyboardForce.x = 1)); - mapper.getBinding("map_move_left").add(() => (this.keyboardForce.x = -1)); + const mapper = this.root.keyMapper; + mapper.getBinding(KEYMAPPINGS.ingame.mapMoveUp).add(() => (this.keyboardForce.y = -1)); + mapper.getBinding(KEYMAPPINGS.ingame.mapMoveDown).add(() => (this.keyboardForce.y = 1)); + mapper.getBinding(KEYMAPPINGS.ingame.mapMoveRight).add(() => (this.keyboardForce.x = 1)); + mapper.getBinding(KEYMAPPINGS.ingame.mapMoveLeft).add(() => (this.keyboardForce.x = -1)); - mapper.getBinding("center_map").add(() => (this.desiredCenter = new Vector(0, 0))); + mapper.getBinding(KEYMAPPINGS.ingame.mapZoomIn).add(() => (this.desiredZoom = this.zoomLevel * 1.2)); + mapper.getBinding(KEYMAPPINGS.ingame.mapZoomOut).add(() => (this.desiredZoom = this.zoomLevel * 0.8)); + + mapper.getBinding(KEYMAPPINGS.ingame.centerMap).add(() => this.centerOnMap()); + } + + centerOnMap() { + this.desiredCenter = new Vector(0, 0); } /** @@ -407,6 +431,10 @@ export class Camera extends BasicSerializableObject { this.touchPostMoveVelocity = new Vector(0, 0); if (event.which === 1) { this.combinedSingleTouchStartHandler(event.clientX, event.clientY); + } else if (event.which === 2) { + this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.middle); + } else if (event.which === 3) { + this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.right); } return false; } @@ -492,10 +520,10 @@ export class Camera extends BasicSerializableObject { const touch = event.touches[0]; this.combinedSingleTouchStartHandler(touch.clientX, touch.clientY); } else if (event.touches.length === 2) { - if (this.pinchPreHandler.dispatch() === STOP_PROPAGATION) { - // Something prevented pinching - return false; - } + // if (this.pinchPreHandler.dispatch() === STOP_PROPAGATION) { + // // Something prevented pinching + // return false; + // } const touch1 = event.touches[0]; const touch2 = event.touches[1]; @@ -616,7 +644,7 @@ export class Camera extends BasicSerializableObject { */ combinedSingleTouchStartHandler(x, y) { const pos = new Vector(x, y); - if (this.downPreHandler.dispatch(pos) === STOP_PROPAGATION) { + if (this.downPreHandler.dispatch(pos, enumMouseButton.left) === STOP_PROPAGATION) { // Somebody else captured it return; } @@ -687,7 +715,6 @@ export class Camera extends BasicSerializableObject { if (G_IS_DEV && globalConfig.debug.disableZoomLimits) { return; } - const wrapper = this.root.app.platformWrapper; assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel); @@ -795,7 +822,7 @@ export class Camera extends BasicSerializableObject { internalUpdateZooming(now, dt) { if (!this.currentlyPinching && this.desiredZoom !== null) { const diff = this.zoomLevel - this.desiredZoom; - if (Math_abs(diff) > 0.05) { + if (Math_abs(diff) > 0.0001) { let fade = 0.94; if (diff > 0) { // Zoom out faster than in @@ -846,20 +873,20 @@ export class Camera extends BasicSerializableObject { let forceX = 0; let forceY = 0; - const actionMapper = this.root.gameState.keyActionMapper; - if (actionMapper.getBinding("map_move_up").currentlyDown) { + const actionMapper = this.root.keyMapper; + if (actionMapper.getBinding(KEYMAPPINGS.ingame.mapMoveUp).currentlyDown) { forceY -= 1; } - if (actionMapper.getBinding("map_move_down").currentlyDown) { + if (actionMapper.getBinding(KEYMAPPINGS.ingame.mapMoveDown).currentlyDown) { forceY += 1; } - if (actionMapper.getBinding("map_move_left").currentlyDown) { + if (actionMapper.getBinding(KEYMAPPINGS.ingame.mapMoveLeft).currentlyDown) { forceX -= 1; } - if (actionMapper.getBinding("map_move_right").currentlyDown) { + if (actionMapper.getBinding(KEYMAPPINGS.ingame.mapMoveRight).currentlyDown) { forceX += 1; } diff --git a/src/js/game/colors.js b/src/js/game/colors.js index 8926dd23..6d483771 100644 --- a/src/js/game/colors.js +++ b/src/js/game/colors.js @@ -45,7 +45,7 @@ export const enumColorsToHexCode = { [enumColors.purple]: "#dd66ff", // blue + green - [enumColors.cyan]: "#87fff5", + [enumColors.cyan]: "#00fcff", // blue + green + red [enumColors.white]: "#ffffff", diff --git a/src/js/game/component_registry.js b/src/js/game/component_registry.js index 16ae01d8..76458d43 100644 --- a/src/js/game/component_registry.js +++ b/src/js/game/component_registry.js @@ -9,6 +9,7 @@ import { ReplaceableMapEntityComponent } from "./components/replaceable_map_enti import { UndergroundBeltComponent } from "./components/underground_belt"; import { UnremovableComponent } from "./components/unremovable"; import { HubComponent } from "./components/hub"; +import { StorageComponent } from "./components/storage"; export function initComponentRegistry() { gComponentRegistry.register(StaticMapEntityComponent); @@ -21,9 +22,9 @@ export function initComponentRegistry() { gComponentRegistry.register(UndergroundBeltComponent); gComponentRegistry.register(UnremovableComponent); gComponentRegistry.register(HubComponent); + gComponentRegistry.register(StorageComponent); - // IMPORTANT ^^^^^ REGENERATE SAVEGAME SCHEMA AFTERWARDS - // IMPORTANT ^^^^^ ALSO UPDATE ENTITY COMPONENT STORAG + // IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS // Sanity check - If this is thrown, you (=me, lol) forgot to add a new component here diff --git a/src/js/game/components/belt.js b/src/js/game/components/belt.js index 6e9569ee..9d64187c 100644 --- a/src/js/game/components/belt.js +++ b/src/js/game/components/belt.js @@ -14,7 +14,7 @@ export class BeltComponent extends Component { static getSchema() { return { direction: types.string, - sortedItems: types.array(types.pair(types.ufloat, types.obj(gItemRegistry))), + sortedItems: types.array(types.pair(types.float, types.obj(gItemRegistry))), }; } @@ -59,9 +59,8 @@ export class BeltComponent extends Component { /** * Returns if the belt can currently accept an item from the given direction - * @param {enumDirection} direction */ - canAcceptNewItem(direction) { + canAcceptItem(leftoverProgress = 0.0) { const firstItem = this.sortedItems[0]; if (!firstItem) { return true; @@ -73,10 +72,20 @@ export class BeltComponent extends Component { /** * Pushes a new item to the belt * @param {BaseItem} item - * @param {enumDirection} direction */ - takeNewItem(item, direction) { - this.sortedItems.unshift([0, item]); + takeItem(item, leftoverProgress = 0.0) { + if (G_IS_DEV) { + assert( + this.sortedItems.length === 0 || + leftoverProgress <= this.sortedItems[0][0] - globalConfig.itemSpacingOnBelts + 0.001, + "Invalid leftover: " + + leftoverProgress + + " items are " + + this.sortedItems.map(item => item[0]) + ); + assert(leftoverProgress < 1.0, "Invalid leftover: " + leftoverProgress); + } + this.sortedItems.unshift([leftoverProgress, item]); } /** diff --git a/src/js/game/components/hub.js b/src/js/game/components/hub.js index 6df1bcf5..f9c13dc3 100644 --- a/src/js/game/components/hub.js +++ b/src/js/game/components/hub.js @@ -1,11 +1,18 @@ import { Component } from "../component"; import { ShapeDefinition } from "../shape_definition"; +import { types } from "../../savegame/serialization"; export class HubComponent extends Component { static getId() { return "Hub"; } + static getSchema() { + return { + definitionsToAnalyze: types.array(types.knownType(ShapeDefinition)), + }; + } + constructor() { super(); diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index bc02330c..d5546d4b 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -1,8 +1,9 @@ import { Component } from "../component"; -import { Vector, enumDirection, enumDirectionToAngle, enumInvertedDirections } from "../../core/vector"; +import { Vector, enumDirection, enumInvertedDirections } from "../../core/vector"; import { BaseItem } from "../base_item"; import { ShapeItem } from "../items/shape_item"; import { ColorItem } from "../items/color_item"; +import { types } from "../../savegame/serialization"; /** * @enum {string?} @@ -26,7 +27,30 @@ export class ItemAcceptorComponent extends Component { static getSchema() { return { - // slots: "TODO", + slots: types.array( + types.structured({ + pos: types.vector, + directions: types.array(types.enum(enumDirection)), + filter: types.nullable(types.enum(enumItemAcceptorItemFilter)), + }) + ), + animated: types.bool, + beltUnderlays: types.array( + types.structured({ + pos: types.vector, + direction: types.enum(enumDirection), + }) + ), + + // We don't actually need to store the animations + // itemConsumptionAnimations: types.array( + // types.structured({ + // item: types.obj(gItemRegistry), + // slotIndex: types.uint, + // animProgress: types.float, + // direction: types.enum(enumDirection), + // }) + // ), }; } @@ -34,10 +58,23 @@ export class ItemAcceptorComponent extends Component { * * @param {object} param0 * @param {Array<{pos: Vector, directions: enumDirection[], filter?: enumItemAcceptorItemFilter}>} param0.slots The slots from which we accept items + * @param {boolean=} param0.animated Whether to animate item consumption + * @param {Array<{pos: Vector, direction: enumDirection}>=} param0.beltUnderlays Where to render belt underlays */ - constructor({ slots }) { + constructor({ slots = [], beltUnderlays = [], animated = true }) { super(); + this.animated = animated; + + /** + * Fixes belt animations + * @type {Array<{ item: BaseItem, slotIndex: number, animProgress: number, direction: enumDirection}>} + */ + this.itemConsumptionAnimations = []; + + /* Which belt underlays to render */ + this.beltUnderlays = beltUnderlays; + this.setSlots(slots); } @@ -79,6 +116,23 @@ export class ItemAcceptorComponent extends Component { } } + /** + * Called when an item has been accepted so that + * @param {number} slotIndex + * @param {enumDirection} direction + * @param {BaseItem} item + */ + onItemAccepted(slotIndex, direction, item) { + if (this.animated) { + this.itemConsumptionAnimations.push({ + item, + slotIndex, + direction, + animProgress: 0.0, + }); + } + } + /** * Tries to find a slot which accepts the current item * @param {Vector} targetLocalTile @@ -99,12 +153,6 @@ export class ItemAcceptorComponent extends Component { for (let slotIndex = 0; slotIndex < this.slots.length; ++slotIndex) { const slot = this.slots[slotIndex]; - // const acceptorLocalPosition = targetStaticComp.applyRotationToVector( - // slot.pos - // ); - - // const acceptorGlobalPosition = acceptorLocalPosition.add(targetStaticComp.origin); - // Make sure the acceptor slot is on the right position if (!slot.pos.equals(targetLocalTile)) { continue; @@ -123,7 +171,6 @@ export class ItemAcceptorComponent extends Component { } } - // && this.canAcceptItem(slotIndex, ejectingItem) return null; } } diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index 786213ce..5cf96754 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -1,7 +1,8 @@ -import { globalConfig } from "../../core/config"; import { Vector, enumDirection, enumDirectionToVector } from "../../core/vector"; import { BaseItem } from "../base_item"; import { Component } from "../component"; +import { types } from "../../savegame/serialization"; +import { gItemRegistry } from "../../core/global_registries"; /** * @typedef {{ @@ -19,17 +20,25 @@ export class ItemEjectorComponent extends Component { static getSchema() { return { - // slots: "TODO" + instantEject: types.bool, + slots: types.array( + types.structured({ + pos: types.vector, + direction: types.enum(enumDirection), + item: types.nullable(types.obj(gItemRegistry)), + progress: types.float, + }) + ), }; } /** * * @param {object} param0 - * @param {Array<{pos: Vector, direction: enumDirection}>} param0.slots The slots to eject on + * @param {Array<{pos: Vector, direction: enumDirection}>=} param0.slots The slots to eject on * @param {boolean=} param0.instantEject If the ejection is instant */ - constructor({ slots, instantEject = false }) { + constructor({ slots = [], instantEject = false }) { super(); // How long items take to eject diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index f5257bcb..0c4e90c6 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -1,16 +1,22 @@ import { BaseItem } from "../base_item"; import { Component } from "../component"; import { enumDirection, Vector } from "../../core/vector"; +import { types } from "../../savegame/serialization"; +import { gItemRegistry } from "../../core/global_registries"; /** @enum {string} */ export const enumItemProcessorTypes = { splitter: "splitter", cutter: "cutter", + cutterQuad: "cutterQuad", rotater: "rotater", + rotaterCCW: "rotaterCCW", stacker: "stacker", trash: "trash", mixer: "mixer", painter: "painter", + painterDouble: "painterDouble", + painterQuad: "painterQuad", hub: "hub", }; @@ -21,19 +27,35 @@ export class ItemProcessorComponent extends Component { static getSchema() { return { - // TODO + nextOutputSlot: types.uint, + type: types.enum(enumItemProcessorTypes), + inputsPerCharge: types.uint, + + inputSlots: types.array( + types.structured({ + item: types.obj(gItemRegistry), + sourceSlot: types.uint, + }) + ), + itemsToEject: types.array( + types.structured({ + item: types.obj(gItemRegistry), + requiredSlot: types.nullable(types.uint), + preferredSlot: types.nullable(types.uint), + }) + ), + secondsUntilEject: types.float, }; } /** * * @param {object} param0 - * @param {enumItemProcessorTypes} param0.processorType Which type of processor this is - * @param {number} param0.inputsPerCharge How many items this machine needs until it can start working - * @param {Array<{pos: Vector, direction: enumDirection}>=} param0.beltUnderlays Where to render belt underlays + * @param {enumItemProcessorTypes=} param0.processorType Which type of processor this is + * @param {number=} param0.inputsPerCharge How many items this machine needs until it can start working * */ - constructor({ processorType = enumItemProcessorTypes.splitter, inputsPerCharge, beltUnderlays = [] }) { + constructor({ processorType = enumItemProcessorTypes.splitter, inputsPerCharge = 1 }) { super(); // Which slot to emit next, this is only a preference and if it can't emit @@ -47,9 +69,6 @@ export class ItemProcessorComponent extends Component { // How many inputs we need for one charge this.inputsPerCharge = inputsPerCharge; - // Which belt underlays to render - this.beltUnderlays = beltUnderlays; - /** * Our current inputs * @type {Array<{ item: BaseItem, sourceSlot: number }>} @@ -68,24 +87,14 @@ export class ItemProcessorComponent extends Component { * How long it takes until we are done with the current items */ this.secondsUntilEject = 0; - - /** - * Fixes belt animations - * @type {Array<{ item: BaseItem, slotIndex: number, animProgress: number, direction: enumDirection}>} - */ - this.itemConsumptionAnimations = []; } /** * Tries to take the item * @param {BaseItem} item + * @param {number} sourceSlot */ - tryTakeItem(item, sourceSlot, sourceDirection) { - if (this.inputSlots.length >= this.inputsPerCharge) { - // Already full - return false; - } - + tryTakeItem(item, sourceSlot) { // Check that we only take one item per slot for (let i = 0; i < this.inputSlots.length; ++i) { const slot = this.inputSlots[i]; @@ -95,12 +104,6 @@ export class ItemProcessorComponent extends Component { } this.inputSlots.push({ item, sourceSlot }); - this.itemConsumptionAnimations.push({ - item, - slotIndex: sourceSlot, - direction: sourceDirection, - animProgress: 0.0, - }); return true; } } diff --git a/src/js/game/components/miner.js b/src/js/game/components/miner.js index 09c4f437..e08d2906 100644 --- a/src/js/game/components/miner.js +++ b/src/js/game/components/miner.js @@ -1,6 +1,10 @@ import { globalConfig } from "../../core/config"; import { types } from "../../savegame/serialization"; import { Component } from "../component"; +import { BaseItem } from "../base_item"; +import { gItemRegistry } from "../../core/global_registries"; + +const chainBufferSize = 3; export class MinerComponent extends Component { static getId() { @@ -10,14 +14,37 @@ export class MinerComponent extends Component { static getSchema() { return { lastMiningTime: types.ufloat, + chainable: types.bool, + itemChainBuffer: types.array(types.obj(gItemRegistry)), }; } /** - * @param {object} param0 */ - constructor({}) { + constructor({ chainable = false }) { super(); this.lastMiningTime = 0; + this.chainable = chainable; + + /** + * Stores items from other miners which were chained to this + * miner. + * @type {Array} + */ + this.itemChainBuffer = []; + } + + /** + * + * @param {BaseItem} item + */ + tryAcceptChainedItem(item) { + if (this.itemChainBuffer.length > chainBufferSize) { + // Well, this one is full + return false; + } + + this.itemChainBuffer.push(item); + return true; } } diff --git a/src/js/game/components/static_map_entity.js b/src/js/game/components/static_map_entity.js index dc1cf0d5..6f9abb87 100644 --- a/src/js/game/components/static_map_entity.js +++ b/src/js/game/components/static_map_entity.js @@ -16,8 +16,10 @@ export class StaticMapEntityComponent extends Component { return { origin: types.tileVector, tileSize: types.tileVector, - rotationDegrees: types.uint, - spriteKey: types.string, + rotation: types.float, + originalRotation: types.float, + spriteKey: types.nullable(types.string), + silhouetteColor: types.nullable(types.string), }; } @@ -26,27 +28,30 @@ export class StaticMapEntityComponent extends Component { * @param {object} param0 * @param {Vector=} param0.origin Origin (Top Left corner) of the entity * @param {Vector=} param0.tileSize Size of the entity in tiles - * @param {number=} param0.rotationDegrees Rotation in degrees. Must be multiple of 90 + * @param {number=} param0.rotation Rotation in degrees. Must be multiple of 90 + * @param {number=} param0.originalRotation Original Rotation in degrees. Must be multiple of 90 * @param {string=} param0.spriteKey Optional sprite * @param {string=} param0.silhouetteColor Optional silhouette color override */ constructor({ origin = new Vector(), tileSize = new Vector(1, 1), - rotationDegrees = 0, + rotation = 0, + originalRotation = 0, spriteKey = null, silhouetteColor = null, }) { super(); assert( - rotationDegrees % 90 === 0, - "Rotation of static map entity must be multiple of 90 (was " + rotationDegrees + ")" + rotation % 90 === 0, + "Rotation of static map entity must be multiple of 90 (was " + rotation + ")" ); this.origin = origin; this.tileSize = tileSize; this.spriteKey = spriteKey; - this.rotationDegrees = rotationDegrees; + this.rotation = rotation; + this.originalRotation = originalRotation; this.silhouetteColor = silhouetteColor; } @@ -55,7 +60,7 @@ export class StaticMapEntityComponent extends Component { * @returns {Rectangle} */ getTileSpaceBounds() { - switch (this.rotationDegrees) { + switch (this.rotation) { case 0: return new Rectangle(this.origin.x, this.origin.y, this.tileSize.x, this.tileSize.y); case 90: @@ -90,7 +95,7 @@ export class StaticMapEntityComponent extends Component { * @returns {Vector} */ applyRotationToVector(vector) { - return vector.rotateFastMultipleOf90(this.rotationDegrees); + return vector.rotateFastMultipleOf90(this.rotation); } /** @@ -99,7 +104,7 @@ export class StaticMapEntityComponent extends Component { * @returns {Vector} */ unapplyRotationToVector(vector) { - return vector.rotateFastMultipleOf90(360 - this.rotationDegrees); + return vector.rotateFastMultipleOf90(360 - this.rotation); } /** @@ -108,7 +113,7 @@ export class StaticMapEntityComponent extends Component { * @returns {enumDirection} */ localDirectionToWorld(direction) { - return Vector.transformDirectionFromMultipleOf90(direction, this.rotationDegrees); + return Vector.transformDirectionFromMultipleOf90(direction, this.rotation); } /** @@ -117,7 +122,7 @@ export class StaticMapEntityComponent extends Component { * @returns {enumDirection} */ worldDirectionToLocal(direction) { - return Vector.transformDirectionFromMultipleOf90(direction, 360 - this.rotationDegrees); + return Vector.transformDirectionFromMultipleOf90(direction, 360 - this.rotation); } /** @@ -140,6 +145,57 @@ export class StaticMapEntityComponent extends Component { return this.unapplyRotationToVector(localUnrotated); } + /** + * Returns whether the entity should be drawn for the given parameters + * @param {DrawParameters} parameters + */ + shouldBeDrawn(parameters) { + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + switch (this.rotation) { + case 0: { + x = this.origin.x; + y = this.origin.y; + w = this.tileSize.x; + h = this.tileSize.y; + break; + } + case 90: { + x = this.origin.x - this.tileSize.y + 1; + y = this.origin.y; + w = this.tileSize.y; + h = this.tileSize.x; + break; + } + case 180: { + x = this.origin.x - this.tileSize.x + 1; + y = this.origin.y - this.tileSize.y + 1; + w = this.tileSize.x; + h = this.tileSize.y; + break; + } + case 270: { + x = this.origin.x; + y = this.origin.y - this.tileSize.x + 1; + w = this.tileSize.y; + h = this.tileSize.x; + break; + } + default: + assert(false, "Invalid rotation"); + } + + return parameters.visibleRect.containsRect4Params( + x * globalConfig.tileSize, + y * globalConfig.tileSize, + w * globalConfig.tileSize, + h * globalConfig.tileSize + ); + } + /** * Draws a sprite over the whole space of the entity * @param {DrawParameters} parameters @@ -151,7 +207,11 @@ export class StaticMapEntityComponent extends Component { const worldX = this.origin.x * globalConfig.tileSize; const worldY = this.origin.y * globalConfig.tileSize; - if (this.rotationDegrees === 0) { + if (!this.shouldBeDrawn(parameters)) { + return; + } + + if (this.rotation === 0) { // Early out, is faster sprite.drawCached( parameters, @@ -159,14 +219,14 @@ export class StaticMapEntityComponent extends Component { worldY - extrudePixels * this.tileSize.y, globalConfig.tileSize * this.tileSize.x + 2 * extrudePixels * this.tileSize.x, globalConfig.tileSize * this.tileSize.y + 2 * extrudePixels * this.tileSize.y, - clipping + false ); } else { const rotationCenterX = worldX + globalConfig.halfTileSize; const rotationCenterY = worldY + globalConfig.halfTileSize; parameters.context.translate(rotationCenterX, rotationCenterY); - parameters.context.rotate(Math_radians(this.rotationDegrees)); + parameters.context.rotate(Math_radians(this.rotation)); sprite.drawCached( parameters, @@ -177,7 +237,7 @@ export class StaticMapEntityComponent extends Component { false ); - parameters.context.rotate(-Math_radians(this.rotationDegrees)); + parameters.context.rotate(-Math_radians(this.rotation)); parameters.context.translate(-rotationCenterX, -rotationCenterY); } } diff --git a/src/js/game/components/storage.js b/src/js/game/components/storage.js new file mode 100644 index 00000000..e024d522 --- /dev/null +++ b/src/js/game/components/storage.js @@ -0,0 +1,80 @@ +import { Component } from "../component"; +import { types } from "../../savegame/serialization"; +import { gItemRegistry } from "../../core/global_registries"; +import { BaseItem } from "../base_item"; +import { ColorItem } from "../items/color_item"; +import { ShapeItem } from "../items/shape_item"; + +export class StorageComponent extends Component { + static getId() { + return "Storage"; + } + + static getSchema() { + return { + maximumStorage: types.uint, + storedCount: types.uint, + storedItem: types.nullable(types.obj(gItemRegistry)), + overlayOpacity: types.ufloat, + }; + } + + /** + * @param {object} param0 + * @param {number=} param0.maximumStorage How much this storage can hold + */ + constructor({ maximumStorage = 1e20 }) { + super(); + this.maximumStorage = maximumStorage; + + /** + * Currently stored item + * @type {BaseItem} + */ + this.storedItem = null; + + /** + * How many of this item we have stored + */ + this.storedCount = 0; + + /** + * We compute an opacity to make sure it doesn't flicker + */ + this.overlayOpacity = 0; + } + + /** + * Returns whether this storage can accept the item + * @param {BaseItem} item + */ + canAcceptItem(item) { + if (this.storedCount >= this.maximumStorage) { + return false; + } + if (!this.storedItem || this.storedCount === 0) { + return true; + } + + if (item instanceof ColorItem) { + return this.storedItem instanceof ColorItem && this.storedItem.color === item.color; + } + + if (item instanceof ShapeItem) { + return ( + this.storedItem instanceof ShapeItem && + this.storedItem.definition.getHash() === item.definition.getHash() + ); + } + + return false; + } + + /** + * @param {BaseItem} item + */ + takeItem(item) { + this.storedItem = item; + this.storedCount++; + } +} diff --git a/src/js/game/components/underground_belt.js b/src/js/game/components/underground_belt.js index 707cd131..e581ebe9 100644 --- a/src/js/game/components/underground_belt.js +++ b/src/js/game/components/underground_belt.js @@ -1,6 +1,8 @@ import { BaseItem } from "../base_item"; import { Component } from "../component"; import { globalConfig } from "../../core/config"; +import { types } from "../../savegame/serialization"; +import { gItemRegistry } from "../../core/global_registries"; /** @enum {string} */ export const enumUndergroundBeltMode = { @@ -13,15 +15,28 @@ export class UndergroundBeltComponent extends Component { return "UndergroundBelt"; } + static getSchema() { + return { + mode: types.enum(enumUndergroundBeltMode), + pendingItems: types.array(types.pair(types.obj(gItemRegistry), types.float)), + tier: types.uint, + }; + } + /** * * @param {object} param0 * @param {enumUndergroundBeltMode=} param0.mode As which type of belt the entity acts + * @param {number=} param0.tier */ - constructor({ mode = enumUndergroundBeltMode.sender }) { + constructor({ mode = enumUndergroundBeltMode.sender, tier = 0 }) { super(); this.mode = mode; + this.tier = tier; + + /** @type {Array<{ item: BaseItem, progress: number }>} */ + this.consumptionAnimations = []; /** * Used on both receiver and sender. @@ -48,8 +63,7 @@ export class UndergroundBeltComponent extends Component { return false; } - console.log("Takes", 1 / beltSpeed); - this.pendingItems.push([item, 1 / beltSpeed]); + this.pendingItems.push([item, 0]); return true; } @@ -66,7 +80,7 @@ export class UndergroundBeltComponent extends Component { } // Notice: We assume that for all items the travel distance is the same - const maxItemsInTunnel = (1 + travelDistance) / globalConfig.itemSpacingOnBelts; + const maxItemsInTunnel = (2 + travelDistance) / globalConfig.itemSpacingOnBelts; if (this.pendingItems.length >= maxItemsInTunnel) { // Simulate a real belt which gets full at some point return false; @@ -75,8 +89,8 @@ export class UndergroundBeltComponent extends Component { // NOTICE: // This corresponds to the item ejector - it needs 0.5 additional tiles to eject the item. // So instead of adding 1 we add 0.5 only. - const travelDuration = (travelDistance + 0.5) / beltSpeed; - console.log(travelDistance, "->", travelDuration); + // Additionally it takes 1 tile for the acceptor which we just add on top. + const travelDuration = (travelDistance + 1.5) / beltSpeed / globalConfig.itemSpacingOnBelts; this.pendingItems.push([item, travelDuration]); diff --git a/src/js/game/components/unremovable.js b/src/js/game/components/unremovable.js index 4f6dd33a..17e9f36b 100644 --- a/src/js/game/components/unremovable.js +++ b/src/js/game/components/unremovable.js @@ -4,4 +4,8 @@ export class UnremovableComponent extends Component { static getId() { return "Unremovable"; } + + static getSchema() { + return {}; + } } diff --git a/src/js/game/core.js b/src/js/game/core.js index 8a9c4857..3d2c1f3d 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -11,7 +11,6 @@ import { getDeviceDPI, resizeHighDPICanvas } from "../core/dpi_manager"; import { DrawParameters } from "../core/draw_parameters"; import { gMetaBuildingRegistry } from "../core/global_registries"; import { createLogger } from "../core/logging"; -import { PerlinNoise } from "../core/perlin_noise"; import { Vector } from "../core/vector"; import { Savegame } from "../savegame/savegame"; import { SavegameSerializer } from "../savegame/savegame_serializer"; @@ -30,6 +29,10 @@ import { GameRoot } from "./root"; import { ShapeDefinitionManager } from "./shape_definition_manager"; import { SoundProxy } from "./sound_proxy"; import { GameTime } from "./time/game_time"; +import { ProductionAnalytics } from "./production_analytics"; +import { randomInt } from "../core/utils"; +import { defaultBuildingVariant } from "./meta_building"; +import { DynamicTickrate } from "./dynamic_tickrate"; const logger = createLogger("ingame/core"); @@ -51,16 +54,6 @@ export class GameCore { /** @type {GameRoot} */ this.root = null; - /** - * Time budget (seconds) for logic updates - */ - this.logicTimeBudget = 0; - - /** - * Time budget (seconds) for user interface updates - */ - this.uiTimeBudget = 0; - /** * Set to true at the beginning of a logic update and cleared when its finished. * This is to prevent doing a recursive logic update which can lead to unexpected @@ -82,6 +75,7 @@ export class GameCore { // Construct the root element, this is the data representation of the game this.root = new GameRoot(this.app); this.root.gameState = parentState; + this.root.keyMapper = parentState.keyActionMapper; this.root.savegame = savegame; this.root.gameWidth = this.app.screenWidth; this.root.gameHeight = this.app.screenHeight; @@ -93,7 +87,10 @@ export class GameCore { const root = this.root; // This isn't nice, but we need it right here - root.gameState.keyActionMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); + root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); + + // Needs to come first + root.dynamicTickrate = new DynamicTickrate(root); // Init classes root.camera = new Camera(root); @@ -109,13 +106,10 @@ export class GameCore { root.entityMgr = new EntityManager(root); root.systemMgr = new GameSystemManager(root); root.shapeDefinitionMgr = new ShapeDefinitionManager(root); - root.mapNoiseGenerator = new PerlinNoise(Math.random()); // TODO: Save seed root.hubGoals = new HubGoals(root); + root.productionAnalytics = new ProductionAnalytics(root); root.buffers = new BufferMaintainer(root); - // root.particleMgr = new ParticleManager(root); - // root.uiParticleMgr = new ParticleManager(root); - // Initialize the hud once everything is loaded this.root.hud.initialize(); @@ -132,15 +126,21 @@ export class GameCore { /** * Initializes a new game, this means creating a new map and centering on the - * plaerbase + * playerbase * */ initNewGame() { logger.log("Initializing new game"); this.root.gameIsFresh = true; + this.root.map.seed = randomInt(0, 100000); - gMetaBuildingRegistry - .findByClass(MetaHubBuilding) - .createAndPlaceEntity(this.root, new Vector(-2, -2), 0); + gMetaBuildingRegistry.findByClass(MetaHubBuilding).createAndPlaceEntity({ + root: this.root, + origin: new Vector(-2, -2), + rotation: 0, + originalRotation: 0, + rotationVariant: 0, + variant: defaultBuildingVariant, + }); } /** @@ -245,16 +245,8 @@ export class GameCore { // Perform logic ticks this.root.time.performTicks(deltaMs, this.boundInternalTick); - // Update UI particles - this.uiTimeBudget += deltaMs; - const maxUiSteps = 3; - if (this.uiTimeBudget > globalConfig.physicsDeltaMs * maxUiSteps) { - this.uiTimeBudget = globalConfig.physicsDeltaMs; - } - while (this.uiTimeBudget >= globalConfig.physicsDeltaMs) { - this.uiTimeBudget -= globalConfig.physicsDeltaMs; - // root.uiParticleMgr.update(); - } + // Update analytics + root.productionAnalytics.update(); // Update automatic save after everything finished root.automaticSave.update(); @@ -280,6 +272,14 @@ export class GameCore { updateLogic() { const root = this.root; + + root.dynamicTickrate.beginTick(); + + if (G_IS_DEV && globalConfig.debug.disableLogicTicks) { + root.dynamicTickrate.endTick(); + return true; + } + this.duringLogicUpdate = true; // Update entities, this removes destroyed entities @@ -288,6 +288,8 @@ export class GameCore { // IMPORTANT: At this point, the game might be game over. Stop if this is the case if (!this.root) { logger.log("Root destructed, returning false"); + root.dynamicTickrate.endTick(); + return false; } @@ -295,7 +297,7 @@ export class GameCore { // root.particleMgr.update(); this.duringLogicUpdate = false; - + root.dynamicTickrate.endTick(); return true; } @@ -331,6 +333,8 @@ export class GameCore { return; } + this.root.dynamicTickrate.onFrameRendered(); + if (!this.shouldRender()) { // Always update hud tho root.hud.update(); @@ -345,6 +349,9 @@ export class GameCore { // Gather context and save all state const context = root.context; context.save(); + if (G_IS_DEV && globalConfig.debug.testClipping) { + context.clearRect(0, 0, window.innerWidth * 3, window.innerHeight * 3); + } // Compute optimal zoom level and atlas scale const zoomLevel = root.camera.zoomLevel; @@ -387,18 +394,18 @@ export class GameCore { // ----- root.map.drawBackground(params); - // systems.mapResources.draw(params); if (!this.root.camera.getIsMapOverlayActive()) { - systems.itemProcessor.drawUnderlays(params); + systems.itemAcceptor.drawUnderlays(params); systems.belt.draw(params); systems.itemEjector.draw(params); - systems.itemProcessor.draw(params); + systems.itemAcceptor.draw(params); } root.map.drawForeground(params); if (!this.root.camera.getIsMapOverlayActive()) { systems.hub.draw(params); + systems.storage.draw(params); } if (G_IS_DEV) { diff --git a/src/js/game/dynamic_tickrate.js b/src/js/game/dynamic_tickrate.js new file mode 100644 index 00000000..b8d2d0d4 --- /dev/null +++ b/src/js/game/dynamic_tickrate.js @@ -0,0 +1,114 @@ +import { GameRoot } from "./root"; +import { createLogger } from "../core/logging"; +import { globalConfig } from "../core/config"; +import { performanceNow, Math_min, Math_round, Math_max } from "../core/builtins"; +import { round3Digits } from "../core/utils"; + +const logger = createLogger("dynamic_tickrate"); + +const fpsAccumulationTime = 1000; + +export class DynamicTickrate { + /** + * + * @param {GameRoot} root + */ + constructor(root) { + this.root = root; + + this.currentTickStart = null; + this.capturedTicks = []; + this.averageTickDuration = 0; + + this.accumulatedFps = 0; + this.accumulatedFpsLastUpdate = 0; + + this.averageFps = 60; + + this.setTickRate(60); + } + + onFrameRendered() { + ++this.accumulatedFps; + + const now = performanceNow(); + const timeDuration = now - this.accumulatedFpsLastUpdate; + if (timeDuration > fpsAccumulationTime) { + const avgFps = (this.accumulatedFps / fpsAccumulationTime) * 1000; + this.averageFps = avgFps; + this.accumulatedFps = 0; + this.accumulatedFpsLastUpdate = now; + } + } + + /** + * Sets the tick rate to N updates per second + * @param {number} rate + */ + setTickRate(rate) { + logger.log("Applying tick-rate of", rate); + this.currentTickRate = rate; + this.deltaMs = 1000.0 / this.currentTickRate; + this.deltaSeconds = 1.0 / this.currentTickRate; + } + + /** + * Increases the tick rate marginally + */ + increaseTickRate() { + const desiredFps = this.root.app.settings.getDesiredFps(); + this.setTickRate(Math_round(Math_min(desiredFps, this.currentTickRate * 1.2))); + } + + /** + * Decreases the tick rate marginally + */ + decreaseTickRate() { + const desiredFps = this.root.app.settings.getDesiredFps(); + this.setTickRate(Math_round(Math_max(desiredFps / 2, this.currentTickRate * 0.8))); + } + + /** + * Call whenever a tick began + */ + beginTick() { + assert(this.currentTickStart === null, "BeginTick called twice"); + this.currentTickStart = performanceNow(); + + if (this.capturedTicks.length > this.currentTickRate * 2) { + // Take only a portion of the ticks + this.capturedTicks.sort(); + this.capturedTicks.splice(0, 10); + this.capturedTicks.splice(this.capturedTicks.length - 11, 10); + + let average = 0; + for (let i = 0; i < this.capturedTicks.length; ++i) { + average += this.capturedTicks[i]; + } + average /= this.capturedTicks.length; + + this.averageTickDuration = average; + + const desiredFps = this.root.app.settings.getDesiredFps(); + + if (this.averageFps > desiredFps * 0.9) { + // if (average < maxTickDuration) { + this.increaseTickRate(); + } else if (this.averageFps < desiredFps * 0.7) { + this.decreaseTickRate(); + } + + this.capturedTicks = []; + } + } + + /** + * Call whenever a tick ended + */ + endTick() { + assert(this.currentTickStart !== null, "EndTick called without BeginTick"); + const duration = performanceNow() - this.currentTickStart; + this.capturedTicks.push(duration); + this.currentTickStart = null; + } +} diff --git a/src/js/game/entity.js b/src/js/game/entity.js index 4161f2a7..50f4cae5 100644 --- a/src/js/game/entity.js +++ b/src/js/game/entity.js @@ -5,14 +5,13 @@ import { Component } from "./component"; /* typehints:end */ import { globalConfig } from "../core/config"; -import { Vector, enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; +import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { EntityComponentStorage } from "./entity_components"; import { Loader } from "../core/loader"; import { drawRotatedSprite } from "../core/draw_utils"; import { Math_radians } from "../core/builtins"; -// import { gFactionRegistry, gComponentRegistry } from "../core/global_registries"; -// import { EntityComponentStorage } from "./entity_components"; +import { gComponentRegistry } from "../core/global_registries"; export class Entity extends BasicSerializableObject { /** @@ -26,11 +25,6 @@ export class Entity extends BasicSerializableObject { */ this.root = root; - /** - * The metaclass of the entity, should be set by subclasses - */ - this.meta = null; - /** * The components of the entity */ @@ -78,7 +72,7 @@ export class Entity extends BasicSerializableObject { static getSchema() { return { uid: types.uint, - // components: types.keyValueMap(types.objData(gComponentRegistry), false) + components: types.keyValueMap(types.objData(gComponentRegistry)), }; } @@ -90,15 +84,6 @@ export class Entity extends BasicSerializableObject { return !this.destroyed && !this.queuedForDestroy; } - /** - * Returns the meta class of the entity. - * @returns {object} - */ - getMetaclass() { - assert(this.meta, "Entity has no metaclass"); - return this.meta; - } - /** * Internal destroy callback */ @@ -114,6 +99,10 @@ export class Entity extends BasicSerializableObject { * @param {boolean} force Used by the entity manager. Internal parameter, do not change */ addComponent(componentInstance, force = false) { + if (!force && this.registered) { + this.root.entityMgr.attachDynamicComponent(this, componentInstance); + return; + } assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent"); const id = /** @type {typeof Component} */ (componentInstance.constructor).getId(); assert(!this.components[id], "Component already present"); @@ -124,9 +113,17 @@ export class Entity extends BasicSerializableObject { * Removes a given component, only possible until the entity is registered on the entity manager, * after that use @see EntityManager.removeDynamicComponent * @param {typeof Component} componentClass + * @param {boolean} force */ - removeComponent(componentClass) { - assert(!this.registered, "Entity already registered, use EntityManager.removeDynamicComponent"); + removeComponent(componentClass, force = false) { + if (!force && this.registered) { + this.root.entityMgr.removeDynamicComponent(this, componentClass); + return; + } + assert( + force || !this.registered, + "Entity already registered, use EntityManager.removeDynamicComponent" + ); const id = componentClass.getId(); assert(this.components[id], "Component does not exist on entity"); delete this.components[id]; diff --git a/src/js/game/entity_components.js b/src/js/game/entity_components.js index 5b58264b..bcc6e9d0 100644 --- a/src/js/game/entity_components.js +++ b/src/js/game/entity_components.js @@ -9,6 +9,7 @@ import { ReplaceableMapEntityComponent } from "./components/replaceable_map_enti import { UndergroundBeltComponent } from "./components/underground_belt"; import { UnremovableComponent } from "./components/unremovable"; import { HubComponent } from "./components/hub"; +import { StorageComponent } from "./components/storage"; /* typehints:end */ /** @@ -52,6 +53,9 @@ export class EntityComponentStorage { /** @type {HubComponent} */ this.Hub; + /** @type {StorageComponent} */ + this.Storage; + /* typehints:end */ } } diff --git a/src/js/game/entity_manager.js b/src/js/game/entity_manager.js index 84a92768..c3c94fbc 100644 --- a/src/js/game/entity_manager.js +++ b/src/js/game/entity_manager.js @@ -1,4 +1,4 @@ -import { arrayDeleteValue, newEmptyMap } from "../core/utils"; +import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils"; import { Component } from "./component"; import { GameRoot } from "./root"; import { Entity } from "./entity"; @@ -128,6 +128,19 @@ export class EntityManager extends BasicSerializableObject { this.root.signals.entityGotNewComponent.dispatch(entity); } + /** + * Call to remove a component after the creation of the entity + * @param {Entity} entity + * @param {typeof Component} component + */ + removeDynamicComponent(entity, component) { + entity.removeComponent(component, true); + const componentId = /** @type {typeof Component} */ (component.constructor).getId(); + + fastArrayDeleteValue(this.componentToEntity[componentId], entity); + this.root.signals.entityComponentRemoved.dispatch(entity); + } + /** * Finds an entity buy its uid, kinda slow since it loops over all entities * @param {number} uid diff --git a/src/js/game/game_loading_overlay.js b/src/js/game/game_loading_overlay.js index 63aadb02..f1e9d6ce 100644 --- a/src/js/game/game_loading_overlay.js +++ b/src/js/game/game_loading_overlay.js @@ -1,6 +1,7 @@ /* typehints:start */ import { Application } from "../application"; /* typehints:end */ +import { T } from "../translations"; export class GameLoadingOverlay { /** @@ -51,7 +52,7 @@ export class GameLoadingOverlay { internalAddSpinnerAndText(element) { const inner = document.createElement("span"); inner.classList.add("prefab_LoadingTextWithAnim"); - inner.innerText = "Loading"; + inner.innerText = T.global.loading; element.appendChild(inner); } } diff --git a/src/js/game/game_speed_registry.js b/src/js/game/game_speed_registry.js new file mode 100644 index 00000000..963e89ac --- /dev/null +++ b/src/js/game/game_speed_registry.js @@ -0,0 +1,8 @@ +import { RegularGameSpeed } from "./time/regular_game_speed"; +import { gGameSpeedRegistry } from "../core/global_registries"; + +export function initGameSpeedRegistry() { + gGameSpeedRegistry.register(RegularGameSpeed); + + // Others are disabled for now +} diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index 810ddec0..a8e9cf0c 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -11,6 +11,8 @@ import { ItemProcessorSystem } from "./systems/item_processor"; import { UndergroundBeltSystem } from "./systems/underground_belt"; import { HubSystem } from "./systems/hub"; import { StaticMapEntitySystem } from "./systems/static_map_entity"; +import { ItemAcceptorSystem } from "./systems/item_acceptor"; +import { StorageSystem } from "./systems/storage"; const logger = createLogger("game_system_manager"); @@ -48,6 +50,12 @@ export class GameSystemManager { /** @type {StaticMapEntitySystem} */ staticMapEntities: null, + /** @type {ItemAcceptorSystem} */ + itemAcceptor: null, + + /** @type {StorageSystem} */ + storage: null, + /* typehints:end */ }; this.systemUpdateOrder = []; @@ -68,20 +76,24 @@ export class GameSystemManager { add("belt", BeltSystem); - add("itemEjector", ItemEjectorSystem); + add("undergroundBelt", UndergroundBeltSystem); add("miner", MinerSystem); - add("mapResources", MapResourcesSystem); + add("storage", StorageSystem); add("itemProcessor", ItemProcessorSystem); - add("undergroundBelt", UndergroundBeltSystem); + add("itemEjector", ItemEjectorSystem); + + add("mapResources", MapResourcesSystem); add("hub", HubSystem); add("staticMapEntities", StaticMapEntitySystem); + add("itemAcceptor", ItemAcceptorSystem); + logger.log("📦 There are", this.systemUpdateOrder.length, "game systems"); } diff --git a/src/js/game/game_system_with_filter.js b/src/js/game/game_system_with_filter.js index 3585c24e..52f8ddfe 100644 --- a/src/js/game/game_system_with_filter.js +++ b/src/js/game/game_system_with_filter.js @@ -5,7 +5,7 @@ import { Entity } from "./entity"; /* typehints:end */ import { GameSystem } from "./game_system"; -import { arrayDelete } from "../core/utils"; +import { arrayDelete, arrayDeleteValue } from "../core/utils"; import { DrawParameters } from "../core/draw_parameters"; import { globalConfig } from "../core/config"; import { Math_floor, Math_ceil } from "../core/builtins"; @@ -30,6 +30,7 @@ export class GameSystemWithFilter extends GameSystem { this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this); this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this); + this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this); this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this); this.root.signals.postLoadHook.add(this.internalPostLoadHook, this); @@ -122,6 +123,24 @@ export class GameSystemWithFilter extends GameSystem { this.internalRegisterEntity(entity); } + /** + * + * @param {Entity} entity + */ + internalCheckEntityAfterComponentRemoval(entity) { + if (this.allEntities.indexOf(entity) < 0) { + // Entity wasn't interesting anyways + return; + } + + for (let i = 0; i < this.requiredComponentIds.length; ++i) { + if (!entity.components[this.requiredComponentIds[i]]) { + // Entity is not interesting anymore + arrayDeleteValue(this.allEntities, entity); + } + } + } + /** * * @param {Entity} entity diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 89aad926..f1fc15c9 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -1,22 +1,60 @@ -import { BasicSerializableObject } from "../savegame/serialization"; -import { GameRoot } from "./root"; -import { ShapeDefinition, enumSubShape } from "./shape_definition"; -import { enumColors } from "./colors"; -import { randomChoice, clamp, randomInt, findNiceIntegerValue } from "../core/utils"; -import { tutorialGoals, enumHubGoalRewards } from "./tutorial_goals"; -import { createLogger } from "../core/logging"; -import { globalConfig } from "../core/config"; import { Math_random } from "../core/builtins"; -import { UPGRADES } from "./upgrades"; +import { globalConfig } from "../core/config"; +import { queryParamOptions } from "../core/query_parameters"; +import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { enumColors } from "./colors"; import { enumItemProcessorTypes } from "./components/item_processor"; - -const logger = createLogger("hub_goals"); +import { GameRoot } from "./root"; +import { enumSubShape, ShapeDefinition } from "./shape_definition"; +import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals"; +import { UPGRADES } from "./upgrades"; export class HubGoals extends BasicSerializableObject { static getId() { return "HubGoals"; } + static getSchema() { + return { + level: types.uint, + storedShapes: types.keyValueMap(types.uint), + upgradeLevels: types.keyValueMap(types.uint), + + currentGoal: types.structured({ + definition: types.knownType(ShapeDefinition), + required: types.uint, + reward: types.nullable(types.enum(enumHubGoalRewards)), + }), + }; + } + + deserialize(data) { + const errorCode = super.deserialize(data); + if (errorCode) { + return errorCode; + } + + // Compute gained rewards + for (let i = 0; i < this.level - 1; ++i) { + if (i < tutorialGoals.length) { + const reward = tutorialGoals[i].reward; + this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1; + } + } + + // Compute upgrade improvements + for (const upgradeId in UPGRADES) { + const upgradeHandle = UPGRADES[upgradeId]; + const level = this.upgradeLevels[upgradeId] || 0; + let totalImprovement = upgradeHandle.baseValue || 1; + for (let i = 0; i < level; ++i) { + totalImprovement += upgradeHandle.tiers[i].improvement; + } + this.upgradeImprovements[upgradeId] = totalImprovement; + } + } + /** * @param {GameRoot} root */ @@ -29,6 +67,7 @@ export class HubGoals extends BasicSerializableObject { /** * Which story rewards we already gained + * @type {Object.} */ this.gainedRewards = {}; @@ -74,6 +113,14 @@ export class HubGoals extends BasicSerializableObject { getShapesStored(definition) { return this.storedShapes[definition.getHash()] || 0; } + /** + * Returns how much of the current shape is stored + * @param {string} key + * @returns {number} + */ + getShapesStoredByKey(key) { + return this.storedShapes[key] || 0; + } /** * Returns how much of the current goal was already delivered @@ -110,6 +157,8 @@ export class HubGoals extends BasicSerializableObject { const hash = definition.getHash(); this.storedShapes[hash] = (this.storedShapes[hash] || 0) + 1; + this.root.signals.shapeDelivered.dispatch(definition); + // Check if we have enough for the next level const targetHash = this.currentGoal.definition.getHash(); if ( @@ -129,22 +178,18 @@ export class HubGoals extends BasicSerializableObject { const { shape, required, reward } = tutorialGoals[storyIndex]; this.currentGoal = { /** @type {ShapeDefinition} */ - definition: this.root.shapeDefinitionMgr.registerOrReturnHandle( - ShapeDefinition.fromShortKey(shape) - ), + definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape), required, reward, }; return; } - const reward = enumHubGoalRewards.no_reward; - this.currentGoal = { /** @type {ShapeDefinition} */ definition: this.createRandomShape(), required: 1000 + findNiceIntegerValue(this.level * 47.5), - reward, + reward: enumHubGoalRewards.no_reward_freeplay, }; } @@ -154,11 +199,19 @@ export class HubGoals extends BasicSerializableObject { onGoalCompleted() { const reward = this.currentGoal.reward; this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1; - this.root.signals.storyGoalCompleted.dispatch(this.level, reward); this.root.app.gameAnalytics.handleLevelCompleted(this.level); ++this.level; this.createNextGoal(); + + this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward); + } + + /** + * Returns whether we are playing in free-play + */ + isFreePlay() { + return this.level >= tutorialGoals.length; } /** @@ -189,6 +242,20 @@ export class HubGoals extends BasicSerializableObject { return true; } + /** + * Returns the number of available upgrades + * @returns {number} + */ + getAvailableUpgradeCount() { + let count = 0; + for (const upgradeId in UPGRADES) { + if (this.canUnlockUpgrade(upgradeId)) { + ++count; + } + } + return count; + } + /** * Tries to unlock the given upgrade * @param {string} upgradeId @@ -311,18 +378,38 @@ export class HubGoals extends BasicSerializableObject { case enumItemProcessorTypes.hub: return 1e30; case enumItemProcessorTypes.splitter: - return (2 / globalConfig.beltSpeedItemsPerSecond) * this.upgradeImprovements.processors; - case enumItemProcessorTypes.cutter: - case enumItemProcessorTypes.rotater: - case enumItemProcessorTypes.stacker: + return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2; + case enumItemProcessorTypes.mixer: case enumItemProcessorTypes.painter: + case enumItemProcessorTypes.painterDouble: + case enumItemProcessorTypes.painterQuad: { + assert( + globalConfig.buildingSpeeds[processorType], + "Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType + ); return ( - (1 / globalConfig.beltSpeedItemsPerSecond) * + globalConfig.beltSpeedItemsPerSecond * + this.upgradeImprovements.painting * + globalConfig.buildingSpeeds[processorType] + ); + } + + case enumItemProcessorTypes.cutter: + case enumItemProcessorTypes.cutterQuad: + case enumItemProcessorTypes.rotater: + case enumItemProcessorTypes.rotaterCCW: + case enumItemProcessorTypes.stacker: { + assert( + globalConfig.buildingSpeeds[processorType], + "Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType + ); + return ( + globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.processors * globalConfig.buildingSpeeds[processorType] ); - + } default: assertAlways(false, "invalid processor type: " + processorType); } diff --git a/src/js/game/hud/base_hud_part.js b/src/js/game/hud/base_hud_part.js index cec7e525..032530fb 100644 --- a/src/js/game/hud/base_hud_part.js +++ b/src/js/game/hud/base_hud_part.js @@ -90,18 +90,6 @@ export class BaseHUDPart { // Helpers - /** - * Calls closeMethod if an overlay is opened - * @param {function=} closeMethod - */ - closeOnOverlayOpen(closeMethod = null) { - this.root.hud.signals.overlayOpened.add(overlay => { - if (overlay !== this) { - (closeMethod || this.close).call(this); - } - }, this); - } - /** * Helper method to construct a new click detector * @param {Element} element The element to listen on @@ -153,10 +141,7 @@ export class BaseHUDPart { * @param {KeyActionMapper} sourceMapper */ forwardGameSpeedKeybindings(sourceMapper) { - sourceMapper.forward(this.root.gameState.keyActionMapper, [ - "gamespeed_pause", - "gamespeed_fastforward", - ]); + sourceMapper.forward(this.root.keyMapper, ["gamespeed_pause", "gamespeed_fastforward"]); } /** @@ -165,11 +150,11 @@ export class BaseHUDPart { * @param {KeyActionMapper} sourceMapper */ forwardMapMovementKeybindings(sourceMapper) { - sourceMapper.forward(this.root.gameState.keyActionMapper, [ - "map_move_up", - "map_move_right", - "map_move_down", - "map_move_left", + sourceMapper.forward(this.root.keyMapper, [ + "mapMoveUp", + "mapMoveRight", + "mapMoveDown", + "mapMoveLeft", ]); } } diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index deae4591..2d317b7f 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -12,7 +12,21 @@ import { HUDKeybindingOverlay } from "./parts/keybinding_overlay"; import { HUDUnlockNotification } from "./parts/unlock_notification"; import { HUDGameMenu } from "./parts/game_menu"; import { HUDShop } from "./parts/shop"; -import { IS_MOBILE } from "../../core/config"; +import { IS_MOBILE, globalConfig, IS_DEMO } from "../../core/config"; +import { HUDMassSelector } from "./parts/mass_selector"; +import { HUDVignetteOverlay } from "./parts/vignette_overlay"; +import { HUDStatistics } from "./parts/statistics"; +import { MetaBuilding } from "../meta_building"; +import { HUDPinnedShapes } from "./parts/pinned_shapes"; +import { ShapeDefinition } from "../shape_definition"; +import { HUDNotifications, enumNotificationType } from "./parts/notifications"; +import { HUDSettingsMenu } from "./parts/settings_menu"; +import { HUDDebugInfo } from "./parts/debug_info"; +import { HUDEntityDebugger } from "./parts/entity_debugger"; +import { KEYMAPPINGS } from "../key_action_mapper"; +import { HUDWatermark } from "./parts/watermark"; +import { HUDModalDialogs } from "./parts/modal_dialogs"; +import { HUDPartTutorialHints } from "./parts/tutorial_hints"; export class GameHUD { /** @@ -26,10 +40,6 @@ export class GameHUD { * Initializes the hud parts */ initialize() { - this.signals = { - overlayOpened: new Signal(/* overlay */), - }; - this.parts = { processingOverlay: new HUDProcessingOverlay(this.root), @@ -40,19 +50,45 @@ export class GameHUD { gameMenu: new HUDGameMenu(this.root), + massSelector: new HUDMassSelector(this.root), + shop: new HUDShop(this.root), + statistics: new HUDStatistics(this.root), + + vignetteOverlay: new HUDVignetteOverlay(this.root), + + pinnedShapes: new HUDPinnedShapes(this.root), + + notifications: new HUDNotifications(this.root), + settingsMenu: new HUDSettingsMenu(this.root), // betaOverlay: new HUDBetaOverlay(this.root), + debugInfo: new HUDDebugInfo(this.root), + + dialogs: new HUDModalDialogs(this.root), }; this.signals = { - selectedPlacementBuildingChanged: new Signal(/* metaBuilding|null */), + selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), + shapePinRequested: /** @type {TypedSignal<[ShapeDefinition, number]>} */ (new Signal()), + notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()), }; if (!IS_MOBILE) { this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root); } + if (G_IS_DEV && globalConfig.debug.enableEntityInspector) { + this.parts.entityDebugger = new HUDEntityDebugger(this.root); + } + + if (IS_DEMO) { + this.parts.watermark = new HUDWatermark(this.root); + } + if (this.root.app.settings.getAllSettings().offerHints) { + this.parts.tutorialHints = new HUDPartTutorialHints(this.root); + } + const frag = document.createDocumentFragment(); for (const key in this.parts) { this.parts[key].createElements(frag); @@ -65,7 +101,7 @@ export class GameHUD { } this.internalInitSignalConnections(); - this.root.gameState.keyActionMapper.getBinding("toggle_hud").add(this.toggleUi, this); + this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.toggleHud).add(this.toggleUi, this); } /** @@ -149,7 +185,7 @@ export class GameHUD { * @param {DrawParameters} parameters */ draw(parameters) { - const partsOrder = ["buildingPlacer"]; + const partsOrder = ["massSelector", "buildingPlacer"]; for (let i = 0; i < partsOrder.length; ++i) { if (this.parts[partsOrder[i]]) { @@ -163,7 +199,7 @@ export class GameHUD { * @param {DrawParameters} parameters */ drawOverlays(parameters) { - const partsOrder = []; + const partsOrder = ["watermark"]; for (let i = 0; i < partsOrder.length; ++i) { if (this.parts[partsOrder[i]]) { diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index bb256dc6..fa477dbd 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -1,22 +1,25 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { MetaBuilding } from "../../meta_building"; -import { DrawParameters } from "../../../core/draw_parameters"; +import { Math_abs, Math_degrees, Math_radians } from "../../../core/builtins"; import { globalConfig } from "../../../core/config"; -import { StaticMapEntityComponent } from "../../components/static_map_entity"; -import { STOP_PROPAGATION, Signal } from "../../../core/signal"; -import { - Vector, - enumDirectionToAngle, - enumInvertedDirections, - enumDirectionToVector, -} from "../../../core/vector"; -import { pulseAnimation, makeDiv } from "../../../core/utils"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { TrackedState } from "../../../core/tracked_state"; -import { Math_abs, Math_radians } from "../../../core/builtins"; -import { Loader } from "../../../core/loader"; +import { DrawParameters } from "../../../core/draw_parameters"; import { drawRotatedSprite } from "../../../core/draw_utils"; +import { Loader } from "../../../core/loader"; +import { STOP_PROPAGATION } from "../../../core/signal"; +import { TrackedState } from "../../../core/tracked_state"; +import { makeDiv, removeAllChildren } from "../../../core/utils"; +import { + enumDirectionToAngle, + enumDirectionToVector, + enumInvertedDirections, + Vector, +} from "../../../core/vector"; +import { enumMouseButton } from "../../camera"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; import { Entity } from "../../entity"; +import { defaultBuildingVariant, MetaBuilding } from "../../meta_building"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { T } from "../../../translations"; +import { KEYMAPPINGS } from "../../key_action_mapper"; export class HUDBuildingPlacer extends BaseHUDPart { initialize() { @@ -27,11 +30,14 @@ export class HUDBuildingPlacer extends BaseHUDPart { /** @type {Entity} */ this.fakeEntity = null; - const keyActionMapper = this.root.gameState.keyActionMapper; - keyActionMapper.getBinding("building_abort_placement").add(() => this.currentMetaBuilding.set(null)); - keyActionMapper.getBinding("back").add(() => this.currentMetaBuilding.set(null)); + const keyActionMapper = this.root.keyMapper; + keyActionMapper + .getBinding(KEYMAPPINGS.placement.abortBuildingPlacement) + .add(this.abortPlacement, this); + keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); - keyActionMapper.getBinding("rotate_while_placing").add(this.tryRotate, this); + keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.tryRotate, this); + keyActionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildingVariants).add(this.cycleVariants, this); this.domAttach = new DynamicDomAttach(this.root, this.element, {}); @@ -40,6 +46,20 @@ export class HUDBuildingPlacer extends BaseHUDPart { this.root.camera.upPostHandler.add(this.abortDragging, this); this.currentlyDragging = false; + this.currentVariant = new TrackedState(this.rerenderVariants, this); + + this.variantsAttach = new DynamicDomAttach(this.root, this.variantsElement, {}); + + /** + * Whether we are currently drag-deleting + */ + this.currentlyDeleting = false; + + /** + * Stores which variants for each building we prefer, this is based on what + * the user last selected + */ + this.preferredVariants = {}; /** * The tile we last dragged onto @@ -52,26 +72,51 @@ export class HUDBuildingPlacer extends BaseHUDPart { * @type {Vector} */ this.initialDragTile = null; + + this.root.signals.storyGoalCompleted.add(this.rerenderVariants, this); + this.root.signals.upgradePurchased.add(this.rerenderVariants, this); } createElements(parent) { - this.element = makeDiv(parent, "ingame_HUD_building_placer", [], ``); + this.element = makeDiv(parent, "ingame_HUD_PlacementHints", [], ``); - this.buildingLabel = makeDiv(this.element, null, ["buildingLabel"], "Extract"); - this.buildingDescription = makeDiv(this.element, null, ["description"], ""); + this.buildingInfoElements = {}; + this.buildingInfoElements.label = makeDiv(this.element, null, ["buildingLabel"], "Extract"); + this.buildingInfoElements.desc = makeDiv(this.element, null, ["description"], ""); + this.buildingInfoElements.descText = makeDiv(this.buildingInfoElements.desc, null, ["text"], ""); + this.buildingInfoElements.additionalInfo = makeDiv( + this.buildingInfoElements.desc, + null, + ["additionalInfo"], + "" + ); + this.buildingInfoElements.hotkey = makeDiv(this.buildingInfoElements.desc, null, ["hotkey"], ""); + this.buildingInfoElements.tutorialImage = makeDiv(this.element, null, ["buildingImage"]); + + this.variantsElement = makeDiv(parent, "ingame_HUD_PlacerVariants"); + } + + abortPlacement() { + if (this.currentMetaBuilding.get()) { + this.currentMetaBuilding.set(null); + return STOP_PROPAGATION; + } } /** * mouse down pre handler * @param {Vector} pos + * @param {enumMouseButton} button */ - onMouseDown(pos) { + onMouseDown(pos, button) { if (this.root.camera.getIsMapOverlayActive()) { return; } - if (this.currentMetaBuilding.get()) { + // Placement + if (button === enumMouseButton.left && this.currentMetaBuilding.get()) { this.currentlyDragging = true; + this.currentlyDeleting = false; this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); // Place initial building @@ -79,6 +124,15 @@ export class HUDBuildingPlacer extends BaseHUDPart { return STOP_PROPAGATION; } + + // Deletion + if (button === enumMouseButton.right && !this.currentMetaBuilding.get()) { + this.currentlyDragging = true; + this.currentlyDeleting = true; + this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); + this.currentMetaBuilding.set(null); + return STOP_PROPAGATION; + } } /** @@ -90,12 +144,33 @@ export class HUDBuildingPlacer extends BaseHUDPart { return; } - if (this.currentMetaBuilding.get() && this.lastDragTile) { + const metaBuilding = this.currentMetaBuilding.get(); + if ((metaBuilding || this.currentlyDeleting) && this.lastDragTile) { const oldPos = this.lastDragTile; const newPos = this.root.camera.screenToWorld(pos).toTileSpace(); + if (this.root.camera.desiredCenter) { + // Camera is moving + this.lastDragTile = newPos; + return; + } + if (!oldPos.equals(newPos)) { - const delta = newPos.sub(oldPos); + if ( + metaBuilding && + metaBuilding.getRotateAutomaticallyWhilePlacing(this.currentVariant.get()) && + !this.root.app.inputMgr.ctrlIsDown + ) { + const delta = newPos.sub(oldPos); + const angleDeg = Math_degrees(delta.angle()); + this.currentBaseRotation = (Math.round(angleDeg / 90) * 90 + 360) % 360; + + // Holding alt inverts the placement + if (this.root.app.inputMgr.altIsDown) { + this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; + } + } + // - Using bresenhams algorithmus let x0 = oldPos.x; @@ -109,8 +184,15 @@ export class HUDBuildingPlacer extends BaseHUDPart { var sy = y0 < y1 ? 1 : -1; var err = dx - dy; - while (true) { - this.tryPlaceCurrentBuildingAt(new Vector(x0, y0)); + while (this.currentlyDeleting || this.currentMetaBuilding.get()) { + if (this.currentlyDeleting) { + const contents = this.root.map.getTileContentXY(x0, y0); + if (contents && !contents.queuedForDestroy && !contents.destroyed) { + this.root.logic.tryDeleteBuilding(contents); + } + } else { + this.tryPlaceCurrentBuildingAt(new Vector(x0, y0)); + } if (x0 === x1 && y0 === y1) break; var e2 = 2 * err; if (e2 > -dy) { @@ -143,6 +225,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { */ abortDragging() { this.currentlyDragging = true; + this.currentlyDeleting = false; this.lastDragTile = null; } @@ -155,28 +238,146 @@ export class HUDBuildingPlacer extends BaseHUDPart { } /** - * * @param {MetaBuilding} metaBuilding */ onSelectedMetaBuildingChanged(metaBuilding) { + this.abortDragging(); this.root.hud.signals.selectedPlacementBuildingChanged.dispatch(metaBuilding); if (metaBuilding) { - this.buildingLabel.innerHTML = metaBuilding.getName(); - this.buildingDescription.innerHTML = metaBuilding.getDescription(); + const variant = this.preferredVariants[metaBuilding.getId()] || defaultBuildingVariant; + this.currentVariant.set(variant); this.fakeEntity = new Entity(null); metaBuilding.setupEntityComponents(this.fakeEntity, null); + this.fakeEntity.addComponent( new StaticMapEntityComponent({ origin: new Vector(0, 0), - rotationDegrees: 0, - tileSize: metaBuilding.getDimensions().copy(), + rotation: 0, + tileSize: metaBuilding.getDimensions(this.currentVariant.get()).copy(), }) ); + metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get()); } else { - this.currentlyDragging = false; this.fakeEntity = null; } + + // Since it depends on both, rerender twice + this.rerenderVariants(); + } + + /** + * Rerenders the building info dialog + */ + rerenderInfoDialog() { + const metaBuilding = this.currentMetaBuilding.get(); + + if (!metaBuilding) { + return; + } + + const variant = this.currentVariant.get(); + + this.buildingInfoElements.label.innerHTML = T.buildings[metaBuilding.id][variant].name; + this.buildingInfoElements.descText.innerHTML = T.buildings[metaBuilding.id][variant].description; + + const binding = this.root.keyMapper.getBinding(KEYMAPPINGS.buildings[metaBuilding.getId()]); + this.buildingInfoElements.hotkey.innerHTML = T.ingame.buildingPlacement.hotkeyLabel.replace( + "", + "" + binding.getKeyCodeString() + "" + ); + + this.buildingInfoElements.tutorialImage.setAttribute( + "data-icon", + "building_tutorials/" + + metaBuilding.getId() + + (variant === defaultBuildingVariant ? "" : "-" + variant) + + ".png" + ); + + removeAllChildren(this.buildingInfoElements.additionalInfo); + const additionalInfo = metaBuilding.getAdditionalStatistics(this.root, this.currentVariant.get()); + for (let i = 0; i < additionalInfo.length; ++i) { + const [label, contents] = additionalInfo[i]; + this.buildingInfoElements.additionalInfo.innerHTML += ` + + ${contents} + `; + } + } + + /** + * Rerenders the variants displayed + */ + rerenderVariants() { + removeAllChildren(this.variantsElement); + this.rerenderInfoDialog(); + + const metaBuilding = this.currentMetaBuilding.get(); + + if (!metaBuilding) { + return; + } + const availableVariants = metaBuilding.getAvailableVariants(this.root); + if (availableVariants.length === 1) { + return; + } + + makeDiv( + this.variantsElement, + null, + ["explanation"], + T.ingame.buildingPlacement.cycleBuildingVariants.replace( + "", + "" + + this.root.keyMapper + .getBinding(KEYMAPPINGS.placement.cycleBuildingVariants) + .getKeyCodeString() + + "" + ) + ); + + const container = makeDiv(this.variantsElement, null, ["variants"]); + + for (let i = 0; i < availableVariants.length; ++i) { + const variant = availableVariants[i]; + + const element = makeDiv(container, null, ["variant"]); + element.classList.toggle("active", variant === this.currentVariant.get()); + makeDiv(element, null, ["label"], variant); + + const iconSize = 64; + + const dimensions = metaBuilding.getDimensions(variant); + const sprite = metaBuilding.getPreviewSprite(0, variant); + const spriteWrapper = makeDiv(element, null, ["iconWrap"]); + spriteWrapper.setAttribute("data-tile-w", dimensions.x); + spriteWrapper.setAttribute("data-tile-h", dimensions.y); + + spriteWrapper.innerHTML = sprite.getAsHTML(iconSize * dimensions.x, iconSize * dimensions.y); + } + } + + /** + * Cycles through the variants + */ + cycleVariants() { + const metaBuilding = this.currentMetaBuilding.get(); + if (!metaBuilding) { + this.currentVariant.set(defaultBuildingVariant); + } else { + const availableVariants = metaBuilding.getAvailableVariants(this.root); + const index = availableVariants.indexOf(this.currentVariant.get()); + assert( + index >= 0, + "Current variant was invalid: " + this.currentVariant.get() + " out of " + availableVariants + ); + const newIndex = (index + 1) % availableVariants.length; + const newVariant = availableVariants[newIndex]; + this.currentVariant.set(newVariant); + + this.preferredVariants[metaBuilding.getId()] = newVariant; + } } /** @@ -187,7 +388,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { if (selectedBuilding) { this.currentBaseRotation = (this.currentBaseRotation + 90) % 360; const staticComp = this.fakeEntity.components.StaticMapEntity; - staticComp.rotationDegrees = this.currentBaseRotation; + staticComp.rotation = this.currentBaseRotation; } } @@ -247,7 +448,8 @@ export class HUDBuildingPlacer extends BaseHUDPart { const { rotation, rotationVariant } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile( this.root, tile, - this.currentBaseRotation + this.currentBaseRotation, + this.currentVariant.get() ); if ( @@ -255,16 +457,22 @@ export class HUDBuildingPlacer extends BaseHUDPart { origin: tile, rotation, rotationVariant, + originalRotation: this.currentBaseRotation, building: this.currentMetaBuilding.get(), + variant: this.currentVariant.get(), }) ) { // Succesfully placed - if (metaBuilding.getFlipOrientationAfterPlacement()) { + if (metaBuilding.getFlipOrientationAfterPlacement() && !this.root.app.inputMgr.ctrlIsDown) { this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; } - if (!metaBuilding.getStayInPlacementMode() && !this.root.app.inputMgr.shiftIsDown) { + if ( + !metaBuilding.getStayInPlacementMode() && + !this.root.app.inputMgr.shiftIsDown && + !this.root.app.settings.getAllSettings().alwaysMultiplace + ) { // Stop placement this.currentMetaBuilding.set(null); } @@ -283,10 +491,12 @@ export class HUDBuildingPlacer extends BaseHUDPart { if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) { // Dont allow placing in overview mode this.domAttach.update(false); + this.variantsAttach.update(false); return; } this.domAttach.update(this.currentMetaBuilding.get()); + this.variantsAttach.update(this.currentMetaBuilding.get()); const metaBuilding = this.currentMetaBuilding.get(); if (!metaBuilding) { @@ -310,7 +520,8 @@ export class HUDBuildingPlacer extends BaseHUDPart { } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile( this.root, tile, - this.currentBaseRotation + this.currentBaseRotation, + this.currentVariant.get() ); // Check if there are connected entities @@ -347,40 +558,55 @@ export class HUDBuildingPlacer extends BaseHUDPart { // Synchronize rotation and origin const staticComp = this.fakeEntity.components.StaticMapEntity; staticComp.origin = tile; - staticComp.rotationDegrees = rotation; - metaBuilding.updateRotationVariant(this.fakeEntity, rotationVariant); + staticComp.rotation = rotation; + staticComp.tileSize = metaBuilding.getDimensions(this.currentVariant.get()); + metaBuilding.updateVariants(this.fakeEntity, rotationVariant, this.currentVariant.get()); // Check if we could place the buildnig - const canBuild = this.root.logic.checkCanPlaceBuilding(tile, rotation, metaBuilding); + const canBuild = this.root.logic.checkCanPlaceBuilding({ + origin: tile, + rotation, + rotationVariant, + building: metaBuilding, + variant: this.currentVariant.get(), + }); + + // Fade in / out + parameters.context.lineWidth = 1; + // parameters.context.globalAlpha = 0.3 + pulseAnimation(this.root.time.realtimeNow(), 0.9) * 0.7; // Determine the bounds and visualize them const entityBounds = staticComp.getTileSpaceBounds(); - const drawBorder = 2; - parameters.context.globalAlpha = 0.5; + const drawBorder = -3; if (canBuild) { - parameters.context.fillStyle = "rgba(0, 255, 0, 0.2)"; + parameters.context.strokeStyle = "rgba(56, 235, 111, 0.5)"; + parameters.context.fillStyle = "rgba(56, 235, 111, 0.2)"; } else { + parameters.context.strokeStyle = "rgba(255, 0, 0, 0.2)"; parameters.context.fillStyle = "rgba(255, 0, 0, 0.2)"; } - parameters.context.fillRect( + + parameters.context.beginRoundedRect( entityBounds.x * globalConfig.tileSize - drawBorder, entityBounds.y * globalConfig.tileSize - drawBorder, entityBounds.w * globalConfig.tileSize + 2 * drawBorder, - entityBounds.h * globalConfig.tileSize + 2 * drawBorder + entityBounds.h * globalConfig.tileSize + 2 * drawBorder, + 4 ); + parameters.context.stroke(); + // parameters.context.fill(); + parameters.context.globalAlpha = 1; + + // HACK to draw the entity sprite + const previewSprite = metaBuilding.getBlueprintSprite(rotationVariant, this.currentVariant.get()); + staticComp.origin = worldPos.divideScalar(globalConfig.tileSize).subScalars(0.5, 0.5); + staticComp.drawSpriteOnFullEntityBounds(parameters, previewSprite); + staticComp.origin = tile; // Draw ejectors if (canBuild) { this.drawMatchingAcceptorsAndEjectors(parameters); } - - // HACK to draw the entity sprite - const previewSprite = metaBuilding.getPreviewSprite(rotationVariant); - parameters.context.globalAlpha = 0.8 + pulseAnimation(this.root.time.realtimeNow(), 1) * 0.1; - staticComp.origin = worldPos.divideScalar(globalConfig.tileSize).subScalars(0.5, 0.5); - staticComp.drawSpriteOnFullEntityBounds(parameters, previewSprite); - staticComp.origin = tile; - parameters.context.globalAlpha = 1; } /** @@ -397,6 +623,8 @@ export class HUDBuildingPlacer extends BaseHUDPart { // Just ignore this code ... + const offsetShift = 10; + if (acceptorComp) { const slots = acceptorComp.slots; for (let acceptorSlotIndex = 0; acceptorSlotIndex < slots.length; ++acceptorSlotIndex) { @@ -437,7 +665,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { y: acceptorSlotWsPos.y, angle: Math_radians(enumDirectionToAngle[enumInvertedDirections[worldDirection]]), size: 13, - offsetY: 15, + offsetY: offsetShift + 13, }); parameters.context.globalAlpha = 1; } @@ -483,7 +711,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { y: ejectorSLotWsPos.y, angle: Math_radians(enumDirectionToAngle[ejectorSlotWsDirection]), size: 13, - offsetY: 15, + offsetY: offsetShift, }); parameters.context.globalAlpha = 1; } diff --git a/src/js/game/hud/parts/buildings_toolbar.js b/src/js/game/hud/parts/buildings_toolbar.js index 4d621a2d..86c99a01 100644 --- a/src/js/game/hud/parts/buildings_toolbar.js +++ b/src/js/game/hud/parts/buildings_toolbar.js @@ -1,26 +1,26 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv } from "../../../core/utils"; import { gMetaBuildingRegistry } from "../../../core/global_registries"; -import { MetaBuilding } from "../../meta_building"; import { Signal } from "../../../core/signal"; -import { MetaSplitterBuilding } from "../../buildings/splitter"; -import { MetaMinerBuilding } from "../../buildings/miner"; +import { TrackedState } from "../../../core/tracked_state"; +import { makeDiv } from "../../../core/utils"; +import { MetaBeltBaseBuilding } from "../../buildings/belt_base"; import { MetaCutterBuilding } from "../../buildings/cutter"; -import { MetaRotaterBuilding } from "../../buildings/rotater"; -import { MetaStackerBuilding } from "../../buildings/stacker"; +import { MetaMinerBuilding } from "../../buildings/miner"; import { MetaMixerBuilding } from "../../buildings/mixer"; import { MetaPainterBuilding } from "../../buildings/painter"; +import { MetaRotaterBuilding } from "../../buildings/rotater"; +import { MetaSplitterBuilding } from "../../buildings/splitter"; +import { MetaStackerBuilding } from "../../buildings/stacker"; import { MetaTrashBuilding } from "../../buildings/trash"; -import { MetaBeltBaseBuilding } from "../../buildings/belt_base"; import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; -import { globalConfig } from "../../../core/config"; -import { TrackedState } from "../../../core/tracked_state"; +import { MetaBuilding } from "../../meta_building"; +import { BaseHUDPart } from "../base_hud_part"; +import { KEYMAPPINGS } from "../../key_action_mapper"; const toolbarBuildings = [ MetaBeltBaseBuilding, - MetaMinerBuilding, - MetaUndergroundBeltBuilding, MetaSplitterBuilding, + MetaUndergroundBeltBuilding, + MetaMinerBuilding, MetaCutterBuilding, MetaRotaterBuilding, MetaStackerBuilding, @@ -33,8 +33,14 @@ export class HUDBuildingsToolbar extends BaseHUDPart { constructor(root) { super(root); - /** @type {Object.} */ - this.buildingUnlockStates = {}; + /** @type {Object.} */ + this.buildingHandles = {}; this.sigBuildingSelected = new Signal(); @@ -54,67 +60,81 @@ export class HUDBuildingsToolbar extends BaseHUDPart { } initialize() { - const actionMapper = this.root.gameState.keyActionMapper; + const actionMapper = this.root.keyMapper; const items = makeDiv(this.element, null, ["buildings"]); - const iconSize = 32; for (let i = 0; i < toolbarBuildings.length; ++i) { const metaBuilding = gMetaBuildingRegistry.findByClass(toolbarBuildings[i]); - const binding = actionMapper.getBinding("building_" + metaBuilding.getId()); + const binding = actionMapper.getBinding(KEYMAPPINGS.buildings[metaBuilding.getId()]); - const dimensions = metaBuilding.getDimensions(); const itemContainer = makeDiv(items, null, ["building"]); - itemContainer.setAttribute("data-tilewidth", dimensions.x); - itemContainer.setAttribute("data-tileheight", dimensions.y); + itemContainer.setAttribute("data-icon", "building_icons/" + metaBuilding.getId() + ".png"); - const label = makeDiv(itemContainer, null, ["label"]); - label.innerText = metaBuilding.getName(); - - const tooltip = makeDiv( - itemContainer, - null, - ["tooltip"], - ` - ${metaBuilding.getName()} - ${metaBuilding.getDescription()} - - ` - ); - - const sprite = metaBuilding.getPreviewSprite(0); - - const spriteWrapper = makeDiv(itemContainer, null, ["iconWrap"]); - spriteWrapper.innerHTML = sprite.getAsHTML(iconSize * dimensions.x, iconSize * dimensions.y); - - binding.appendLabelToElement(itemContainer); binding.add(() => this.selectBuildingForPlacement(metaBuilding)); - this.trackClicks(itemContainer, () => this.selectBuildingForPlacement(metaBuilding), {}); + this.trackClicks(itemContainer, () => this.selectBuildingForPlacement(metaBuilding), { + clickSound: null, + }); - this.buildingUnlockStates[metaBuilding.id] = { + this.buildingHandles[metaBuilding.id] = { metaBuilding, element: itemContainer, - status: false, + unlocked: false, + selected: false, + index: i, }; } + + this.root.hud.signals.selectedPlacementBuildingChanged.add( + this.onSelectedPlacementBuildingChanged, + this + ); + + this.lastSelectedIndex = 0; + actionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildings).add(this.cycleBuildings, this); } update() { this.trackedIsVisisible.set(!this.root.camera.getIsMapOverlayActive()); - for (const buildingId in this.buildingUnlockStates) { - const handle = this.buildingUnlockStates[buildingId]; + for (const buildingId in this.buildingHandles) { + const handle = this.buildingHandles[buildingId]; const newStatus = handle.metaBuilding.getIsUnlocked(this.root); - if (handle.status !== newStatus) { - handle.status = newStatus; + if (handle.unlocked !== newStatus) { + handle.unlocked = newStatus; handle.element.classList.toggle("unlocked", newStatus); } } } + cycleBuildings() { + const newIndex = (this.lastSelectedIndex + 1) % toolbarBuildings.length; + const metaBuildingClass = toolbarBuildings[newIndex]; + const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass); + this.selectBuildingForPlacement(metaBuilding); + } + + /** + * @param {MetaBuilding} metaBuilding + */ + onSelectedPlacementBuildingChanged(metaBuilding) { + for (const buildingId in this.buildingHandles) { + const handle = this.buildingHandles[buildingId]; + const newStatus = handle.metaBuilding === metaBuilding; + if (handle.selected !== newStatus) { + handle.selected = newStatus; + handle.element.classList.toggle("selected", newStatus); + } + if (handle.selected) { + this.lastSelectedIndex = handle.index; + } + } + + this.element.classList.toggle("buildingSelected", !!metaBuilding); + } + /** - * * @param {MetaBuilding} metaBuilding */ selectBuildingForPlacement(metaBuilding) { @@ -123,6 +143,17 @@ export class HUDBuildingsToolbar extends BaseHUDPart { return; } + // Allow clicking an item again to deselect it + for (const buildingId in this.buildingHandles) { + const handle = this.buildingHandles[buildingId]; + if (handle.selected && handle.metaBuilding === metaBuilding) { + metaBuilding = null; + break; + } + } + + this.root.soundProxy.playUiClick(); this.sigBuildingSelected.dispatch(metaBuilding); + this.onSelectedPlacementBuildingChanged(metaBuilding); } } diff --git a/src/js/game/hud/parts/debug_info.js b/src/js/game/hud/parts/debug_info.js new file mode 100644 index 00000000..4f4c052e --- /dev/null +++ b/src/js/game/hud/parts/debug_info.js @@ -0,0 +1,45 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv, round3Digits, round2Digits } from "../../../core/utils"; +import { Math_round } from "../../../core/builtins"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { KEYMAPPINGS } from "../../key_action_mapper"; + +export class HUDDebugInfo extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_DebugInfo", []); + + this.tickRateElement = makeDiv(this.element, null, ["tickRate"], "Ticks /s: 120"); + this.fpsElement = makeDiv(this.element, null, ["fps"], "FPS: 60"); + this.tickDurationElement = makeDiv(this.element, null, ["tickDuration"], "Update time: 0.5ms"); + } + + initialize() { + this.lastTick = 0; + + this.visible = false; + this.domAttach = new DynamicDomAttach(this.root, this.element); + + this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.toggleFPSInfo).add(() => this.toggle()); + } + + toggle() { + this.visible = !this.visible; + this.domAttach.update(this.visible); + } + + update() { + const now = this.root.time.realtimeNow(); + if (now - this.lastTick > 0.25 && this.visible) { + this.lastTick = now; + this.tickRateElement.innerText = "Tickrate: " + this.root.dynamicTickrate.currentTickRate; + this.fpsElement.innerText = + "FPS: " + + Math_round(this.root.dynamicTickrate.averageFps) + + " (" + + round2Digits(1000 / this.root.dynamicTickrate.averageFps) + + " ms)"; + this.tickDurationElement.innerText = + "Tick Dur: " + round3Digits(this.root.dynamicTickrate.averageTickDuration) + "ms"; + } + } +} diff --git a/src/js/game/hud/parts/entity_debugger.js b/src/js/game/hud/parts/entity_debugger.js new file mode 100644 index 00000000..816ce77c --- /dev/null +++ b/src/js/game/hud/parts/entity_debugger.js @@ -0,0 +1,68 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv, removeAllChildren } from "../../../core/utils"; +import { globalConfig } from "../../../core/config"; + +export class HUDEntityDebugger extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv( + parent, + "ingame_HUD_EntityDebugger", + [], + ` + Tile below cursor:
+ Chunk below cursor:
+
+ ` + ); + + this.mousePosElem = this.element.querySelector(".mousePos"); + this.chunkPosElem = this.element.querySelector(".chunkPos"); + this.entityInfoElem = this.element.querySelector(".entityInfo"); + } + + initialize() { + this.root.camera.downPreHandler.add(this.onMouseDown, this); + } + + update() { + const mousePos = this.root.app.mousePosition; + const worldPos = this.root.camera.screenToWorld(mousePos); + const worldTile = worldPos.toTileSpace(); + + const chunk = worldTile.divideScalar(globalConfig.mapChunkSize).floor(); + this.mousePosElem.innerText = worldTile.x + " / " + worldTile.y; + this.chunkPosElem.innerText = chunk.x + " / " + chunk.y; + + const entity = this.root.map.getTileContent(worldTile); + if (entity) { + removeAllChildren(this.entityInfoElem); + let html = "Entity"; + + const flag = (name, val) => + `${name} ${val}`; + + html += "
"; + html += flag("registered", entity.registered); + html += flag("uid", entity.uid); + html += flag("destroyed", entity.destroyed); + html += "
"; + + html += "
"; + + for (const componentId in entity.components) { + const data = entity.components[componentId]; + html += "
"; + html += "" + componentId + ""; + html += ""; + + html += "
"; + } + + html += "
"; + + this.entityInfoElem.innerHTML = html; + } + } + + onMouseDown() {} +} diff --git a/src/js/game/hud/parts/game_menu.js b/src/js/game/hud/parts/game_menu.js index 105b0970..ef05bdbf 100644 --- a/src/js/game/hud/parts/game_menu.js +++ b/src/js/game/hud/parts/game_menu.js @@ -1,5 +1,10 @@ import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv } from "../../../core/utils"; +import { makeDiv, randomInt } from "../../../core/utils"; +import { SOUNDS } from "../../../platform/sound"; +import { enumNotificationType } from "./notifications"; +import { T } from "../../../translations"; +import { KEYMAPPINGS } from "../../key_action_mapper"; +import { IS_DEMO } from "../../../core/config"; export class HUDGameMenu extends BaseHUDPart { initialize() {} @@ -11,27 +16,132 @@ export class HUDGameMenu extends BaseHUDPart { id: "shop", label: "Upgrades", handler: () => this.root.hud.parts.shop.show(), - keybinding: "menu_open_shop", + keybinding: KEYMAPPINGS.ingame.menuOpenShop, + badge: () => this.root.hubGoals.getAvailableUpgradeCount(), + notification: /** @type {[string, enumNotificationType]} */ ([ + T.ingame.notifications.newUpgrade, + enumNotificationType.upgrade, + ]), }, { id: "stats", label: "Stats", - handler: () => null, - keybinding: "menu_open_stats", + handler: () => this.root.hud.parts.statistics.show(), + keybinding: KEYMAPPINGS.ingame.menuOpenStats, }, ]; - buttons.forEach(({ id, label, handler, keybinding }) => { + /** @type {Array<{ + * badge: function, + * button: HTMLElement, + * badgeElement: HTMLElement, + * lastRenderAmount: number, + * notification: [string, enumNotificationType] + * }>} */ + this.badgesToUpdate = []; + + buttons.forEach(({ id, label, handler, keybinding, badge, notification }) => { const button = document.createElement("button"); button.setAttribute("data-button-id", id); this.element.appendChild(button); this.trackClicks(button, handler); if (keybinding) { - const binding = this.root.gameState.keyActionMapper.getBinding(keybinding); + const binding = this.root.keyMapper.getBinding(keybinding); binding.add(handler); binding.appendLabelToElement(button); } + + if (badge) { + const badgeElement = makeDiv(button, null, ["badge"]); + this.badgesToUpdate.push({ + badge, + lastRenderAmount: 0, + button, + badgeElement, + notification, + }); + } + }); + + const menuButtons = makeDiv(this.element, null, ["menuButtons"]); + + this.musicButton = makeDiv(menuButtons, null, ["button", "music"]); + this.sfxButton = makeDiv(menuButtons, null, ["button", "sfx"]); + this.saveButton = makeDiv(menuButtons, null, ["button", "save", "animEven"]); + this.settingsButton = makeDiv(menuButtons, null, ["button", "settings"]); + + this.trackClicks(this.musicButton, this.toggleMusic); + this.trackClicks(this.sfxButton, this.toggleSfx); + this.trackClicks(this.saveButton, this.startSave); + this.trackClicks(this.settingsButton, this.openSettings); + + this.musicButton.classList.toggle("muted", this.root.app.settings.getAllSettings().musicMuted); + this.sfxButton.classList.toggle("muted", this.root.app.settings.getAllSettings().soundsMuted); + + this.root.signals.gameSaved.add(this.onGameSaved, this); + } + + update() { + let playSound = false; + let notifications = new Set(); + for (let i = 0; i < this.badgesToUpdate.length; ++i) { + const { badge, button, badgeElement, lastRenderAmount, notification } = this.badgesToUpdate[i]; + const amount = badge(); + if (lastRenderAmount !== amount) { + if (amount > 0) { + badgeElement.innerText = amount; + } + // Check if the badge increased + if (amount > lastRenderAmount) { + playSound = true; + if (notification) { + notifications.add(notification); + } + } + this.badgesToUpdate[i].lastRenderAmount = amount; + button.classList.toggle("hasBadge", amount > 0); + } + } + + if (playSound) { + this.root.soundProxy.playUi(SOUNDS.badgeNotification); + } + notifications.forEach(([notification, type]) => { + this.root.hud.signals.notification.dispatch(notification, type); }); } + + onGameSaved() { + this.saveButton.classList.toggle("animEven"); + this.saveButton.classList.toggle("animOdd"); + } + + startSave() { + // if (IS_DEMO) { + // this.root.hud.parts.dialogs.showFeatureRestrictionInfo( + // null, + // T.dialogs.saveNotPossibleInDemo.desc + // ); + // } + + this.root.gameState.doSave(); + } + + openSettings() { + this.root.hud.parts.settingsMenu.show(); + } + + toggleMusic() { + const newValue = !this.root.app.settings.getAllSettings().musicMuted; + this.root.app.settings.updateSetting("musicMuted", newValue); + + this.musicButton.classList.toggle("muted", newValue); + } + + toggleSfx() { + const newValue = !this.root.app.settings.getAllSettings().soundsMuted; + this.root.app.settings.updateSetting("soundsMuted", newValue); + this.sfxButton.classList.toggle("muted", newValue); + } } diff --git a/src/js/game/hud/parts/keybinding_overlay.js b/src/js/game/hud/parts/keybinding_overlay.js index 96be31e3..b143186a 100644 --- a/src/js/game/hud/parts/keybinding_overlay.js +++ b/src/js/game/hud/parts/keybinding_overlay.js @@ -1,11 +1,18 @@ import { BaseHUDPart } from "../base_hud_part"; import { makeDiv } from "../../../core/utils"; -import { getStringForKeyCode } from "../../key_action_mapper"; +import { getStringForKeyCode, KEYMAPPINGS } from "../../key_action_mapper"; import { TrackedState } from "../../../core/tracked_state"; +import { queryParamOptions } from "../../../core/query_parameters"; +import { T } from "../../../translations"; export class HUDKeybindingOverlay extends BaseHUDPart { initialize() { this.shiftDownTracker = new TrackedState(this.onShiftStateChanged, this); + + this.root.hud.signals.selectedPlacementBuildingChanged.add( + this.onSelectedBuildingForPlacementChanged, + this + ); } onShiftStateChanged(shiftDown) { @@ -13,7 +20,7 @@ export class HUDKeybindingOverlay extends BaseHUDPart { } createElements(parent) { - const mapper = this.root.gameState.keyActionMapper; + const mapper = this.root.keyMapper; const getKeycode = id => { return getStringForKeyCode(mapper.getBinding(id).keyCode); @@ -25,39 +32,46 @@ export class HUDKeybindingOverlay extends BaseHUDPart { [], `
- ${getKeycode("center_map")} - + ${getKeycode(KEYMAPPINGS.ingame.centerMap)} +
- - ${getKeycode("map_move_up")} - ${getKeycode("map_move_left")} - ${getKeycode("map_move_down")} - ${getKeycode("map_move_right")} - + + ${getKeycode(KEYMAPPINGS.ingame.mapMoveUp)} + ${getKeycode(KEYMAPPINGS.ingame.mapMoveLeft)} + ${getKeycode(KEYMAPPINGS.ingame.mapMoveDown)} + ${getKeycode(KEYMAPPINGS.ingame.mapMoveRight)} +
- - + + ${T.global.keys.control}+ + +
- - ${getKeycode("building_abort_placement")} - + + +
+ +
+ + ${getKeycode(KEYMAPPINGS.placement.abortBuildingPlacement)} +
- ${getKeycode("rotate_while_placing")} - + ${getKeycode(KEYMAPPINGS.placement.rotateWhilePlacing)} +
-
- SHIFT - +
+ ⇧ ${T.global.keys.shift} +
` ); diff --git a/src/js/game/hud/parts/mass_selector.js b/src/js/game/hud/parts/mass_selector.js new file mode 100644 index 00000000..ddcf9117 --- /dev/null +++ b/src/js/game/hud/parts/mass_selector.js @@ -0,0 +1,210 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { Vector } from "../../../core/vector"; +import { STOP_PROPAGATION } from "../../../core/signal"; +import { DrawParameters } from "../../../core/draw_parameters"; +import { Entity } from "../../entity"; +import { Loader } from "../../../core/loader"; +import { globalConfig } from "../../../core/config"; +import { makeDiv } from "../../../core/utils"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { createLogger } from "../../../core/logging"; +import { enumMouseButton } from "../../camera"; +import { T } from "../../../translations"; +import { KEYMAPPINGS } from "../../key_action_mapper"; + +const logger = createLogger("hud/mass_selector"); + +export class HUDMassSelector extends BaseHUDPart { + createElements(parent) { + const removalKeybinding = this.root.keyMapper + .getBinding(KEYMAPPINGS.massSelect.confirmMassDelete) + .getKeyCodeString(); + const abortKeybinding = this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).getKeyCodeString(); + + this.element = makeDiv( + parent, + "ingame_HUD_MassSelector", + [], + T.ingame.massDelete.infoText + .replace("", removalKeybinding) + .replace("", abortKeybinding) + ); + } + + initialize() { + this.deletionMarker = Loader.getSprite("sprites/misc/deletion_marker.png"); + + this.currentSelectionStart = null; + this.currentSelectionEnd = null; + this.entityUidsMarkedForDeletion = new Set(); + + this.root.signals.entityQueuedForDestroy.add(this.onEntityDestroyed, this); + + this.root.camera.downPreHandler.add(this.onMouseDown, this); + this.root.camera.movePreHandler.add(this.onMouseMove, this); + this.root.camera.upPostHandler.add(this.onMouseUp, this); + + this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).add(this.onBack, this); + this.root.keyMapper + .getBinding(KEYMAPPINGS.massSelect.confirmMassDelete) + .add(this.confirmDelete, this); + + this.domAttach = new DynamicDomAttach(this.root, this.element); + } + + /** + * Handles the destroy callback and makes sure we clean our list + * @param {Entity} entity + */ + onEntityDestroyed(entity) { + this.entityUidsMarkedForDeletion.delete(entity.uid); + } + + /** + * + */ + onBack() { + // Clear entities on escape + if (this.entityUidsMarkedForDeletion.size > 0) { + this.entityUidsMarkedForDeletion = new Set(); + return STOP_PROPAGATION; + } + } + + confirmDelete() { + const entityUids = Array.from(this.entityUidsMarkedForDeletion); + for (let i = 0; i < entityUids.length; ++i) { + const uid = entityUids[i]; + const entity = this.root.entityMgr.findByUid(uid); + if (!this.root.logic.tryDeleteBuilding(entity)) { + logger.error("Error in mass delete, could not remove building"); + this.entityUidsMarkedForDeletion.delete(uid); + } + } + } + + /** + * mouse down pre handler + * @param {Vector} pos + * @param {enumMouseButton} mouseButton + */ + onMouseDown(pos, mouseButton) { + if (!this.root.app.inputMgr.ctrlIsDown) { + return; + } + + if (mouseButton !== enumMouseButton.left) { + return; + } + + if (!this.root.app.inputMgr.shiftIsDown) { + // Start new selection + this.entityUidsMarkedForDeletion = new Set(); + } + + this.currentSelectionStart = pos.copy(); + this.currentSelectionEnd = pos.copy(); + return STOP_PROPAGATION; + } + + /** + * mouse move pre handler + * @param {Vector} pos + */ + onMouseMove(pos) { + if (this.currentSelectionStart) { + this.currentSelectionEnd = pos.copy(); + } + } + + onMouseUp() { + if (this.currentSelectionStart) { + const worldStart = this.root.camera.screenToWorld(this.currentSelectionStart); + const worldEnd = this.root.camera.screenToWorld(this.currentSelectionEnd); + + const tileStart = worldStart.toTileSpace(); + const tileEnd = worldEnd.toTileSpace(); + + const realTileStart = tileStart.min(tileEnd); + const realTileEnd = tileStart.max(tileEnd); + + for (let x = realTileStart.x; x <= realTileEnd.x; ++x) { + for (let y = realTileStart.y; y <= realTileEnd.y; ++y) { + const contents = this.root.map.getTileContentXY(x, y); + if (contents && this.root.logic.canDeleteBuilding(contents)) { + this.entityUidsMarkedForDeletion.add(contents.uid); + } + } + } + + this.currentSelectionStart = null; + this.currentSelectionEnd = null; + } + } + + update() { + this.domAttach.update(this.entityUidsMarkedForDeletion.size > 0); + } + + /** + * + * @param {DrawParameters} parameters + */ + draw(parameters) { + if (this.currentSelectionStart) { + const worldStart = this.root.camera.screenToWorld(this.currentSelectionStart); + const worldEnd = this.root.camera.screenToWorld(this.currentSelectionEnd); + + const realWorldStart = worldStart.min(worldEnd); + const realWorldEnd = worldStart.max(worldEnd); + + const tileStart = worldStart.toTileSpace(); + const tileEnd = worldEnd.toTileSpace(); + + const realTileStart = tileStart.min(tileEnd); + const realTileEnd = tileStart.max(tileEnd); + + parameters.context.lineWidth = 1; + parameters.context.fillStyle = "rgba(255, 127, 127, 0.2)"; + parameters.context.strokeStyle = "rgba(255, 127, 127, 0.5)"; + parameters.context.beginPath(); + parameters.context.rect( + realWorldStart.x, + realWorldStart.y, + realWorldEnd.x - realWorldStart.x, + realWorldEnd.y - realWorldStart.y + ); + parameters.context.fill(); + parameters.context.stroke(); + + for (let x = realTileStart.x; x <= realTileEnd.x; ++x) { + for (let y = realTileStart.y; y <= realTileEnd.y; ++y) { + const contents = this.root.map.getTileContentXY(x, y); + if (contents && this.root.logic.canDeleteBuilding(contents)) { + const staticComp = contents.components.StaticMapEntity; + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + this.deletionMarker.drawCachedCentered( + parameters, + center.x, + center.y, + globalConfig.tileSize * 0.5 + ); + } + } + } + } + + this.entityUidsMarkedForDeletion.forEach(uid => { + const entity = this.root.entityMgr.findByUid(uid); + const staticComp = entity.components.StaticMapEntity; + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + + this.deletionMarker.drawCachedCentered( + parameters, + center.x, + center.y, + globalConfig.tileSize * 0.5 + ); + }); + } +} diff --git a/src/js/game/hud/parts/modal_dialogs.js b/src/js/game/hud/parts/modal_dialogs.js new file mode 100644 index 00000000..95428691 --- /dev/null +++ b/src/js/game/hud/parts/modal_dialogs.js @@ -0,0 +1,211 @@ +/* typehints:start */ +import { Application } from "../../../application"; +/* typehints:end */ + +import { SOUNDS } from "../../../platform/sound"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { BaseHUDPart } from "../base_hud_part"; +import { Dialog, DialogLoading, DialogOptionChooser } from "../../../core/modal_dialog_elements"; +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { THIRDPARTY_URLS } from "../../../core/config"; + +export class HUDModalDialogs extends BaseHUDPart { + constructor(root, app) { + // Important: Root is not always available here! Its also used in the main menu + super(root); + + /** @type {Application} */ + this.app = root ? root.app : app; + + this.dialogParent = null; + this.dialogStack = []; + } + + // For use inside of the game, implementation of base hud part + initialize() { + this.dialogParent = document.getElementById("ingame_HUD_ModalDialogs"); + this.domWatcher = new DynamicDomAttach(this.root, this.dialogParent); + } + + shouldPauseRendering() { + return this.dialogStack.length > 0; + } + + shouldPauseGame() { + return this.shouldPauseRendering(); + } + + createElements(parent) { + return makeDiv(parent, "ingame_HUD_ModalDialogs"); + } + + // For use outside of the game + initializeToElement(element) { + assert(element, "No element for dialogs given"); + this.dialogParent = element; + } + + // Methods + + /** + * @param {string} title + * @param {string} text + * @param {Array} buttons + */ + showInfo(title, text, buttons = ["ok:good"]) { + const dialog = new Dialog({ + app: this.app, + title: title, + contentHTML: text, + buttons: buttons, + type: "info", + }); + this.internalShowDialog(dialog); + + if (this.app) { + this.app.sound.playUiSound(SOUNDS.dialogOk); + } + + return dialog.buttonSignals; + } + + /** + * @param {string} title + * @param {string} text + * @param {Array} buttons + */ + showWarning(title, text, buttons = ["ok:good"]) { + const dialog = new Dialog({ + app: this.app, + title: title, + contentHTML: text, + buttons: buttons, + type: "warning", + }); + this.internalShowDialog(dialog); + + if (this.app) { + this.app.sound.playUiSound(SOUNDS.dialogError); + } + + return dialog.buttonSignals; + } + + /** + * @param {string} feature + * @param {string} textPrefab + */ + showFeatureRestrictionInfo(feature, textPrefab = T.dialogs.featureRestriction.desc) { + const dialog = new Dialog({ + app: this.app, + title: T.dialogs.featureRestriction.title, + contentHTML: textPrefab.replace("", feature), + buttons: ["cancel:bad", "getStandalone:good"], + type: "warning", + }); + this.internalShowDialog(dialog); + + if (this.app) { + this.app.sound.playUiSound(SOUNDS.dialogOk); + } + + this.app.analytics.trackUiClick("demo_dialog_show"); + + dialog.buttonSignals.cancel.add(() => { + this.app.analytics.trackUiClick("demo_dialog_cancel"); + }); + + dialog.buttonSignals.getStandalone.add(() => { + this.app.analytics.trackUiClick("demo_dialog_click"); + window.open(THIRDPARTY_URLS.standaloneStorePage); + }); + + return dialog.buttonSignals; + } + + showOptionChooser(title, options) { + const dialog = new DialogOptionChooser({ + app: this.app, + title, + options, + }); + this.internalShowDialog(dialog); + return dialog.buttonSignals; + } + + // Returns method to be called when laoding finishd + showLoadingDialog() { + const dialog = new DialogLoading(this.app); + this.internalShowDialog(dialog); + return this.closeDialog.bind(this, dialog); + } + + internalShowDialog(dialog) { + const elem = dialog.createElement(); + dialog.setIndex(this.dialogStack.length); + + // Hide last dialog in queue + if (this.dialogStack.length > 0) { + this.dialogStack[this.dialogStack.length - 1].hide(); + } + + this.dialogStack.push(dialog); + + // Append dialog + dialog.show(); + dialog.closeRequested.add(this.closeDialog.bind(this, dialog)); + + // Append to HTML + this.dialogParent.appendChild(elem); + + document.body.classList.toggle("modalDialogActive", this.dialogStack.length > 0); + + // IMPORTANT: Attach element directly, otherwise double submit is possible + this.update(); + } + + update() { + if (this.domWatcher) { + this.domWatcher.update(this.dialogStack.length > 0); + } + } + + closeDialog(dialog) { + dialog.destroy(); + + let index = -1; + for (let i = 0; i < this.dialogStack.length; ++i) { + if (this.dialogStack[i] === dialog) { + index = i; + break; + } + } + assert(index >= 0, "Dialog not in dialog stack"); + this.dialogStack.splice(index, 1); + + if (this.dialogStack.length > 0) { + // Show the dialog which was previously open + this.dialogStack[this.dialogStack.length - 1].show(); + } + + document.body.classList.toggle("modalDialogActive", this.dialogStack.length > 0); + } + + close() { + for (let i = 0; i < this.dialogStack.length; ++i) { + const dialog = this.dialogStack[i]; + dialog.destroy(); + } + this.dialogStack = []; + } + + cleanup() { + super.cleanup(); + for (let i = 0; i < this.dialogStack.length; ++i) { + this.dialogStack[i].destroy(); + } + this.dialogStack = []; + this.dialogParent = null; + } +} diff --git a/src/js/game/hud/parts/notifications.js b/src/js/game/hud/parts/notifications.js new file mode 100644 index 00000000..aef0cc75 --- /dev/null +++ b/src/js/game/hud/parts/notifications.js @@ -0,0 +1,56 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { IS_DEMO } from "../../../core/config"; + +/** @enum {string} */ +export const enumNotificationType = { + saved: "saved", + upgrade: "upgrade", + success: "success", +}; + +const notificationDuration = 3; + +export class HUDNotifications extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_Notifications", [], ``); + } + + initialize() { + this.root.hud.signals.notification.add(this.onNotification, this); + + /** @type {Array<{ element: HTMLElement, expireAt: number}>} */ + this.notificationElements = []; + + // Automatic notifications + this.root.signals.gameSaved.add(() => + this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved) + ); + } + + /** + * @param {string} message + * @param {enumNotificationType} type + */ + onNotification(message, type) { + const element = makeDiv(this.element, null, ["notification", "type-" + type], message); + element.setAttribute("data-icon", "icons/notification_" + type + ".png"); + + this.notificationElements.push({ + element, + expireAt: this.root.time.realtimeNow() + notificationDuration, + }); + } + + update() { + const now = this.root.time.realtimeNow(); + for (let i = 0; i < this.notificationElements.length; ++i) { + const handle = this.notificationElements[i]; + if (handle.expireAt <= now) { + handle.element.remove(); + this.notificationElements.splice(i, 1); + } + } + } +} diff --git a/src/js/game/hud/parts/pinned_shapes.js b/src/js/game/hud/parts/pinned_shapes.js new file mode 100644 index 00000000..a463c602 --- /dev/null +++ b/src/js/game/hud/parts/pinned_shapes.js @@ -0,0 +1,151 @@ +import { Math_max } from "../../../core/builtins"; +import { ClickDetector } from "../../../core/click_detector"; +import { formatBigNumber, makeDiv } from "../../../core/utils"; +import { ShapeDefinition } from "../../shape_definition"; +import { BaseHUDPart } from "../base_hud_part"; + +export class HUDPinnedShapes extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_PinnedShapes", []); + } + + initialize() { + /** @type {Array<{ key: string, goal: number }>} */ + this.pinnedShapes = []; + + /** @type {Array<{key: string, amountLabel: HTMLElement, lastRenderedValue: number, element: HTMLElement, detector?: ClickDetector}>} */ + this.handles = []; + this.rerenderFull(); + + this.root.signals.storyGoalCompleted.add(this.rerenderFull, this); + this.root.signals.postLoadHook.add(this.rerenderFull, this); + this.root.hud.signals.shapePinRequested.add(this.pinNewShape, this); + } + + /** + * Returns whether a given shape is pinned + * @param {string} key + */ + isShapePinned(key) { + if (!this.pinnedShapes) { + return false; + } + if (key === this.root.hubGoals.currentGoal.definition.getHash()) { + return true; + } + for (let i = 0; i < this.pinnedShapes.length; ++i) { + if (this.pinnedShapes[i].key === key) { + return true; + } + } + return false; + } + + rerenderFull() { + const currentGoal = this.root.hubGoals.currentGoal; + const currentKey = currentGoal.definition.getHash(); + + // First, remove old ones + for (let i = 0; i < this.handles.length; ++i) { + this.handles[i].element.remove(); + const detector = this.handles[i].detector; + if (detector) { + detector.cleanup(); + } + } + this.handles = []; + + this.internalPinShape(currentKey, currentGoal.required, false); + + for (let i = 0; i < this.pinnedShapes.length; ++i) { + const key = this.pinnedShapes[i].key; + if (key !== currentKey) { + this.internalPinShape(key, this.pinnedShapes[i].goal); + } + } + } + + /** + * Pins a shape + * @param {string} key + * @param {number} goal + * @param {boolean} canUnpin + */ + internalPinShape(key, goal, canUnpin = true) { + const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); + + const element = makeDiv(this.element, null, ["shape"]); + const canvas = definition.generateAsCanvas(120); + element.appendChild(canvas); + + let detector = null; + if (canUnpin) { + element.classList.add("unpinable"); + detector = new ClickDetector(element, { + consumeEvents: true, + preventDefault: true, + }); + detector.click.add(() => this.unpinShape(key)); + } else { + element.classList.add("marked"); + } + + const amountLabel = makeDiv(element, null, ["amountLabel"], ""); + const goalLabel = makeDiv(element, null, ["goalLabel"], "/" + formatBigNumber(goal)); + + this.handles.push({ + key, + element, + amountLabel, + lastRenderedValue: -1, + }); + } + + update() { + for (let i = 0; i < this.handles.length; ++i) { + const handle = this.handles[i]; + + const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key); + if (currentValue !== handle.lastRenderedValue) { + handle.lastRenderedValue = currentValue; + handle.amountLabel.innerText = formatBigNumber(currentValue); + } + } + } + + /** + * Unpins a shape + * @param {string} key + */ + unpinShape(key) { + for (let i = 0; i < this.pinnedShapes.length; ++i) { + if (this.pinnedShapes[i].key === key) { + this.pinnedShapes.splice(i, 1); + this.rerenderFull(); + return; + } + } + } + + /** + * @param {ShapeDefinition} definition + * @param {number} goal + */ + pinNewShape(definition, goal) { + const key = definition.getHash(); + if (key === this.root.hubGoals.currentGoal.definition.getHash()) { + // Can not pin current goal + return; + } + for (let i = 0; i < this.pinnedShapes.length; ++i) { + if (this.pinnedShapes[i].key === key) { + // Already pinned + this.pinnedShapes[i].goal = Math_max(this.pinnedShapes[i].goal, goal); + return; + } + } + + this.pinnedShapes.push({ key, goal }); + this.rerenderFull(); + } +} diff --git a/src/js/game/hud/parts/processing_overlay.js b/src/js/game/hud/parts/processing_overlay.js index a8debea9..3354966a 100644 --- a/src/js/game/hud/parts/processing_overlay.js +++ b/src/js/game/hud/parts/processing_overlay.js @@ -49,10 +49,6 @@ export class HUDProcessingOverlay extends BaseHUDPart { task(); return; } - // if (name) { - // console.warn("QUEUE", name); - // } - task.__name = name; this.tasks.push(task); } diff --git a/src/js/game/hud/parts/settings_menu.js b/src/js/game/hud/parts/settings_menu.js new file mode 100644 index 00000000..c70d6afc --- /dev/null +++ b/src/js/game/hud/parts/settings_menu.js @@ -0,0 +1,133 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv, formatSeconds, formatBigNumberFull } from "../../../core/utils"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { InputReceiver } from "../../../core/input_receiver"; +import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; +import { T } from "../../../translations"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; +import { ItemProcessorComponent } from "../../components/item_processor"; +import { BeltComponent } from "../../components/belt"; +import { IS_DEMO } from "../../../core/config"; + +export class HUDSettingsMenu extends BaseHUDPart { + createElements(parent) { + this.background = makeDiv(parent, "ingame_HUD_SettingsMenu", ["ingameDialog"]); + + this.menuElement = makeDiv(this.background, null, ["menuElement"]); + + this.statsElement = makeDiv( + this.background, + null, + ["statsElement"], + ` + ${T.ingame.settingsMenu.beltsPlaced} + ${T.ingame.settingsMenu.buildingsPlaced} + ${T.ingame.settingsMenu.playtime} + + ` + ); + + this.buttonContainer = makeDiv(this.menuElement, null, ["buttons"]); + + const buttons = [ + { + title: T.ingame.settingsMenu.buttons.continue, + action: () => this.close(), + }, + { + title: T.ingame.settingsMenu.buttons.settings, + action: () => this.goToSettings(), + }, + { + title: T.ingame.settingsMenu.buttons.menu, + action: () => this.returnToMenu(), + }, + ]; + + for (let i = 0; i < buttons.length; ++i) { + const { title, action } = buttons[i]; + + const element = document.createElement("button"); + element.classList.add("styledButton"); + element.innerText = title; + this.buttonContainer.appendChild(element); + + this.trackClicks(element, action); + } + } + + returnToMenu() { + // if (IS_DEMO) { + // const { cancel, deleteGame } = this.root.hud.parts.dialogs.showWarning( + // T.dialogs.leaveNotPossibleInDemo.title, + // T.dialogs.leaveNotPossibleInDemo.desc, + // ["cancel:good", "deleteGame:bad"] + // ); + // deleteGame.add(() => this.root.gameState.goBackToMenu()); + // } else { + this.root.gameState.goBackToMenu(); + // } + } + + goToSettings() { + this.root.gameState.goToSettings(); + } + + shouldPauseGame() { + return this.visible; + } + + shouldPauseRendering() { + return this.visible; + } + + initialize() { + this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).add(this.show, this); + + this.domAttach = new DynamicDomAttach(this.root, this.background, { + attachClass: "visible", + }); + + this.inputReciever = new InputReceiver("settingsmenu"); + this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); + this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); + + this.close(); + } + + cleanup() { + document.body.classList.remove("ingameDialogOpen"); + } + + show() { + this.visible = true; + document.body.classList.add("ingameDialogOpen"); + // this.background.classList.add("visible"); + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + + const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60); + this.statsElement.querySelector(".playtime").innerText = T.global.time.xMinutes.replace( + "", + "" + totalMinutesPlayed + ); + + this.statsElement.querySelector(".buildingsPlaced").innerText = formatBigNumberFull( + this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length - + this.root.entityMgr.getAllWithComponent(BeltComponent).length + ); + this.statsElement.querySelector(".beltsPlaced").innerText = formatBigNumberFull( + this.root.entityMgr.getAllWithComponent(BeltComponent).length + ); + } + + close() { + this.visible = false; + document.body.classList.remove("ingameDialogOpen"); + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + this.update(); + } + + update() { + this.domAttach.update(this.visible); + } +} diff --git a/src/js/game/hud/parts/shop.js b/src/js/game/hud/parts/shop.js index db92ded3..0445e604 100644 --- a/src/js/game/hud/parts/shop.js +++ b/src/js/game/hud/parts/shop.js @@ -1,11 +1,12 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv, removeAllChildren, formatBigNumber } from "../../../core/utils"; -import { UPGRADES, TIER_LABELS } from "../../upgrades"; -import { ShapeDefinition } from "../../shape_definition"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { InputReceiver } from "../../../core/input_receiver"; -import { KeyActionMapper } from "../../key_action_mapper"; import { Math_min } from "../../../core/builtins"; +import { ClickDetector } from "../../../core/click_detector"; +import { InputReceiver } from "../../../core/input_receiver"; +import { formatBigNumber, makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; +import { UPGRADES } from "../../upgrades"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; export class HUDShop extends BaseHUDPart { createElements(parent) { @@ -13,7 +14,7 @@ export class HUDShop extends BaseHUDPart { // DIALOG Inner / Wrapper this.dialogInner = makeDiv(this.background, null, ["dialogInner"]); - this.title = makeDiv(this.dialogInner, null, ["title"], `Upgrades`); + this.title = makeDiv(this.dialogInner, null, ["title"], T.ingame.shop.title); this.closeButton = makeDiv(this.title, null, ["closeButton"]); this.trackClicks(this.closeButton, this.close); this.contentDiv = makeDiv(this.dialogInner, null, ["content"]); @@ -22,7 +23,6 @@ export class HUDShop extends BaseHUDPart { // Upgrades for (const upgradeId in UPGRADES) { - const { label } = UPGRADES[upgradeId]; const handle = {}; handle.requireIndexToElement = []; @@ -31,10 +31,10 @@ export class HUDShop extends BaseHUDPart { handle.elem.setAttribute("data-upgrade-id", upgradeId); // Title - const title = makeDiv(handle.elem, null, ["title"], label); + const title = makeDiv(handle.elem, null, ["title"], T.shopUpgrades[upgradeId].name); // Title > Tier - handle.elemTierLabel = makeDiv(title, null, ["tier"], "Tier ?"); + handle.elemTierLabel = makeDiv(title, null, ["tier"]); // Icon handle.icon = makeDiv(handle.elem, null, ["icon"]); @@ -47,7 +47,7 @@ export class HUDShop extends BaseHUDPart { // Buy button handle.buyButton = document.createElement("button"); handle.buyButton.classList.add("buy", "styledButton"); - handle.buyButton.innerText = "Upgrade"; + handle.buyButton.innerText = T.ingame.shop.buttonUnlock; handle.elem.appendChild(handle.buyButton); this.trackClicks(handle.buyButton, () => this.tryUnlockNextTier(upgradeId)); @@ -60,42 +60,52 @@ export class HUDShop extends BaseHUDPart { rerenderFull() { for (const upgradeId in this.upgradeToElements) { const handle = this.upgradeToElements[upgradeId]; - const { description, tiers } = UPGRADES[upgradeId]; - // removeAllChildren(handle.elem); + const { tiers } = UPGRADES[upgradeId]; const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); const tierHandle = tiers[currentTier]; // Set tier - handle.elemTierLabel.innerText = "Tier " + TIER_LABELS[currentTier]; + handle.elemTierLabel.innerText = T.ingame.shop.tier.replace( + "", + "" + T.ingame.shop.tierLabels[currentTier] + ); + handle.elemTierLabel.setAttribute("data-tier", currentTier); + // Cleanup detectors + for (let i = 0; i < handle.requireIndexToElement.length; ++i) { + const requiredHandle = handle.requireIndexToElement[i]; + requiredHandle.container.remove(); + requiredHandle.pinDetector.cleanup(); + } + // Cleanup handle.requireIndexToElement = []; - removeAllChildren(handle.elemRequirements); handle.elem.classList.toggle("maxLevel", !tierHandle); if (!tierHandle) { // Max level - handle.elemDescription.innerText = "Maximum level"; + handle.elemDescription.innerText = T.ingame.shop.maximumLevel; continue; } // Set description - handle.elemDescription.innerText = description(tierHandle.improvement); + handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description.replace( + "", + Math.floor(tierHandle.improvement * 100.0) + ); tierHandle.required.forEach(({ shape, amount }) => { - const requireDiv = makeDiv(handle.elemRequirements, null, ["requirement"]); + const container = makeDiv(handle.elemRequirements, null, ["requirement"]); - const shapeDef = this.root.shapeDefinitionMgr.registerOrReturnHandle( - ShapeDefinition.fromShortKey(shape) - ); + const shapeDef = this.root.shapeDefinitionMgr.getShapeFromShortKey(shape); const shapeCanvas = shapeDef.generateAsCanvas(120); shapeCanvas.classList.add(); - requireDiv.appendChild(shapeCanvas); + container.appendChild(shapeCanvas); - const progressContainer = makeDiv(requireDiv, null, ["amount"]); + const progressContainer = makeDiv(container, null, ["amount"]); const progressBar = document.createElement("label"); progressBar.classList.add("progressBar"); progressContainer.appendChild(progressBar); @@ -103,11 +113,30 @@ export class HUDShop extends BaseHUDPart { const progressLabel = document.createElement("label"); progressContainer.appendChild(progressLabel); + const pinButton = document.createElement("button"); + pinButton.classList.add("pin"); + container.appendChild(pinButton); + + if (this.root.hud.parts.pinnedShapes.isShapePinned(shape)) { + pinButton.classList.add("alreadyPinned"); + } + + const pinDetector = new ClickDetector(pinButton, { + consumeEvents: true, + preventDefault: true, + }); + pinDetector.click.add(() => { + this.root.hud.signals.shapePinRequested.dispatch(shapeDef, amount); + pinButton.classList.add("pinned"); + }); + handle.requireIndexToElement.push({ + container, progressLabel, progressBar, definition: shapeDef, required: amount, + pinDetector, }); }); } @@ -139,8 +168,8 @@ export class HUDShop extends BaseHUDPart { this.inputReciever = new InputReceiver("shop"); this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); - this.keyActionMapper.getBinding("back").add(this.close, this); - this.keyActionMapper.getBinding("menu_open_shop").add(this.close, this); + this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); + this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuOpenShop).add(this.close, this); this.close(); @@ -150,6 +179,17 @@ export class HUDShop extends BaseHUDPart { cleanup() { document.body.classList.remove("ingameDialogOpen"); + + // Cleanup detectors + for (const upgradeId in this.upgradeToElements) { + const handle = this.upgradeToElements[upgradeId]; + for (let i = 0; i < handle.requireIndexToElement.length; ++i) { + const requiredHandle = handle.requireIndexToElement[i]; + requiredHandle.container.remove(); + requiredHandle.pinDetector.cleanup(); + } + handle.requireIndexToElement = []; + } } show() { @@ -157,7 +197,7 @@ export class HUDShop extends BaseHUDPart { document.body.classList.add("ingameDialogOpen"); // this.background.classList.add("visible"); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); - this.update(); + this.rerenderFull(); } close() { diff --git a/src/js/game/hud/parts/statistics.js b/src/js/game/hud/parts/statistics.js new file mode 100644 index 00000000..fc19b3fd --- /dev/null +++ b/src/js/game/hud/parts/statistics.js @@ -0,0 +1,214 @@ +import { Math_min } from "../../../core/builtins"; +import { InputReceiver } from "../../../core/input_receiver"; +import { makeButton, makeDiv, removeAllChildren, capitalizeFirstLetter } from "../../../core/utils"; +import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; +import { enumAnalyticsDataSource } from "../../production_analytics"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { enumDisplayMode, HUDShapeStatisticsHandle } from "./statistics_handle"; +import { T } from "../../../translations"; + +export class HUDStatistics extends BaseHUDPart { + createElements(parent) { + this.background = makeDiv(parent, "ingame_HUD_Statistics", ["ingameDialog"]); + + // DIALOG Inner / Wrapper + this.dialogInner = makeDiv(this.background, null, ["dialogInner"]); + this.title = makeDiv(this.dialogInner, null, ["title"], T.ingame.statistics.title); + this.closeButton = makeDiv(this.title, null, ["closeButton"]); + this.trackClicks(this.closeButton, this.close); + + this.filterHeader = makeDiv(this.dialogInner, null, ["filterHeader"]); + this.sourceExplanation = makeDiv(this.dialogInner, null, ["sourceExplanation"]); + + this.filtersDataSource = makeDiv(this.filterHeader, null, ["filtersDataSource"]); + this.filtersDisplayMode = makeDiv(this.filterHeader, null, ["filtersDisplayMode"]); + + const dataSources = [ + enumAnalyticsDataSource.produced, + enumAnalyticsDataSource.delivered, + enumAnalyticsDataSource.stored, + ]; + + for (let i = 0; i < dataSources.length; ++i) { + const dataSource = dataSources[i]; + const button = makeButton( + this.filtersDataSource, + ["mode" + capitalizeFirstLetter(dataSource)], + T.ingame.statistics.dataSources[dataSource].title + ); + this.trackClicks(button, () => this.setDataSource(dataSource)); + } + + const buttonDisplayDetailed = makeButton(this.filtersDisplayMode, ["displayDetailed"]); + const buttonDisplayIcons = makeButton(this.filtersDisplayMode, ["displayIcons"]); + + this.trackClicks(buttonDisplayIcons, () => this.setDisplayMode(enumDisplayMode.icons)); + this.trackClicks(buttonDisplayDetailed, () => this.setDisplayMode(enumDisplayMode.detailed)); + + this.contentDiv = makeDiv(this.dialogInner, null, ["content"]); + } + + /** + * @param {enumAnalyticsDataSource} source + */ + setDataSource(source) { + this.dataSource = source; + this.dialogInner.setAttribute("data-datasource", source); + + this.sourceExplanation.innerText = T.ingame.statistics.dataSources[source].description; + if (this.visible) { + this.rerenderFull(); + } + } + + /** + * @param {enumDisplayMode} mode + */ + setDisplayMode(mode) { + this.displayMode = mode; + this.dialogInner.setAttribute("data-displaymode", mode); + if (this.visible) { + this.rerenderFull(); + } + } + + initialize() { + this.domAttach = new DynamicDomAttach(this.root, this.background, { + attachClass: "visible", + }); + + this.inputReciever = new InputReceiver("statistics"); + this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); + + this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); + this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuOpenStats).add(this.close, this); + + /** @type {Object.} */ + this.activeHandles = {}; + + this.setDataSource(enumAnalyticsDataSource.produced); + this.setDisplayMode(enumDisplayMode.detailed); + + this.intersectionObserver = new IntersectionObserver(this.intersectionCallback.bind(this), { + root: this.contentDiv, + }); + + this.lastFullRerender = 0; + + this.close(); + this.rerenderFull(); + } + + intersectionCallback(entries) { + for (let i = 0; i < entries.length; ++i) { + const entry = entries[i]; + const handle = this.activeHandles[entry.target.getAttribute("data-shape-key")]; + if (handle) { + handle.setVisible(entry.intersectionRatio > 0); + } + } + } + + cleanup() { + document.body.classList.remove("ingameDialogOpen"); + } + + show() { + this.visible = true; + document.body.classList.add("ingameDialogOpen"); + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + this.rerenderFull(); + this.update(); + } + + close() { + this.visible = false; + document.body.classList.remove("ingameDialogOpen"); + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + this.update(); + } + + update() { + this.domAttach.update(this.visible); + if (this.visible) { + if (this.root.time.now() - this.lastFullRerender > 1) { + this.lastFullRerender = this.root.time.now(); + this.lastPartialRerender = this.root.time.now(); + this.rerenderFull(); + } + this.rerenderPartial(); + } + } + + /** + * Performs a partial rerender, only updating graphs and counts + */ + rerenderPartial() { + for (const key in this.activeHandles) { + const handle = this.activeHandles[key]; + handle.update(this.displayMode, this.dataSource); + } + } + + /** + * Performs a full rerender, regenerating everything + */ + rerenderFull() { + for (const key in this.activeHandles) { + this.activeHandles[key].detach(); + } + removeAllChildren(this.contentDiv); + + // Now, attach new ones + + let entries = null; + switch (this.dataSource) { + case enumAnalyticsDataSource.stored: { + entries = Object.entries(this.root.hubGoals.storedShapes); + break; + } + case enumAnalyticsDataSource.produced: + case enumAnalyticsDataSource.delivered: { + entries = Object.entries(this.root.productionAnalytics.getCurrentShapeRates(this.dataSource)); + break; + } + } + + entries.sort((a, b) => b[1] - a[1]); + + let rendered = new Set(); + + for (let i = 0; i < Math_min(entries.length, 200); ++i) { + const entry = entries[i]; + const shapeKey = entry[0]; + + let handle = this.activeHandles[shapeKey]; + if (!handle) { + const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(shapeKey); + handle = this.activeHandles[shapeKey] = new HUDShapeStatisticsHandle( + this.root, + definition, + this.intersectionObserver + ); + } + + rendered.add(shapeKey); + handle.attach(this.contentDiv); + } + + for (const key in this.activeHandles) { + if (!rendered.has(key)) { + this.activeHandles[key].destroy(); + delete this.activeHandles[key]; + } + } + + if (entries.length === 0) { + this.contentDiv.innerHTML = ` + ${T.ingame.statistics.noShapesProduced}`; + } + + this.contentDiv.classList.toggle("hasEntries", entries.length > 0); + } +} diff --git a/src/js/game/hud/parts/statistics_handle.js b/src/js/game/hud/parts/statistics_handle.js new file mode 100644 index 00000000..d3612d92 --- /dev/null +++ b/src/js/game/hud/parts/statistics_handle.js @@ -0,0 +1,221 @@ +import { GameRoot } from "../../root"; +import { ShapeDefinition } from "../../shape_definition"; +import { enumAnalyticsDataSource } from "../../production_analytics"; +import { formatBigNumber, clamp } from "../../../core/utils"; +import { globalConfig } from "../../../core/config"; +import { makeOffscreenBuffer } from "../../../core/buffer_utils"; +import { T } from "../../../translations"; + +/** @enum {string} */ +export const enumDisplayMode = { + icons: "icons", + detailed: "detailed", +}; + +/** + * Simple wrapper for a shape definition within the shape statistics + */ +export class HUDShapeStatisticsHandle { + /** + * @param {GameRoot} root + * @param {ShapeDefinition} definition + * @param {IntersectionObserver} intersectionObserver + */ + constructor(root, definition, intersectionObserver) { + this.definition = definition; + this.root = root; + this.intersectionObserver = intersectionObserver; + + this.visible = false; + } + + initElement() { + this.element = document.createElement("div"); + this.element.setAttribute("data-shape-key", this.definition.getHash()); + + this.counter = document.createElement("span"); + this.counter.classList.add("counter"); + this.element.appendChild(this.counter); + } + + /** + * Sets whether the shape handle is visible currently + * @param {boolean} visibility + */ + setVisible(visibility) { + if (visibility === this.visible) { + return; + } + this.visible = visibility; + if (visibility) { + if (!this.shapeCanvas) { + // Create elements + this.shapeCanvas = this.definition.generateAsCanvas(100); + this.shapeCanvas.classList.add("icon"); + this.element.appendChild(this.shapeCanvas); + } + } else { + // Drop elements + this.cleanupChildElements(); + } + } + + /** + * + * @param {enumDisplayMode} displayMode + * @param {enumAnalyticsDataSource} dataSource + * @param {boolean=} forced + */ + update(displayMode, dataSource, forced = false) { + if (!this.element) { + return; + } + if (!this.visible && !forced) { + return; + } + + switch (dataSource) { + case enumAnalyticsDataSource.stored: { + this.counter.innerText = formatBigNumber( + this.root.hubGoals.storedShapes[this.definition.getHash()] || 0 + ); + break; + } + case enumAnalyticsDataSource.delivered: + case enumAnalyticsDataSource.produced: { + let rate = + (this.root.productionAnalytics.getCurrentShapeRate(dataSource, this.definition) / + globalConfig.analyticsSliceDurationSeconds) * + 60; + this.counter.innerText = T.ingame.statistics.shapesPerMinute.replace( + "", + formatBigNumber(rate) + ); + break; + } + } + + if (displayMode === enumDisplayMode.detailed) { + const graphDpi = globalConfig.statisticsGraphDpi; + + const w = 270; + const h = 40; + + if (!this.graphCanvas) { + const [canvas, context] = makeOffscreenBuffer(w * graphDpi, h * graphDpi, { + smooth: true, + reusable: false, + label: "statgraph-" + this.definition.getHash(), + }); + context.scale(graphDpi, graphDpi); + canvas.classList.add("graph"); + this.graphCanvas = canvas; + this.graphContext = context; + this.element.appendChild(this.graphCanvas); + } + + this.graphContext.clearRect(0, 0, w, h); + + this.graphContext.fillStyle = "#bee0db"; + this.graphContext.strokeStyle = "#66ccbc"; + this.graphContext.lineWidth = 1.5; + + const sliceWidth = w / (globalConfig.statisticsGraphSlices - 1); + + let values = []; + let maxValue = 1; + + for (let i = 0; i < globalConfig.statisticsGraphSlices - 2; ++i) { + const value = this.root.productionAnalytics.getPastShapeRate( + dataSource, + this.definition, + globalConfig.statisticsGraphSlices - i - 2 + ); + if (value > maxValue) { + maxValue = value; + } + values.push(value); + } + + this.graphContext.beginPath(); + this.graphContext.moveTo(0.75, h + 5); + for (let i = 0; i < values.length; ++i) { + const yValue = clamp((1 - values[i] / maxValue) * h, 0.75, h - 0.75); + const x = i * sliceWidth; + if (i === 0) { + this.graphContext.lineTo(0.75, yValue); + } + this.graphContext.lineTo(x, yValue); + if (i === values.length - 1) { + this.graphContext.lineTo(w + 100, yValue); + this.graphContext.lineTo(w + 100, h + 5); + } + } + + this.graphContext.closePath(); + this.graphContext.stroke(); + this.graphContext.fill(); + } else { + if (this.graphCanvas) { + this.graphCanvas.remove(); + delete this.graphCanvas; + delete this.graphContext; + } + } + } + + /** + * Attaches the handle + * @param {HTMLElement} parent + */ + attach(parent) { + if (!this.element) { + this.initElement(); + } + if (this.element.parentElement !== parent) { + parent.appendChild(this.element); + this.intersectionObserver.observe(this.element); + } + } + + /** + * Detaches the handle + */ + detach() { + if (this.element && this.element.parentElement) { + this.element.parentElement.removeChild(this.element); + this.intersectionObserver.unobserve(this.element); + } + } + + /** + * Cleans up all child elements + */ + cleanupChildElements() { + if (this.shapeCanvas) { + this.shapeCanvas.remove(); + delete this.shapeCanvas; + } + + if (this.graphCanvas) { + this.graphCanvas.remove(); + delete this.graphCanvas; + delete this.graphContext; + } + } + + /** + * Destroys the handle + */ + destroy() { + this.cleanupChildElements(); + if (this.element) { + this.intersectionObserver.unobserve(this.element); + this.element.remove(); + delete this.element; + + // Remove handle + delete this.counter; + } + } +} diff --git a/src/js/game/hud/parts/tutorial_hints.js b/src/js/game/hud/parts/tutorial_hints.js new file mode 100644 index 00000000..853d054f --- /dev/null +++ b/src/js/game/hud/parts/tutorial_hints.js @@ -0,0 +1,119 @@ +import { cachebust } from "../../../core/cachebust"; +import { InputReceiver } from "../../../core/input_receiver"; +import { TrackedState } from "../../../core/tracked_state"; +import { makeDiv } from "../../../core/utils"; +import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { T } from "../../../translations"; + +const tutorialVideos = [1, 2, 3, 4, 5, 6, 7, 9, 10, 11]; + +export class HUDPartTutorialHints extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv( + parent, + "ingame_HUD_TutorialHints", + [], + ` +
+ ${T.ingame.tutorialHints.title} + +
+ + + ` + ); + + this.videoElement = this.element.querySelector("video"); + } + + shouldPauseGame() { + return this.enlarged; + } + + initialize() { + this.trackClicks(this.element.querySelector(".toggleHint"), this.toggleHintEnlarged); + + this.videoAttach = new DynamicDomAttach(this.root, this.videoElement, { + timeToKeepSeconds: 0.3, + }); + + this.videoAttach.update(false); + this.enlarged = false; + + this.inputReciever = new InputReceiver("tutorial_hints"); + this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); + this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); + + this.domAttach = new DynamicDomAttach(this.root, this.element); + + this.currentShownLevel = new TrackedState(this.updateVideoUrl, this); + + this.root.signals.postLoadHook.add(() => { + if (this.root.hubGoals.level === 1) { + this.root.hud.parts.dialogs.showInfo( + T.dialogs.hintDescription.title, + T.dialogs.hintDescription.desc + ); + } + }); + } + + updateVideoUrl(level) { + if (tutorialVideos.indexOf(level) < 0) { + this.videoElement.querySelector("source").setAttribute("src", ""); + this.videoElement.pause(); + } else { + this.videoElement + .querySelector("source") + .setAttribute("src", "https://static.shapez.io/tutorial_videos/level_" + level + ".webm"); + this.videoElement.currentTime = 0; + this.videoElement.load(); + } + } + + close() { + this.enlarged = false; + document.body.classList.remove("ingameDialogOpen"); + this.element.classList.remove("enlarged", "noBlur"); + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + this.update(); + } + + show() { + this.root.app.analytics.trackUiClick("tutorial_hint_show"); + this.root.app.analytics.trackUiClick("tutorial_hint_show_lvl_" + this.root.hubGoals.level); + + document.body.classList.add("ingameDialogOpen"); + this.element.classList.add("enlarged", "noBlur"); + this.enlarged = true; + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + this.update(); + + this.videoElement.currentTime = 0; + this.videoElement.play(); + } + + update() { + this.videoAttach.update(this.enlarged); + + this.currentShownLevel.set(this.root.hubGoals.level); + + const tutorialVisible = tutorialVideos.indexOf(this.root.hubGoals.level) >= 0; + this.domAttach.update(tutorialVisible); + } + + toggleHintEnlarged() { + if (this.enlarged) { + this.close(); + } else { + this.show(); + } + } +} diff --git a/src/js/game/hud/parts/unlock_notification.js b/src/js/game/hud/parts/unlock_notification.js index 62e103b7..570d04d9 100644 --- a/src/js/game/hud/parts/unlock_notification.js +++ b/src/js/game/hud/parts/unlock_notification.js @@ -1,19 +1,14 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv } from "../../../core/utils"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { gMetaBuildingRegistry } from "../../../core/global_registries"; -import { MetaBuilding } from "../../meta_building"; -import { MetaSplitterBuilding } from "../../buildings/splitter"; -import { MetaCutterBuilding } from "../../buildings/cutter"; -import { enumHubGoalRewards } from "../../tutorial_goals"; -import { MetaTrashBuilding } from "../../buildings/trash"; -import { MetaMinerBuilding } from "../../buildings/miner"; -import { MetaPainterBuilding } from "../../buildings/painter"; -import { MetaMixerBuilding } from "../../buildings/mixer"; -import { MetaRotaterBuilding } from "../../buildings/rotater"; -import { MetaStackerBuilding } from "../../buildings/stacker"; -import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; import { globalConfig } from "../../../core/config"; +import { gMetaBuildingRegistry } from "../../../core/global_registries"; +import { makeDiv } from "../../../core/utils"; +import { SOUNDS } from "../../../platform/sound"; +import { T } from "../../../translations"; +import { defaultBuildingVariant } from "../../meta_building"; +import { enumHubGoalRewards } from "../../tutorial_goals"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings"; +import { InputReceiver } from "../../../core/input_receiver"; export class HUDUnlockNotification extends BaseHUDPart { initialize() { @@ -26,97 +21,116 @@ export class HUDUnlockNotification extends BaseHUDPart { if (!(G_IS_DEV && globalConfig.debug.disableUnlockDialog)) { this.root.signals.storyGoalCompleted.add(this.showForLevel, this); } - } - shouldPauseGame() { - return this.visible; + this.buttonShowTimeout = null; } createElements(parent) { + this.inputReciever = new InputReceiver("unlock-notification"); + this.element = makeDiv(parent, "ingame_HUD_UnlockNotification", []); const dialog = makeDiv(this.element, null, ["dialog"]); - this.elemTitle = makeDiv(dialog, null, ["title"], ``); - this.elemSubTitle = makeDiv(dialog, null, ["subTitle"], `Completed`); + this.elemTitle = makeDiv(dialog, null, ["title"]); + this.elemSubTitle = makeDiv(dialog, null, ["subTitle"], T.ingame.levelCompleteNotification.completed); - this.elemContents = makeDiv( - dialog, - null, - ["contents"], - ` - Ready for the next one? - ` - ); + this.elemContents = makeDiv(dialog, null, ["contents"]); this.btnClose = document.createElement("button"); this.btnClose.classList.add("close", "styledButton"); this.btnClose.innerText = "Next level"; dialog.appendChild(this.btnClose); - this.trackClicks(this.btnClose, this.close); + this.trackClicks(this.btnClose, this.requestClose); } + /** + * @param {number} level + * @param {enumHubGoalRewards} reward + */ showForLevel(level, reward) { - this.elemTitle.innerText = "Level " + ("" + level).padStart(2, "0"); + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace( + "", + ("" + level).padStart(2, "0") + ); - let html = `Unlocked ${reward}!`; + const rewardName = T.storyRewards[reward].title; - const addBuildingExplanation = metaBuildingClass => { - const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass); - html += `
`; - }; + let html = ` +
+ ${T.ingame.levelCompleteNotification.unlockText.replace("", rewardName)} +
+ +
+ ${T.storyRewards[reward].desc} +
- switch (reward) { - case enumHubGoalRewards.reward_cutter_and_trash: { - addBuildingExplanation(MetaCutterBuilding); - addBuildingExplanation(MetaTrashBuilding); - break; - } - case enumHubGoalRewards.reward_mixer: { - addBuildingExplanation(MetaMixerBuilding); - break; - } + `; - case enumHubGoalRewards.reward_painter: { - addBuildingExplanation(MetaPainterBuilding); - break; - } - - case enumHubGoalRewards.reward_rotater: { - addBuildingExplanation(MetaRotaterBuilding); - break; - } - - case enumHubGoalRewards.reward_splitter: { - addBuildingExplanation(MetaSplitterBuilding); - break; - } - - case enumHubGoalRewards.reward_stacker: { - addBuildingExplanation(MetaStackerBuilding); - break; - } - - case enumHubGoalRewards.reward_tunnel: { - addBuildingExplanation(MetaUndergroundBeltBuilding); - break; - } + html += "
"; + const gained = enumHubGoalRewardsToContentUnlocked[reward]; + if (gained) { + gained.forEach(([metaBuildingClass, variant]) => { + const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass); + html += `
`; + }); } - - // addBuildingExplanation(MetaSplitterBuilding); - // addBuildingExplanation(MetaCutterBuilding); + html += "
"; this.elemContents.innerHTML = html; - this.visible = true; + this.root.soundProxy.playUi(SOUNDS.levelComplete); + + if (this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + } + + this.buttonShowTimeout = setTimeout( + () => this.element.querySelector("button.close").classList.add("unlocked"), + G_IS_DEV ? 1000 : 10000 + ); + } + + cleanup() { + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + if (this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + this.buttonShowTimeout = null; + } + } + + requestClose() { + this.root.app.adProvider.showVideoAd().then(() => { + this.close(); + if (this.root.hubGoals.level === 3) { + const { showUpgrades } = this.root.hud.parts.dialogs.showInfo( + T.dialogs.upgradesIntroduction.title, + T.dialogs.upgradesIntroduction.desc, + ["showUpgrades:good:timeout"] + ); + showUpgrades.add(() => this.root.hud.parts.shop.show()); + } + }); } close() { + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + if (this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + this.buttonShowTimeout = null; + } this.visible = false; } update() { this.domAttach.update(this.visible); + if (!this.visible && this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + this.buttonShowTimeout = null; + } } } diff --git a/src/js/game/hud/parts/vignette_overlay.js b/src/js/game/hud/parts/vignette_overlay.js new file mode 100644 index 00000000..dc0e5db7 --- /dev/null +++ b/src/js/game/hud/parts/vignette_overlay.js @@ -0,0 +1,10 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv } from "../../../core/utils"; + +export class HUDVignetteOverlay extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_VignetteOverlay"); + } + + initialize() {} +} diff --git a/src/js/game/hud/parts/watermark.js b/src/js/game/hud/parts/watermark.js new file mode 100644 index 00000000..94df6257 --- /dev/null +++ b/src/js/game/hud/parts/watermark.js @@ -0,0 +1,43 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { DrawParameters } from "../../../core/draw_parameters"; +import { makeDiv } from "../../../core/utils"; +import { THIRDPARTY_URLS } from "../../../core/config"; + +export class HUDWatermark extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_Watermark"); + } + + initialize() { + this.trackClicks(this.element, this.onWatermarkClick); + } + + onWatermarkClick() { + this.root.app.analytics.trackUiClick("watermark_click"); + this.root.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneStorePage); + } + + /** + * + * @param {DrawParameters} parameters + */ + drawOverlays(parameters) { + const w = this.root.gameWidth; + const x = 280 * this.root.app.getEffectiveUiScale(); + + parameters.context.fillStyle = "#f77"; + parameters.context.font = "bold " + this.root.app.getEffectiveUiScale() * 17 + "px GameFont"; + // parameters.context.textAlign = "center"; + parameters.context.fillText("DEMO VERSION", x, this.root.app.getEffectiveUiScale() * 27); + + parameters.context.font = "bold " + this.root.app.getEffectiveUiScale() * 12 + "px GameFont"; + // parameters.context.textAlign = "center"; + parameters.context.fillText( + "Please consider to buy the full version!", + x, + this.root.app.getEffectiveUiScale() * 45 + ); + + // parameters.context.textAlign = "left"; + } +} diff --git a/src/js/game/item_registry.js b/src/js/game/item_registry.js index 2ecb89c5..b6a77836 100644 --- a/src/js/game/item_registry.js +++ b/src/js/game/item_registry.js @@ -1,6 +1,8 @@ import { gItemRegistry } from "../core/global_registries"; import { ShapeItem } from "./items/shape_item"; +import { ColorItem } from "./items/color_item"; export function initItemRegistry() { gItemRegistry.register(ShapeItem); + gItemRegistry.register(ColorItem); } diff --git a/src/js/game/items/color_item.js b/src/js/game/items/color_item.js index cc618228..b2a3cd74 100644 --- a/src/js/game/items/color_item.js +++ b/src/js/game/items/color_item.js @@ -1,20 +1,10 @@ -import { DrawParameters } from "../../core/draw_parameters"; -import { createLogger } from "../../core/logging"; -import { extendSchema } from "../../savegame/serialization"; -import { BaseItem } from "../base_item"; -import { enumColorsToHexCode, enumColors } from "../colors"; -import { makeOffscreenBuffer } from "../../core/buffer_utils"; import { globalConfig } from "../../core/config"; -import { round1Digit } from "../../core/utils"; -import { Math_max, Math_round } from "../../core/builtins"; import { smoothenDpi } from "../../core/dpi_manager"; - -/** @enum {string} */ -const enumColorToMapBackground = { - [enumColors.red]: "#ffbfc1", - [enumColors.green]: "#cbffc4", - [enumColors.blue]: "#bfdaff", -}; +import { DrawParameters } from "../../core/draw_parameters"; +import { types } from "../../savegame/serialization"; +import { BaseItem } from "../base_item"; +import { enumColors, enumColorsToHexCode } from "../colors"; +import { THEME } from "../theme"; export class ColorItem extends BaseItem { static getId() { @@ -22,23 +12,28 @@ export class ColorItem extends BaseItem { } static getSchema() { - return extendSchema(BaseItem.getCachedSchema(), { - // TODO - }); + return types.enum(enumColors); + } + + serialize() { + return this.color; + } + + deserialize(data) { + this.color = data; } /** - * @param {string} color + * @param {enumColors} color */ constructor(color) { super(); this.color = color; - - this.bufferGenerator = this.internalGenerateColorBuffer.bind(this); + this.bufferGenerator = null; } getBackgroundColorAsResource() { - return enumColorToMapBackground[this.color]; + return THEME.map.resources[this.color]; } /** @@ -48,6 +43,10 @@ export class ColorItem extends BaseItem { * @param {DrawParameters} parameters */ draw(x, y, parameters, size = 12) { + if (!this.bufferGenerator) { + this.bufferGenerator = this.internalGenerateColorBuffer.bind(this); + } + const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel); const key = size + "/" + dpi; @@ -74,8 +73,8 @@ export class ColorItem extends BaseItem { context.scale((dpi * w) / 12, (dpi * h) / 12); context.fillStyle = enumColorsToHexCode[this.color]; - context.strokeStyle = "rgba(100,102, 110, 1)"; - context.lineWidth = 2; + context.strokeStyle = THEME.items.outline; + context.lineWidth = 2 * THEME.items.outlineWidth; context.beginCircle(2, -1, 3); context.stroke(); context.fill(); diff --git a/src/js/game/items/shape_item.js b/src/js/game/items/shape_item.js index 9aa1b78d..cfdb9830 100644 --- a/src/js/game/items/shape_item.js +++ b/src/js/game/items/shape_item.js @@ -1,10 +1,8 @@ -import { BaseItem } from "../base_item"; import { DrawParameters } from "../../core/draw_parameters"; -import { extendSchema } from "../../savegame/serialization"; +import { types } from "../../savegame/serialization"; +import { BaseItem } from "../base_item"; import { ShapeDefinition } from "../shape_definition"; -import { createLogger } from "../../core/logging"; - -const logger = createLogger("shape_item"); +import { THEME } from "../theme"; export class ShapeItem extends BaseItem { static getId() { @@ -12,9 +10,15 @@ export class ShapeItem extends BaseItem { } static getSchema() { - return extendSchema(BaseItem.getCachedSchema(), { - // TODO - }); + return types.string; + } + + serialize() { + return this.definition.getHash(); + } + + deserialize(data) { + this.definition = ShapeDefinition.fromShortKey(data); } /** @@ -30,6 +34,10 @@ export class ShapeItem extends BaseItem { this.definition = definition; } + getBackgroundColorAsResource() { + return THEME.map.resources.shape; + } + /** * @param {number} x * @param {number} y diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index dae0c57e..9ab089c3 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -6,49 +6,77 @@ import { Application } from "../application"; import { Signal, STOP_PROPAGATION } from "../core/signal"; import { IS_MOBILE } from "../core/config"; +import { T } from "../translations"; +import { JSON_stringify } from "../core/builtins"; function key(str) { return str.toUpperCase().charCodeAt(0); } -// TODO: Configurable -export const defaultKeybindings = { +export const KEYMAPPINGS = { general: { confirm: { keyCode: 13 }, // enter back: { keyCode: 27, builtin: true }, // escape }, ingame: { - map_move_up: { keyCode: key("W") }, - map_move_right: { keyCode: key("D") }, - map_move_down: { keyCode: key("S") }, - map_move_left: { keyCode: key("A") }, - toggle_hud: { keyCode: 113 }, + mapMoveUp: { keyCode: key("W") }, + mapMoveRight: { keyCode: key("D") }, + mapMoveDown: { keyCode: key("S") }, + mapMoveLeft: { keyCode: key("A") }, - center_map: { keyCode: 32 }, + centerMap: { keyCode: 32 }, - menu_open_shop: { keyCode: key("F") }, - menu_open_stats: { keyCode: key("G") }, + menuOpenShop: { keyCode: key("F") }, + menuOpenStats: { keyCode: key("G") }, + + toggleHud: { keyCode: 113 }, // F2 + toggleFPSInfo: { keyCode: 115 }, // F1 + + mapZoomIn: { keyCode: 187, repeated: true }, // "+" + mapZoomOut: { keyCode: 189, repeated: true }, // "-" }, - toolbar: { - building_belt: { keyCode: key("1") }, - building_miner: { keyCode: key("2") }, - building_underground_belt: { keyCode: key("3") }, - building_splitter: { keyCode: key("4") }, - building_cutter: { keyCode: key("5") }, - building_rotater: { keyCode: key("6") }, - building_stacker: { keyCode: key("7") }, - building_mixer: { keyCode: key("8") }, - building_painter: { keyCode: key("9") }, - building_trash: { keyCode: key("0") }, + buildings: { + belt: { keyCode: key("1") }, + splitter: { keyCode: key("2") }, + underground_belt: { keyCode: key("3") }, + miner: { keyCode: key("4") }, + cutter: { keyCode: key("5") }, + rotater: { keyCode: key("6") }, + stacker: { keyCode: key("7") }, + mixer: { keyCode: key("8") }, + painter: { keyCode: key("9") }, + trash: { keyCode: key("0") }, + }, - building_abort_placement: { keyCode: key("Q") }, + placement: { + abortBuildingPlacement: { keyCode: key("Q") }, + rotateWhilePlacing: { keyCode: key("R") }, + cycleBuildingVariants: { keyCode: key("T") }, + cycleBuildings: { keyCode: 9 }, // TAB + }, - rotate_while_placing: { keyCode: key("R") }, + massSelect: { + massSelectStart: { keyCode: 17, builtin: true }, // CTRL + massSelectSelectMultiple: { keyCode: 16, builtin: true }, // SHIFT + confirmMassDelete: { keyCode: key("X") }, + }, + + placementModifiers: { + placementDisableAutoOrientation: { keyCode: 17, builtin: true }, // CTRL + placeMultiple: { keyCode: 16, builtin: true }, // SHIFT + placeInverse: { keyCode: 18, builtin: true }, // ALT }, }; +// Assign ids +for (const categoryId in KEYMAPPINGS) { + for (const mappingId in KEYMAPPINGS[categoryId]) { + KEYMAPPINGS[categoryId][mappingId].id = mappingId; + } +} + /** * Returns a keycode -> string * @param {number} code @@ -59,23 +87,23 @@ export function getStringForKeyCode(code) { case 8: return "⌫"; case 9: - return "TAB"; + return T.global.keys.tab; case 13: return "⏎"; case 16: return "⇪"; case 17: - return "CTRL"; + return T.global.keys.control; case 18: - return "ALT"; + return T.global.keys.alt; case 19: return "PAUSE"; case 20: return "CAPS"; case 27: - return "ESC"; + return T.global.keys.escape; case 32: - return "SPACE"; + return T.global.keys.space; case 33: return "PGUP"; case 34: @@ -168,13 +196,11 @@ export function getStringForKeyCode(code) { case 186: return ";"; case 187: - return "="; + return "+"; case 188: return ","; case 189: return "-"; - case 189: - return "."; case 191: return "/"; case 219: @@ -198,12 +224,14 @@ export class Keybinding { * @param {object} param0 * @param {number} param0.keyCode * @param {boolean=} param0.builtin + * @param {boolean=} param0.repeated */ - constructor(app, { keyCode, builtin = false }) { + constructor(app, { keyCode, builtin = false, repeated = false }) { assert(keyCode && Number.isInteger(keyCode), "Invalid key code: " + keyCode); this.app = app; this.keyCode = keyCode; this.builtin = builtin; + this.repeated = repeated; this.currentlyDown = false; @@ -264,14 +292,14 @@ export class KeyActionMapper { /** @type {Object.} */ this.keybindings = {}; - // const overrides = root.app.settings.getKeybindingOverrides(); + const overrides = root.app.settings.getKeybindingOverrides(); - for (const category in defaultKeybindings) { - for (const key in defaultKeybindings[category]) { - let payload = Object.assign({}, defaultKeybindings[category][key]); - // if (overrides[key]) { - // payload.keyCode = overrides[key]; - // } + for (const category in KEYMAPPINGS) { + for (const key in KEYMAPPINGS[category]) { + let payload = Object.assign({}, KEYMAPPINGS[category][key]); + if (overrides[key]) { + payload.keyCode = overrides[key]; + } this.keybindings[key] = new Keybinding(this.root.app, payload); } @@ -338,7 +366,7 @@ export class KeyActionMapper { for (const key in this.keybindings) { /** @type {Keybinding} */ const binding = this.keybindings[key]; - if (binding.keyCode === keyCode /* && binding.shift === shift && binding.alt === alt */) { + if (binding.keyCode === keyCode && (!binding.currentlyDown || binding.repeated)) { binding.currentlyDown = true; /** @type {Signal} */ @@ -373,10 +401,13 @@ export class KeyActionMapper { /** * Returns a given keybinding - * @param {string} id + * @param {{ keyCode: number }} binding * @returns {Keybinding} */ - getBinding(id) { + getBinding(binding) { + // @ts-ignore + const id = binding.id; + assert(id, "Not a valid keybinding: " + JSON_stringify(binding)); assert(this.keybindings[id], "Keybinding " + id + " not known!"); return this.keybindings[id]; } diff --git a/src/js/game/logic.js b/src/js/game/logic.js index fe58ee4e..c96364da 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -4,8 +4,9 @@ import { Vector, enumDirectionToVector, enumDirection } from "../core/vector"; import { MetaBuilding } from "./meta_building"; import { StaticMapEntityComponent } from "./components/static_map_entity"; import { Math_abs } from "../core/builtins"; -import { Rectangle } from "../core/rectangle"; import { createLogger } from "../core/logging"; +import { MetaBeltBaseBuilding, arrayBeltVariantToRotation } from "./buildings/belt_base"; +import { SOUNDS } from "../platform/sound"; const logger = createLogger("ingame/logic"); @@ -46,16 +47,19 @@ export class GameLogic { } /** - * - * @param {Vector} origin - * @param {number} rotation - * @param {MetaBuilding} building + * @param {object} param0 + * @param {Vector} param0.origin + * @param {number} param0.rotation + * @param {number} param0.rotationVariant + * @param {string} param0.variant + * @param {MetaBuilding} param0.building + * @returns {boolean} */ - isAreaFreeToBuild(origin, rotation, building) { + isAreaFreeToBuild({ origin, rotation, rotationVariant, variant, building }) { const checker = new StaticMapEntityComponent({ origin, - tileSize: building.getDimensions(), - rotationDegrees: rotation, + tileSize: building.getDimensions(variant), + rotation, }); const rect = checker.getTileSpaceBounds(); @@ -63,8 +67,19 @@ export class GameLogic { for (let x = rect.x; x < rect.x + rect.w; ++x) { for (let y = rect.y; y < rect.y + rect.h; ++y) { const contents = this.root.map.getTileContentXY(x, y); - if (contents && !contents.components.ReplaceableMapEntity) { - return false; + if (contents) { + if ( + !this.checkCanReplaceBuilding({ + original: contents, + origin, + building, + rotation, + rotationVariant, + }) + ) { + // Content already has same rotation + return false; + } } } } @@ -72,16 +87,68 @@ export class GameLogic { } /** - * - * @param {Vector} origin - * @param {number} rotation - * @param {MetaBuilding} building + * Checks if the given building can be replaced by another + * @param {object} param0 + * @param {Entity} param0.original + * @param {Vector} param0.origin + * @param {number} param0.rotation + * @param {number} param0.rotationVariant + * @param {MetaBuilding} param0.building + * @returns {boolean} */ - checkCanPlaceBuilding(origin, rotation, building) { + checkCanReplaceBuilding({ original, origin, building, rotation, rotationVariant }) { + if (!original.components.ReplaceableMapEntity) { + // Can not get replaced at all + return false; + } + + const staticComp = original.components.StaticMapEntity; + assert(staticComp, "Building is not static"); + const beltComp = original.components.Belt; + if (beltComp && building instanceof MetaBeltBaseBuilding) { + // Its a belt, check if it differs in either rotation or rotation variant + if (staticComp.rotation !== rotation) { + return true; + } + if (beltComp.direction !== arrayBeltVariantToRotation[rotationVariant]) { + return true; + } + } + + return true; + } + + /** + * @param {object} param0 + * @param {Vector} param0.origin + * @param {number} param0.rotation + * @param {number} param0.rotationVariant + * @param {string} param0.variant + * @param {MetaBuilding} param0.building + */ + checkCanPlaceBuilding({ origin, rotation, rotationVariant, variant, building }) { if (!building.getIsUnlocked(this.root)) { return false; } - return this.isAreaFreeToBuild(origin, rotation, building); + + if ( + !building.performAdditionalPlacementChecks(this.root, { + origin, + rotation, + rotationVariant, + variant, + }) + ) { + return false; + } + + return this.isAreaFreeToBuild({ + origin, + rotation, + rotationVariant, + variant, + building, + }); } /** @@ -89,16 +156,18 @@ export class GameLogic { * @param {object} param0 * @param {Vector} param0.origin * @param {number} param0.rotation + * @param {number} param0.originalRotation * @param {number} param0.rotationVariant + * @param {string} param0.variant * @param {MetaBuilding} param0.building */ - tryPlaceBuilding({ origin, rotation, rotationVariant, building }) { - if (this.checkCanPlaceBuilding(origin, rotation, building)) { + tryPlaceBuilding({ origin, rotation, rotationVariant, originalRotation, variant, building }) { + if (this.checkCanPlaceBuilding({ origin, rotation, rotationVariant, variant, building })) { // Remove any removeable entities below const checker = new StaticMapEntityComponent({ origin, - tileSize: building.getDimensions(), - rotationDegrees: rotation, + tileSize: building.getDimensions(variant), + rotation, }); const rect = checker.getTileSpaceBounds(); @@ -106,7 +175,7 @@ export class GameLogic { for (let x = rect.x; x < rect.x + rect.w; ++x) { for (let y = rect.y; y < rect.y + rect.h; ++y) { const contents = this.root.map.getTileContentXY(x, y); - if (contents && contents.components.ReplaceableMapEntity) { + if (contents) { if (!this.tryDeleteBuilding(contents)) { logger.error("Building has replaceable component but is also unremovable"); return false; @@ -115,7 +184,17 @@ export class GameLogic { } } - building.createAndPlaceEntity(this.root, origin, rotation, rotationVariant); + building.createAndPlaceEntity({ + root: this.root, + origin, + rotation, + rotationVariant, + originalRotation, + variant, + }); + + this.root.soundProxy.playUi(building.getPlacementSound()); + return true; } return false; diff --git a/src/js/game/map.js b/src/js/game/map.js index 97f6d3e6..3fd82844 100644 --- a/src/js/game/map.js +++ b/src/js/game/map.js @@ -9,17 +9,32 @@ import { Math_floor } from "../core/builtins"; import { createLogger } from "../core/logging"; import { BaseItem } from "./base_item"; import { MapChunkView } from "./map_chunk_view"; +import { randomInt } from "../core/utils"; +import { BasicSerializableObject, types } from "../savegame/serialization"; const logger = createLogger("map"); -export class BaseMap { +export class BaseMap extends BasicSerializableObject { + static getId() { + return "Map"; + } + + static getSchema() { + return { + seed: types.uint, + }; + } + /** * * @param {GameRoot} root */ constructor(root) { + super(); this.root = root; + this.seed = 0; + /** * Mapping of 'X|Y' to chunk * @type {Map} */ diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index a6d4ab55..bffaf9e8 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -2,16 +2,10 @@ import { GameRoot } from "./root"; /* typehints:end */ -import { Math_ceil, Math_max, Math_min, Math_random, Math_round } from "../core/builtins"; +import { Math_ceil, Math_max, Math_min, Math_round } from "../core/builtins"; import { globalConfig } from "../core/config"; import { createLogger } from "../core/logging"; -import { - clamp, - fastArrayDeleteValueIfContained, - make2DUndefinedArray, - randomChoice, - randomInt, -} from "../core/utils"; +import { clamp, fastArrayDeleteValueIfContained, make2DUndefinedArray } from "../core/utils"; import { Vector } from "../core/vector"; import { BaseItem } from "./base_item"; import { enumColors } from "./colors"; @@ -19,6 +13,7 @@ import { Entity } from "./entity"; import { ColorItem } from "./items/color_item"; import { ShapeItem } from "./items/shape_item"; import { enumSubShape } from "./shape_definition"; +import { RandomNumberGenerator } from "../core/rng"; const logger = createLogger("map_chunk"); @@ -64,17 +59,18 @@ export class MapChunk { /** * Generates a patch filled with the given item + * @param {RandomNumberGenerator} rng * @param {number} patchSize * @param {BaseItem} item * @param {number=} overrideX Override the X position of the patch * @param {number=} overrideY Override the Y position of the patch */ - internalGeneratePatch(patchSize, item, overrideX = null, overrideY = null) { + internalGeneratePatch(rng, patchSize, item, overrideX = null, overrideY = null) { const border = Math_ceil(patchSize / 2 + 3); // Find a position within the chunk which is not blocked - let patchX = randomInt(border, globalConfig.mapChunkSize - border - 1); - let patchY = randomInt(border, globalConfig.mapChunkSize - border - 1); + let patchX = rng.nextIntRange(border, globalConfig.mapChunkSize - border - 1); + let patchY = rng.nextIntRange(border, globalConfig.mapChunkSize - border - 1); if (overrideX !== null) { patchX = overrideX; @@ -89,7 +85,6 @@ export class MapChunk { // Each patch consists of multiple circles const numCircles = patchSize; - // const numCircles = 1; for (let i = 0; i <= numCircles; ++i) { // Determine circle parameters @@ -98,11 +93,11 @@ export class MapChunk { const circleOffsetRadius = (numCircles - i) / 2 + 2; // We draw an elipsis actually - const circleScaleY = 1 + (Math_random() * 2 - 1) * 0.1; - const circleScaleX = 1 + (Math_random() * 2 - 1) * 0.1; + const circleScaleX = rng.nextRange(0.9, 1.1); + const circleScaleY = rng.nextRange(0.9, 1.1); - const circleX = patchX + randomInt(-circleOffsetRadius, circleOffsetRadius); - const circleY = patchY + randomInt(-circleOffsetRadius, circleOffsetRadius); + const circleX = patchX + rng.nextIntRange(-circleOffsetRadius, circleOffsetRadius); + const circleY = patchY + rng.nextIntRange(-circleOffsetRadius, circleOffsetRadius); for (let dx = -circleRadius * circleScaleX - 2; dx <= circleRadius * circleScaleX + 2; ++dx) { for (let dy = -circleRadius * circleScaleY - 2; dy <= circleRadius * circleScaleY + 2; ++dy) { @@ -135,24 +130,26 @@ export class MapChunk { /** * Generates a color patch + * @param {RandomNumberGenerator} rng * @param {number} colorPatchSize * @param {number} distanceToOriginInChunks */ - internalGenerateColorPatch(colorPatchSize, distanceToOriginInChunks) { + internalGenerateColorPatch(rng, colorPatchSize, distanceToOriginInChunks) { // First, determine available colors let availableColors = [enumColors.red, enumColors.green]; if (distanceToOriginInChunks > 2) { availableColors.push(enumColors.blue); } - this.internalGeneratePatch(colorPatchSize, new ColorItem(randomChoice(availableColors))); + this.internalGeneratePatch(rng, colorPatchSize, new ColorItem(rng.choice(availableColors))); } /** * Generates a shape patch + * @param {RandomNumberGenerator} rng * @param {number} shapePatchSize * @param {number} distanceToOriginInChunks */ - internalGenerateShapePatch(shapePatchSize, distanceToOriginInChunks) { + internalGenerateShapePatch(rng, shapePatchSize, distanceToOriginInChunks) { /** @type {[enumSubShape, enumSubShape, enumSubShape, enumSubShape]} */ let subShapes = null; @@ -162,43 +159,62 @@ export class MapChunk { weights = { [enumSubShape.rect]: 100, [enumSubShape.circle]: Math_round(50 + clamp(distanceToOriginInChunks * 2, 0, 50)), - [enumSubShape.star]: Math_round(20 + clamp(distanceToOriginInChunks * 2, 0, 30)), - [enumSubShape.windmill]: Math_round(5 + clamp(distanceToOriginInChunks * 2, 0, 20)), + [enumSubShape.star]: Math_round(20 + clamp(distanceToOriginInChunks, 0, 30)), + [enumSubShape.windmill]: Math_round(6 + clamp(distanceToOriginInChunks / 2, 0, 20)), }; if (distanceToOriginInChunks < 7) { + // Initial chunks can not spawn the good stuff + weights[enumSubShape.star] = 0; + weights[enumSubShape.windmill] = 0; + } + + if (distanceToOriginInChunks < 10) { // Initial chunk patches always have the same shape - const subShape = this.internalGenerateRandomSubShape(weights); + const subShape = this.internalGenerateRandomSubShape(rng, weights); subShapes = [subShape, subShape, subShape, subShape]; - } else if (distanceToOriginInChunks < 12) { + } else if (distanceToOriginInChunks < 15) { // Later patches can also have mixed ones - const subShapeA = this.internalGenerateRandomSubShape(weights); - const subShapeB = this.internalGenerateRandomSubShape(weights); + const subShapeA = this.internalGenerateRandomSubShape(rng, weights); + const subShapeB = this.internalGenerateRandomSubShape(rng, weights); subShapes = [subShapeA, subShapeA, subShapeB, subShapeB]; } else { // Finally there is a mix of everything subShapes = [ - this.internalGenerateRandomSubShape(weights), - this.internalGenerateRandomSubShape(weights), - this.internalGenerateRandomSubShape(weights), - this.internalGenerateRandomSubShape(weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), ]; } + // Makes sure windmills never spawn as whole + let windmillCount = 0; + for (let i = 0; i < subShapes.length; ++i) { + if (subShapes[i] === enumSubShape.windmill) { + ++windmillCount; + } + } + if (windmillCount > 1) { + subShapes[0] = enumSubShape.rect; + subShapes[1] = enumSubShape.rect; + } + const definition = this.root.shapeDefinitionMgr.getDefinitionFromSimpleShapes(subShapes); - this.internalGeneratePatch(shapePatchSize, new ShapeItem(definition)); + this.internalGeneratePatch(rng, shapePatchSize, new ShapeItem(definition)); } /** * Chooses a random shape with the given weights + * @param {RandomNumberGenerator} rng * @param {Object.} weights * @returns {enumSubShape} */ - internalGenerateRandomSubShape(weights) { + internalGenerateRandomSubShape(rng, weights) { // @ts-ignore const sum = Object.values(weights).reduce((a, b) => a + b, 0); - const chosenNumber = randomInt(0, sum - 1); + const chosenNumber = rng.nextIntRange(0, sum - 1); let accumulated = 0; for (const key in weights) { const weight = weights[key]; @@ -216,7 +232,9 @@ export class MapChunk { * Generates the lower layer "terrain" */ generateLowerLayer() { - if (this.generatePredefined()) { + const rng = new RandomNumberGenerator(this.x + "|" + this.y + "|" + this.root.map.seed); + + if (this.generatePredefined(rng)) { return; } @@ -225,52 +243,50 @@ export class MapChunk { // Determine how likely it is that there is a color patch const colorPatchChance = 0.9 - clamp(distanceToOriginInChunks / 25, 0, 1) * 0.5; - if (Math_random() < colorPatchChance) { + + if (rng.next() < colorPatchChance / 4) { const colorPatchSize = Math_max(2, Math_round(1 + clamp(distanceToOriginInChunks / 8, 0, 4))); - this.internalGenerateColorPatch(colorPatchSize, distanceToOriginInChunks); + this.internalGenerateColorPatch(rng, colorPatchSize, distanceToOriginInChunks); } // Determine how likely it is that there is a shape patch const shapePatchChance = 0.9 - clamp(distanceToOriginInChunks / 25, 0, 1) * 0.5; - if (Math_random() < shapePatchChance) { + if (rng.next() < shapePatchChance / 4) { const shapePatchSize = Math_max(2, Math_round(1 + clamp(distanceToOriginInChunks / 8, 0, 4))); - this.internalGenerateShapePatch(shapePatchSize, distanceToOriginInChunks); + this.internalGenerateShapePatch(rng, shapePatchSize, distanceToOriginInChunks); } } /** * Checks if this chunk has predefined contents, and if so returns true and generates the * predefined contents + * @param {RandomNumberGenerator} rng * @returns {boolean} */ - generatePredefined() { + generatePredefined(rng) { if (this.x === 0 && this.y === 0) { - this.internalGeneratePatch(2, new ColorItem(enumColors.red), 7, 7); + this.internalGeneratePatch(rng, 2, new ColorItem(enumColors.red), 7, 7); return true; } if (this.x === -1 && this.y === 0) { - const definition = this.root.shapeDefinitionMgr.getDefinitionFromSimpleShapes([ - enumSubShape.circle, - enumSubShape.circle, - enumSubShape.circle, - enumSubShape.circle, - ]); - this.internalGeneratePatch(2, new ShapeItem(definition), globalConfig.mapChunkSize - 9, 7); + const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey("CuCuCuCu"); + this.internalGeneratePatch(rng, 2, new ShapeItem(definition), globalConfig.mapChunkSize - 9, 7); return true; } if (this.x === 0 && this.y === -1) { - const definition = this.root.shapeDefinitionMgr.getDefinitionFromSimpleShapes([ - enumSubShape.rect, - enumSubShape.rect, - enumSubShape.rect, - enumSubShape.rect, - ]); - this.internalGeneratePatch(2, new ShapeItem(definition), 5, globalConfig.mapChunkSize - 7); + const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey("RuRuRuRu"); + this.internalGeneratePatch(rng, 2, new ShapeItem(definition), 5, globalConfig.mapChunkSize - 7); return true; } if (this.x === -1 && this.y === -1) { - this.internalGeneratePatch(2, new ColorItem(enumColors.green)); + this.internalGeneratePatch(rng, 2, new ColorItem(enumColors.green)); + return true; + } + + if (this.x === 5 && this.y === -2) { + const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey("SuSuSuSu"); + this.internalGeneratePatch(rng, 2, new ShapeItem(definition), 5, globalConfig.mapChunkSize - 7); return true; } diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 7634db64..cc0734d8 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -7,6 +7,7 @@ import { Math_max, Math_round } from "../core/builtins"; import { Rectangle } from "../core/rectangle"; import { createLogger } from "../core/logging"; import { smoothenDpi } from "../core/dpi_manager"; +import { THEME } from "./theme"; const logger = createLogger("chunk"); const chunkSizePixels = globalConfig.mapChunkSize * globalConfig.tileSize; @@ -119,9 +120,9 @@ export class MapChunkView extends MapChunk { context.scale(bgDpi, bgDpi); } else { if (this.containedEntities.length > 0) { - context.fillStyle = "#c5ccd6"; + context.fillStyle = THEME.map.chunkOverview.filled; } else { - context.fillStyle = "#a6afbb"; + context.fillStyle = THEME.map.chunkOverview.empty; } context.fillRect(0, 0, 10000, 10000); } @@ -148,19 +149,7 @@ export class MapChunkView extends MapChunk { -this.tileX * globalConfig.tileSize, -this.tileY * globalConfig.tileSize ); - // parameters.context.save(); - // parameters.context.transform( - // 1, - // 0, - // 0, - // zoomLevel, - // this.tileX * globalConfig.tileSize, - // this.tileY * globalConfig.tileSize - // ); - this.internalDrawBackgroundSystems(parameters); - - // parameters.context.restore(); } /** @@ -186,24 +175,11 @@ export class MapChunkView extends MapChunk { zoomLevel, root: this.root, }); - // parameters.context.save(); - // parameters.context.save(); - // parameters.context.transform( - // zoomLevel, - // 0, - // 0, - // zoomLevel, - // this.tileX * globalConfig.tileSize, - // this.tileY * globalConfig.tileSize - // ); - parameters.context.translate( -this.tileX * globalConfig.tileSize, -this.tileY * globalConfig.tileSize ); this.internalDrawForegroundSystems(parameters); - - // parameters.context.restore(); } /** diff --git a/src/js/game/map_view.js b/src/js/game/map_view.js index 6c42b266..90919a2a 100644 --- a/src/js/game/map_view.js +++ b/src/js/game/map_view.js @@ -4,6 +4,7 @@ import { DrawParameters } from "../core/draw_parameters"; import { BaseMap } from "./map"; import { freeCanvas, makeOffscreenBuffer } from "../core/buffer_utils"; import { Entity } from "./entity"; +import { THEME } from "./theme"; /** * This is the view of the map, it extends the map which is the raw model and allows @@ -16,7 +17,7 @@ export class MapView extends BaseMap { /** * DPI of the background cache images, required in some places */ - this.backgroundCacheDPI = 4; + this.backgroundCacheDPI = 2; /** * The cached background sprite, containing the flat background @@ -109,14 +110,16 @@ export class MapView extends BaseMap { }); context.scale(dpi, dpi); - context.fillStyle = "#fff"; + context.fillStyle = THEME.map.background; context.fillRect(0, 0, dims, dims); - context.fillStyle = "#fafafa"; - context.fillRect(0, 0, dims, 1); - context.fillRect(0, 0, 1, dims); - context.fillRect(dims - 1, 0, 1, dims); - context.fillRect(0, dims - 1, dims, 1); + const borderWidth = THEME.map.gridLineWidth; + context.fillStyle = THEME.map.grid; + context.fillRect(0, 0, dims, borderWidth); + context.fillRect(0, borderWidth, borderWidth, dims); + + context.fillRect(dims - borderWidth, borderWidth, borderWidth, dims - 2 * borderWidth); + context.fillRect(borderWidth, dims - borderWidth, dims, borderWidth); this.cachedBackgroundCanvas = canvas; this.cachedBackgroundContext = context; diff --git a/src/js/game/meta_building.js b/src/js/game/meta_building.js index a5fc8c25..ad360ac0 100644 --- a/src/js/game/meta_building.js +++ b/src/js/game/meta_building.js @@ -4,6 +4,9 @@ import { GameRoot } from "./root"; import { AtlasSprite } from "../core/sprites"; import { Entity } from "./entity"; import { StaticMapEntityComponent } from "./components/static_map_entity"; +import { SOUNDS } from "../platform/sound"; + +export const defaultBuildingVariant = "default"; export class MetaBuilding { /** @@ -24,24 +27,10 @@ export class MetaBuilding { /** * Should return the dimensions of the building */ - getDimensions() { + getDimensions(variant = defaultBuildingVariant) { return new Vector(1, 1); } - /** - * Should return the name of this building - */ - getName() { - return this.id; - } - - /** - * Should return the description of this building - */ - getDescription() { - return "No Description"; - } - /** * Whether to stay in placement mode after having placed a building */ @@ -49,6 +38,16 @@ export class MetaBuilding { return false; } + /** + * Should return additional statistics about this building + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + return []; + } + /** * Whether to flip the orientation after a building has been placed - useful * for tunnels. @@ -57,19 +56,61 @@ export class MetaBuilding { return false; } + /** + * Whether to rotate automatically in the dragging direction while placing + * @param {string} variant + */ + getRotateAutomaticallyWhilePlacing(variant) { + return false; + } + + /** + * Returns the placement sound + * @returns {string} + */ + getPlacementSound() { + return SOUNDS.placeBuilding; + } + + /** + * @param {GameRoot} root + */ + getAvailableVariants(root) { + return [defaultBuildingVariant]; + } + /** * Returns a preview sprite * @returns {AtlasSprite} */ - getPreviewSprite(rotationVariant = 0) { - return Loader.getSprite("sprites/buildings/" + this.id + ".png"); + getPreviewSprite(rotationVariant = 0, variant = defaultBuildingVariant) { + return Loader.getSprite( + "sprites/buildings/" + + this.id + + (variant === defaultBuildingVariant ? "" : "-" + variant) + + ".png" + ); + } + + /** + * Returns a sprite for blueprints + * @returns {AtlasSprite} + */ + getBlueprintSprite(rotationVariant = 0, variant = defaultBuildingVariant) { + return Loader.getSprite( + "sprites/blueprints/" + + this.id + + (variant === defaultBuildingVariant ? "" : "-" + variant) + + ".png" + ); } /** * Returns whether this building is rotateable + * @param {string} variant * @returns {boolean} */ - isRotateable() { + isRotateable(variant) { return true; } @@ -89,29 +130,50 @@ export class MetaBuilding { } /** - * Creates the entity at the given location + * Should perform additional placement checks * @param {GameRoot} root - * @param {Vector} origin Origin tile - * @param {number=} rotation Rotation - * @param {number=} rotationVariant Rotation variant + * @param {object} param0 + * @param {Vector} param0.origin + * @param {number} param0.rotation + * @param {number} param0.rotationVariant + * @param {string} param0.variant */ - createAndPlaceEntity(root, origin, rotation = 0, rotationVariant = 0) { + performAdditionalPlacementChecks(root, { origin, rotation, rotationVariant, variant }) { + return true; + } + + /** + * Creates the entity at the given location + * @param {object} param0 + * @param {GameRoot} param0.root + * @param {Vector} param0.origin Origin tile + * @param {number=} param0.rotation Rotation + * @param {number} param0.originalRotation Original Rotation + * @param {number} param0.rotationVariant Rotation variant + * @param {string} param0.variant + */ + createAndPlaceEntity({ root, origin, rotation, originalRotation, rotationVariant, variant }) { const entity = new Entity(root); entity.addComponent( new StaticMapEntityComponent({ - spriteKey: "sprites/buildings/" + this.id + ".png", + spriteKey: + "sprites/buildings/" + + this.id + + (variant === defaultBuildingVariant ? "" : "-" + variant) + + ".png", origin: new Vector(origin.x, origin.y), - rotationDegrees: rotation, - tileSize: this.getDimensions().copy(), + rotation, + originalRotation, + tileSize: this.getDimensions(variant).copy(), silhouetteColor: this.getSilhouetteColor(), }) ); this.setupEntityComponents(entity, root); - this.updateRotationVariant(entity, rotationVariant); + this.updateVariants(entity, rotationVariant, variant); - root.entityMgr.registerEntity(entity); root.map.placeStaticEntity(entity); + root.entityMgr.registerEntity(entity); return entity; } @@ -120,10 +182,11 @@ export class MetaBuilding { * @param {GameRoot} root * @param {Vector} tile * @param {number} rotation + * @param {string} variant * @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array }} */ - computeOptimalDirectionAndRotationVariantAtTile(root, tile, rotation) { - if (!this.isRotateable()) { + computeOptimalDirectionAndRotationVariantAtTile(root, tile, rotation, variant) { + if (!this.isRotateable(variant)) { return { rotation: 0, rotationVariant: 0, @@ -136,11 +199,12 @@ export class MetaBuilding { } /** - * Should update the entity to match the given rotation variant + * Should update the entity to match the given variants * @param {Entity} entity * @param {number} rotationVariant + * @param {string} variant */ - updateRotationVariant(entity, rotationVariant) {} + updateVariants(entity, rotationVariant, variant) {} // PRIVATE INTERFACE diff --git a/src/js/game/production_analytics.js b/src/js/game/production_analytics.js new file mode 100644 index 00000000..1abac940 --- /dev/null +++ b/src/js/game/production_analytics.js @@ -0,0 +1,134 @@ +import { GameRoot } from "./root"; +import { ShapeDefinition } from "./shape_definition"; +import { globalConfig } from "../core/config"; +import { BaseItem } from "./base_item"; +import { ShapeItem } from "./items/shape_item"; +import { BasicSerializableObject } from "../savegame/serialization"; + +/** @enum {string} */ +export const enumAnalyticsDataSource = { + produced: "produced", + stored: "stored", + delivered: "delivered", +}; + +export class ProductionAnalytics extends BasicSerializableObject { + static getId() { + return "ProductionAnalytics"; + } + + /** + * @param {GameRoot} root + */ + constructor(root) { + super(); + this.root = root; + + this.history = { + [enumAnalyticsDataSource.produced]: [], + [enumAnalyticsDataSource.stored]: [], + [enumAnalyticsDataSource.delivered]: [], + }; + + for (let i = 0; i < globalConfig.statisticsGraphSlices; ++i) { + this.startNewSlice(); + } + + this.root.signals.shapeDelivered.add(this.onShapeDelivered, this); + this.root.signals.itemProduced.add(this.onItemProduced, this); + + this.lastAnalyticsSlice = 0; + } + + /** + * @param {ShapeDefinition} definition + */ + onShapeDelivered(definition) { + const key = definition.getHash(); + const entry = this.history[enumAnalyticsDataSource.delivered]; + entry[entry.length - 1][key] = (entry[entry.length - 1][key] || 0) + 1; + } + + /** + * @param {BaseItem} item + */ + onItemProduced(item) { + if (item instanceof ShapeItem) { + const definition = item.definition; + const key = definition.getHash(); + const entry = this.history[enumAnalyticsDataSource.produced]; + entry[entry.length - 1][key] = (entry[entry.length - 1][key] || 0) + 1; + } + } + + /** + * Starts a new time slice + */ + startNewSlice() { + for (const key in this.history) { + if (key === enumAnalyticsDataSource.stored) { + // Copy stored data + this.history[key].push(Object.assign({}, this.root.hubGoals.storedShapes)); + } else { + this.history[key].push({}); + } + while (this.history[key].length > globalConfig.statisticsGraphSlices) { + this.history[key].shift(); + } + } + } + + /** + * Returns the current rate of a given shape + * @param {enumAnalyticsDataSource} dataSource + * @param {ShapeDefinition} definition + */ + getCurrentShapeRate(dataSource, definition) { + const slices = this.history[dataSource]; + return slices[slices.length - 2][definition.getHash()] || 0; + } + + /** + * Returns the rate of a given shape, frames ago + * @param {enumAnalyticsDataSource} dataSource + * @param {ShapeDefinition} definition + * @param {number} historyOffset + */ + getPastShapeRate(dataSource, definition, historyOffset) { + assertAlways( + historyOffset >= 0 && historyOffset < globalConfig.statisticsGraphSlices - 1, + "Invalid slice offset: " + historyOffset + ); + + const slices = this.history[dataSource]; + return slices[slices.length - 2 - historyOffset][definition.getHash()] || 0; + } + + /** + * Returns the rates of all shapes + * @param {enumAnalyticsDataSource} dataSource + */ + getCurrentShapeRates(dataSource) { + const slices = this.history[dataSource]; + + // First, copy current slice + const baseValues = Object.assign({}, slices[slices.length - 2]); + + // Add past values + for (let i = 0; i < 10; ++i) { + const pastValues = slices[slices.length - i - 3]; + for (const key in pastValues) { + baseValues[key] = baseValues[key] || 0; + } + } + + return baseValues; + } + + update() { + if (this.root.time.now() - this.lastAnalyticsSlice > globalConfig.analyticsSliceDurationSeconds) { + this.lastAnalyticsSlice = this.root.time.now(); + this.startNewSlice(); + } + } +} diff --git a/src/js/game/root.js b/src/js/game/root.js index 7a6910f3..0c4e6792 100644 --- a/src/js/game/root.js +++ b/src/js/game/root.js @@ -2,7 +2,6 @@ import { Signal } from "../core/signal"; import { RandomNumberGenerator } from "../core/rng"; -// import { gFactionRegistry } from "./global_registries"; import { createLogger } from "../core/logging"; // Type hints @@ -11,12 +10,9 @@ import { GameTime } from "./time/game_time"; import { EntityManager } from "./entity_manager"; import { GameSystemManager } from "./game_system_manager"; import { GameHUD } from "./hud/hud"; -// import { GameLogic } from "./game_logic"; import { MapView } from "./map_view"; import { Camera } from "./camera"; -// import { ParticleManager } from "../particles/particle_manager"; import { InGameState } from "../states/ingame"; -// import { CanvasClickInterceptor } from "/canvas_click_interceptor"; import { AutomaticSave } from "./automatic_save"; import { Application } from "../application"; import { SoundProxy } from "./sound_proxy"; @@ -24,9 +20,14 @@ import { Savegame } from "../savegame/savegame"; import { GameLogic } from "./logic"; import { ShapeDefinitionManager } from "./shape_definition_manager"; import { CanvasClickInterceptor } from "./canvas_click_interceptor"; -import { PerlinNoise } from "../core/perlin_noise"; import { HubGoals } from "./hub_goals"; import { BufferMaintainer } from "../core/buffer_maintainer"; +import { ProductionAnalytics } from "./production_analytics"; +import { Entity } from "./entity"; +import { ShapeDefinition } from "./shape_definition"; +import { BaseItem } from "./base_item"; +import { DynamicTickrate } from "./dynamic_tickrate"; +import { KeyActionMapper } from "./key_action_mapper"; /* typehints:end */ const logger = createLogger("game/root"); @@ -50,6 +51,9 @@ export class GameRoot { /** @type {InGameState} */ this.gameState = null; + /** @type {KeyActionMapper} */ + this.keyMapper = null; + // Store game dimensions this.gameWidth = 500; this.gameHeight = 500; @@ -95,21 +99,12 @@ export class GameRoot { /** @type {GameTime} */ this.time = null; - /** @type {PerlinNoise} */ - this.mapNoiseGenerator = null; - /** @type {HubGoals} */ this.hubGoals = null; /** @type {BufferMaintainer} */ this.buffers = null; - // /** @type {ParticleManager} */ - // this.particleMgr = null; - - // /** @type {ParticleManager} */ - // this.uiParticleMgr = null; - /** @type {CanvasClickInterceptor} */ this.canvasClickInterceptor = null; @@ -119,37 +114,43 @@ export class GameRoot { /** @type {SoundProxy} */ this.soundProxy = null; - // /** @type {MinimapRenderer} */ - // this.minimapRenderer = null; - /** @type {ShapeDefinitionManager} */ this.shapeDefinitionMgr = null; + /** @type {ProductionAnalytics} */ + this.productionAnalytics = null; + + /** @type {DynamicTickrate} */ + this.dynamicTickrate = null; + this.signals = { // Entities - entityAdded: new Signal(/* entity */), - entityGotNewComponent: new Signal(/* entity */), - entityQueuedForDestroy: new Signal(/* entity */), - entityDestroyed: new Signal(/* entity */), + entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()), // Global - resized: new Signal(/* w, h */), // Game got resized, - readyToRender: new Signal(), - aboutToDestruct: new Signal(), + resized: /** @type {TypedSignal<[number, number]>} */ (new Signal()), + readyToRender: /** @type {TypedSignal<[]>} */ (new Signal()), + aboutToDestruct: /** @type {TypedSignal<[]>} */ new Signal(), // Game Hooks - gameSaved: new Signal(), // Game got saved - gameRestored: new Signal(), // Game got restored - gameOver: new Signal(), // Game over + gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved + gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored - storyGoalCompleted: new Signal(/* level, reward */), - upgradePurchased: new Signal(), + storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()), + upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()), // Called right after game is initialized - postLoadHook: new Signal(), + postLoadHook: /** @type {TypedSignal<[]>} */ (new Signal()), // Can be used to trigger an async task - performAsync: new Signal(), + performAsync: /** @type {TypedSignal<[function]>} */ (new Signal()), + + shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), + itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()), }; // RNG's @@ -172,20 +173,6 @@ export class GameRoot { this.reset(); } - /** - * Prepares the root for game over, this sets the right flags and - * detaches all signals so no bad stuff happens - */ - prepareGameOver() { - this.gameInitialized = false; - this.logicInitialized = false; - // for (const key in this.signals) { - // if (key !== "aboutToDestruct") { - // this.signals[key].removeAll(); - // } - // } - } - /** * Resets the whole root and removes all properties */ diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index a3f91224..64cc3eab 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -5,8 +5,9 @@ import { smoothenDpi } from "../core/dpi_manager"; import { DrawParameters } from "../core/draw_parameters"; import { createLogger } from "../core/logging"; import { Vector } from "../core/vector"; -import { BasicSerializableObject } from "../savegame/serialization"; +import { BasicSerializableObject, types } from "../savegame/serialization"; import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors"; +import { THEME } from "./theme"; const rusha = require("rusha"); @@ -74,6 +75,23 @@ export class ShapeDefinition extends BasicSerializableObject { return "ShapeDefinition"; } + static getSchema() { + return {}; + } + + deserialize(data) { + const errorCode = super.deserialize(data); + if (errorCode) { + return errorCode; + } + const definition = ShapeDefinition.fromShortKey(data); + this.layers = definition.layers; + } + + serialize() { + return this.getHash(); + } + /** * * @param {object} param0 @@ -123,7 +141,10 @@ export class ShapeDefinition extends BasicSerializableObject { layers.push(quads); } - return new ShapeDefinition({ layers }); + const definition = new ShapeDefinition({ layers }); + // We know the hash so save some work + definition.cachedHash = key; + return definition; } /** @@ -163,6 +184,10 @@ export class ShapeDefinition extends BasicSerializableObject { id += "--"; } } + + if (layerIndex < this.layers.length - 1) { + id += ":"; + } } this.cachedHash = id; return id; @@ -197,7 +222,7 @@ export class ShapeDefinition extends BasicSerializableObject { * Generates this shape as a canvas * @param {number} size */ - generateAsCanvas(size = 20) { + generateAsCanvas(size = 120) { const [canvas, context] = makeOffscreenBuffer(size, size, { smooth: true, label: "definition-canvas-cache-" + this.getHash(), @@ -250,8 +275,8 @@ export class ShapeDefinition extends BasicSerializableObject { context.rotate(rotation); context.fillStyle = enumColorsToHexCode[color]; - context.strokeStyle = "#555"; - context.lineWidth = 1; + context.strokeStyle = THEME.items.outline; + context.lineWidth = THEME.items.outlineWidth; const insetPadding = 0.0; @@ -368,6 +393,20 @@ export class ShapeDefinition extends BasicSerializableObject { return new ShapeDefinition({ layers: newLayers }); } + /** + * Returns a definition which was rotated counter clockwise + * @returns {ShapeDefinition} + */ + cloneRotateCCW() { + const newLayers = this.internalCloneLayers(); + for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { + const quadrants = newLayers[layerIndex]; + quadrants.push(quadrants[0]); + quadrants.shift(); + } + return new ShapeDefinition({ layers: newLayers }); + } + /** * Stacks the given shape definition on top. * @param {ShapeDefinition} definition @@ -444,4 +483,23 @@ export class ShapeDefinition extends BasicSerializableObject { } return new ShapeDefinition({ layers: newLayers }); } + + /** + * Clones the shape and colors everything in the given colors + * @param {[enumColors, enumColors, enumColors, enumColors]} colors + */ + cloneAndPaintWith4Colors(colors) { + const newLayers = this.internalCloneLayers(); + + for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { + const quadrants = newLayers[layerIndex]; + for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { + const item = quadrants[quadrantIndex]; + if (item) { + item.color = colors[quadrantIndex]; + } + } + } + return new ShapeDefinition({ layers: newLayers }); + } } diff --git a/src/js/game/shape_definition_manager.js b/src/js/game/shape_definition_manager.js index 25f456cd..ad682bf0 100644 --- a/src/js/game/shape_definition_manager.js +++ b/src/js/game/shape_definition_manager.js @@ -26,6 +26,19 @@ export class ShapeDefinitionManager extends BasicSerializableObject { this.operationCache = {}; } + /** + * + * @param {string} hash + * @returns {ShapeDefinition} + */ + getShapeFromShortKey(hash) { + const cached = this.shapeKeyToDefinition[hash]; + if (cached) { + return cached; + } + return (this.shapeKeyToDefinition[hash] = ShapeDefinition.fromShortKey(hash)); + } + /** * Registers a new shape definition * @param {ShapeDefinition} definition @@ -47,8 +60,8 @@ export class ShapeDefinitionManager extends BasicSerializableObject { if (this.operationCache[key]) { return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key]); } - const rightSide = definition.cloneFilteredByQuadrants([0, 1]); - const leftSide = definition.cloneFilteredByQuadrants([2, 3]); + const rightSide = definition.cloneFilteredByQuadrants([2, 3]); + const leftSide = definition.cloneFilteredByQuadrants([0, 1]); return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key] = [ this.registerOrReturnHandle(rightSide), @@ -56,13 +69,35 @@ export class ShapeDefinitionManager extends BasicSerializableObject { ]); } + /** + * Generates a definition for splitting a shape definition in four quads + * @param {ShapeDefinition} definition + * @returns {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} + */ + shapeActionCutQuad(definition) { + const key = "cut-quad:" + definition.getHash(); + if (this.operationCache[key]) { + return /** @type {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} */ (this + .operationCache[key]); + } + + return /** @type {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} */ (this.operationCache[ + key + ] = [ + this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([0])), + this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([1])), + this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([2])), + this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([3])), + ]); + } + /** * Generates a definition for rotating a shape clockwise * @param {ShapeDefinition} definition * @returns {ShapeDefinition} */ shapeActionRotateCW(definition) { - const key = "rotate:" + definition.getHash(); + const key = "rotate-cw:" + definition.getHash(); if (this.operationCache[key]) { return /** @type {ShapeDefinition} */ (this.operationCache[key]); } @@ -74,6 +109,24 @@ export class ShapeDefinitionManager extends BasicSerializableObject { )); } + /** + * Generates a definition for rotating a shape counter clockwise + * @param {ShapeDefinition} definition + * @returns {ShapeDefinition} + */ + shapeActionRotateCCW(definition) { + const key = "rotate-ccw:" + definition.getHash(); + if (this.operationCache[key]) { + return /** @type {ShapeDefinition} */ (this.operationCache[key]); + } + + const rotated = definition.cloneRotateCCW(); + + return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( + rotated + )); + } + /** * Generates a definition for stacking the upper definition onto the lower one * @param {ShapeDefinition} lowerDefinition @@ -94,7 +147,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject { /** * Generates a definition for painting it with the given color * @param {ShapeDefinition} definition - * @param {string} color + * @param {enumColors} color * @returns {ShapeDefinition} */ shapeActionPaintWith(definition, color) { @@ -108,6 +161,23 @@ export class ShapeDefinitionManager extends BasicSerializableObject { )); } + /** + * Generates a definition for painting it with the 4 colors + * @param {ShapeDefinition} definition + * @param {[enumColors, enumColors, enumColors, enumColors]} colors + * @returns {ShapeDefinition} + */ + shapeActionPaintWith4Colors(definition, colors) { + const key = "paint4:" + definition.getHash() + ":" + colors.join(","); + if (this.operationCache[key]) { + return /** @type {ShapeDefinition} */ (this.operationCache[key]); + } + const colorized = definition.cloneAndPaintWith4Colors(colors); + return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( + colorized + )); + } + /** * Checks if we already have cached this definition, and if so throws it away and returns the already * cached variant @@ -130,11 +200,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject { */ getDefinitionFromSimpleShapes(subShapes, color = enumColors.uncolored) { const shapeLayer = /** @type {import("./shape_definition").ShapeLayer} */ (subShapes.map( - subShape => ({ - subShape, - rotation: 0, - color, - }) + subShape => ({ subShape, color }) )); return this.registerOrReturnHandle(new ShapeDefinition({ layers: [shapeLayer] })); diff --git a/src/js/game/sound_proxy.js b/src/js/game/sound_proxy.js index 120136d4..1c19c9fb 100644 --- a/src/js/game/sound_proxy.js +++ b/src/js/game/sound_proxy.js @@ -55,11 +55,6 @@ export class SoundProxy { if (this.playingSounds.length > maxOngoingSounds) { // Too many ongoing sounds - // console.warn( - // "Not playing", - // id, - // "because there are too many sounds playing" - // ); return false; } diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index a2853998..dd173045 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -1,4 +1,4 @@ -import { Math_radians, Math_min } from "../../core/builtins"; +import { Math_radians, Math_min, Math_max } from "../../core/builtins"; import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; import { Loader } from "../../core/loader"; @@ -6,11 +6,20 @@ import { AtlasSprite } from "../../core/sprites"; import { BeltComponent } from "../components/belt"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; -import { enumDirection, enumDirectionToVector, Vector } from "../../core/vector"; +import { enumDirection, enumDirectionToVector, Vector, enumInvertedDirections } from "../../core/vector"; import { MapChunkView } from "../map_chunk_view"; +import { gMetaBuildingRegistry } from "../../core/global_registries"; +import { MetaBeltBaseBuilding } from "../buildings/belt_base"; +import { defaultBuildingVariant } from "../meta_building"; +import { GameRoot } from "../root"; +import { createLogger } from "../../core/logging"; const BELT_ANIM_COUNT = 6; +const logger = createLogger("belt"); + +/** @typedef {Array<{ entity: Entity, followUp: Entity }>} BeltCache */ + export class BeltSystem extends GameSystemWithFilter { constructor(root) { super(root, [BeltComponent]); @@ -22,7 +31,7 @@ export class BeltSystem extends GameSystemWithFilter { [enumDirection.left]: Loader.getSprite("sprites/belt/left_0.png"), [enumDirection.right]: Loader.getSprite("sprites/belt/right_0.png"), }; - /** + /**b * @type {Object.>} */ this.beltAnimations = { @@ -51,20 +60,163 @@ export class BeltSystem extends GameSystemWithFilter { Loader.getSprite("sprites/belt/right_5.png"), ], }; + + this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this); + this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this); + + this.cacheNeedsUpdate = true; + + /** @type {BeltCache} */ + this.beltCache = []; + } + + /** + * Updates the belt placement after an entity has been added / deleted + * @param {Entity} entity + */ + updateSurroundingBeltPlacement(entity) { + if (!this.root.gameInitialized) { + return; + } + + const staticComp = entity.components.StaticMapEntity; + if (!staticComp) { + return; + } + + if (entity.components.Belt) { + this.cacheNeedsUpdate = true; + } + + const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding); + + // Compute affected area + const originalRect = staticComp.getTileSpaceBounds(); + const affectedArea = originalRect.expandedInAllDirections(1); + for (let x = affectedArea.x; x < affectedArea.right(); ++x) { + for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) { + if (!originalRect.containsPoint(x, y)) { + const targetEntity = this.root.map.getTileContentXY(x, y); + if (targetEntity) { + const targetBeltComp = targetEntity.components.Belt; + if (targetBeltComp) { + const targetStaticComp = targetEntity.components.StaticMapEntity; + const { + rotation, + rotationVariant, + } = metaBelt.computeOptimalDirectionAndRotationVariantAtTile( + this.root, + new Vector(x, y), + targetStaticComp.originalRotation, + defaultBuildingVariant + ); + targetStaticComp.rotation = rotation; + metaBelt.updateVariants(targetEntity, rotationVariant, defaultBuildingVariant); + this.cacheNeedsUpdate = true; + } + } + } + } + } } draw(parameters) { this.forEachMatchingEntityOnScreen(parameters, this.drawEntityItems.bind(this)); } - update() { - const beltSpeed = this.root.hubGoals.getBeltBaseSpeed() * globalConfig.physicsDeltaSeconds; + /** + * Finds the follow up entity for a given belt. Used for building the dependencies + * @param {Entity} entity + */ + findFollowUpEntity(entity) { + const staticComp = entity.components.StaticMapEntity; + const beltComp = entity.components.Belt; + const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction); + const followUpVector = enumDirectionToVector[followUpDirection]; + + const followUpTile = staticComp.origin.add(followUpVector); + const followUpEntity = this.root.map.getTileContent(followUpTile); + + // Check if theres a belt at the tile we point to + if (followUpEntity) { + const followUpBeltComp = followUpEntity.components.Belt; + if (followUpBeltComp) { + const followUpStatic = followUpEntity.components.StaticMapEntity; + const followUpAcceptor = followUpEntity.components.ItemAcceptor; + + // Check if the belt accepts items from our direction + const acceptorSlots = followUpAcceptor.slots; + for (let i = 0; i < acceptorSlots.length; ++i) { + const slot = acceptorSlots[i]; + for (let k = 0; k < slot.directions.length; ++k) { + const localDirection = followUpStatic.localDirectionToWorld(slot.directions[k]); + if (enumInvertedDirections[localDirection] === followUpDirection) { + return followUpEntity; + } + } + } + } + } + + return null; + } + + /** + * Adds a single entity to the cache + * @param {Entity} entity + * @param {BeltCache} cache + * @param {Set} visited + */ + computeSingleBeltCache(entity, cache, visited) { + // Check for double visit + if (visited.has(entity.uid)) { + return; + } + visited.add(entity.uid); + + const followUp = this.findFollowUpEntity(entity); + if (followUp) { + // Process followup first + this.computeSingleBeltCache(followUp, cache, visited); + } + + cache.push({ entity, followUp }); + } + + computeBeltCache() { + logger.log("Updating belt cache"); + + let cache = []; + let visited = new Set(); for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + this.computeSingleBeltCache(this.allEntities[i], cache, visited); + } + assert( + cache.length === this.allEntities.length, + "Belt cache mismatch: Has " + cache.length + " entries but should have " + this.allEntities.length + ); + + this.beltCache = cache; + } + + update() { + if (this.cacheNeedsUpdate) { + this.cacheNeedsUpdate = false; + this.computeBeltCache(); + } + + for (let i = 0; i < this.beltCache.length; ++i) { + const { entity, followUp } = this.beltCache[i]; + + // Divide by item spacing on belts since we use throughput and not speed + const beltSpeed = + this.root.hubGoals.getBeltBaseSpeed() * + this.root.dynamicTickrate.deltaSeconds * + globalConfig.itemSpacingOnBelts; const beltComp = entity.components.Belt; - const staticComp = entity.components.StaticMapEntity; const items = beltComp.sortedItems; + if (items.length === 0) { // Fast out for performance continue; @@ -78,19 +230,11 @@ export class BeltSystem extends GameSystemWithFilter { if (ejectorComp.isAnySlotEjecting()) { maxProgress = 1 - globalConfig.itemSpacingOnBelts; } else { - // Find follow up belt to make sure we don't clash items - const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction); - const followUpVector = enumDirectionToVector[followUpDirection]; - - const followUpTile = staticComp.origin.add(followUpVector); - const followUpEntity = this.root.map.getTileContent(followUpTile); - - if (followUpEntity) { - const followUpBeltComp = followUpEntity.components.Belt; - if (followUpBeltComp) { - const spacingOnBelt = followUpBeltComp.getDistanceToFirstItemCenter(); - maxProgress = Math_min(1, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt); - } + // Otherwise our progress depends on the follow up + if (followUp) { + const spacingOnBelt = followUp.components.Belt.getDistanceToFirstItemCenter(); + maxProgress = Math_min(2, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt); + assert(maxProgress >= 0.0, "max progress < 0 (I)"); } } @@ -100,29 +244,49 @@ export class BeltSystem extends GameSystemWithFilter { speedMultiplier = 1.41; } + // Not really nice. haven't found the reason for this yet. + if (items.length > 2 / globalConfig.itemSpacingOnBelts) { + logger.error("Fixing broken belt:", entity, items); + beltComp.sortedItems = []; + } + for (let itemIndex = items.length - 1; itemIndex >= 0; --itemIndex) { - const itemAndProgress = items[itemIndex]; + const progressAndItem = items[itemIndex]; - const newProgress = itemAndProgress[0] + speedMultiplier * beltSpeed; - if (newProgress >= 1.0) { - // Try to give this item to a new belt - const freeSlot = ejectorComp.getFirstFreeSlot(); + progressAndItem[0] = Math_min(maxProgress, progressAndItem[0] + speedMultiplier * beltSpeed); - if (freeSlot === null) { - // So, we don't have a free slot - damned! - itemAndProgress[0] = 1.0; - maxProgress = 1 - globalConfig.itemSpacingOnBelts; - } else { - // We got a free slot, remove this item and keep it on the ejector slot - if (!ejectorComp.tryEject(freeSlot, itemAndProgress[1])) { - assert(false, "Ejection failed"); + if (progressAndItem[0] >= 1.0) { + if (followUp) { + const followUpBelt = followUp.components.Belt; + if (followUpBelt.canAcceptItem()) { + followUpBelt.takeItem(progressAndItem[1], progressAndItem[0] - 1.0); + items.splice(itemIndex, 1); + } else { + // Well, we couldn't really take it to a follow up belt, keep it at + // max progress + progressAndItem[0] = 1.0; + maxProgress = 1 - globalConfig.itemSpacingOnBelts; + } + } else { + // Try to give this item to a new belt + const freeSlot = ejectorComp.getFirstFreeSlot(); + if (freeSlot === null) { + // So, we don't have a free slot - damned! + progressAndItem[0] = 1.0; + maxProgress = 1 - globalConfig.itemSpacingOnBelts; + } else { + // We got a free slot, remove this item and keep it on the ejector slot + if (!ejectorComp.tryEject(freeSlot, progressAndItem[1])) { + assert(false, "Ejection failed"); + } + items.splice(itemIndex, 1); + + // NOTICE: Do not override max progress here at all, this leads to issues } - items.splice(itemIndex, 1); - maxProgress = 1; } } else { - itemAndProgress[0] = Math_min(newProgress, maxProgress); - maxProgress = itemAndProgress[0] - globalConfig.itemSpacingOnBelts; + // We just moved this item forward, so determine the maximum progress of other items + maxProgress = Math_max(0, progressAndItem[0] - globalConfig.itemSpacingOnBelts); } } } @@ -136,15 +300,15 @@ export class BeltSystem extends GameSystemWithFilter { drawChunk(parameters, chunk) { if (parameters.zoomLevel < globalConfig.mapChunkOverviewMinZoom) { return; - 1; } const speedMultiplier = this.root.hubGoals.getBeltBaseSpeed(); - // SYNC with systems/item_processor.js:drawEntityUnderlays! + // SYNC with systems/item_acceptor.js:drawEntityUnderlays! // 126 / 42 is the exact animation speed of the png animation const animationIndex = Math.floor( - (this.root.time.now() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42 + ((this.root.time.now() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) * + globalConfig.itemSpacingOnBelts ); const contents = chunk.contents; for (let y = 0; y < globalConfig.mapChunkSize; ++y) { @@ -182,6 +346,10 @@ export class BeltSystem extends GameSystemWithFilter { return; } + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + for (let i = 0; i < items.length; ++i) { const itemAndProgress = items[i]; diff --git a/src/js/game/systems/hub.js b/src/js/game/systems/hub.js index 9a768c81..b371de6e 100644 --- a/src/js/game/systems/hub.js +++ b/src/js/game/systems/hub.js @@ -3,10 +3,14 @@ import { HubComponent } from "../components/hub"; import { DrawParameters } from "../../core/draw_parameters"; import { Entity } from "../entity"; import { formatBigNumber } from "../../core/utils"; +import { Loader } from "../../core/loader"; +import { T } from "../../translations"; export class HubSystem extends GameSystemWithFilter { constructor(root) { super(root, [HubComponent]); + + this.hubSprite = Loader.getSprite("sprites/buildings/hub.png"); } draw(parameters) { @@ -37,8 +41,15 @@ export class HubSystem extends GameSystemWithFilter { const context = parameters.context; const staticComp = entity.components.StaticMapEntity; + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + const pos = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + // Background + staticComp.drawSpriteOnFullEntityBounds(parameters, this.hubSprite, 2.2); + const definition = this.root.hubGoals.currentGoal.definition; definition.draw(pos.x - 25, pos.y - 10, parameters, 40); @@ -49,17 +60,23 @@ export class HubSystem extends GameSystemWithFilter { const textOffsetY = -6; // Deliver count - context.font = "bold 25px GameFont"; + const delivered = this.root.hubGoals.getCurrentGoalDelivered(); + + if (delivered > 9999) { + context.font = "bold 16px GameFont"; + } else if (delivered > 999) { + context.font = "bold 20px GameFont"; + } else { + context.font = "bold 25px GameFont"; + } context.fillStyle = "#64666e"; context.textAlign = "left"; - context.fillText( - "" + formatBigNumber(this.root.hubGoals.getCurrentGoalDelivered()), - pos.x + textOffsetX, - pos.y + textOffsetY - ); + context.fillText("" + formatBigNumber(delivered), pos.x + textOffsetX, pos.y + textOffsetY); // Required + context.font = "13px GameFont"; + context.fillStyle = "#a4a6b0"; context.fillText( "/ " + formatBigNumber(goals.required), @@ -71,7 +88,7 @@ export class HubSystem extends GameSystemWithFilter { context.font = "bold 11px GameFont"; context.fillStyle = "#fd0752"; context.textAlign = "center"; - context.fillText(goals.reward.toUpperCase(), pos.x, pos.y + 46); + context.fillText(T.storyRewards[goals.reward].title.toUpperCase(), pos.x, pos.y + 46); // Level context.font = "bold 11px GameFont"; diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js new file mode 100644 index 00000000..69209ca9 --- /dev/null +++ b/src/js/game/systems/item_acceptor.js @@ -0,0 +1,126 @@ +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { Entity } from "../entity"; +import { enumDirectionToVector, enumDirectionToAngle } from "../../core/vector"; +import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { Loader } from "../../core/loader"; +import { drawRotatedSprite } from "../../core/draw_utils"; +import { Math_radians } from "../../core/builtins"; + +export class ItemAcceptorSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [ItemAcceptorComponent]); + + this.underlayBeltSprites = [ + Loader.getSprite("sprites/belt/forward_0.png"), + Loader.getSprite("sprites/belt/forward_1.png"), + Loader.getSprite("sprites/belt/forward_2.png"), + Loader.getSprite("sprites/belt/forward_3.png"), + Loader.getSprite("sprites/belt/forward_4.png"), + Loader.getSprite("sprites/belt/forward_5.png"), + ]; + } + + update() { + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const aceptorComp = entity.components.ItemAcceptor; + + // Process item consumption animations to avoid items popping from the belts + for (let animIndex = 0; animIndex < aceptorComp.itemConsumptionAnimations.length; ++animIndex) { + const anim = aceptorComp.itemConsumptionAnimations[animIndex]; + anim.animProgress += + this.root.dynamicTickrate.deltaSeconds * + this.root.hubGoals.getBeltBaseSpeed() * + 2 * + globalConfig.itemSpacingOnBelts; + if (anim.animProgress > 1) { + aceptorComp.itemConsumptionAnimations.splice(animIndex, 1); + animIndex -= 1; + } + } + } + } + + draw(parameters) { + this.forEachMatchingEntityOnScreen(parameters, this.drawEntity.bind(this)); + } + + drawUnderlays(parameters) { + this.forEachMatchingEntityOnScreen(parameters, this.drawEntityUnderlays.bind(this)); + } + + /** + * @param {DrawParameters} parameters + * @param {Entity} entity + */ + drawEntity(parameters, entity) { + const staticComp = entity.components.StaticMapEntity; + const acceptorComp = entity.components.ItemAcceptor; + + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + + for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { + const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ + animIndex + ]; + + const slotData = acceptorComp.slots[slotIndex]; + const slotWorldPos = staticComp.applyRotationToVector(slotData.pos).add(staticComp.origin); + + const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)]; + const finalTile = slotWorldPos.subScalars( + fadeOutDirection.x * (animProgress / 2 - 0.5), + fadeOutDirection.y * (animProgress / 2 - 0.5) + ); + item.draw( + (finalTile.x + 0.5) * globalConfig.tileSize, + (finalTile.y + 0.5) * globalConfig.tileSize, + parameters + ); + } + } + + /** + * @param {DrawParameters} parameters + * @param {Entity} entity + */ + drawEntityUnderlays(parameters, entity) { + const staticComp = entity.components.StaticMapEntity; + const acceptorComp = entity.components.ItemAcceptor; + + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + + const underlays = acceptorComp.beltUnderlays; + for (let i = 0; i < underlays.length; ++i) { + const { pos, direction } = underlays[i]; + + const transformedPos = staticComp.localTileToWorld(pos); + const angle = enumDirectionToAngle[staticComp.localDirectionToWorld(direction)]; + + // SYNC with systems/belt.js:drawSingleEntity! + const animationIndex = Math.floor( + ((this.root.time.now() * + this.root.hubGoals.getBeltBaseSpeed() * + this.underlayBeltSprites.length * + 126) / + 42) * + globalConfig.itemSpacingOnBelts + ); + + drawRotatedSprite({ + parameters, + sprite: this.underlayBeltSprites[animationIndex % this.underlayBeltSprites.length], + x: (transformedPos.x + 0.5) * globalConfig.tileSize, + y: (transformedPos.y + 0.5) * globalConfig.tileSize, + angle: Math_radians(angle), + size: globalConfig.tileSize, + }); + } + } +} diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index ee448dd9..503356ad 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -13,8 +13,8 @@ export class ItemEjectorSystem extends GameSystemWithFilter { } update() { - const effectiveBeltSpeed = this.root.hubGoals.getBeltBaseSpeed(); - const progressGrowth = (effectiveBeltSpeed / 0.5) * globalConfig.physicsDeltaSeconds; + const effectiveBeltSpeed = this.root.hubGoals.getBeltBaseSpeed() * globalConfig.itemSpacingOnBelts; + const progressGrowth = (effectiveBeltSpeed / 0.5) * this.root.dynamicTickrate.deltaSeconds; // Try to find acceptors for every ejector for (let i = 0; i < this.allEntities.length; ++i) { @@ -72,14 +72,12 @@ export class ItemEjectorSystem extends GameSystemWithFilter { continue; } - if ( - this.tryPassOverItem( - ejectingItem, - targetEntity, + if (this.tryPassOverItem(ejectingItem, targetEntity, matchingSlot.index)) { + targetAcceptorComp.onItemAccepted( matchingSlot.index, - matchingSlot.acceptedDirection - ) - ) { + matchingSlot.acceptedDirection, + ejectingItem + ); ejectorSlot.item = null; continue; } @@ -92,9 +90,8 @@ export class ItemEjectorSystem extends GameSystemWithFilter { * @param {BaseItem} item * @param {Entity} receiver * @param {number} slotIndex - * @param {string} localDirection */ - tryPassOverItem(item, receiver, slotIndex, localDirection) { + tryPassOverItem(item, receiver, slotIndex) { // Try figuring out how what to do with the item // TODO: Kinda hacky. How to solve this properly? Don't want to go through inheritance hell. // Also its just a few cases (hope it stays like this .. :x). @@ -102,8 +99,17 @@ export class ItemEjectorSystem extends GameSystemWithFilter { const beltComp = receiver.components.Belt; if (beltComp) { // Ayy, its a belt! - if (beltComp.canAcceptNewItem(localDirection)) { - beltComp.takeNewItem(item, localDirection); + if (beltComp.canAcceptItem()) { + beltComp.takeItem(item); + return true; + } + } + + const storageComp = receiver.components.Storage; + if (storageComp) { + // It's a storage + if (storageComp.canAcceptItem(item)) { + storageComp.takeItem(item); return true; } } @@ -111,16 +117,16 @@ export class ItemEjectorSystem extends GameSystemWithFilter { const itemProcessorComp = receiver.components.ItemProcessor; if (itemProcessorComp) { // Its an item processor .. - if (itemProcessorComp.tryTakeItem(item, slotIndex, localDirection)) { + if (itemProcessorComp.tryTakeItem(item, slotIndex)) { return true; } } - const undergroundBeltCmop = receiver.components.UndergroundBelt; - if (undergroundBeltCmop) { + const undergroundBeltComp = receiver.components.UndergroundBelt; + if (undergroundBeltComp) { // Its an underground belt. yay. if ( - undergroundBeltCmop.tryAcceptExternalItem( + undergroundBeltComp.tryAcceptExternalItem( item, this.root.hubGoals.getUndergroundBeltBaseSpeed() ) @@ -144,6 +150,10 @@ export class ItemEjectorSystem extends GameSystemWithFilter { const ejectorComp = entity.components.ItemEjector; const staticComp = entity.components.StaticMapEntity; + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + for (let i = 0; i < ejectorComp.slots.length; ++i) { const slot = ejectorComp.slots[i]; const ejectedItem = slot.item; @@ -152,10 +162,10 @@ export class ItemEjectorSystem extends GameSystemWithFilter { continue; } - const realPosition = slot.pos.rotateFastMultipleOf90(staticComp.rotationDegrees); + const realPosition = slot.pos.rotateFastMultipleOf90(staticComp.rotation); const realDirection = Vector.transformDirectionFromMultipleOf90( slot.direction, - staticComp.rotationDegrees + staticComp.rotation ); const realDirectionVector = enumDirectionToVector[realDirection]; diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 63e921a8..d3f68c04 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -1,42 +1,16 @@ +import { Math_max } from "../../core/builtins"; import { globalConfig } from "../../core/config"; -import { DrawParameters } from "../../core/draw_parameters"; -import { Loader } from "../../core/loader"; +import { BaseItem } from "../base_item"; +import { enumColorMixingResults } from "../colors"; +import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; -import { ItemProcessorComponent, enumItemProcessorTypes } from "../components/item_processor"; -import { Math_max, Math_radians } from "../../core/builtins"; -import { BaseItem } from "../base_item"; -import { ShapeItem } from "../items/shape_item"; -import { enumDirectionToVector, enumDirection, enumDirectionToAngle } from "../../core/vector"; import { ColorItem } from "../items/color_item"; -import { enumColorMixingResults } from "../colors"; -import { drawRotatedSprite } from "../../core/draw_utils"; +import { ShapeItem } from "../items/shape_item"; export class ItemProcessorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemProcessorComponent]); - - this.sprites = {}; - for (const key in enumItemProcessorTypes) { - this.sprites[key] = Loader.getSprite("sprites/buildings/" + key + ".png"); - } - - this.underlayBeltSprites = [ - Loader.getSprite("sprites/belt/forward_0.png"), - Loader.getSprite("sprites/belt/forward_1.png"), - Loader.getSprite("sprites/belt/forward_2.png"), - Loader.getSprite("sprites/belt/forward_3.png"), - Loader.getSprite("sprites/belt/forward_4.png"), - Loader.getSprite("sprites/belt/forward_5.png"), - ]; - } - - draw(parameters) { - this.forEachMatchingEntityOnScreen(parameters, this.drawEntity.bind(this)); - } - - drawUnderlays(parameters) { - this.forEachMatchingEntityOnScreen(parameters, this.drawEntityUnderlays.bind(this)); } update() { @@ -49,20 +23,9 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // First of all, process the current recipe processorComp.secondsUntilEject = Math_max( 0, - processorComp.secondsUntilEject - globalConfig.physicsDeltaSeconds + processorComp.secondsUntilEject - this.root.dynamicTickrate.deltaSeconds ); - // Also, process item consumption animations to avoid items popping from the belts - for (let animIndex = 0; animIndex < processorComp.itemConsumptionAnimations.length; ++animIndex) { - const anim = processorComp.itemConsumptionAnimations[animIndex]; - anim.animProgress += - globalConfig.physicsDeltaSeconds * this.root.hubGoals.getBeltBaseSpeed() * 2; - if (anim.animProgress > 1) { - processorComp.itemConsumptionAnimations.splice(animIndex, 1); - animIndex -= 1; - } - } - // Check if we have any finished items we can eject if ( processorComp.secondsUntilEject === 0 && // it was processed in time @@ -103,7 +66,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // Check if we have an empty queue and can start a new charge if (processorComp.itemsToEject.length === 0) { - if (processorComp.inputSlots.length === processorComp.inputsPerCharge) { + if (processorComp.inputSlots.length >= processorComp.inputsPerCharge) { this.startNewCharge(entity); } } @@ -121,22 +84,34 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const items = processorComp.inputSlots; processorComp.inputSlots = []; + /** @type {Object.} */ + const itemsBySlot = {}; + for (let i = 0; i < items.length; ++i) { + itemsBySlot[items[i].sourceSlot] = items[i]; + } + const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type); processorComp.secondsUntilEject = 1 / baseSpeed; /** @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>} */ const outItems = []; + // Whether to track the production towards the analytics + let trackProduction = true; + // DO SOME MAGIC switch (processorComp.type) { // SPLITTER case enumItemProcessorTypes.splitter: { - let nextSlot = processorComp.nextOutputSlot++ % 2; + trackProduction = false; + const availableSlots = entity.components.ItemEjector.slots.length; + + let nextSlot = processorComp.nextOutputSlot++ % availableSlots; for (let i = 0; i < items.length; ++i) { outItems.push({ item: items[i].item, - preferredSlot: (nextSlot + i) % 2, + preferredSlot: (nextSlot + i) % availableSlots, }); } break; @@ -148,22 +123,37 @@ export class ItemProcessorSystem extends GameSystemWithFilter { assert(inputItem instanceof ShapeItem, "Input for cut is not a shape"); const inputDefinition = inputItem.definition; - const [cutDefinition1, cutDefinition2] = this.root.shapeDefinitionMgr.shapeActionCutHalf( - inputDefinition - ); + const cutDefinitions = this.root.shapeDefinitionMgr.shapeActionCutHalf(inputDefinition); - if (!cutDefinition1.isEntirelyEmpty()) { - outItems.push({ - item: new ShapeItem(cutDefinition1), - requiredSlot: 0, - }); + for (let i = 0; i < cutDefinitions.length; ++i) { + const definition = cutDefinitions[i]; + if (!definition.isEntirelyEmpty()) { + outItems.push({ + item: new ShapeItem(definition), + requiredSlot: i, + }); + } } - if (!cutDefinition2.isEntirelyEmpty()) { - outItems.push({ - item: new ShapeItem(cutDefinition2), - requiredSlot: 1, - }); + break; + } + + // CUTTER (Quad) + case enumItemProcessorTypes.cutterQuad: { + const inputItem = /** @type {ShapeItem} */ (items[0].item); + assert(inputItem instanceof ShapeItem, "Input for cut is not a shape"); + const inputDefinition = inputItem.definition; + + const cutDefinitions = this.root.shapeDefinitionMgr.shapeActionCutQuad(inputDefinition); + + for (let i = 0; i < cutDefinitions.length; ++i) { + const definition = cutDefinitions[i]; + if (!definition.isEntirelyEmpty()) { + outItems.push({ + item: new ShapeItem(definition), + requiredSlot: i, + }); + } } break; @@ -172,7 +162,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // ROTATER case enumItemProcessorTypes.rotater: { const inputItem = /** @type {ShapeItem} */ (items[0].item); - assert(inputItem instanceof ShapeItem, "Input for cut is not a shape"); + assert(inputItem instanceof ShapeItem, "Input for rotation is not a shape"); const inputDefinition = inputItem.definition; const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCW(inputDefinition); @@ -182,14 +172,25 @@ export class ItemProcessorSystem extends GameSystemWithFilter { break; } + // ROTATER ( CCW) + case enumItemProcessorTypes.rotaterCCW: { + const inputItem = /** @type {ShapeItem} */ (items[0].item); + assert(inputItem instanceof ShapeItem, "Input for rotation is not a shape"); + const inputDefinition = inputItem.definition; + + const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCCW(inputDefinition); + outItems.push({ + item: new ShapeItem(rotatedDefinition), + }); + break; + } + // STACKER case enumItemProcessorTypes.stacker: { - const item1 = items[0]; - const item2 = items[1]; + const lowerItem = /** @type {ShapeItem} */ (itemsBySlot[0].item); + const upperItem = /** @type {ShapeItem} */ (itemsBySlot[1].item); - const lowerItem = /** @type {ShapeItem} */ (item1.sourceSlot === 0 ? item1.item : item2.item); - const upperItem = /** @type {ShapeItem} */ (item1.sourceSlot === 1 ? item1.item : item2.item); assert(lowerItem instanceof ShapeItem, "Input for lower stack is not a shape"); assert(upperItem instanceof ShapeItem, "Input for upper stack is not a shape"); @@ -238,11 +239,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // PAINTER case enumItemProcessorTypes.painter: { - const item1 = items[0]; - const item2 = items[1]; - - const shapeItem = /** @type {ShapeItem} */ (item1.sourceSlot === 0 ? item1.item : item2.item); - const colorItem = /** @type {ColorItem} */ (item1.sourceSlot === 1 ? item1.item : item2.item); + const shapeItem = /** @type {ShapeItem} */ (itemsBySlot[0].item); + const colorItem = /** @type {ColorItem} */ (itemsBySlot[1].item); const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith( shapeItem.definition, @@ -256,15 +254,77 @@ export class ItemProcessorSystem extends GameSystemWithFilter { break; } + // PAINTER (DOUBLE) + + case enumItemProcessorTypes.painterDouble: { + const shapeItem1 = /** @type {ShapeItem} */ (itemsBySlot[0].item); + const shapeItem2 = /** @type {ShapeItem} */ (itemsBySlot[1].item); + const colorItem = /** @type {ColorItem} */ (itemsBySlot[2].item); + + assert(shapeItem1 instanceof ShapeItem, "Input for painter is not a shape"); + assert(shapeItem2 instanceof ShapeItem, "Input for painter is not a shape"); + assert(colorItem instanceof ColorItem, "Input for painter is not a color"); + + const colorizedDefinition1 = this.root.shapeDefinitionMgr.shapeActionPaintWith( + shapeItem1.definition, + colorItem.color + ); + + const colorizedDefinition2 = this.root.shapeDefinitionMgr.shapeActionPaintWith( + shapeItem2.definition, + colorItem.color + ); + outItems.push({ + item: new ShapeItem(colorizedDefinition1), + }); + + outItems.push({ + item: new ShapeItem(colorizedDefinition2), + }); + + break; + } + + // PAINTER (QUAD) + + case enumItemProcessorTypes.painterQuad: { + const shapeItem = /** @type {ShapeItem} */ (itemsBySlot[0].item); + const colorItem1 = /** @type {ColorItem} */ (itemsBySlot[1].item); + const colorItem2 = /** @type {ColorItem} */ (itemsBySlot[2].item); + const colorItem3 = /** @type {ColorItem} */ (itemsBySlot[3].item); + const colorItem4 = /** @type {ColorItem} */ (itemsBySlot[4].item); + + assert(shapeItem instanceof ShapeItem, "Input for painter is not a shape"); + assert(colorItem1 instanceof ColorItem, "Input for painter is not a color"); + assert(colorItem2 instanceof ColorItem, "Input for painter is not a color"); + assert(colorItem3 instanceof ColorItem, "Input for painter is not a color"); + assert(colorItem4 instanceof ColorItem, "Input for painter is not a color"); + + const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith4Colors( + shapeItem.definition, + [colorItem2.color, colorItem3.color, colorItem4.color, colorItem1.color] + ); + + outItems.push({ + item: new ShapeItem(colorizedDefinition), + }); + + break; + } + // HUB case enumItemProcessorTypes.hub: { - const shapeItem = /** @type {ShapeItem} */ (items[0].item); + trackProduction = false; const hubComponent = entity.components.Hub; assert(hubComponent, "Hub item processor has no hub component"); - hubComponent.queueShapeDefinition(shapeItem.definition); + for (let i = 0; i < items.length; ++i) { + const shapeItem = /** @type {ShapeItem} */ (items[i].item); + hubComponent.queueShapeDefinition(shapeItem.definition); + } + break; } @@ -272,70 +332,13 @@ export class ItemProcessorSystem extends GameSystemWithFilter { assertAlways(false, "Unkown item processor type: " + processorComp.type); } + // Track produced items + if (trackProduction) { + for (let i = 0; i < outItems.length; ++i) { + this.root.signals.itemProduced.dispatch(outItems[i].item); + } + } + processorComp.itemsToEject = outItems; } - - /** - * @param {DrawParameters} parameters - * @param {Entity} entity - */ - drawEntity(parameters, entity) { - const staticComp = entity.components.StaticMapEntity; - const processorComp = entity.components.ItemProcessor; - const acceptorComp = entity.components.ItemAcceptor; - - for (let animIndex = 0; animIndex < processorComp.itemConsumptionAnimations.length; ++animIndex) { - const { item, slotIndex, animProgress, direction } = processorComp.itemConsumptionAnimations[ - animIndex - ]; - - const slotData = acceptorComp.slots[slotIndex]; - const slotWorldPos = staticComp.applyRotationToVector(slotData.pos).add(staticComp.origin); - - const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)]; - const finalTile = slotWorldPos.subScalars( - fadeOutDirection.x * (animProgress / 2 - 0.5), - fadeOutDirection.y * (animProgress / 2 - 0.5) - ); - item.draw( - (finalTile.x + 0.5) * globalConfig.tileSize, - (finalTile.y + 0.5) * globalConfig.tileSize, - parameters - ); - } - } - /** - * @param {DrawParameters} parameters - * @param {Entity} entity - */ - drawEntityUnderlays(parameters, entity) { - const staticComp = entity.components.StaticMapEntity; - const processorComp = entity.components.ItemProcessor; - - const underlays = processorComp.beltUnderlays; - for (let i = 0; i < underlays.length; ++i) { - const { pos, direction } = underlays[i]; - - const transformedPos = staticComp.localTileToWorld(pos); - const angle = enumDirectionToAngle[staticComp.localDirectionToWorld(direction)]; - - // SYNC with systems/belt.js:drawSingleEntity! - const animationIndex = Math.floor( - (this.root.time.now() * - this.root.hubGoals.getBeltBaseSpeed() * - this.underlayBeltSprites.length * - 126) / - 42 - ); - - drawRotatedSprite({ - parameters, - sprite: this.underlayBeltSprites[animationIndex % this.underlayBeltSprites.length], - x: (transformedPos.x + 0.5) * globalConfig.tileSize, - y: (transformedPos.y + 0.5) * globalConfig.tileSize, - angle: Math_radians(angle), - size: globalConfig.tileSize, - }); - } - } } diff --git a/src/js/game/systems/map_resources.js b/src/js/game/systems/map_resources.js index 7edb827c..6504d457 100644 --- a/src/js/game/systems/map_resources.js +++ b/src/js/game/systems/map_resources.js @@ -13,23 +13,34 @@ export class MapResourcesSystem extends GameSystem { const renderItems = parameters.zoomLevel >= globalConfig.mapChunkOverviewMinZoom; parameters.context.globalAlpha = 0.5; + const layer = chunk.lowerLayer; for (let x = 0; x < globalConfig.mapChunkSize; ++x) { const row = layer[x]; + const worldX = (chunk.tileX + x) * globalConfig.tileSize; for (let y = 0; y < globalConfig.mapChunkSize; ++y) { const lowerItem = row[y]; if (lowerItem) { + const worldY = (chunk.tileY + y) * globalConfig.tileSize; + + if ( + !parameters.visibleRect.containsRect4Params( + worldX, + worldY, + globalConfig.tileSize, + globalConfig.tileSize + ) + ) { + // Clipped + continue; + } + parameters.context.fillStyle = lowerItem.getBackgroundColorAsResource(); - parameters.context.fillRect( - (chunk.tileX + x) * globalConfig.tileSize, - (chunk.tileY + y) * globalConfig.tileSize, - globalConfig.tileSize, - globalConfig.tileSize - ); + parameters.context.fillRect(worldX, worldY, globalConfig.tileSize, globalConfig.tileSize); if (renderItems) { lowerItem.draw( - (chunk.tileX + x + 0.5) * globalConfig.tileSize, - (chunk.tileY + y + 0.5) * globalConfig.tileSize, + worldX + globalConfig.halfTileSize, + worldY + globalConfig.halfTileSize, parameters ); } diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index 4c9d5cca..4ecf1e2d 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -1,6 +1,9 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; +import { enumDirectionToVector } from "../../core/vector"; +import { BaseItem } from "../base_item"; import { MinerComponent } from "../components/miner"; +import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { MapChunkView } from "../map_chunk_view"; @@ -16,34 +19,77 @@ export class MinerSystem extends GameSystemWithFilter { const minerComp = entity.components.Miner; const staticComp = entity.components.StaticMapEntity; - const ejectComp = entity.components.ItemEjector; - if (this.root.time.isIngameTimerExpired(minerComp.lastMiningTime, 1 / miningSpeed)) { - if (!ejectComp.canEjectOnSlot(0)) { - // We can't eject further + // First, try to get rid of chained items + if (minerComp.itemChainBuffer.length > 0) { + if (this.tryPerformMinerEject(entity, minerComp.itemChainBuffer[0])) { + minerComp.itemChainBuffer.shift(); continue; } + } - // Actually mine - minerComp.lastMiningTime = this.root.time.now(); - + if (this.root.time.isIngameTimerExpired(minerComp.lastMiningTime, 1 / miningSpeed)) { const lowerLayerItem = this.root.map.getLowerLayerContentXY( staticComp.origin.x, staticComp.origin.y ); + + // TODO: Should not be required actually if (!lowerLayerItem) { // Nothing below; continue; } - // Try actually ejecting - if (!ejectComp.tryEject(0, lowerLayerItem)) { - assert(false, "Failed to eject"); + if (this.tryPerformMinerEject(entity, lowerLayerItem)) { + // Analytics hook + this.root.signals.itemProduced.dispatch(lowerLayerItem); + + // Actually mine + minerComp.lastMiningTime = this.root.time.now(); } } } } + /** + * + * @param {Entity} entity + * @param {BaseItem} item + */ + tryPerformMinerEject(entity, item) { + const minerComp = entity.components.Miner; + const ejectComp = entity.components.ItemEjector; + const staticComp = entity.components.StaticMapEntity; + + // Check if we are a chained miner + if (minerComp.chainable) { + const ejectingSlot = ejectComp.slots[0]; + const ejectingPos = staticComp.localTileToWorld(ejectingSlot.pos); + const ejectingDirection = staticComp.localDirectionToWorld(ejectingSlot.direction); + + const targetTile = ejectingPos.add(enumDirectionToVector[ejectingDirection]); + const targetContents = this.root.map.getTileContent(targetTile); + + // Check if we are connected to another miner and thus do not eject directly + if (targetContents) { + const targetMinerComp = targetContents.components.Miner; + if (targetMinerComp) { + if (targetMinerComp.tryAcceptChainedItem(item)) { + return true; + } else { + return false; + } + } + } + } + + // Seems we are a regular miner or at the end of a row, try actually ejecting + if (ejectComp.tryEject(0, item)) { + return true; + } + return false; + } + /** * * @param {DrawParameters} parameters @@ -57,6 +103,10 @@ export class MinerSystem extends GameSystemWithFilter { if (entity && entity.components.Miner) { const staticComp = entity.components.StaticMapEntity; + if (!staticComp.shouldBeDrawn(parameters)) { + continue; + } + const lowerLayerItem = this.root.map.getLowerLayerContentXY( staticComp.origin.x, staticComp.origin.y diff --git a/src/js/game/systems/static_map_entity.js b/src/js/game/systems/static_map_entity.js index 5c57b492..616a0aa7 100644 --- a/src/js/game/systems/static_map_entity.js +++ b/src/js/game/systems/static_map_entity.js @@ -28,12 +28,19 @@ export class StaticMapEntitySystem extends GameSystem { const drawOutlinesOnly = parameters.zoomLevel < globalConfig.mapChunkOverviewMinZoom; + const drawnUids = new Set(); + const contents = chunk.contents; for (let y = 0; y < globalConfig.mapChunkSize; ++y) { for (let x = 0; x < globalConfig.mapChunkSize; ++x) { const entity = contents[x][y]; if (entity) { + if (drawnUids.has(entity.uid)) { + continue; + } + drawnUids.add(entity.uid); + const staticComp = entity.components.StaticMapEntity; if (drawOutlinesOnly) { const rect = staticComp.getTileSpaceBounds(); @@ -53,16 +60,8 @@ export class StaticMapEntitySystem extends GameSystem { } else { const spriteKey = staticComp.spriteKey; if (spriteKey) { - // Check if origin is contained to avoid drawing entities multiple times - if ( - staticComp.origin.x >= chunk.tileX && - staticComp.origin.x < chunk.tileX + globalConfig.mapChunkSize && - staticComp.origin.y >= chunk.tileY && - staticComp.origin.y < chunk.tileY + globalConfig.mapChunkSize - ) { - const sprite = Loader.getSprite(spriteKey); - staticComp.drawSpriteOnFullEntityBounds(parameters, sprite, 2, false); - } + const sprite = Loader.getSprite(spriteKey); + staticComp.drawSpriteOnFullEntityBounds(parameters, sprite, 2, false); } } } diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js new file mode 100644 index 00000000..0917e56f --- /dev/null +++ b/src/js/game/systems/storage.js @@ -0,0 +1,75 @@ +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { StorageComponent } from "../components/storage"; +import { Entity } from "../entity"; +import { DrawParameters } from "../../core/draw_parameters"; +import { formatBigNumber, lerp } from "../../core/utils"; +import { Loader } from "../../core/loader"; + +export class StorageSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [StorageComponent]); + + this.storageOverlaySprite = Loader.getSprite("sprites/misc/storage_overlay.png"); + } + + update() { + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const storageComp = entity.components.Storage; + + // Eject from storage + if (storageComp.storedItem && storageComp.storedCount > 0) { + const ejectorComp = entity.components.ItemEjector; + const nextSlot = ejectorComp.getFirstFreeSlot(); + if (nextSlot !== null) { + if (ejectorComp.tryEject(nextSlot, storageComp.storedItem)) { + storageComp.storedCount--; + + if (storageComp.storedCount === 0) { + storageComp.storedItem = null; + } + } + } + } + + let targetAlpha = storageComp.storedCount > 0 ? 1 : 0; + storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05); + } + } + + draw(parameters) { + this.forEachMatchingEntityOnScreen(parameters, this.drawEntity.bind(this)); + } + + /** + * @param {DrawParameters} parameters + * @param {Entity} entity + */ + drawEntity(parameters, entity) { + const context = parameters.context; + const staticComp = entity.components.StaticMapEntity; + + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + + const storageComp = entity.components.Storage; + + const storedItem = storageComp.storedItem; + if (storedItem !== null) { + context.globalAlpha = storageComp.overlayOpacity; + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + storedItem.draw(center.x, center.y, parameters, 30); + + this.storageOverlaySprite.drawCached(parameters, center.x - 15, center.y + 15, 30, 15); + + context.font = "bold 10px GameFont"; + context.textAlign = "center"; + context.fillStyle = "#64666e"; + context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5); + + context.textAlign = "left"; + context.globalAlpha = 1; + } + } +} diff --git a/src/js/game/systems/underground_belt.js b/src/js/game/systems/underground_belt.js index c8baec5b..538fab36 100644 --- a/src/js/game/systems/underground_belt.js +++ b/src/js/game/systems/underground_belt.js @@ -31,7 +31,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { // Decrease remaining time of all items in belt for (let k = 0; k < undergroundComp.pendingItems.length; ++k) { const item = undergroundComp.pendingItems[k]; - item[1] = Math_max(0, item[1] - globalConfig.physicsDeltaSeconds); + item[1] = Math_max(0, item[1] - this.root.dynamicTickrate.deltaSeconds); } if (undergroundComp.mode === enumUndergroundBeltMode.sender) { @@ -67,7 +67,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { for ( let searchOffset = 0; - searchOffset < globalConfig.undergroundBeltMaxTiles; + searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; ++searchOffset ) { currentTile = currentTile.add(searchVector); @@ -75,9 +75,12 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { const contents = this.root.map.getTileContent(currentTile); if (contents) { const receiverUndergroundComp = contents.components.UndergroundBelt; - if (receiverUndergroundComp) { + if ( + receiverUndergroundComp && + receiverUndergroundComp.tier === undergroundComp.tier + ) { const receiverStaticComp = contents.components.StaticMapEntity; - if (receiverStaticComp.rotationDegrees === targetRotation) { + if (receiverStaticComp.rotation === targetRotation) { if (receiverUndergroundComp.mode === enumUndergroundBeltMode.receiver) { // Try to pass over the item to the receiver if ( diff --git a/src/js/game/theme.js b/src/js/game/theme.js new file mode 100644 index 00000000..251f4433 --- /dev/null +++ b/src/js/game/theme.js @@ -0,0 +1,10 @@ +export const THEMES = { + dark: require("./themes/dark.json"), + light: require("./themes/light.json"), +}; + +export let THEME = THEMES.light; + +export function applyGameTheme(id) { + THEME = THEMES[id]; +} diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json new file mode 100644 index 00000000..caf28bfb --- /dev/null +++ b/src/js/game/themes/dark.json @@ -0,0 +1,24 @@ +{ + "uiStyle": "dark", + "map": { + "background": "#2e2f37", + "grid": "rgba(255, 255, 255, 0.02)", + "gridLineWidth": 0.5, + + "resources": { + "shape": "#3d3f4a", + "red": "#4a3d3f", + "green": "#3e4a3d", + "blue": "#35384a" + }, + "chunkOverview": { + "empty": "#444856", + "filled": "#646b7d" + } + }, + + "items": { + "outline": "#111418", + "outlineWidth": 0.75 + } +} diff --git a/src/js/game/themes/light.json b/src/js/game/themes/light.json new file mode 100644 index 00000000..4837574c --- /dev/null +++ b/src/js/game/themes/light.json @@ -0,0 +1,25 @@ +{ + "uiStyle": "light", + "map": { + "background": "#fff", + "grid": "#fafafa", + "gridLineWidth": 1, + + "resources": { + "shape": "#eaebec", + "red": "#ffbfc1", + "green": "#cbffc4", + "blue": "#bfdaff" + }, + + "chunkOverview": { + "empty": "#a6afbb", + "filled": "#c5ccd6" + } + }, + + "items": { + "outline": "#55575a", + "outlineWidth": 0.75 + } +} diff --git a/src/js/game/time/game_time.js b/src/js/game/time/game_time.js index 3fe48453..4bcc6869 100644 --- a/src/js/game/time/game_time.js +++ b/src/js/game/time/game_time.js @@ -6,7 +6,7 @@ import { types, BasicSerializableObject } from "../../savegame/serialization"; import { RegularGameSpeed } from "./regular_game_speed"; import { BaseGameSpeed } from "./base_game_speed"; import { PausedGameSpeed } from "./paused_game_speed"; -import { performanceNow } from "../../core/builtins"; +import { performanceNow, Math_max } from "../../core/builtins"; import { FastForwardGameSpeed } from "./fast_forward_game_speed"; import { gGameSpeedRegistry } from "../../core/global_registries"; import { globalConfig } from "../../core/config"; @@ -102,7 +102,7 @@ export class GameTime extends BasicSerializableObject { * Internal method to generate new logic time budget * @param {number} deltaMs */ - înternalAddDeltaToBudget(deltaMs) { + internalAddDeltaToBudget(deltaMs) { // Only update if game is supposed to update if (this.root.hud.shouldPauseGame()) { this.logicTimeBudget = 0; @@ -112,9 +112,17 @@ export class GameTime extends BasicSerializableObject { } // Check for too big pile of updates -> reduce it to 1 - const maxLogicSteps = this.speed.getMaxLogicStepsInQueue(); - if (this.logicTimeBudget > globalConfig.physicsDeltaMs * maxLogicSteps) { - this.logicTimeBudget = globalConfig.physicsDeltaMs * maxLogicSteps; + let maxLogicSteps = Math_max( + 3, + (this.speed.getMaxLogicStepsInQueue() * this.root.dynamicTickrate.currentTickRate) / 60 + ); + if (G_IS_DEV && globalConfig.debug.framePausesBetweenTicks) { + maxLogicSteps *= 1 + globalConfig.debug.framePausesBetweenTicks; + } + + if (this.logicTimeBudget > this.root.dynamicTickrate.deltaMs * maxLogicSteps) { + // logger.warn("Skipping logic time steps since more than", maxLogicSteps, "are in queue"); + this.logicTimeBudget = this.root.dynamicTickrate.deltaMs * maxLogicSteps; } } @@ -124,13 +132,18 @@ export class GameTime extends BasicSerializableObject { * @param {function():boolean} updateMethod */ performTicks(deltaMs, updateMethod) { - this.înternalAddDeltaToBudget(deltaMs); + this.internalAddDeltaToBudget(deltaMs); const speedAtStart = this.root.time.getSpeed(); + let effectiveDelta = this.root.dynamicTickrate.deltaMs; + if (G_IS_DEV && globalConfig.debug.framePausesBetweenTicks) { + effectiveDelta += globalConfig.debug.framePausesBetweenTicks * this.root.dynamicTickrate.deltaMs; + } + // Update physics & logic - while (this.logicTimeBudget >= globalConfig.physicsDeltaMs) { - this.logicTimeBudget -= globalConfig.physicsDeltaMs; + while (this.logicTimeBudget >= effectiveDelta) { + this.logicTimeBudget -= effectiveDelta; if (!updateMethod()) { // Gameover happened or so, do not update anymore @@ -138,7 +151,7 @@ export class GameTime extends BasicSerializableObject { } // Step game time - this.timeSeconds = quantizeFloat(this.timeSeconds + globalConfig.physicsDeltaSeconds); + this.timeSeconds = quantizeFloat(this.timeSeconds + this.root.dynamicTickrate.deltaSeconds); // Game time speed changed, need to abort since our logic steps are no longer valid if (speedAtStart.getId() !== this.speed.getId()) { diff --git a/src/js/game/tutorial_goals.js b/src/js/game/tutorial_goals.js index 8a57484b..b2f5b0ef 100644 --- a/src/js/game/tutorial_goals.js +++ b/src/js/game/tutorial_goals.js @@ -1,153 +1,160 @@ import { ShapeDefinition } from "./shape_definition"; +import { finalGameShape } from "./upgrades"; /** + * Don't forget to also update tutorial_goals_mappings.js as well as the translations! * @enum {string} */ export const enumHubGoalRewards = { - reward_cutter_and_trash: "Cutting Shapes", - reward_rotater: "Rotating", - reward_painter: "Painting", - reward_mixer: "Color Mixing", - reward_stacker: "Combiner", - reward_splitter: "Splitter/Merger", - reward_tunnel: "Tunnel", + reward_cutter_and_trash: "reward_cutter_and_trash", + reward_rotater: "reward_rotater", + reward_painter: "reward_painter", + reward_mixer: "reward_mixer", + reward_stacker: "reward_stacker", + reward_splitter: "reward_splitter", + reward_tunnel: "reward_tunnel", - no_reward: "Next level", + reward_rotater_ccw: "reward_rotater_ccw", + reward_miner_chainable: "reward_miner_chainable", + reward_underground_belt_tier_2: "reward_underground_belt_tier_2", + reward_splitter_compact: "reward_splitter_compact", + reward_cutter_quad: "reward_cutter_quad", + reward_painter_double: "reward_painter_double", + reward_painter_quad: "reward_painter_quad", + reward_storage: "reward_storage", + + reward_freeplay: "reward_freeplay", + + no_reward: "no_reward", + no_reward_freeplay: "no_reward_freeplay", }; export const tutorialGoals = [ + // 1 // Circle { - shape: "CuCuCuCu", - required: 40, + shape: "CuCuCuCu", // belts t1 + required: 35, reward: enumHubGoalRewards.reward_cutter_and_trash, }, + // 2 // Cutter { - shape: "CuCu----", - required: 150, + shape: "----CuCu", // + required: 50, reward: enumHubGoalRewards.no_reward, }, + // 3 + // Rectangle { - shape: "----CuCu", - required: 200, + shape: "RuRuRuRu", // miners t1 + required: 100, reward: enumHubGoalRewards.reward_splitter, }, - // Rectangle + // 4 { - shape: "RuRuRuRu", - required: 80, - reward: enumHubGoalRewards.no_reward, - }, - - { - shape: "RuRu----", - required: 250, + shape: "RuRu----", // processors t2 + required: 150, reward: enumHubGoalRewards.reward_rotater, }, + // 5 // Rotater { - shape: "--CuCu--", + shape: "Cu----Cu", // belts t2 required: 300, - reward: enumHubGoalRewards.no_reward, - }, - - { - shape: "Ru----Ru", - required: 400, reward: enumHubGoalRewards.reward_tunnel, }, + // 6 { - shape: "Cu------", - required: 800, - reward: enumHubGoalRewards.no_reward, - }, - - { - shape: "------Ru", - required: 1000, + shape: "Cu------", // miners t2 + required: 700, reward: enumHubGoalRewards.reward_painter, }, + // 7 // Painter { - shape: "CrCrCrCr", - required: 1500, - reward: enumHubGoalRewards.no_reward, + shape: "CrCrCrCr", // unused + required: 1300, + reward: enumHubGoalRewards.reward_rotater_ccw, }, + // 8 { - shape: "RbRb----", + shape: "RbRb----", // painter t2 required: 2500, reward: enumHubGoalRewards.reward_mixer, }, + // 9 // Mixing (purple) { - shape: "CpCpCpCp", + shape: "CpCpCpCp", // belts t3 required: 4000, - reward: enumHubGoalRewards.no_reward, + reward: enumHubGoalRewards.reward_splitter_compact, }, + // 10 // Star shape + cyan { - shape: "ScScScSc", - required: 500, + shape: "ScScScSc", // miners t3 + required: 5000, reward: enumHubGoalRewards.reward_stacker, }, + // 11 // Stacker { - shape: "CcCcRgRg", - required: 3000, - reward: enumHubGoalRewards.no_reward, - }, - - { - shape: "RgRgRgRg:CcCcCcCc", - required: 4000, - reward: enumHubGoalRewards.no_reward, - }, - - { - shape: "RgCwRgCw:CpCpCpCp", + shape: "CgScScCg", // processors t3 required: 6000, - reward: enumHubGoalRewards.no_reward, + reward: enumHubGoalRewards.reward_miner_chainable, }, + // 12 { - shape: "CwSwCwSw:CpCrCpCr", - required: 6000, - reward: enumHubGoalRewards.no_reward, + shape: "RpRpRpRp:CwCwCwCw", // painting t3 + required: 7000, + reward: enumHubGoalRewards.reward_underground_belt_tier_2, }, + // 13 { - shape: "WyWyWyWy", - required: 2000, - reward: enumHubGoalRewards.no_reward, + shape: "SrSrSrSr:CyCyCyCy", // unused + required: 7850, + reward: enumHubGoalRewards.reward_storage, }, + // 14 { - shape: "WyWgWyWg:CbCpCbCp", - required: 4000, - reward: enumHubGoalRewards.no_reward, + shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants) + required: 8000, + reward: enumHubGoalRewards.reward_cutter_quad, }, + // 15 { - shape: "WyRgWyRg:CbCpCbCp:CpCgCpCg", + shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants) required: 9000, - reward: enumHubGoalRewards.no_reward, + reward: enumHubGoalRewards.reward_painter_double, }, + // 16 { - shape: "CwRrCbRy:ScSrSpSb:CwCwCwCw:RgRgRgRg", - required: 15000, - reward: enumHubGoalRewards.no_reward, + shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", // processors t4 (two varinats) + required: 10000, + reward: enumHubGoalRewards.reward_painter_quad, + }, + + // 17 + { + shape: finalGameShape, + required: 50000, + reward: enumHubGoalRewards.reward_freeplay, }, ]; diff --git a/src/js/game/tutorial_goals_mappings.js b/src/js/game/tutorial_goals_mappings.js new file mode 100644 index 00000000..905a623a --- /dev/null +++ b/src/js/game/tutorial_goals_mappings.js @@ -0,0 +1,51 @@ +import { MetaBuilding, defaultBuildingVariant } from "./meta_building"; +import { MetaCutterBuilding, enumCutterVariants } from "./buildings/cutter"; +import { MetaRotaterBuilding, enumRotaterVariants } from "./buildings/rotater"; +import { MetaPainterBuilding, enumPainterVariants } from "./buildings/painter"; +import { MetaMixerBuilding } from "./buildings/mixer"; +import { MetaStackerBuilding } from "./buildings/stacker"; +import { MetaSplitterBuilding, enumSplitterVariants } from "./buildings/splitter"; +import { MetaUndergroundBeltBuilding, enumUndergroundBeltVariants } from "./buildings/underground_belt"; +import { MetaMinerBuilding, enumMinerVariants } from "./buildings/miner"; +import { MetaTrashBuilding, enumTrashVariants } from "./buildings/trash"; + +/** @typedef {Array<[typeof MetaBuilding, string]>} TutorialGoalReward */ + +import { enumHubGoalRewards } from "./tutorial_goals"; + +/** + * Helper method for proper types + * @returns {TutorialGoalReward} + */ +const typed = x => x; + +/** + * Stores which reward unlocks what + * @enum {TutorialGoalReward?} + */ +export const enumHubGoalRewardsToContentUnlocked = { + [enumHubGoalRewards.reward_cutter_and_trash]: typed([[MetaCutterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_rotater]: typed([[MetaRotaterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_painter]: typed([[MetaPainterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_mixer]: typed([[MetaMixerBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_stacker]: typed([[MetaStackerBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_splitter]: typed([[MetaSplitterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_tunnel]: typed([[MetaUndergroundBeltBuilding, defaultBuildingVariant]]), + + [enumHubGoalRewards.reward_rotater_ccw]: typed([[MetaRotaterBuilding, enumRotaterVariants.ccw]]), + [enumHubGoalRewards.reward_miner_chainable]: typed([[MetaMinerBuilding, enumMinerVariants.chainable]]), + [enumHubGoalRewards.reward_underground_belt_tier_2]: typed([ + [MetaUndergroundBeltBuilding, enumUndergroundBeltVariants.tier2], + ]), + [enumHubGoalRewards.reward_splitter_compact]: typed([ + [MetaSplitterBuilding, enumSplitterVariants.compact], + ]), + [enumHubGoalRewards.reward_cutter_quad]: typed([[MetaCutterBuilding, enumCutterVariants.quad]]), + [enumHubGoalRewards.reward_painter_double]: typed([[MetaPainterBuilding, enumPainterVariants.double]]), + [enumHubGoalRewards.reward_painter_quad]: typed([[MetaPainterBuilding, enumPainterVariants.quad]]), + [enumHubGoalRewards.reward_storage]: typed([[MetaTrashBuilding, enumTrashVariants.storage]]), + + [enumHubGoalRewards.reward_freeplay]: null, + [enumHubGoalRewards.no_reward]: null, + [enumHubGoalRewards.no_reward_freeplay]: null, +}; diff --git a/src/js/game/upgrades.js b/src/js/game/upgrades.js index 3a261904..8864c5e2 100644 --- a/src/js/game/upgrades.js +++ b/src/js/game/upgrades.js @@ -1,284 +1,132 @@ import { findNiceIntegerValue } from "../core/utils"; import { ShapeDefinition } from "./shape_definition"; -export const TIER_LABELS = [ - "I", - "II", - "III", - "IV", - "V", - "VI", - "VII", - "VIII", - "IX", - "X", - "XI", - "XII", - "XIII", - "XIV", - "XV", - "XVI", - "XVII", - "XVIII", - "XIX", - "XX", -]; +export const finalGameShape = "RuCw--Cw:----Ru--"; export const UPGRADES = { belt: { - label: "Belts, Distributer & Tunnels", - description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", tiers: [ { - required: [{ shape: "CuCuCuCu", amount: 80 }], + required: [{ shape: "CuCuCuCu", amount: 150 }], improvement: 1, }, { - required: [{ shape: "Ru----Ru", amount: 4000 }], + required: [{ shape: "--CuCu--", amount: 1500 }], improvement: 2, }, { - required: [{ shape: "CwSwCwSw", amount: 30000 }], - improvement: 4, + required: [{ shape: "CpCpCpCp", amount: 15000 }], + improvement: 2, }, { - required: [{ shape: "RgRgSpSp:CwSwCwSw:Cr--Sw--", amount: 80000 }], - improvement: 8, + required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 40000 }], + improvement: 2, + }, + { + required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 40000 }], + improvement: 2, + }, + { + required: [{ shape: finalGameShape, amount: 150000 }], + improvement: 5, + excludePrevious: true, }, ], }, miner: { - label: "Extraction", - description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", tiers: [ { - required: [{ shape: "RuRuRuRu", amount: 200 }], + required: [{ shape: "RuRuRuRu", amount: 400 }], improvement: 1, }, { - required: [{ shape: "Cu------", amount: 4000 }], + required: [{ shape: "Cu------", amount: 5500 }], improvement: 2, }, { - required: [{ shape: "WyWgWyWg:CbCpCbCp", amount: 30000 }], - improvement: 4, + required: [{ shape: "ScScScSc", amount: 20000 }], + improvement: 2, }, { - required: [{ shape: "WyWgWyWg:CbCpCbCp:Rp----Rp", amount: 90000 }], - improvement: 8, + required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 40000 }], + improvement: 2, + }, + { + required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 40000 }], + improvement: 2, + }, + { + required: [{ shape: finalGameShape, amount: 150000 }], + improvement: 5, + excludePrevious: true, }, ], }, processors: { - label: "Shape Processing", - description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", tiers: [ { - required: [{ shape: "RuRuRuRu", amount: 200 }], + required: [{ shape: "SuSuSuSu", amount: 1000 }], improvement: 1, }, { - required: [{ shape: "Cu------", amount: 4000 }], + required: [{ shape: "RuRu----", amount: 2000 }], improvement: 2, }, { - required: [{ shape: "WyWgWyWg:CbCpCbCp", amount: 30000 }], - improvement: 4, + required: [{ shape: "CgScScCg", amount: 25000 }], + improvement: 2, }, { - required: [{ shape: "WyWgWyWg:CbCpCbCp:Rp----Rp", amount: 90000 }], - improvement: 8, + required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 40000 }], + improvement: 2, + }, + { + required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 40000 }], + improvement: 2, + }, + { + required: [{ shape: finalGameShape, amount: 150000 }], + improvement: 5, + excludePrevious: true, }, ], }, painting: { - label: "Mixing & Painting", - description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", tiers: [ { - required: [{ shape: "RuRuRuRu", amount: 200 }], + required: [{ shape: "WrWrWrWr", amount: 500 }], improvement: 1, }, { - required: [{ shape: "Cu------", amount: 4000 }], + required: [{ shape: "RbRb----", amount: 4000 }], improvement: 2, }, { - required: [{ shape: "WyWgWyWg:CbCpCbCp", amount: 30000 }], - improvement: 4, + required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 30000 }], + improvement: 2, }, { - required: [{ shape: "WyWgWyWg:CbCpCbCp:Rp----Rp", amount: 90000 }], - improvement: 8, + required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 40000 }], + improvement: 2, + }, + { + required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 40000 }], + improvement: 2, + }, + { + required: [{ shape: finalGameShape, amount: 150000 }], + improvement: 5, + excludePrevious: true, }, ], }, - - // cutter: { - // label: "Cut Half", - // description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", - // tiers: [ - // { - // required: [{ shape: "----CuCu", amount: 450 }], - // improvement: 1, - // }, - // { - // required: [{ shape: "CpCpCpCp", amount: 12000 }], - // improvement: 2, - // }, - // { - // required: [{ shape: "CwRrWbSp:WcWrCpCw", amount: 45000 }], - // improvement: 4, - // }, - // { - // required: [{ shape: "CwRrWbSp:WcWrCpCw:WpWpWb--", amount: 100000 }], - // improvement: 8, - // }, - // ], - // }, - // splitter: { - // label: "Distribute", - // description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", - // tiers: [ - // { - // required: [{ shape: "CuCu----", amount: 350 }], - // improvement: 1, - // }, - // { - // required: [{ shape: "CrCrCrCr", amount: 7000 }], - // improvement: 2, - // }, - // { - // required: [{ shape: "WyWyWyWy", amount: 30000 }], - // improvement: 4, - // }, - // { - // required: [{ shape: "WyWyWyWy:CwSpRgRc", amount: 100000 }], - // improvement: 8, - // }, - // ], - // }, - - // rotater: { - // label: "Rotate", - // description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", - // tiers: [ - // { - // required: [{ shape: "RuRu----", amount: 750 }], - // improvement: 1, - // }, - // { - // required: [{ shape: "ScScScSc", amount: 3000 }], - // improvement: 2, - // }, - // { - // required: [{ shape: "ScSpRwRw:Cw----Cw", amount: 15000 }], - // improvement: 4, - // }, - // { - // required: [{ shape: "ScSpRwRw:Cw----Cw:CpCpCpCp", amount: 80000 }], - // improvement: 8, - // }, - // ], - // }, - - // underground_belt: { - // label: "Tunnel", - // description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", - // tiers: [ - // { - // required: [{ shape: "--CuCu--", amount: 1000 }], - // improvement: 1, - // }, - // { - // required: [{ shape: "RbRb----", amount: 9000 }], - // improvement: 2, - // }, - // { - // required: [{ shape: "RbRb----:WpWpWpWp", amount: 25000 }], - // improvement: 4, - // }, - // { - // required: [{ shape: "RbRb----:WpWpWpWp:RwRwRpRp", amount: 100000 }], - // improvement: 8, - // }, - // ], - // }, - - // painter: { - // label: "Dye", - // description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", - // tiers: [ - // { - // required: [{ shape: "------Ru", amount: 4000 }], - // improvement: 1, - // }, - // { - // required: [{ shape: "CcCcRgRg", amount: 15000 }], - // improvement: 2, - // }, - // { - // required: [{ shape: "CcCcRgRg:WgWgWgWg", amount: 35000 }], - // improvement: 4, - // }, - // { - // required: [{ shape: "CcCcRgRg:WgWgWgWg:CpRpCpRp", amount: 100000 }], - // improvement: 8, - // }, - // ], - // }, - - // mixer: { - // label: "Mix Colors", - // description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", - // tiers: [ - // { - // required: [{ shape: "RgRgRgRg:CcCcCcCc", amount: 11000 }], - // improvement: 1, - // }, - // { - // required: [{ shape: "WyWgWyWg:CbCpCbCp", amount: 15000 }], - // improvement: 2, - // }, - // { - // required: [{ shape: "CcCcRgRg:WgWgWgWg:CpRpCpRp", amount: 45000 }], - // improvement: 4, - // }, - // { - // required: [{ shape: "CcCcRgRg:WgWgWgWg:CpRpCpRp:CpCpCpCp", amount: 100000 }], - // improvement: 8, - // }, - // ], - // }, - // stacker: { - // label: "Combine", - // description: improvement => "Speed +" + Math.floor(improvement * 100.0) + "%", - // tiers: [ - // { - // required: [{ shape: "CgCgRgRg", amount: 20000 }], - // improvement: 1, - // }, - // { - // required: [{ shape: "CgCgRgRg:WpRpWpRp", amount: 50000 }], - // improvement: 2, - // }, - // { - // required: [{ shape: "CgCgRgRg:WpRpWpRp:SpSwSpSw", amount: 70000 }], - // improvement: 4, - // }, - // { - // required: [{ shape: "CgCgRgRg:WpRpWpRp:SpSwSpSw:CwCwCwCw", amount: 100000 }], - // improvement: 8, - // }, - // ], - // }, }; // Tiers need % of the previous tier as requirement too -const tierGrowth = 2; +const tierGrowth = 2.5; // Automatically generate tier levels for (const upgradeId in UPGRADES) { @@ -291,10 +139,12 @@ for (const upgradeId in UPGRADES) { for (let k = currentTierRequirements.length - 1; k >= 0; --k) { const oldTierRequirement = currentTierRequirements[k]; - tierHandle.required.unshift({ - shape: oldTierRequirement.shape, - amount: oldTierRequirement.amount, - }); + if (!tierHandle.excludePrevious) { + tierHandle.required.unshift({ + shape: oldTierRequirement.shape, + amount: oldTierRequirement.amount, + }); + } } currentTierRequirements.push( ...originalRequired.map(req => ({ diff --git a/src/js/game_analytics.d.ts b/src/js/game_analytics.d.ts deleted file mode 100644 index 3324213e..00000000 --- a/src/js/game_analytics.d.ts +++ /dev/null @@ -1,737 +0,0 @@ -declare module gameanalytics { - enum EGAErrorSeverity { - Undefined = 0, - Debug = 1, - Info = 2, - Warning = 3, - Error = 4, - Critical = 5, - } - enum EGAProgressionStatus { - Undefined = 0, - Start = 1, - Complete = 2, - Fail = 3, - } - enum EGAResourceFlowType { - Undefined = 0, - Source = 1, - Sink = 2, - } - module http { - enum EGAHTTPApiResponse { - NoResponse = 0, - BadResponse = 1, - RequestTimeout = 2, - JsonEncodeFailed = 3, - JsonDecodeFailed = 4, - InternalServerError = 5, - BadRequest = 6, - Unauthorized = 7, - UnknownResponseCode = 8, - Ok = 9, - Created = 10, - } - } - module events { - enum EGASdkErrorCategory { - Undefined = 0, - EventValidation = 1, - Database = 2, - Init = 3, - Http = 4, - Json = 5, - } - enum EGASdkErrorArea { - Undefined = 0, - BusinessEvent = 1, - ResourceEvent = 2, - ProgressionEvent = 3, - DesignEvent = 4, - ErrorEvent = 5, - InitHttp = 9, - EventsHttp = 10, - ProcessEvents = 11, - AddEventsToStore = 12, - } - enum EGASdkErrorAction { - Undefined = 0, - InvalidCurrency = 1, - InvalidShortString = 2, - InvalidEventPartLength = 3, - InvalidEventPartCharacters = 4, - InvalidStore = 5, - InvalidFlowType = 6, - StringEmptyOrNull = 7, - NotFoundInAvailableCurrencies = 8, - InvalidAmount = 9, - NotFoundInAvailableItemTypes = 10, - WrongProgressionOrder = 11, - InvalidEventIdLength = 12, - InvalidEventIdCharacters = 13, - InvalidProgressionStatus = 15, - InvalidSeverity = 16, - InvalidLongString = 17, - DatabaseTooLarge = 18, - DatabaseOpenOrCreate = 19, - JsonError = 25, - FailHttpJsonDecode = 29, - FailHttpJsonEncode = 30, - } - enum EGASdkErrorParameter { - Undefined = 0, - Currency = 1, - CartType = 2, - ItemType = 3, - ItemId = 4, - Store = 5, - FlowType = 6, - Amount = 7, - Progression01 = 8, - Progression02 = 9, - Progression03 = 10, - EventId = 11, - ProgressionStatus = 12, - Severity = 13, - Message = 14, - } - } -} -export declare var EGAErrorSeverity: typeof gameanalytics.EGAErrorSeverity; -export declare var EGAProgressionStatus: typeof gameanalytics.EGAProgressionStatus; -export declare var EGAResourceFlowType: typeof gameanalytics.EGAResourceFlowType; -declare module gameanalytics { - module logging { - class GALogger { - private static readonly instance; - private infoLogEnabled; - private infoLogVerboseEnabled; - private static debugEnabled; - private static readonly Tag; - private constructor(); - static setInfoLog(value: boolean): void; - static setVerboseLog(value: boolean): void; - static i(format: string): void; - static w(format: string): void; - static e(format: string): void; - static ii(format: string): void; - static d(format: string): void; - private sendNotificationMessage; - } - } -} -declare module gameanalytics { - module utilities { - class GAUtilities { - static getHmac(key: string, data: string): string; - static stringMatch(s: string, pattern: RegExp): boolean; - static joinStringArray(v: Array, delimiter: string): string; - static stringArrayContainsString(array: Array, search: string): boolean; - private static readonly keyStr; - static encode64(input: string): string; - static decode64(input: string): string; - static timeIntervalSince1970(): number; - static createGuid(): string; - private static s4; - } - } -} -declare module gameanalytics { - module validators { - import EGASdkErrorCategory = gameanalytics.events.EGASdkErrorCategory; - import EGASdkErrorArea = gameanalytics.events.EGASdkErrorArea; - import EGASdkErrorAction = gameanalytics.events.EGASdkErrorAction; - import EGASdkErrorParameter = gameanalytics.events.EGASdkErrorParameter; - class ValidationResult { - category: EGASdkErrorCategory; - area: EGASdkErrorArea; - action: EGASdkErrorAction; - parameter: EGASdkErrorParameter; - reason: string; - constructor( - category: EGASdkErrorCategory, - area: EGASdkErrorArea, - action: EGASdkErrorAction, - parameter: EGASdkErrorParameter, - reason: string - ); - } - class GAValidator { - static validateBusinessEvent( - currency: string, - amount: number, - cartType: string, - itemType: string, - itemId: string - ): ValidationResult; - static validateResourceEvent( - flowType: EGAResourceFlowType, - currency: string, - amount: number, - itemType: string, - itemId: string, - availableCurrencies: Array, - availableItemTypes: Array - ): ValidationResult; - static validateProgressionEvent( - progressionStatus: EGAProgressionStatus, - progression01: string, - progression02: string, - progression03: string - ): ValidationResult; - static validateDesignEvent(eventId: string): ValidationResult; - static validateErrorEvent(severity: EGAErrorSeverity, message: string): ValidationResult; - static validateSdkErrorEvent( - gameKey: string, - gameSecret: string, - category: EGASdkErrorCategory, - area: EGASdkErrorArea, - action: EGASdkErrorAction - ): boolean; - static validateKeys(gameKey: string, gameSecret: string): boolean; - static validateCurrency(currency: string): boolean; - static validateEventPartLength(eventPart: string, allowNull: boolean): boolean; - static validateEventPartCharacters(eventPart: string): boolean; - static validateEventIdLength(eventId: string): boolean; - static validateEventIdCharacters(eventId: string): boolean; - static validateAndCleanInitRequestResponse( - initResponse: { - [key: string]: any; - }, - configsCreated: boolean - ): { - [key: string]: any; - }; - static validateBuild(build: string): boolean; - static validateSdkWrapperVersion(wrapperVersion: string): boolean; - static validateEngineVersion(engineVersion: string): boolean; - static validateUserId(uId: string): boolean; - static validateShortString(shortString: string, canBeEmpty: boolean): boolean; - static validateString(s: string, canBeEmpty: boolean): boolean; - static validateLongString(longString: string, canBeEmpty: boolean): boolean; - static validateConnectionType(connectionType: string): boolean; - static validateCustomDimensions(customDimensions: Array): boolean; - static validateResourceCurrencies(resourceCurrencies: Array): boolean; - static validateResourceItemTypes(resourceItemTypes: Array): boolean; - static validateDimension01(dimension01: string, availableDimensions: Array): boolean; - static validateDimension02(dimension02: string, availableDimensions: Array): boolean; - static validateDimension03(dimension03: string, availableDimensions: Array): boolean; - static validateArrayOfStrings( - maxCount: number, - maxStringLength: number, - allowNoValues: boolean, - logTag: string, - arrayOfStrings: Array - ): boolean; - static validateClientTs(clientTs: number): boolean; - } - } -} -declare module gameanalytics { - module device { - class NameValueVersion { - name: string; - value: string; - version: string; - constructor(name: string, value: string, version: string); - } - class NameVersion { - name: string; - version: string; - constructor(name: string, version: string); - } - class GADevice { - private static readonly sdkWrapperVersion; - private static readonly osVersionPair; - static readonly buildPlatform: string; - static readonly deviceModel: string; - static readonly deviceManufacturer: string; - static readonly osVersion: string; - static readonly browserVersion: string; - static sdkGameEngineVersion: string; - static gameEngineVersion: string; - private static connectionType; - static touch(): void; - static getRelevantSdkVersion(): string; - static getConnectionType(): string; - static updateConnectionType(): void; - private static getOSVersionString; - private static runtimePlatformToString; - private static getBrowserVersionString; - private static getDeviceModel; - private static getDeviceManufacturer; - private static matchItem; - } - } -} -declare module gameanalytics { - module threading { - class TimedBlock { - readonly deadline: Date; - block: () => void; - readonly id: number; - ignore: boolean; - async: boolean; - running: boolean; - private static idCounter; - constructor(deadline: Date); - } - } -} -declare module gameanalytics { - module threading { - interface IComparer { - compare(x: T, y: T): number; - } - class PriorityQueue { - _subQueues: { - [key: number]: Array; - }; - _sortedKeys: Array; - private comparer; - constructor(priorityComparer: IComparer); - enqueue(priority: number, item: TItem): void; - private addQueueOfPriority; - peek(): TItem; - hasItems(): boolean; - dequeue(): TItem; - private dequeueFromHighPriorityQueue; - } - } -} -declare module gameanalytics { - module store { - enum EGAStoreArgsOperator { - Equal = 0, - LessOrEqual = 1, - NotEqual = 2, - } - enum EGAStore { - Events = 0, - Sessions = 1, - Progression = 2, - } - class GAStore { - private static readonly instance; - private static storageAvailable; - private static readonly MaxNumberOfEntries; - private eventsStore; - private sessionsStore; - private progressionStore; - private storeItems; - private static readonly StringFormat; - private static readonly KeyFormat; - private static readonly EventsStoreKey; - private static readonly SessionsStoreKey; - private static readonly ProgressionStoreKey; - private static readonly ItemsStoreKey; - private constructor(); - static isStorageAvailable(): boolean; - static isStoreTooLargeForEvents(): boolean; - static select( - store: EGAStore, - args?: Array<[string, EGAStoreArgsOperator, any]>, - sort?: boolean, - maxCount?: number - ): Array<{ - [key: string]: any; - }>; - static update( - store: EGAStore, - setArgs: Array<[string, any]>, - whereArgs?: Array<[string, EGAStoreArgsOperator, any]> - ): boolean; - static delete(store: EGAStore, args: Array<[string, EGAStoreArgsOperator, any]>): void; - static insert( - store: EGAStore, - newEntry: { - [key: string]: any; - }, - replace?: boolean, - replaceKey?: string - ): void; - static save(gameKey: string): void; - static load(gameKey: string): void; - static setItem(gameKey: string, key: string, value: string): void; - static getItem(gameKey: string, key: string): string; - private static getStore; - } - } -} -declare module gameanalytics { - module state { - class GAState { - private static readonly CategorySdkError; - private static readonly MAX_CUSTOM_FIELDS_COUNT; - private static readonly MAX_CUSTOM_FIELDS_KEY_LENGTH; - private static readonly MAX_CUSTOM_FIELDS_VALUE_STRING_LENGTH; - static readonly instance: GAState; - private constructor(); - private userId; - static setUserId(userId: string): void; - private identifier; - static getIdentifier(): string; - private initialized; - static isInitialized(): boolean; - static setInitialized(value: boolean): void; - sessionStart: number; - static getSessionStart(): number; - private sessionNum; - static getSessionNum(): number; - private transactionNum; - static getTransactionNum(): number; - sessionId: string; - static getSessionId(): string; - private currentCustomDimension01; - static getCurrentCustomDimension01(): string; - private currentCustomDimension02; - static getCurrentCustomDimension02(): string; - private currentCustomDimension03; - static getCurrentCustomDimension03(): string; - private gameKey; - static getGameKey(): string; - private gameSecret; - static getGameSecret(): string; - private availableCustomDimensions01; - static getAvailableCustomDimensions01(): Array; - static setAvailableCustomDimensions01(value: Array): void; - private availableCustomDimensions02; - static getAvailableCustomDimensions02(): Array; - static setAvailableCustomDimensions02(value: Array): void; - private availableCustomDimensions03; - static getAvailableCustomDimensions03(): Array; - static setAvailableCustomDimensions03(value: Array): void; - private availableResourceCurrencies; - static getAvailableResourceCurrencies(): Array; - static setAvailableResourceCurrencies(value: Array): void; - private availableResourceItemTypes; - static getAvailableResourceItemTypes(): Array; - static setAvailableResourceItemTypes(value: Array): void; - private build; - static getBuild(): string; - static setBuild(value: string): void; - private useManualSessionHandling; - static getUseManualSessionHandling(): boolean; - private _isEventSubmissionEnabled; - static isEventSubmissionEnabled(): boolean; - sdkConfigCached: { - [key: string]: any; - }; - private configurations; - private remoteConfigsIsReady; - private remoteConfigsListeners; - initAuthorized: boolean; - clientServerTimeOffset: number; - configsHash: string; - abId: string; - static getABTestingId(): string; - abVariantId: string; - static getABTestingVariantId(): string; - private defaultUserId; - private setDefaultId; - static getDefaultId(): string; - sdkConfigDefault: { - [key: string]: string; - }; - sdkConfig: { - [key: string]: any; - }; - static getSdkConfig(): { - [key: string]: any; - }; - private progressionTries; - static readonly DefaultUserIdKey: string; - static readonly SessionNumKey: string; - static readonly TransactionNumKey: string; - private static readonly Dimension01Key; - private static readonly Dimension02Key; - private static readonly Dimension03Key; - static readonly SdkConfigCachedKey: string; - static isEnabled(): boolean; - static setCustomDimension01(dimension: string): void; - static setCustomDimension02(dimension: string): void; - static setCustomDimension03(dimension: string): void; - static incrementSessionNum(): void; - static incrementTransactionNum(): void; - static incrementProgressionTries(progression: string): void; - static getProgressionTries(progression: string): number; - static clearProgressionTries(progression: string): void; - static setKeys(gameKey: string, gameSecret: string): void; - static setManualSessionHandling(flag: boolean): void; - static setEnabledEventSubmission(flag: boolean): void; - static getEventAnnotations(): { - [key: string]: any; - }; - static getSdkErrorEventAnnotations(): { - [key: string]: any; - }; - static getInitAnnotations(): { - [key: string]: any; - }; - static getClientTsAdjusted(): number; - static sessionIsStarted(): boolean; - private static cacheIdentifier; - static ensurePersistedStates(): void; - static calculateServerTimeOffset(serverTs: number): number; - static validateAndCleanCustomFields(fields: { - [id: string]: any; - }): { - [id: string]: any; - }; - static validateAndFixCurrentDimensions(): void; - static getConfigurationStringValue(key: string, defaultValue: string): string; - static isRemoteConfigsReady(): boolean; - static addRemoteConfigsListener(listener: { onRemoteConfigsUpdated: () => void }): void; - static removeRemoteConfigsListener(listener: { onRemoteConfigsUpdated: () => void }): void; - static getRemoteConfigsContentAsString(): string; - static populateConfigurations(sdkConfig: { [key: string]: any }): void; - } - } -} -declare module gameanalytics { - module tasks { - class SdkErrorTask { - private static readonly MaxCount; - private static readonly countMap; - private static readonly timestampMap; - static execute(url: string, type: string, payloadData: string, secretKey: string): void; - } - } -} -declare module gameanalytics { - module http { - import EGASdkErrorCategory = gameanalytics.events.EGASdkErrorCategory; - import EGASdkErrorArea = gameanalytics.events.EGASdkErrorArea; - import EGASdkErrorAction = gameanalytics.events.EGASdkErrorAction; - import EGASdkErrorParameter = gameanalytics.events.EGASdkErrorParameter; - class GAHTTPApi { - static readonly instance: GAHTTPApi; - private protocol; - private hostName; - private version; - private remoteConfigsVersion; - private baseUrl; - private remoteConfigsBaseUrl; - private initializeUrlPath; - private eventsUrlPath; - private useGzip; - private static readonly MAX_ERROR_MESSAGE_LENGTH; - private constructor(); - requestInit( - configsHash: string, - callback: ( - response: EGAHTTPApiResponse, - json: { - [key: string]: any; - } - ) => void - ): void; - sendEventsInArray( - eventArray: Array<{ - [key: string]: any; - }>, - requestId: string, - callback: ( - response: EGAHTTPApiResponse, - json: { - [key: string]: any; - }, - requestId: string, - eventCount: number - ) => void - ): void; - sendSdkErrorEvent( - category: EGASdkErrorCategory, - area: EGASdkErrorArea, - action: EGASdkErrorAction, - parameter: EGASdkErrorParameter, - reason: string, - gameKey: string, - secretKey: string - ): void; - private static sendEventInArrayRequestCallback; - private static sendRequest; - private static initRequestCallback; - private createPayloadData; - private processRequestResponse; - private static sdkErrorCategoryString; - private static sdkErrorAreaString; - private static sdkErrorActionString; - private static sdkErrorParameterString; - } - } -} -declare module gameanalytics { - module events { - class GAEvents { - private static readonly CategorySessionStart; - private static readonly CategorySessionEnd; - private static readonly CategoryDesign; - private static readonly CategoryBusiness; - private static readonly CategoryProgression; - private static readonly CategoryResource; - private static readonly CategoryError; - private static readonly MaxEventCount; - private constructor(); - static addSessionStartEvent(): void; - static addSessionEndEvent(): void; - static addBusinessEvent( - currency: string, - amount: number, - itemType: string, - itemId: string, - cartType: string, - fields: { - [id: string]: any; - } - ): void; - static addResourceEvent( - flowType: EGAResourceFlowType, - currency: string, - amount: number, - itemType: string, - itemId: string, - fields: { - [id: string]: any; - } - ): void; - static addProgressionEvent( - progressionStatus: EGAProgressionStatus, - progression01: string, - progression02: string, - progression03: string, - score: number, - sendScore: boolean, - fields: { - [id: string]: any; - } - ): void; - static addDesignEvent( - eventId: string, - value: number, - sendValue: boolean, - fields: { - [id: string]: any; - } - ): void; - static addErrorEvent( - severity: EGAErrorSeverity, - message: string, - fields: { - [id: string]: any; - } - ): void; - static processEvents(category: string, performCleanUp: boolean): void; - private static processEventsCallback; - private static cleanupEvents; - private static fixMissingSessionEndEvents; - private static addEventToStore; - private static updateSessionStore; - private static addDimensionsToEvent; - private static addFieldsToEvent; - private static resourceFlowTypeToString; - private static progressionStatusToString; - private static errorSeverityToString; - } - } -} -declare module gameanalytics { - module threading { - class GAThreading { - private static readonly instance; - readonly blocks: PriorityQueue; - private readonly id2TimedBlockMap; - private static runTimeoutId; - private static readonly ThreadWaitTimeInMs; - private static ProcessEventsIntervalInSeconds; - private keepRunning; - private isRunning; - private constructor(); - static createTimedBlock(delayInSeconds?: number): TimedBlock; - static performTaskOnGAThread(taskBlock: () => void, delayInSeconds?: number): void; - static performTimedBlockOnGAThread(timedBlock: TimedBlock): void; - static scheduleTimer(interval: number, callback: () => void): number; - static getTimedBlockById(blockIdentifier: number): TimedBlock; - static ensureEventQueueIsRunning(): void; - static endSessionAndStopQueue(): void; - static stopEventQueue(): void; - static ignoreTimer(blockIdentifier: number): void; - static setEventProcessInterval(interval: number): void; - private addTimedBlock; - private static run; - private static startThread; - private static getNextBlock; - private static processEventQueue; - } - } -} -declare module gameanalytics { - class GameAnalytics { - private static initTimedBlockId; - static methodMap: { - [id: string]: (...args: any[]) => void; - }; - static init(): void; - static gaCommand(...args: any[]): void; - static configureAvailableCustomDimensions01(customDimensions?: Array): void; - static configureAvailableCustomDimensions02(customDimensions?: Array): void; - static configureAvailableCustomDimensions03(customDimensions?: Array): void; - static configureAvailableResourceCurrencies(resourceCurrencies?: Array): void; - static configureAvailableResourceItemTypes(resourceItemTypes?: Array): void; - static configureBuild(build?: string): void; - static configureSdkGameEngineVersion(sdkGameEngineVersion?: string): void; - static configureGameEngineVersion(gameEngineVersion?: string): void; - static configureUserId(uId?: string): void; - static initialize(gameKey?: string, gameSecret?: string): void; - static addBusinessEvent( - currency?: string, - amount?: number, - itemType?: string, - itemId?: string, - cartType?: string - ): void; - static addResourceEvent( - flowType?: EGAResourceFlowType, - currency?: string, - amount?: number, - itemType?: string, - itemId?: string - ): void; - static addProgressionEvent( - progressionStatus?: EGAProgressionStatus, - progression01?: string, - progression02?: string, - progression03?: string, - score?: any - ): void; - static addDesignEvent(eventId: string, value?: any): void; - static addErrorEvent(severity?: EGAErrorSeverity, message?: string): void; - static setEnabledInfoLog(flag?: boolean): void; - static setEnabledVerboseLog(flag?: boolean): void; - static setEnabledManualSessionHandling(flag?: boolean): void; - static setEnabledEventSubmission(flag?: boolean): void; - static setCustomDimension01(dimension?: string): void; - static setCustomDimension02(dimension?: string): void; - static setCustomDimension03(dimension?: string): void; - static setEventProcessInterval(intervalInSeconds: number): void; - static startSession(): void; - static endSession(): void; - static onStop(): void; - static onResume(): void; - static getRemoteConfigsValueAsString(key: string, defaultValue?: string): string; - static isRemoteConfigsReady(): boolean; - static addRemoteConfigsListener(listener: { onRemoteConfigsUpdated: () => void }): void; - static removeRemoteConfigsListener(listener: { onRemoteConfigsUpdated: () => void }): void; - static getRemoteConfigsContentAsString(): string; - static getABTestingId(): string; - static getABTestingVariantId(): string; - private static internalInitialize; - private static newSession; - private static startNewSessionCallback; - private static resumeSessionAndStartQueue; - private static isSdkReady; - } -} -declare var GameAnalyticsCommand: typeof gameanalytics.GameAnalytics.gaCommand; -export declare var GameAnalytics: typeof gameanalytics.GameAnalytics; -export default GameAnalytics; diff --git a/src/js/globals.d.ts b/src/js/globals.d.ts index 5a17daa8..977e8ff3 100644 --- a/src/js/globals.d.ts +++ b/src/js/globals.d.ts @@ -107,8 +107,6 @@ declare interface Window { assert(condition: boolean, failureMessage: string); coreThreadLoadedCb(); - - gameanalytics: typeof import("./game_analytics"); } declare interface Navigator { @@ -138,13 +136,17 @@ declare interface Element { innerHTML: string; } +declare interface Object { + entries(obj: object): Array<[string, any]>; +} + declare interface Math { radians(number): number; degrees(number): number; } declare interface String { - padStart(size: number, fill: string): string; + padStart(size: number, fill?: string): string; padEnd(size: number, fill: string): string; } @@ -166,6 +168,8 @@ declare interface SingletonFactoryTemplate { entries: Array; idToEntry: any; + getId(): string; + getAllIds(): Array; register(classHandle: new (...args: any[]) => T): void; hasId(id: string): boolean; findById(id: string): T; @@ -189,3 +193,14 @@ declare class TypedTrackedState { setSilent(value: any): void; get(): T; } + +declare const STOP_PROPAGATION = "stop_propagation"; + +declare interface TypedSignal> { + add(receiver: (...args: T) => /* STOP_PROPAGATION */ string | void, scope?: object); + remove(receiver: (...args: T) => /* STOP_PROPAGATION */ string | void); + + dispatch(...args: T): /* STOP_PROPAGATION */ string | void; + + removeAll(); +} diff --git a/jsconfig.json b/src/js/jsconfig.json similarity index 62% rename from jsconfig.json rename to src/js/jsconfig.json index 6879acfe..e28a1c04 100644 --- a/jsconfig.json +++ b/src/js/jsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "target": "es6", - "checkJs": true, - + "checkJs": true } -} \ No newline at end of file +} diff --git a/src/js/main.js b/src/js/main.js index 5eef3a3f..4803d980 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -9,6 +9,7 @@ import { initComponentRegistry } from "./game/component_registry"; import { initDrawUtils } from "./core/draw_utils"; import { initItemRegistry } from "./game/item_registry"; import { initMetaBuildingRegistry } from "./game/meta_building_registry"; +import { initGameSpeedRegistry } from "./game/game_speed_registry"; const logger = createLogger("main"); @@ -18,7 +19,7 @@ if (window.coreThreadLoadedCb) { } console.log( - `%cshapez.io ️%c\n© 2019 Tobias Springer IT Solutions\nCommit %c${G_BUILD_COMMIT_HASH}%c on %c${new Date( + `%cshapez.io ️%c\n© 2020 Tobias Springer IT Solutions\nCommit %c${G_BUILD_COMMIT_HASH}%c on %c${new Date( G_BUILD_TIME ).toLocaleString()}\n`, "font-size: 35px; font-family: Arial;font-weight: bold; padding: 10px 0;", @@ -49,6 +50,7 @@ initDrawUtils(); initComponentRegistry(); initItemRegistry(); initMetaBuildingRegistry(); +initGameSpeedRegistry(); let app = null; diff --git a/src/js/platform/ad_providers/adinplay.js b/src/js/platform/ad_providers/adinplay.js index a5731dfa..efad0574 100644 --- a/src/js/platform/ad_providers/adinplay.js +++ b/src/js/platform/ad_providers/adinplay.js @@ -7,6 +7,7 @@ import { createLogger } from "../../core/logging"; import { ClickDetector } from "../../core/click_detector"; import { performanceNow } from "../../core/builtins"; import { clamp } from "../../core/utils"; +import { T } from "../../translations"; const logger = createLogger("adprovider/adinplay"); @@ -111,7 +112,7 @@ export class AdinplayAdProvider extends AdProviderInterface { AD_HEIGHT: h, AD_FULLSCREEN: false, AD_CENTERPLAYER: false, - LOADING_TEXT: "Loading", + LOADING_TEXT: T.global.loading, PREROLL_ELEM: function () { return videoElement; }, diff --git a/src/js/platform/ad_providers/gamedistribution.js b/src/js/platform/ad_providers/gamedistribution.js new file mode 100644 index 00000000..431d6096 --- /dev/null +++ b/src/js/platform/ad_providers/gamedistribution.js @@ -0,0 +1,125 @@ +/* typehints:start */ +import { Application } from "../../application"; +/* typehints:end */ + +import { AdProviderInterface } from "../ad_provider"; +import { performanceNow } from "../../core/builtins"; +import { createLogger } from "../../core/logging"; + +const minimumTimeBetweenVideoAdsMs = G_IS_DEV ? 1 : 5 * 60 * 1000; + +const logger = createLogger("gamedistribution"); + +export class GamedistributionAdProvider extends AdProviderInterface { + /** + * + * @param {Application} app + */ + constructor(app) { + super(app); + + /** + * The resolve function to finish the current video ad. Null if none is currently running + * @type {Function} + */ + this.videoAdResolveFunction = null; + + /** + * The current timer which will timeout the resolve + */ + this.videoAdResolveTimer = null; + + /** + * When we showed the last video ad + */ + this.lastVideoAdShowTime = -1e20; + + console.error("X"); + } + + getHasAds() { + return true; + } + + getCanShowVideoAd() { + return ( + this.getHasAds() && + !this.videoAdResolveFunction && + performanceNow() - this.lastVideoAdShowTime > minimumTimeBetweenVideoAdsMs + ); + } + + initialize() { + // No point to initialize everything if ads are not supported + if (!this.getHasAds()) { + return Promise.resolve(); + } + + logger.log("🎬 Initializing gamedistribution ads"); + + try { + parent.postMessage("shapezio://gd.game_loaded", "*"); + } catch (ex) { + return Promise.reject("Frame communication not allowed"); + } + + window.addEventListener( + "message", + event => { + if (event.data === "shapezio://gd.ad_started") { + console.log("🎬 Got ad started callback"); + } else if (event.data === "shapezio://gd.ad_finished") { + console.log("🎬 Got ad finished callback"); + if (this.videoAdResolveFunction) { + this.videoAdResolveFunction(); + } + } + }, + false + ); + + return Promise.resolve(); + } + + showVideoAd() { + assert(this.getHasAds(), "Called showVideoAd but ads are not supported!"); + assert(!this.videoAdResolveFunction, "Video ad still running, can not show again!"); + this.lastVideoAdShowTime = performanceNow(); + + console.log("🎬 Gamedistribution: Start ad"); + try { + parent.postMessage("shapezio://gd.show_ad", "*"); + } catch (ex) { + logger.warn("🎬 Failed to send message for gd ad:", ex); + return Promise.resolve(); + } + + document.body.classList.add("externalAdOpen"); + + return new Promise(resolve => { + // So, wait for the remove call but also remove after N seconds + this.videoAdResolveFunction = () => { + this.videoAdResolveFunction = null; + clearTimeout(this.videoAdResolveTimer); + this.videoAdResolveTimer = null; + + // When the ad closed, also set the time + this.lastVideoAdShowTime = performanceNow(); + resolve(); + }; + + this.videoAdResolveTimer = setTimeout(() => { + logger.warn("Automatically closing ad after not receiving callback"); + if (this.videoAdResolveFunction) { + this.videoAdResolveFunction(); + } + }, 35000); + }) + .catch(err => { + logger.error(this, "Error while resolving video ad:", err); + }) + .then(() => { + document.body.classList.remove("externalAdOpen"); + }); + } +} diff --git a/src/js/platform/browser/embed_provider.js b/src/js/platform/browser/embed_provider.js deleted file mode 100644 index 8703ec27..00000000 --- a/src/js/platform/browser/embed_provider.js +++ /dev/null @@ -1,47 +0,0 @@ -import { AdProviderInterface } from "../ad_provider"; - -/** - * Stores information about where we are iframed - */ -export class EmbedProvider { - /** - * @returns {string} - */ - getId() { - abstract; - return ""; - } - - /** - * Whether this provider supports ads - * @returns {boolean} - */ - getSupportsAds() { - return false; - } - - /** - * Returns the ad provider - * @returns {typeof AdProviderInterface} - */ - getAdProvider() { - abstract; - return null; - } - - /** - * Whetherexternal links are supported - * @returns {boolean} - */ - getSupportsExternalLinks() { - return true; - } - - /** - * Returns whether this provider is iframed - * @returns {boolean} - */ - getIsIframed() { - return true; - } -} diff --git a/src/js/platform/browser/embed_providers/armorgames.js b/src/js/platform/browser/embed_providers/armorgames.js deleted file mode 100644 index 867e0a71..00000000 --- a/src/js/platform/browser/embed_providers/armorgames.js +++ /dev/null @@ -1,16 +0,0 @@ -import { AdinplayAdProvider } from "../../ad_providers/adinplay"; -import { ShapezioWebsiteEmbedProvider } from "./shapezio_website"; - -export class ArmorgamesEmbedProvider extends ShapezioWebsiteEmbedProvider { - getId() { - return "armorgames"; - } - - getAdProvider() { - return AdinplayAdProvider; - } - - getIsIframed() { - return true; - } -} diff --git a/src/js/platform/browser/embed_providers/crazygames.js b/src/js/platform/browser/embed_providers/crazygames.js deleted file mode 100644 index dc0b09dc..00000000 --- a/src/js/platform/browser/embed_providers/crazygames.js +++ /dev/null @@ -1,11 +0,0 @@ -import { ShapezioWebsiteEmbedProvider } from "./shapezio_website"; - -export class CrazygamesEmbedProvider extends ShapezioWebsiteEmbedProvider { - getId() { - return "crazygames"; - } - - getIsIframed() { - return true; - } -} diff --git a/src/js/platform/browser/embed_providers/gamedistribution.js b/src/js/platform/browser/embed_providers/gamedistribution.js deleted file mode 100644 index 49dda339..00000000 --- a/src/js/platform/browser/embed_providers/gamedistribution.js +++ /dev/null @@ -1,20 +0,0 @@ -import { AdinplayAdProvider } from "../../ad_providers/adinplay"; -import { ShapezioWebsiteEmbedProvider } from "./shapezio_website"; - -export class GamedistributionEmbedProvider extends ShapezioWebsiteEmbedProvider { - getId() { - return "gamedistribution"; - } - - getAdProvider() { - return AdinplayAdProvider; - } - - getSupportsExternalLinks() { - return false; - } - - getIsIframed() { - return true; - } -} diff --git a/src/js/platform/browser/embed_providers/iogames_space.js b/src/js/platform/browser/embed_providers/iogames_space.js deleted file mode 100644 index 5f7d317d..00000000 --- a/src/js/platform/browser/embed_providers/iogames_space.js +++ /dev/null @@ -1,15 +0,0 @@ -import { ShapezioWebsiteEmbedProvider } from "./shapezio_website"; - -export class IogamesSpaceEmbedProvider extends ShapezioWebsiteEmbedProvider { - getId() { - return "iogames.space"; - } - - getShowUpvoteHints() { - return true; - } - - getIsIframed() { - return true; - } -} diff --git a/src/js/platform/browser/embed_providers/kongregate.js b/src/js/platform/browser/embed_providers/kongregate.js deleted file mode 100644 index b0ed4d8b..00000000 --- a/src/js/platform/browser/embed_providers/kongregate.js +++ /dev/null @@ -1,24 +0,0 @@ -import { NoAdProvider } from "../../ad_providers/no_ad_provider"; -import { EmbedProvider } from "../embed_provider"; - -export class KongregateEmbedProvider extends EmbedProvider { - getId() { - return "kongregate"; - } - - getSupportsAds() { - return false; - } - - getAdProvider() { - return NoAdProvider; - } - - getSupportsExternalLinks() { - return true; - } - - getIsIframed() { - return true; - } -} diff --git a/src/js/platform/browser/embed_providers/miniclip.js b/src/js/platform/browser/embed_providers/miniclip.js deleted file mode 100644 index e0612978..00000000 --- a/src/js/platform/browser/embed_providers/miniclip.js +++ /dev/null @@ -1,11 +0,0 @@ -import { ShapezioWebsiteEmbedProvider } from "./shapezio_website"; - -export class MiniclipEmbedProvider extends ShapezioWebsiteEmbedProvider { - getId() { - return "miniclip"; - } - - getIsIframed() { - return true; - } -} diff --git a/src/js/platform/browser/embed_providers/shapezio_website.js b/src/js/platform/browser/embed_providers/shapezio_website.js deleted file mode 100644 index 2a5359ed..00000000 --- a/src/js/platform/browser/embed_providers/shapezio_website.js +++ /dev/null @@ -1,24 +0,0 @@ -import { EmbedProvider } from "../embed_provider"; -import { AdinplayAdProvider } from "../../ad_providers/adinplay"; - -export class ShapezioWebsiteEmbedProvider extends EmbedProvider { - getId() { - return "shapezio"; - } - - getSupportsAds() { - return true; - } - - getAdProvider() { - return AdinplayAdProvider; - } - - getIsIframed() { - return false; - } - - getSupportsExternalLinks() { - return true; - } -} diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index 1c06a602..be1a8940 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -1,39 +1,207 @@ import { GameAnalyticsInterface } from "../game_analytics"; import { createLogger } from "../../core/logging"; import { ShapeDefinition } from "../../game/shape_definition"; -import { gameCreationAction } from "../../states/ingame"; +import { Savegame } from "../../savegame/savegame"; +import { FILE_NOT_FOUND } from "../storage"; +import { globalConfig } from "../../core/config"; +import { InGameState } from "../../states/ingame"; +import { GameRoot } from "../../game/root"; +import { StaticMapEntityComponent } from "../../game/components/static_map_entity"; -const logger = createLogger("ga_com"); +const logger = createLogger("game_analytics"); -export class GameAnalyticsDotCom extends GameAnalyticsInterface { +const analyticsUrl = G_IS_DEV ? "http://localhost:8001" : "https://analytics.shapez.io"; + +// Be sure to increment the ID whenever it changes to make sure all +// users are tracked +const analyticsLocalFile = "analytics_token.3.bin"; + +export class ShapezGameAnalytics extends GameAnalyticsInterface { /** * @returns {Promise} */ initialize() { - try { - const ga = window.gameanalytics.GameAnalytics; - ga.configureBuild(G_APP_ENVIRONMENT + "@" + G_BUILD_VERSION + "@" + G_BUILD_COMMIT_HASH); + this.syncKey = null; - ga.setEnabledInfoLog(G_IS_DEV); - ga.setEnabledVerboseLog(G_IS_DEV); + setInterval(() => this.sendTimePoints(), 120 * 1000); - // @ts-ignore - ga.initialize(window.ga_comKey, window.ga_comToken); + // Retrieve sync key from player + return this.app.storage.readFileAsync(analyticsLocalFile).then( + syncKey => { + this.syncKey = syncKey; + logger.log("Player sync key read:", this.syncKey); + }, + error => { + // File was not found, retrieve new key + if (error === FILE_NOT_FOUND) { + logger.log("Retrieving new player key"); - // start new session - ga.startSession(); - } catch (ex) { - logger.warn("ga_com init error:", ex); + // Perform call to get a new key from the API + this.sendToApi("/v1/register", { + environment: G_APP_ENVIRONMENT, + }) + .then(res => { + // Try to read and parse the key from the api + if (res.key && typeof res.key === "string" && res.key.length === 40) { + this.syncKey = res.key; + logger.log("Key retrieved:", this.syncKey); + this.app.storage.writeFileAsync(analyticsLocalFile, res.key); + } else { + throw new Error("Bad response from analytics server: " + res); + } + }) + .catch(err => { + logger.error("Failed to register on analytics api:", err); + }); + } else { + logger.error("Failed to read ga key:", error); + } + return; + } + ); + } + + /** + * Sends a request to the api + * @param {string} endpoint Endpoint without base url + * @param {object} data payload + * @returns {Promise} + */ + sendToApi(endpoint, data) { + return Promise.race([ + new Promise((resolve, reject) => { + setTimeout(() => reject("Request to " + endpoint + " timed out"), 20000); + }), + fetch(analyticsUrl + endpoint, { + method: "POST", + mode: "cors", + cache: "no-cache", + referrer: "no-referrer", + credentials: "omit", + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "x-api-key": globalConfig.info.analyticsApiKey, + }, + body: JSON.stringify(data), + }) + .then(res => { + if (!res.ok || res.status !== 200) { + throw new Error("Fetch error: Bad status " + res.status); + } + return res; + }) + .then(res => res.json()), + ]); + } + + /** + * Sends a game event to the analytics + * @param {string} category + * @param {string} value + */ + sendGameEvent(category, value) { + if (!this.syncKey) { + logger.warn("Can not send event due to missing sync key"); + return; } - return Promise.resolve(); + + const gameState = this.app.stateMgr.currentState; + if (!(gameState instanceof InGameState)) { + logger.warn("Trying to send analytics event outside of ingame state"); + return; + } + + const savegame = gameState.savegame; + if (!savegame) { + logger.warn("Ingame state has empty savegame"); + return; + } + + const savegameId = savegame.internalId; + if (!gameState.core) { + logger.warn("Game state has no core"); + return; + } + const root = gameState.core.root; + if (!root) { + logger.warn("Root is not initialized"); + return; + } + + logger.log("Sending event", category, value); + + this.sendToApi("/v1/game-event", { + playerKey: this.syncKey, + gameKey: savegameId, + ingameTime: root.time.now(), + category, + value, + version: G_BUILD_VERSION, + gameDump: this.generateGameDump(root, category === "sync"), + }); + } + + sendTimePoints() { + const gameState = this.app.stateMgr.currentState; + if (gameState instanceof InGameState) { + logger.log("Syncing analytics"); + this.sendGameEvent("sync", ""); + } + } + + /** + * Generates a game dump + * @param {GameRoot} root + * @param {boolean=} metaOnly + */ + generateGameDump(root, metaOnly = false) { + let staticEntities = []; + + const entities = root.entityMgr.getAllWithComponent(StaticMapEntityComponent); + + // Limit the entities + if (!metaOnly && entities.length < 500) { + for (let i = 0; i < entities.length; ++i) { + const entity = entities[i]; + const staticComp = entity.components.StaticMapEntity; + const payload = {}; + payload.origin = staticComp.origin; + payload.tileSize = staticComp.tileSize; + payload.rotation = staticComp.rotation; + + if (entity.components.Belt) { + payload.type = "belt"; + } else if (entity.components.UndergroundBelt) { + payload.type = "tunnel"; + } else if (entity.components.ItemProcessor) { + payload.type = entity.components.ItemProcessor.type; + } else if (entity.components.Miner) { + payload.type = "extractor"; + } else { + logger.warn("Unkown entity type", entity); + } + staticEntities.push(payload); + } + } + + return { + storedShapes: root.hubGoals.storedShapes, + gainedRewards: root.hubGoals.gainedRewards, + upgradeLevels: root.hubGoals.upgradeLevels, + staticEntities, + }; } /** * @param {ShapeDefinition} definition */ - handleShapeDelivered(definition) { - const hash = definition.getHash(); - logger.log("Deliver:", hash); + handleShapeDelivered(definition) {} + + /** + */ + handleGameStarted() { + this.sendGameEvent("game_start", ""); } /** @@ -42,13 +210,7 @@ export class GameAnalyticsDotCom extends GameAnalyticsInterface { */ handleLevelCompleted(level) { logger.log("Complete level", level); - try { - const gaD = window.gameanalytics; - const ga = gaD.GameAnalytics; - ga.addProgressionEvent(gaD.EGAProgressionStatus.Complete, "story", "" + level); - } catch (ex) { - logger.error("ga_com lvl complete error:", ex); - } + this.sendGameEvent("level_complete", "" + level); } /** @@ -58,12 +220,6 @@ export class GameAnalyticsDotCom extends GameAnalyticsInterface { */ handleUpgradeUnlocked(id, level) { logger.log("Unlock upgrade", id, level); - try { - const gaD = window.gameanalytics; - const ga = gaD.GameAnalytics; - ga.addProgressionEvent(gaD.EGAProgressionStatus.Complete, "upgrade", id, "" + level); - } catch (ex) { - logger.error("ga_com upgrade unlock error:", ex); - } + this.sendGameEvent("upgrade_unlock", id + "@" + level); } } diff --git a/src/js/platform/browser/no_game_analytics.js b/src/js/platform/browser/no_game_analytics.js new file mode 100644 index 00000000..805e6fb1 --- /dev/null +++ b/src/js/platform/browser/no_game_analytics.js @@ -0,0 +1,7 @@ +import { GameAnalyticsInterface } from "../game_analytics"; + +export class NoGameAnalytics extends GameAnalyticsInterface { + initialize() { + return Promise.resolve(); + } +} diff --git a/src/js/platform/browser/sound.js b/src/js/platform/browser/sound.js index 6776a6d1..508dcf8c 100644 --- a/src/js/platform/browser/sound.js +++ b/src/js/platform/browser/sound.js @@ -1,55 +1,59 @@ -import { MusicInstanceInterface, SoundInstanceInterface, SoundInterface } from "../sound"; +import { MusicInstanceInterface, SoundInstanceInterface, SoundInterface, MUSIC, SOUNDS } from "../sound"; import { cachebust } from "../../core/cachebust"; import { createLogger } from "../../core/logging"; +import { globalConfig } from "../../core/config"; const { Howl, Howler } = require("howler"); const logger = createLogger("sound/browser"); -class SoundInstance extends SoundInstanceInterface { - constructor(key, url) { - super(key, url); +// @ts-ignore +const sprites = require("../../built-temp/sfx.json"); + +class SoundSpritesContainer { + constructor() { this.howl = null; - this.instance = null; + + this.loadingPromise = null; } load() { - return Promise.race([ + if (this.loadingPromise) { + return this.loadingPromise; + } + return (this.loadingPromise = Promise.race([ new Promise((resolve, reject) => { - setTimeout(reject, G_IS_DEV ? 5000 : 60000); + setTimeout(reject, G_IS_DEV ? 500 : 5000); }), new Promise(resolve => { this.howl = new Howl({ - src: cachebust("res/sounds/" + this.url), + src: cachebust("res/sounds/sfx.mp3"), + sprite: sprites.sprite, autoplay: false, loop: false, volume: 0, preload: true, + pool: 20, onload: () => { resolve(); }, onloaderror: (id, err) => { - logger.warn("Sound", this.url, "failed to load:", id, err); + logger.warn("SFX failed to load:", id, err); this.howl = null; resolve(); }, onplayerror: (id, err) => { - logger.warn("Sound", this.url, "failed to play:", id, err); + logger.warn("SFX failed to play:", id, err); }, }); }), - ]); + ])); } - play(volume) { + play(volume, key) { if (this.howl) { - if (!this.instance) { - this.instance = this.howl.play(); - } else { - this.howl.play(this.instance); - this.howl.seek(0, this.instance); - } - this.howl.volume(volume, this.instance); + const instance = this.howl.play(key); + this.howl.volume(volume, instance); } } @@ -57,11 +61,35 @@ class SoundInstance extends SoundInstanceInterface { if (this.howl) { this.howl.unload(); this.howl = null; - this.instance = null; } } } +class WrappedSoundInstance extends SoundInstanceInterface { + /** + * + * @param {SoundSpritesContainer} spriteContainer + * @param {string} key + */ + constructor(spriteContainer, key) { + super(key, "sfx.mp3"); + this.spriteContainer = spriteContainer; + } + + /** @returns {Promise} */ + load() { + return this.spriteContainer.load(); + } + + play(volume) { + this.spriteContainer.play(volume, this.key); + } + + deinitialize() { + return this.spriteContainer.deinitialize(); + } +} + class MusicInstance extends MusicInstanceInterface { constructor(key, url) { super(key, url); @@ -72,11 +100,11 @@ class MusicInstance extends MusicInstanceInterface { load() { return Promise.race([ new Promise((resolve, reject) => { - setTimeout(reject, G_IS_DEV ? 5000 : 60000); + setTimeout(reject, G_IS_DEV ? 500 : 5000); }), new Promise((resolve, reject) => { this.howl = new Howl({ - src: cachebust("res/sounds/music/" + this.url), + src: cachebust("res/sounds/music/" + this.url + ".mp3"), autoplay: false, loop: true, html5: true, @@ -84,6 +112,13 @@ class MusicInstance extends MusicInstanceInterface { preload: true, pool: 2, + onunlock: () => { + if (this.playing) { + logger.log("Playing music after manual unlock"); + this.play(); + } + }, + onload: () => { resolve(); }, @@ -133,11 +168,37 @@ class MusicInstance extends MusicInstanceInterface { export class SoundImplBrowser extends SoundInterface { constructor(app) { - super(app, SoundInstance, MusicInstance); + Howler.mobileAutoEnable = true; + Howler.autoUnlock = true; + Howler.autoSuspend = false; + Howler.html5PoolSize = 20; + Howler.pos(0, 0, 0); + + super(app, WrappedSoundInstance, MusicInstance); } initialize() { - return super.initialize(); + this.sfxHandle = new SoundSpritesContainer(); + + // @ts-ignore + const keys = Object.values(SOUNDS); + keys.forEach(key => { + this.sounds[key] = new WrappedSoundInstance(this.sfxHandle, key); + }); + for (const musicKey in MUSIC) { + const musicPath = MUSIC[musicKey]; + const music = new this.musicClass(musicKey, musicPath); + this.music[musicPath] = music; + } + + this.musicMuted = this.app.settings.getAllSettings().musicMuted; + this.soundsMuted = this.app.settings.getAllSettings().soundsMuted; + + if (G_IS_DEV && globalConfig.debug.disableMusic) { + this.musicMuted = true; + } + + return Promise.resolve(); } deinitialize() { diff --git a/src/js/platform/browser/storage.js b/src/js/platform/browser/storage.js index 23cbe700..2a399e54 100644 --- a/src/js/platform/browser/storage.js +++ b/src/js/platform/browser/storage.js @@ -20,6 +20,7 @@ export class StorageImplBrowser extends StorageInterface { } initialize() { + logger.error("Using localStorage, please update to a newer browser"); return new Promise((resolve, reject) => { // Check for local storage availability in general if (!window.localStorage) { diff --git a/src/js/platform/browser/storage_indexed_db.js b/src/js/platform/browser/storage_indexed_db.js new file mode 100644 index 00000000..0c1dbe4f --- /dev/null +++ b/src/js/platform/browser/storage_indexed_db.js @@ -0,0 +1,160 @@ +import { FILE_NOT_FOUND, StorageInterface } from "../storage"; +import { createLogger } from "../../core/logging"; + +const logger = createLogger("storage/browserIDB"); + +const LOCAL_STORAGE_UNAVAILABLE = "local-storage-unavailable"; +const LOCAL_STORAGE_NO_WRITE_PERMISSION = "local-storage-no-write-permission"; + +let randomDelay = () => 0; + +if (G_IS_DEV) { + // Random delay for testing + // randomDelay = () => 500; +} + +export class StorageImplBrowserIndexedDB extends StorageInterface { + constructor(app) { + super(app); + this.currentBusyFilename = false; + + /** @type {IDBDatabase} */ + this.database = null; + } + + initialize() { + logger.log("Using indexed DB storage"); + return new Promise((resolve, reject) => { + const request = window.indexedDB.open("app_storage", 10); + request.onerror = event => { + logger.error("IDB error:", event); + alert( + "Sorry, it seems your browser has blocked the access to the storage system. This might be the case if you are browsing in private mode for example. I recommend to use google chrome or disable private browsing." + ); + reject("Indexed DB access error"); + }; + + // @ts-ignore + request.onsuccess = event => resolve(event.target.result); + + request.onupgradeneeded = /** @type {IDBVersionChangeEvent} */ event => { + /** @type {IDBDatabase} */ + // @ts-ignore + const database = event.target.result; + + const objectStore = database.createObjectStore("files", { + keyPath: "filename", + }); + + objectStore.createIndex("filename", "filename", { unique: true }); + + objectStore.transaction.onerror = event => { + logger.error("IDB transaction error:", event); + reject("Indexed DB transaction error during migration, check console output."); + }; + + objectStore.transaction.oncomplete = event => { + logger.log("Object store completely initialized"); + resolve(database); + }; + }; + }).then(database => { + this.database = database; + }); + } + + writeFileAsync(filename, contents) { + if (this.currentBusyFilename === filename) { + logger.warn("Attempt to write", filename, "while write process is not finished!"); + } + if (!this.database) { + return Promise.reject("Storage not ready"); + } + + this.currentBusyFilename = filename; + const transaction = this.database.transaction(["files"], "readwrite"); + + return new Promise((resolve, reject) => { + transaction.oncomplete = () => { + this.currentBusyFilename = null; + resolve(); + }; + + transaction.onerror = error => { + this.currentBusyFilename = null; + logger.error("Error while writing", filename, ":", error); + reject(error); + }; + + const store = transaction.objectStore("files"); + store.put({ + filename, + contents, + }); + }); + } + + writeFileSyncIfSupported(filename, contents) { + // Not supported + this.writeFileAsync(filename, contents); + return true; + } + + readFileAsync(filename) { + if (!this.database) { + return Promise.reject("Storage not ready"); + } + + this.currentBusyFilename = filename; + const transaction = this.database.transaction(["files"], "readonly"); + + return new Promise((resolve, reject) => { + const store = transaction.objectStore("files"); + const request = store.get(filename); + + request.onsuccess = event => { + this.currentBusyFilename = null; + if (!request.result) { + reject(FILE_NOT_FOUND); + return; + } + resolve(request.result.contents); + }; + + request.onerror = error => { + this.currentBusyFilename = null; + logger.error("Error while reading", filename, ":", error); + reject(error); + }; + }); + } + + deleteFileAsync(filename) { + if (this.currentBusyFilename === filename) { + logger.warn("Attempt to delete", filename, "while write progres on it is ongoing!"); + } + + if (!this.database) { + return Promise.reject("Storage not ready"); + } + + this.currentBusyFilename = filename; + const transaction = this.database.transaction(["files"], "readwrite"); + + return new Promise((resolve, reject) => { + transaction.oncomplete = () => { + this.currentBusyFilename = null; + resolve(); + }; + + transaction.onerror = error => { + this.currentBusyFilename = null; + logger.error("Error while deleting", filename, ":", error); + reject(error); + }; + + const store = transaction.objectStore("files"); + store.delete(filename); + }); + } +} diff --git a/src/js/platform/browser/wrapper.js b/src/js/platform/browser/wrapper.js index 93565342..fa3f80b3 100644 --- a/src/js/platform/browser/wrapper.js +++ b/src/js/platform/browser/wrapper.js @@ -1,18 +1,11 @@ import { Math_min } from "../../core/builtins"; +import { globalConfig, IS_MOBILE, IS_DEBUG, IS_DEMO } from "../../core/config"; import { createLogger } from "../../core/logging"; import { queryParamOptions } from "../../core/query_parameters"; import { clamp } from "../../core/utils"; -import { globalConfig, IS_MOBILE } from "../../core/config"; import { NoAdProvider } from "../ad_providers/no_ad_provider"; import { PlatformWrapperInterface } from "../wrapper"; -import { ShapezioWebsiteEmbedProvider } from "./embed_providers/shapezio_website"; -import { ArmorgamesEmbedProvider } from "./embed_providers/armorgames"; -import { IogamesSpaceEmbedProvider } from "./embed_providers/iogames_space"; -import { MiniclipEmbedProvider } from "./embed_providers/miniclip"; -import { GamedistributionEmbedProvider } from "./embed_providers/gamedistribution"; -import { KongregateEmbedProvider } from "./embed_providers/kongregate"; -import { CrazygamesEmbedProvider } from "./embed_providers/crazygames"; -import { EmbedProvider } from "./embed_provider"; +import { GamedistributionAdProvider } from "../ad_providers/gamedistribution"; const logger = createLogger("platform/browser"); @@ -20,39 +13,54 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { initialize() { this.recaptchaTokenCallback = null; - this.embedProvider = new ShapezioWebsiteEmbedProvider(); + this.embedProvider = { + id: "shapezio-website", + adProvider: NoAdProvider, + iframed: false, + externalLinks: true, + iogLink: true, + unlimitedSavegames: IS_DEMO ? false : true, + showDemoBadge: IS_DEMO, + }; if (!G_IS_STANDALONE && queryParamOptions.embedProvider) { const providerId = queryParamOptions.embedProvider; + this.embedProvider.iframed = true; + this.embedProvider.iogLink = false; switch (providerId) { case "armorgames": { - this.embedProvider = new ArmorgamesEmbedProvider(); + this.embedProvider.id = "armorgames"; break; } case "iogames.space": { - this.embedProvider = new IogamesSpaceEmbedProvider(); + this.embedProvider.id = "iogames.space"; + this.embedProvider.iogLink = true; + this.embedProvider.unlimitedSavegames = true; + this.embedProvider.showDemoBadge = false; break; } case "miniclip": { - this.embedProvider = new MiniclipEmbedProvider(); + this.embedProvider.id = "miniclip"; break; } case "gamedistribution": { - this.embedProvider = new GamedistributionEmbedProvider(); + this.embedProvider.id = "gamedistribution"; + this.embedProvider.externalLinks = false; + this.embedProvider.adProvider = GamedistributionAdProvider; break; } case "kongregate": { - this.embedProvider = new KongregateEmbedProvider(); + this.embedProvider.id = "kongregate"; break; } case "crazygames": { - this.embedProvider = new CrazygamesEmbedProvider(); + this.embedProvider.id = "crazygames"; break; } @@ -62,27 +70,17 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { } } - logger.log("Embed provider:", this.embedProvider.getId()); + logger.log("Embed provider:", this.embedProvider.id); - return super.initialize().then(() => { - // SENTRY - if (!G_IS_DEV && false) { - logger.log(this, "Loading sentry"); - const sentryTag = document.createElement("script"); - sentryTag.src = "https://browser.sentry-cdn.com/5.7.1/bundle.min.js"; - sentryTag.setAttribute("integrity", "TODO_SENTRY"); - sentryTag.setAttribute("crossorigin", "anonymous"); - sentryTag.addEventListener("load", this.onSentryLoaded.bind(this)); - document.head.appendChild(sentryTag); - } - }); + return super.initialize().then(() => this.initializeAdProvider()); } - /** - * @returns {EmbedProvider} - */ - getEmbedProvider() { - return this.embedProvider; + getHasUnlimitedSavegames() { + return this.embedProvider.unlimitedSavegames; + } + + getShowDemoBadges() { + return this.embedProvider.showDemoBadge; } onSentryLoaded() { @@ -151,7 +149,7 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { } getId() { - return "browser@" + this.embedProvider.getId(); + return "browser@" + this.embedProvider.id; } getUiScale() { @@ -173,16 +171,15 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { openExternalLink(url, force = false) { logger.log("Opening external:", url); - // if (force || this.embedProvider.getSupportsExternalLinks()) { - window.open(url); - // } else { - // // Do nothing - // alert("This platform does not allow opening external links. You can play on the website directly to open them."); - // } - } - - getSupportsAds() { - return this.embedProvider.getSupportsAds(); + if (force || this.embedProvider.externalLinks) { + window.open(url); + } else { + // Do nothing + alert( + "This platform does not allow opening external links. You can play on https://shapez.io directly to open them.\n\nClicked Link: " + + url + ); + } } performRestart() { @@ -218,16 +215,18 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { initializeAdProvider() { if (G_IS_DEV && !globalConfig.debug.testAds) { + logger.log("Ads disabled in local environment"); return Promise.resolve(); } // First, detect adblocker return this.detectAdblock().then(hasAdblocker => { if (hasAdblocker) { + logger.log("Adblock detected"); return; } - const adProvider = this.embedProvider.getAdProvider(); + const adProvider = this.embedProvider.adProvider; this.app.adProvider = new adProvider(this.app); return this.app.adProvider.initialize().catch(err => { logger.error("Failed to initialize ad provider, disabling ads:", err); diff --git a/src/js/platform/electron/storage.js b/src/js/platform/electron/storage.js new file mode 100644 index 00000000..7736fcb4 --- /dev/null +++ b/src/js/platform/electron/storage.js @@ -0,0 +1,83 @@ +import { StorageInterface } from "../storage"; +import { getIPCRenderer } from "../../core/utils"; +import { createLogger } from "../../core/logging"; + +const logger = createLogger("electron-storage"); + +export class StorageImplElectron extends StorageInterface { + constructor(app) { + super(app); + + /** @type {Object.} */ + this.jobs = {}; + this.jobId = 0; + + getIPCRenderer().on("fs-response", (event, arg) => { + const id = arg.id; + if (!this.jobs[id]) { + logger.warn("Got unhandled FS response, job not known:", id); + return; + } + const { resolve, reject } = this.jobs[id]; + if (arg.result.success) { + resolve(arg.result.data); + } else { + reject(arg.result.error); + } + }); + } + + initialize() { + return Promise.resolve(); + } + + writeFileAsync(filename, contents) { + return new Promise((resolve, reject) => { + // ipcMain + const jobId = ++this.jobId; + this.jobs[jobId] = { resolve, reject }; + + getIPCRenderer().send("fs-job", { + type: "write", + filename, + contents, + id: jobId, + }); + }); + } + + writeFileSyncIfSupported(filename, contents) { + return getIPCRenderer().sendSync("fs-sync-job", { + type: "write", + filename, + contents, + }); + } + + readFileAsync(filename) { + return new Promise((resolve, reject) => { + // ipcMain + const jobId = ++this.jobId; + this.jobs[jobId] = { resolve, reject }; + + getIPCRenderer().send("fs-job", { + type: "read", + filename, + id: jobId, + }); + }); + } + + deleteFileAsync(filename) { + return new Promise((resolve, reject) => { + // ipcMain + const jobId = ++this.jobId; + this.jobs[jobId] = { resolve, reject }; + getIPCRenderer().send("fs-job", { + type: "delete", + filename, + id: jobId, + }); + }); + } +} diff --git a/src/js/platform/electron/wrapper.js b/src/js/platform/electron/wrapper.js new file mode 100644 index 00000000..70ebf3fc --- /dev/null +++ b/src/js/platform/electron/wrapper.js @@ -0,0 +1,50 @@ +import { PlatformWrapperImplBrowser } from "../browser/wrapper"; +import { getIPCRenderer } from "../../core/utils"; +import { createLogger } from "../../core/logging"; + +const logger = createLogger("electron-wrapper"); + +export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser { + getId() { + return "electron"; + } + + getSupportsRestart() { + return true; + } + + openExternalLink(url) { + logger.log(this, "Opening external:", url); + window.open(url, "about:blank"); + } + + getSupportsAds() { + return false; + } + + performRestart() { + logger.log(this, "Performing restart"); + window.location.reload(true); + } + + initializeAdProvider() { + return Promise.resolve(); + } + + getSupportsFullscreen() { + return true; + } + + setFullscreen(flag) { + getIPCRenderer().send("set-fullscreen", flag); + } + + getSupportsAppExit() { + return true; + } + + exitApp() { + logger.log(this, "Sending app exit signal"); + getIPCRenderer().send("exit-app"); + } +} diff --git a/src/js/platform/game_analytics.js b/src/js/platform/game_analytics.js index 5b70e565..c3e2fa64 100644 --- a/src/js/platform/game_analytics.js +++ b/src/js/platform/game_analytics.js @@ -1,6 +1,7 @@ /* typehints:start */ import { Application } from "../application"; import { ShapeDefinition } from "../game/shape_definition"; +import { Savegame } from "../savegame/savegame"; /* typehints:end */ export class GameAnalyticsInterface { @@ -18,6 +19,11 @@ export class GameAnalyticsInterface { return Promise.reject(); } + /** + * Handles a new game which was started + */ + handleGameStarted() {} + /** * @param {ShapeDefinition} definition */ diff --git a/src/js/platform/sound.js b/src/js/platform/sound.js index c62db515..dc6b073f 100644 --- a/src/js/platform/sound.js +++ b/src/js/platform/sound.js @@ -12,17 +12,23 @@ const logger = createLogger("sound"); export const SOUNDS = { // Menu and such - uiClick: "ui/ui_click.mp3", - uiError: "ui/ui_error.mp3", - dialogError: "ui/dialog_error.mp3", - dialogOk: "ui/dialog_ok.mp3", - swishHide: "ui/ui_swish_hide.mp3", - swishShow: "ui/ui_swish_show.mp3", + uiClick: "ui_click", + uiError: "ui_error", + dialogError: "dialog_error", + dialogOk: "dialog_ok", + swishHide: "ui_swish_hide", + swishShow: "ui_swish_show", + badgeNotification: "badge_notification", + + levelComplete: "level_complete", + + placeBuilding: "place_building", + placeBelt: "place_belt", }; export const MUSIC = { - mainMenu: "main_menu.mp3", - gameBg: "theme_full.mp3", + theme: "theme", + menu: "menu", }; export class SoundInstanceInterface { @@ -113,11 +119,8 @@ export class SoundInterface { this.music[musicPath] = music; } - // this.musicMuted = this.app.userProfile.getMusicMuted(); - // this.soundsMuted = this.app.userProfile.getSoundsMuted(); - - this.musicMuted = false; - this.soundsMuted = false; + this.musicMuted = this.app.settings.getAllSettings().musicMuted; + this.soundsMuted = this.app.settings.getAllSettings().soundsMuted; if (G_IS_DEV && globalConfig.debug.disableMusic) { this.musicMuted = true; @@ -142,7 +145,9 @@ export class SoundInterface { } } - /** Deinits the sound */ + /** Deinits the sound + * @returns {Promise} + */ deinitialize() { const promises = []; for (const key in this.sounds) { @@ -151,7 +156,8 @@ export class SoundInterface { for (const key in this.music) { promises.push(this.music[key].deinitialize()); } - return Promise.all(promises); + // @ts-ignore + return Promise.all(...promises); } /** diff --git a/src/js/platform/wrapper.js b/src/js/platform/wrapper.js index bda56fc2..5754a8a2 100644 --- a/src/js/platform/wrapper.js +++ b/src/js/platform/wrapper.js @@ -29,6 +29,17 @@ export class PlatformWrapperInterface { return false; } + /** + * Whether the user has unlimited savegames + */ + getHasUnlimitedSavegames() { + return true; + } + + getShowDemoBadges() { + return false; + } + /** * Returns the strength of touch pans with the mouse */ @@ -55,7 +66,7 @@ export class PlatformWrapperInterface { * @returns {number} */ getMinimumZoom() { - return 0.2 * this.getScreenScale(); + return 0.1 * this.getScreenScale(); } /** diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index 6d09c148..96cbed2f 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -6,6 +6,8 @@ import { ReadWriteProxy } from "../core/read_write_proxy"; import { BoolSetting, EnumSetting, BaseSetting } from "./setting_types"; import { createLogger } from "../core/logging"; import { ExplainedResult } from "../core/explained_result"; +import { THEMES, THEME, applyGameTheme } from "../game/theme"; +import { IS_DEMO } from "../core/config"; const logger = createLogger("application_settings"); @@ -65,10 +67,56 @@ export const allApplicationSettings = [ app.platformWrapper.setFullscreen(value); } }, - G_IS_STANDALONE + !IS_DEMO + ), + + new BoolSetting( + "soundsMuted", + categoryApp, + /** + * @param {Application} app + */ + (app, value) => app.sound.setSoundsMuted(value) + ), + new BoolSetting( + "musicMuted", + categoryApp, + /** + * @param {Application} app + */ + (app, value) => app.sound.setMusicMuted(value) ), // GAME + new EnumSetting("theme", { + options: Object.keys(THEMES), + valueGetter: theme => theme, + textGetter: theme => theme.substr(0, 1).toUpperCase() + theme.substr(1), + category: categoryGame, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => { + applyGameTheme(id); + document.body.setAttribute("data-theme", id); + }, + enabled: !IS_DEMO, + }), + + new EnumSetting("refreshRate", { + options: ["60", "100", "144", "165"], + valueGetter: rate => rate, + textGetter: rate => rate + " Hz", + category: categoryGame, + restartRequired: false, + changeCb: (app, id) => {}, + enabled: !IS_DEMO, + }), + + new BoolSetting("alwaysMultiplace", categoryGame, (app, value) => {}), + new BoolSetting("offerHints", categoryGame, (app, value) => {}), ]; export function getApplicationSettingById(id) { @@ -79,6 +127,19 @@ class SettingsStorage { constructor() { this.uiScale = "regular"; this.fullscreen = G_IS_STANDALONE; + + this.soundsMuted = false; + this.musicMuted = false; + this.theme = "light"; + this.refreshRate = "60"; + + this.alwaysMultiplace = false; + this.offerHints = true; + + /** + * @type {Object.} + */ + this.keybindingOverrides = {}; } } @@ -89,7 +150,17 @@ export class ApplicationSettings extends ReadWriteProxy { initialize() { // Read and directly write latest data back - return this.readAsync().then(() => this.writeAsync()); + return this.readAsync() + .then(() => { + // Apply default setting callbacks + const settings = this.getAllSettings(); + for (let i = 0; i < allApplicationSettings.length; ++i) { + const handle = allApplicationSettings[i]; + handle.apply(this.app, settings[handle.id]); + } + }) + + .then(() => this.writeAsync()); } save() { @@ -121,6 +192,10 @@ export class ApplicationSettings extends ReadWriteProxy { return this.getAllSettings().uiScale; } + getDesiredFps() { + return parseInt(this.getAllSettings().refreshRate); + } + getInterfaceScaleValue() { const id = this.getInterfaceScaleId(); for (let i = 0; i < uiScales.length; ++i) { @@ -136,6 +211,10 @@ export class ApplicationSettings extends ReadWriteProxy { return this.getAllSettings().fullscreen; } + getKeybindingOverrides() { + return this.getAllSettings().keybindingOverrides; + } + // Setters /** @@ -143,8 +222,46 @@ export class ApplicationSettings extends ReadWriteProxy { * @param {string|boolean} value */ updateSetting(key, value) { - assert(this.getAllSettings().hasOwnProperty(key), "Setting not known: " + key); - this.getAllSettings()[key] = value; + for (let i = 0; i < allApplicationSettings.length; ++i) { + const setting = allApplicationSettings[i]; + if (setting.id === key) { + if (!setting.validate(value)) { + assertAlways(false, "Bad setting value: " + key); + } + this.getAllSettings()[key] = value; + if (setting.changeCb) { + setting.changeCb(this.app, value); + } + return this.writeAsync(); + } + } + assertAlways(false, "Unknown setting: " + key); + } + + /** + * Sets a new keybinding override + * @param {string} keybindingId + * @param {number} keyCode + */ + updateKeybindingOverride(keybindingId, keyCode) { + assert(Number.isInteger(keyCode), "Not a valid key code: " + keyCode); + this.getAllSettings().keybindingOverrides[keybindingId] = keyCode; + return this.writeAsync(); + } + + /** + * Resets a given keybinding override + * @param {string} id + */ + resetKeybindingOverride(id) { + delete this.getAllSettings().keybindingOverrides[id]; + return this.writeAsync(); + } + /** + * Resets all keybinding overrides + */ + resetKeybindingOverrides() { + this.getAllSettings().keybindingOverrides = {}; return this.writeAsync(); } @@ -176,14 +293,26 @@ export class ApplicationSettings extends ReadWriteProxy { } getCurrentVersion() { - return 1; + return 7; } + /** @param {{settings: SettingsStorage, version: number}} data */ migrate(data) { - // Simply reset - if (data.version < 1) { + // Simply reset before + if (data.version < 5) { data.settings = new SettingsStorage(); - data.version = 1; + data.version = this.getCurrentVersion(); + return ExplainedResult.good(); + } + + if (data.version < 6) { + data.settings.alwaysMultiplace = false; + data.version = 6; + } + + if (data.version < 7) { + data.settings.offerHints = true; + data.version = 7; } return ExplainedResult.good(); diff --git a/src/js/profile/setting_types.js b/src/js/profile/setting_types.js index f35d17a6..3b1b3dd7 100644 --- a/src/js/profile/setting_types.js +++ b/src/js/profile/setting_types.js @@ -3,9 +3,12 @@ import { Application } from "../application"; /* typehints:end */ import { createLogger } from "../core/logging"; +import { T } from "../translations"; const logger = createLogger("setting_types"); +const standaloneOnlySettingHtml = `${T.demo.settingNotAvailable}`; + export class BaseSetting { /** * @@ -27,10 +30,20 @@ export class BaseSetting { this.dialogs = null; } + /** + * @param {Application} app + * @param {any} value + */ + apply(app, value) { + if (this.changeCb) { + this.changeCb(app, value); + } + } + /** * @param {Application} app * @param {Element} element - * @param {HUDModalDialogs} dialogs + * @param {any} dialogs */ bind(app, element, dialogs) { this.app = app; @@ -53,8 +66,8 @@ export class BaseSetting { showRestartRequiredDialog() { const { restart } = this.dialogs.showInfo( - "Restart required", - "You need to restart the game to apply the settings.", + T.dialogs.restartRequired.title, + T.dialogs.restartRequired.text, this.app.platformWrapper.getSupportsRestart() ? ["later:grey", "restart:misc"] : ["ok:good"] ); if (restart) { @@ -102,12 +115,13 @@ export class EnumSetting extends BaseSetting { getHtml() { return `
+ ${this.enabled ? "" : standaloneOnlySettingHtml}
- +
- TODO: SETTING DESC + ${T.settings.labels[this.id].description}
`; } @@ -143,7 +157,7 @@ export class EnumSetting extends BaseSetting { } modify() { - const { optionSelected } = this.dialogs.showOptionChooser("TODO: SETTING TITLE", { + const { optionSelected } = this.dialogs.showOptionChooser(T.settings.labels[this.id].title, { active: this.app.settings.getSetting(this.id), options: this.options.map(option => ({ value: this.valueGetter(option), @@ -175,14 +189,16 @@ export class BoolSetting extends BaseSetting { getHtml() { return `
+ ${this.enabled ? "" : standaloneOnlySettingHtml} +
- +
- TODO: SETTING DESC + ${T.settings.labels[this.id].description}
`; } diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index f752c836..8b9d2b3b 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -11,6 +11,8 @@ import { createLogger } from "../core/logging"; import { globalConfig } from "../core/config"; import { SavegameInterface_V1000 } from "./schemas/1000"; import { getSavegameInterface } from "./savegame_interface_registry"; +import { compressObject } from "./savegame_compressor"; +import { compressX64 } from "../core/lzstring"; const logger = createLogger("savegame"); @@ -37,7 +39,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1015; + return 1000; } /** @@ -74,13 +76,9 @@ export class Savegame extends ReadWriteProxy { * @param {SavegameData} data */ migrate(data) { - // if (data.version === 1014) { - // if (data.dump) { - // const reader = new SavegameInterface_V1015(fakeLogger, data); - // reader.migrateFrom1014(); - // } - // data.version = 1015; - // } + if (data.version < 1000) { + return ExplainedResult.bad("Can not migrate savegame, too old"); + } return ExplainedResult.good(); } @@ -129,7 +127,7 @@ export class Savegame extends ReadWriteProxy { * Returns if this game has a serialized game dump */ hasGameDump() { - return !!this.currentData.dump; + return !!this.currentData.dump && this.currentData.dump.entities.length > 0; } /** @@ -185,8 +183,6 @@ export class Savegame extends ReadWriteProxy { if (!dump) { return false; } - // let duration = performanceNow() - timer; - // console.log("TOOK", duration, "ms to generate dump:", dump); const shadowData = Object.assign({}, this.currentData); shadowData.dump = dump; @@ -218,7 +214,6 @@ export class Savegame extends ReadWriteProxy { * Updates the savegames metadata */ saveMetadata() { - const reader = this.getDumpReader(); this.metaDataRef.lastUpdate = new Date().getTime(); this.metaDataRef.version = this.getCurrentVersion(); return this.app.savegameMgr.writeAsync(); diff --git a/src/js/savegame/savegame_compressor.js b/src/js/savegame/savegame_compressor.js new file mode 100644 index 00000000..4962171e --- /dev/null +++ b/src/js/savegame/savegame_compressor.js @@ -0,0 +1,134 @@ +const charmap = + "!#%&'()*+,-./:;<=>?@[]^_`{|}~¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +let compressionCache = {}; +let decompressionCache = {}; + +/** + * Compresses an integer into a tight string representation + * @param {number} i + * @returns {string} + */ +function compressInt(i) { + // Zero value breaks + i += 1; + + if (compressionCache[i]) { + return compressionCache[i]; + } + let result = ""; + do { + result += charmap[i % charmap.length]; + i = Math.floor(i / charmap.length); + } while (i > 0); + return (compressionCache[i] = result); +} + +/** + * Decompresses an integer from its tight string representation + * @param {string} s + * @returns {number} + */ +function decompressInt(s) { + if (decompressionCache[s]) { + return decompressionCache[s]; + } + s = "" + s; + let result = 0; + for (let i = s.length - 1; i >= 0; --i) { + result = result * charmap.length + charmap.indexOf(s.charAt(i)); + } + // Fixes zero value break fix from above + result -= 1; + return (decompressionCache[s] = result); +} + +// Sanity +for (let i = 0; i < 10000; ++i) { + if (decompressInt(compressInt(i)) !== i) { + throw new Error( + "Bad compression for: " + + i + + " compressed: " + + compressInt(i) + + " decompressed: " + + decompressInt(compressInt(i)) + ); + } +} + +function compressObjectInternal(obj, keys = [], values = []) { + if (Array.isArray(obj)) { + let result = []; + for (let i = 0; i < obj.length; ++i) { + result.push(compressObjectInternal(obj[i], keys, values)); + } + return result; + } else if (typeof obj === "object" && obj !== null) { + let result = {}; + for (const key in obj) { + let index = keys.indexOf(key); + if (index < 0) { + keys.push(key); + index = keys.length - 1; + } + const value = obj[key]; + result[compressInt(index)] = compressObjectInternal(value, keys, values); + } + return result; + } else if (typeof obj === "string") { + let index = values.indexOf(obj); + if (index < 0) { + values.push(obj); + index = values.length - 1; + } + return compressInt(index); + } + return obj; +} + +export function compressObject(obj) { + if (G_IS_DEV) { + return obj; + } + const keys = []; + const values = []; + const data = compressObjectInternal(obj, keys, values); + return { + keys, + values, + data, + }; +} + +function decompressObjectInternal(obj, keys = [], values = []) { + if (Array.isArray(obj)) { + let result = []; + for (let i = 0; i < obj.length; ++i) { + result.push(decompressObjectInternal(obj[i], keys, values)); + } + return result; + } else if (typeof obj === "object" && obj !== null) { + let result = {}; + for (const key in obj) { + const realIndex = decompressInt(key); + const value = obj[key]; + result[keys[realIndex]] = decompressObjectInternal(value, keys, values); + } + return result; + } else if (typeof obj === "string") { + const realIndex = decompressInt(obj); + return values[realIndex]; + } + return obj; +} + +export function decompressObject(obj) { + if (obj.keys && obj.values && obj.data) { + const keys = obj.keys; + const values = obj.values; + const result = decompressObjectInternal(obj.data, keys, values); + return result; + } + return obj; +} diff --git a/src/js/savegame/savegame_interface.js b/src/js/savegame/savegame_interface.js index 66cada22..8671471d 100644 --- a/src/js/savegame/savegame_interface.js +++ b/src/js/savegame/savegame_interface.js @@ -26,7 +26,6 @@ export class BaseSavegameInterface { */ getSchemaUncached() { throw new Error("Implement get schema"); - return {}; } getValidator() { @@ -98,7 +97,7 @@ export class BaseSavegameInterface { //////// ANTICHEAT /////// /** - * Detects cheats in the savegmae - returns false if the game looks cheated + * Detects cheats in the savegame - returns false if the game looks cheated */ performAnticheatCheck() { // TODO diff --git a/src/js/savegame/savegame_manager.js b/src/js/savegame/savegame_manager.js index 69a275bb..6b63d721 100644 --- a/src/js/savegame/savegame_manager.js +++ b/src/js/savegame/savegame_manager.js @@ -154,6 +154,22 @@ export class SavegameManager extends ReadWriteProxy { }); } + importSavegame(data) { + const savegame = this.createNewSavegame(); + const migrationResult = savegame.migrate(data); + if (migrationResult.isBad()) { + return Promise.reject("Failed to migrate: " + migrationResult.reason); + } + + savegame.currentData = data; + const verification = savegame.verify(data); + if (verification.isBad()) { + return Promise.reject("Verification failed: " + verification.result); + } + + return savegame.writeSavegameAndMetadata().then(() => this.sortSavegames()); + } + /** * Sorts all savegames by their creation time descending * @returns {Promise} @@ -161,7 +177,7 @@ export class SavegameManager extends ReadWriteProxy { sortSavegames() { this.currentData.savegames.sort((a, b) => b.lastUpdate - a.lastUpdate); let promiseChain = Promise.resolve(); - while (this.currentData.savegames.length > 100) { + while (this.currentData.savegames.length > 30) { const toRemove = this.currentData.savegames.pop(); // Try to remove the savegame since its no longer available @@ -186,14 +202,9 @@ export class SavegameManager extends ReadWriteProxy { * Helper method to generate a new internal savegame id */ generateInternalId() { - const timestamp = ("" + Math_floor(Date.now() / 1000.0 - 1565641619)).padStart(10, "0"); - return ( - timestamp + - "." + - Rusha.createHash() - .update(Date.now() + "/" + Math.random()) - .digest("hex") - ); + return Rusha.createHash() + .update(Date.now() + "/" + Math.random()) + .digest("hex"); } // End @@ -205,7 +216,7 @@ export class SavegameManager extends ReadWriteProxy { if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) { return Promise.resolve(); } - return this.writeAsync(); + return this.sortSavegames().then(() => this.writeAsync()); }); } } diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index 0c02e550..5c46e07c 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -24,7 +24,7 @@ export class SavegameSerializer { * Serializes the game root into a dump * @param {GameRoot} root * @param {boolean=} sanityChecks Whether to check for validity - * @returns {SerializedGame} + * @returns {object} */ generateDumpFromGameRoot(root, sanityChecks = true) { // Finalize particles before saving (Like granting destroy indicator rewards) @@ -32,21 +32,15 @@ export class SavegameSerializer { // root.uiParticleMgr.finalizeBeforeSave(); // Now store generic savegame payload - const data = /** @type {SerializedGame} */ ({ + const data = { camera: root.camera.serialize(), time: root.time.serialize(), + map: root.map.serialize(), entityMgr: root.entityMgr.serialize(), - entities: {}, - }); + hubGoals: root.hubGoals.serialize(), + }; - // Serialize all types of entities - const serializeEntities = component => - this.internal.serializeEntityArray(root.entityMgr.getAllWithComponent(component)); - const serializeEntitiesFixed = component => - this.internal.serializeEntityArrayFixedType(root.entityMgr.getAllWithComponent(component)); - - // data.entities.resources = serializeEntitiesFixed(RawMaterialComponent); - // data.entities.buildings = serializeEntities(BuildingComponent); + data.entities = this.internal.serializeEntityArray(root.entityMgr.entities); if (!G_IS_RELEASE) { if (sanityChecks) { @@ -58,13 +52,12 @@ export class SavegameSerializer { } } } - return data; } /** * Verifies if there are logical errors in the savegame - * @param {SerializedGame} savegame + * @param {object} savegame * @returns {ExplainedResult} */ verifyLogicalErrors(savegame) { @@ -135,33 +128,14 @@ export class SavegameSerializer { if (!verifyResult.result) { return ExplainedResult.bad(verifyResult.reason); } - let errorReason = null; - // entities errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr); - - // resources - errorReason = - errorReason || - this.internal.deserializeEntityArrayFixedType( - root, - savegame.entities.resources, - this.internal.deserializeResource - ); - - // buildings - errorReason = - errorReason || - this.internal.deserializeEntityArray( - root, - savegame.entities.buildings, - this.internal.deserializeBuilding - ); - - // other stuff errorReason = errorReason || root.time.deserialize(savegame.time); errorReason = errorReason || root.camera.deserialize(savegame.camera); + errorReason = errorReason || root.map.deserialize(savegame.map); + errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals); + errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); // Check for errors if (errorReason) { diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index 3ab73761..ca72d856 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -18,10 +18,9 @@ * camera: any, * time: any, * entityMgr: any, - * entities: { - * resources: Array, - * buildings: Array - * } + * map: any, + * hubGoals: any, + * entities: Array * }} SerializedGame */ diff --git a/src/js/savegame/serialization.js b/src/js/savegame/serialization.js index a6c8f4c0..7ab6f678 100644 --- a/src/js/savegame/serialization.js +++ b/src/js/savegame/serialization.js @@ -21,6 +21,7 @@ import { TypeVector, TypeClassFromMetaclass, TypeClassData, + TypeStructuredObject, } from "./serialization_data_types"; import { createLogger } from "../core/logging"; @@ -61,7 +62,7 @@ export const types = { }, /** - * @param {Array} values + * @param {Object} values */ enum(values) { return new TypeEnum(values); @@ -102,6 +103,13 @@ export const types = { return new TypeMetaClass(registry); }, + /** + * @param {Object.} descriptor + */ + structured(descriptor) { + return new TypeStructuredObject(descriptor); + }, + /** * @param {BaseDataType} a * @param {BaseDataType} b @@ -215,7 +223,7 @@ export function serializeSchema(obj, schema, mergeWith = {}) { ); } if (!schema[key]) { - assert(false, "Invalid schema: " + JSON_stringify(schema) + " / " + key); + assert(false, "Invalid schema (bad key '" + key + "'): " + JSON_stringify(schema)); } if (G_IS_DEV) { @@ -270,8 +278,7 @@ export function deserializeSchema(obj, schema, data, baseclassErrorResult = null const errorStatus = schema[key].deserializeWithVerify(data[key], obj, key, obj.root); if (errorStatus) { - error( - "serialization", + logger.error( "Deserialization failed with error '" + errorStatus + "' on object", obj, "and key", @@ -294,17 +301,17 @@ export function deserializeSchema(obj, schema, data, baseclassErrorResult = null export function verifySchema(schema, data) { for (const key in schema) { if (!data.hasOwnProperty(key)) { - error("verify", "Data", data, "does not contain", key, "(schema:", schema, ")"); + logger.error("Data", data, "does not contain", key, "(schema:", schema, ")"); return "verify: missing key required by schema in stored data: " + key; } if (!schema[key].allowNull() && (data[key] === null || data[key] === undefined)) { - error("verify", "Data", data, "has null value for", key, "(schema:", schema, ")"); + logger.error("Data", data, "has null value for", key, "(schema:", schema, ")"); return "verify: non-nullable entry is null: " + key; } const errorStatus = schema[key].verifySerializedValue(data[key]); if (errorStatus) { - error("verify", errorStatus); + logger.error(errorStatus); return "verify: " + errorStatus; } } diff --git a/src/js/savegame/serialization_data_types.js b/src/js/savegame/serialization_data_types.js index 240e2313..332dc274 100644 --- a/src/js/savegame/serialization_data_types.js +++ b/src/js/savegame/serialization_data_types.js @@ -4,7 +4,7 @@ import { BasicSerializableObject } from "./serialization"; /* typehints:end */ import { Vector } from "../core/vector"; -import { round4Digits, schemaObject } from "../core/utils"; +import { round4Digits, schemaObject, accessNestedPropertyReverse } from "../core/utils"; import { JSON_stringify } from "../core/builtins"; export const globalJsonSchemaDefs = {}; @@ -458,11 +458,11 @@ export class TypePositiveNumber extends BaseDataType { export class TypeEnum extends BaseDataType { /** - * @param {Array} availableValues + * @param {Object.} enumeration */ - constructor(availableValues = []) { + constructor(enumeration = {}) { super(); - this.availableValues = availableValues; + this.availableValues = Object.keys(enumeration); } serialize(value) { @@ -664,7 +664,7 @@ export class TypeClass extends BaseDataType { } if (!this.registry.hasId(value.$)) { - return "Invalid class id: " + value.$; + return "Invalid class id: " + value.$ + " (factory is " + this.registry.getId() + ")"; } } @@ -709,7 +709,7 @@ export class TypeClassData extends BaseDataType { * @returns {string|void} String error code or null on success */ deserialize(value, targetObject, targetKey, root) { - assert(false, "can not deserialize class data"); + assert(false, "can not deserialize class data of type " + this.registry.getId()); } verifySerializedValue(value) { @@ -785,7 +785,7 @@ export class TypeClassFromMetaclass extends BaseDataType { } if (!this.registry.hasId(value.$)) { - return "Invalid class id: " + value.$; + return "Invalid class id: " + value.$ + " (factory is " + this.registry.getId() + ")"; } } @@ -841,7 +841,7 @@ export class TypeMetaClass extends BaseDataType { } if (!this.registry.hasId(value)) { - return "Invalid class id: " + value; + return "Invalid class id: " + value + " (factory is " + this.registry.getId() + ")"; } } @@ -1079,6 +1079,8 @@ export class TypePair extends BaseDataType { */ constructor(type1, type2) { super(); + assert(type1 && type1 instanceof BaseDataType, "bad first type given for pair"); + assert(type2 && type2 instanceof BaseDataType, "bad second type given for pair"); this.type1 = type1; this.type2 = type2; } @@ -1100,12 +1102,11 @@ export class TypePair extends BaseDataType { deserialize(value, targetObject, targetKey, root) { const result = [undefined, undefined]; - let errorCode = this.type1.deserialize(value, result, 0, root); + let errorCode = this.type1.deserialize(value[0], result, 0, root); if (errorCode) { return errorCode; } - - errorCode = this.type2.deserialize(value, result, 1, root); + errorCode = this.type2.deserialize(value[1], result, 1, root); if (errorCode) { return errorCode; } @@ -1202,3 +1203,79 @@ export class TypeNullable extends BaseDataType { return "nullable." + this.wrapped.getCacheKey(); } } + +export class TypeStructuredObject extends BaseDataType { + /** + * @param {Object.} descriptor + */ + constructor(descriptor) { + super(); + this.descriptor = descriptor; + } + + serialize(value) { + assert(typeof value === "object", "not an object"); + let result = {}; + for (const key in this.descriptor) { + // assert(value.hasOwnProperty(key), "Serialization: Object does not have", key, "property!"); + result[key] = this.descriptor[key].serialize(value[key]); + } + return result; + } + + /** + * @see BaseDataType.deserialize + * @param {any} value + * @param {GameRoot} root + * @param {object} targetObject + * @param {string|number} targetKey + * @returns {string|void} String error code or null on success + */ + deserialize(value, targetObject, targetKey, root) { + let result = {}; + for (const key in value) { + const valueType = this.descriptor[key]; + const errorCode = valueType.deserializeWithVerify(value[key], result, key, root); + if (errorCode) { + return errorCode; + } + } + targetObject[targetKey] = result; + } + + getAsJsonSchemaUncached() { + let properties = {}; + for (const key in this.descriptor) { + properties[key] = this.descriptor[key].getAsJsonSchema(); + } + + return { + type: "object", + required: Object.keys(this.descriptor), + properties, + }; + } + + verifySerializedValue(value) { + if (typeof value !== "object") { + return "structured object is not an object"; + } + for (const key in this.descriptor) { + if (!value.hasOwnProperty(key)) { + return "structured object is missing key " + key; + } + const subError = this.descriptor[key].verifySerializedValue(value[key]); + if (subError) { + return "structured object::" + subError; + } + } + } + + getCacheKey() { + let props = []; + for (const key in this.descriptor) { + props.push(key + "=" + this.descriptor[key].getCacheKey()); + } + return "structured[" + props.join(",") + "]"; + } +} diff --git a/src/js/savegame/serializer_internal.js b/src/js/savegame/serializer_internal.js index 298871e5..3eb0f72b 100644 --- a/src/js/savegame/serializer_internal.js +++ b/src/js/savegame/serializer_internal.js @@ -2,13 +2,10 @@ import { GameRoot } from "../game/root"; /* typehints:end */ -import { Vector } from "../core/vector"; +import { gComponentRegistry } from "../core/global_registries"; import { createLogger } from "../core/logging"; -import { gMetaBuildingRegistry } from "../core/global_registries"; import { Entity } from "../game/entity"; -const logger = createLogger("serializer_internal"); - // Internal serializer methods export class SerializerInternal { constructor() {} @@ -18,24 +15,6 @@ export class SerializerInternal { * @param {Array} array */ serializeEntityArray(array) { - const serialized = []; - for (let i = 0; i < array.length; ++i) { - const entity = array[i]; - if (!entity.queuedForDestroy && !entity.destroyed) { - serialized.push({ - $: entity.getMetaclass().getId(), - data: entity.serialize(), - }); - } - } - return serialized; - } - - /** - * Serializes an array of entities where we know the type of - * @param {Array} array - */ - serializeEntityArrayFixedType(array) { const serialized = []; for (let i = 0; i < array.length; ++i) { const entity = array[i]; @@ -50,89 +29,28 @@ export class SerializerInternal { * * @param {GameRoot} root * @param {Array} array - * @param {function(GameRoot, { $: string, data: object }):string|void} deserializerMethod * @returns {string|void} */ - deserializeEntityArray(root, array, deserializerMethod) { + deserializeEntityArray(root, array) { for (let i = 0; i < array.length; ++i) { - const errorState = deserializerMethod.call(this, root, array[i]); - if (errorState) { - return errorState; - } + this.deserializeEntity(root, array[i]); } - return null; } /** * * @param {GameRoot} root - * @param {Array} array - * @param {function(GameRoot, object):string|void} deserializerMethod - * @returns {string|void} + * @param {Entity} payload */ - deserializeEntityArrayFixedType(root, array, deserializerMethod) { - for (let i = 0; i < array.length; ++i) { - const errorState = deserializerMethod.call(this, root, array[i]); - if (errorState) { - return errorState; - } + deserializeEntity(root, payload) { + const entity = new Entity(null); + this.deserializeComponents(entity, payload.components); + + root.entityMgr.registerEntity(entity, payload.uid); + + if (entity.components.StaticMapEntity) { + root.map.placeStaticEntity(entity); } - return null; - } - - /** - * Deserializes a building - * @param {GameRoot} root - * @param {{ $: string, data: any }} payload - */ - deserializeBuilding(root, payload) { - const data = payload.data; - const id = payload.$; - if (!gMetaBuildingRegistry.hasId(id)) { - return "Metaclass not found for building: '" + id + "'"; - } - const meta = gMetaBuildingRegistry.findById(id); - if (!meta) { - return "Metaclass not found for building: '" + id + "'"; - } - - const tile = new Vector(data.x, data.y).toTileSpace(); - const instance = root.logic.internalPlaceBuildingLocalClientOnly({ - tile: tile, - metaBuilding: meta, - uid: data.uid, - }); - - // Apply component specific properties - const errorStatus = this.deserializeComponents(instance, data.components); - if (errorStatus) { - return errorStatus; - } - - // Apply enhancements - instance.updateEnhancements(); - } - - /** - * Deserializes a blueprint - * @param {GameRoot} root - * @param {any} data - * @returns {string|void} - */ - deserializeBlueprint(root, data) { - const id = data.meta; - const metaClass = gMetaBuildingRegistry.findById(id); - if (!metaClass) { - return "Metaclass not found for blueprint: '" + id + "'"; - } - - const tile = new Vector(data.x, data.y).toTileSpace(); - const instance = root.logic.internalPlaceBlueprintLocalClientOnly({ - tile: tile, - metaBuilding: metaClass, - uid: data.uid, - }); - return this.deserializeComponents(instance, data.components); } /////// COMPONENTS //// @@ -145,36 +63,13 @@ export class SerializerInternal { */ deserializeComponents(entity, data) { for (const componentId in data) { - const componentHandle = entity.components[componentId]; - if (!componentHandle) { - logger.warn( - "Loading outdated savegame, where entity had component", - componentId, - "but now no longer has" - ); - continue; - } - const componentData = data[componentId]; - const errorStatus = componentHandle.deserialize(componentData); + const componentClass = gComponentRegistry.findById(componentId); + const componentHandle = new componentClass({}); + entity.addComponent(componentHandle); + const errorStatus = componentHandle.deserialize(data[componentId]); if (errorStatus) { return errorStatus; } } } - - /** - * Deserializes a resource - * @param {GameRoot} root - * @param {object} data - * @returns {string|void} - */ - deserializeResource(root, data) { - const id = data.key; - const instance = new MapResource(root, this.neutralFaction, id); - root.logic.internalPlaceMapEntityLocalClientOnly( - new Vector(data.x, data.y).toTileSpace(), - instance, - data.uid - ); - } } diff --git a/src/js/states/about.js b/src/js/states/about.js new file mode 100644 index 00000000..e527b737 --- /dev/null +++ b/src/js/states/about.js @@ -0,0 +1,45 @@ +import { TextualGameState } from "../core/textual_game_state"; +import { SOUNDS } from "../platform/sound"; +import { T } from "../translations"; +import { KEYMAPPINGS, getStringForKeyCode } from "../game/key_action_mapper"; +import { Dialog } from "../core/modal_dialog_elements"; +import { THIRDPARTY_URLS } from "../core/config"; + +export class AboutState extends TextualGameState { + constructor() { + super("AboutState"); + } + + getStateHeaderTitle() { + return T.about.title; + } + + getMainContentHTML() { + return ` + This game is open source and developed by Tobias Springer (this is me). +

+ If you want to contribute, check out shapez.io on github. +

+ This game wouldn't have been possible without the great discord community arround my games - You should really join the discord server! +

+ The soundtrack was made by Peppsen - He's awesome. +

+ Finally, huge thanks to my best friend Niklas - Without our factorio sessions this game would never have existed. + `; + } + + onEnter() { + const links = this.htmlElement.querySelectorAll("a[href]"); + links.forEach(link => { + this.trackClicks( + link, + () => this.app.platformWrapper.openExternalLink(link.getAttribute("href")), + { preventClick: true } + ); + }); + } + + getDefaultPreviousState() { + return "SettingsState"; + } +} diff --git a/src/js/states/changelog.js b/src/js/states/changelog.js new file mode 100644 index 00000000..498162d3 --- /dev/null +++ b/src/js/states/changelog.js @@ -0,0 +1,45 @@ +import { TextualGameState } from "../core/textual_game_state"; +import { T } from "../translations"; +import { CHANGELOG } from "../changelog"; + +export class ChangelogState extends TextualGameState { + constructor() { + super("ChangelogState"); + } + + getStateHeaderTitle() { + return T.changelog.title; + } + + getMainContentHTML() { + const entries = CHANGELOG; + + let html = ""; + + for (let i = 0; i < entries.length; ++i) { + const entry = entries[i]; + html += ` +
+ ${entry.version} + ${entry.date} +
    + ${entry.entries.map(text => `
  • ${text}
  • `).join("")} +
+
+ `; + } + + return html; + } + + onEnter() { + const links = this.htmlElement.querySelectorAll("a[href]"); + links.forEach(link => { + this.trackClicks( + link, + () => this.app.platformWrapper.openExternalLink(link.getAttribute("href")), + { preventClick: true } + ); + }); + } +} diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index 6a400a14..6307a22c 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -91,13 +91,12 @@ export class InGameState extends GameState { } getThemeMusic() { - // set later - return MUSIC.gameBg; + return MUSIC.theme; } onBeforeExit() { logger.log("Saving before quitting"); - return this.doSave(true, true).then(() => { + return this.doSave().then(() => { logger.log(this, "Successfully saved"); // this.stageDestroyed(); }); @@ -106,7 +105,7 @@ export class InGameState extends GameState { onAppPause() { if (this.stage === stages.s10_gameRunning) { logger.log("Saving because app got paused"); - this.doSave(true, true); + this.doSave(); } } @@ -115,7 +114,7 @@ export class InGameState extends GameState { } getPauseOnFocusLost() { - return !this.isMultiplayer(); + return false; } getHasUnloadConfirmation() { @@ -145,6 +144,16 @@ export class InGameState extends GameState { this.saveThenGoToState("MainMenuState"); } + /** + * Goes back to the settings state + */ + goToSettings() { + this.saveThenGoToState("SettingsState", { + backToStateId: this.key, + backToStatePayload: this.creationPayload, + }); + } + /** * Moves to a state outside of the game * @param {string} stateId @@ -162,7 +171,7 @@ export class InGameState extends GameState { return; } this.stageLeavingGame(); - this.doSave(false, true).then(() => { + this.doSave().then(() => { this.stageDestroyed(); this.moveToState(stateId, payload); }); @@ -198,8 +207,10 @@ export class InGameState extends GameState { this.core.initializeRoot(this, this.savegame); if (this.savegame.hasGameDump()) { + this.app.gameAnalytics.handleGameStarted(); this.stage4bResumeGame(); } else { + this.app.gameAnalytics.handleGameStarted(); this.stage4aInitEmptyGame(); } } @@ -396,16 +407,10 @@ export class InGameState extends GameState { /** * Saves the game - * @param {boolean=} syncWithServer - * @param {boolean} force */ - doSave(syncWithServer = true, force = false) { - // TODO - return; - + doSave() { if (!this.savegame || !this.savegame.isSaveable()) { - // Can not save in multiplayer return Promise.resolve(); } @@ -424,19 +429,10 @@ export class InGameState extends GameState { } // First update the game data - logger.log("Starting to save game ..."); + this.core.root.signals.gameSaved.dispatch(); this.savegame.updateData(this.core.root); - - let savePromise = this.savegame.writeSavegameAndMetadata(); - - if (syncWithServer) { - // Sync in parallel - // @ts-ignore - savePromise = savePromise.then(() => this.syncer.sync(this.core, this.savegame, force)); - } - - return savePromise.catch(err => { + return this.savegame.writeSavegameAndMetadata().catch(err => { logger.warn("Failed to save:", err); }); } diff --git a/src/js/states/keybindings.js b/src/js/states/keybindings.js new file mode 100644 index 00000000..aa6ec2d6 --- /dev/null +++ b/src/js/states/keybindings.js @@ -0,0 +1,177 @@ +import { TextualGameState } from "../core/textual_game_state"; +import { SOUNDS } from "../platform/sound"; +import { T } from "../translations"; +import { KEYMAPPINGS, getStringForKeyCode } from "../game/key_action_mapper"; +import { Dialog } from "../core/modal_dialog_elements"; +import { IS_DEMO } from "../core/config"; + +export class KeybindingsState extends TextualGameState { + constructor() { + super("KeybindingsState"); + } + + getStateHeaderTitle() { + return T.keybindings.title; + } + + getMainContentHTML() { + return ` + +
+ ${T.keybindings.hint} + + +
+ +
+ +
+ `; + } + + onEnter() { + const keybindingsElem = this.htmlElement.querySelector(".keybindings"); + + this.trackClicks(this.htmlElement.querySelector(".resetBindings"), this.resetBindings); + + for (const category in KEYMAPPINGS) { + const categoryDiv = document.createElement("div"); + categoryDiv.classList.add("category"); + keybindingsElem.appendChild(categoryDiv); + + const labelDiv = document.createElement("strong"); + labelDiv.innerText = T.keybindings.categoryLabels[category]; + labelDiv.classList.add("categoryLabel"); + categoryDiv.appendChild(labelDiv); + + for (const keybindingId in KEYMAPPINGS[category]) { + const mapped = KEYMAPPINGS[category][keybindingId]; + + const elem = document.createElement("div"); + elem.classList.add("entry"); + elem.setAttribute("data-keybinding", keybindingId); + categoryDiv.appendChild(elem); + + const title = document.createElement("span"); + title.classList.add("title"); + title.innerText = T.keybindings.mappings[keybindingId]; + elem.appendChild(title); + + const mappingDiv = document.createElement("span"); + mappingDiv.classList.add("mapping"); + elem.appendChild(mappingDiv); + + const editBtn = document.createElement("button"); + editBtn.classList.add("styledButton", "editKeybinding"); + + const resetBtn = document.createElement("button"); + resetBtn.classList.add("styledButton", "resetKeybinding"); + + if (mapped.builtin) { + editBtn.classList.add("disabled"); + resetBtn.classList.add("disabled"); + } else { + this.trackClicks(editBtn, () => this.editKeybinding(keybindingId)); + this.trackClicks(resetBtn, () => this.resetKeybinding(keybindingId)); + } + elem.appendChild(editBtn); + elem.appendChild(resetBtn); + } + } + this.updateKeybindings(); + } + + editKeybinding(id) { + if (IS_DEMO) { + this.dialogs.showFeatureRestrictionInfo(T.demo.features.customizeKeybindings); + return; + } + + const dialog = new Dialog({ + app: this.app, + title: T.dialogs.editKeybinding.title, + contentHTML: T.dialogs.editKeybinding.desc, + buttons: ["cancel:good"], + type: "info", + }); + + dialog.inputReciever.keydown.add(({ keyCode, shift, alt, event }) => { + if (keyCode === 27) { + this.dialogs.closeDialog(dialog); + return; + } + + if (event) { + event.preventDefault(); + } + + if ( + // Enter + keyCode === 13 || + // TAB + keyCode === 9 + ) { + // Ignore builtins + return; + } + + this.app.settings.updateKeybindingOverride(id, keyCode); + + this.dialogs.closeDialog(dialog); + this.updateKeybindings(); + }); + + dialog.inputReciever.backButton.add(() => {}); + + this.dialogs.internalShowDialog(dialog); + this.app.sound.playUiSound(SOUNDS.dialogOk); + } + + updateKeybindings() { + const overrides = this.app.settings.getKeybindingOverrides(); + for (const category in KEYMAPPINGS) { + for (const keybindingId in KEYMAPPINGS[category]) { + const mapped = KEYMAPPINGS[category][keybindingId]; + + const container = this.htmlElement.querySelector("[data-keybinding='" + keybindingId + "']"); + assert(container, "Container for keybinding not found: " + keybindingId); + + let keyCode = mapped.keyCode; + if (overrides[keybindingId]) { + keyCode = overrides[keybindingId]; + } + + const mappingDiv = container.querySelector(".mapping"); + mappingDiv.innerHTML = getStringForKeyCode(keyCode); + mappingDiv.classList.toggle("changed", !!overrides[keybindingId]); + + const resetBtn = container.querySelector("button.resetKeybinding"); + resetBtn.classList.toggle("disabled", mapped.builtin || !overrides[keybindingId]); + } + } + } + + resetKeybinding(id) { + this.app.settings.resetKeybindingOverride(id); + this.updateKeybindings(); + } + + resetBindings() { + const { reset } = this.dialogs.showWarning( + T.dialogs.resetKeybindingsConfirmation.title, + T.dialogs.resetKeybindingsConfirmation.desc, + ["cancel:good", "reset:bad"] + ); + + reset.add(() => { + this.app.settings.resetKeybindingOverrides(); + this.updateKeybindings(); + + this.dialogs.showInfo(T.dialogs.keybindingsResetOk.title, T.dialogs.keybindingsResetOk.desc); + }); + } + + getDefaultPreviousState() { + return "SettingsState"; + } +} diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index b5ea8c63..9dd18909 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -1,6 +1,17 @@ import { GameState } from "../core/game_state"; import { cachebust } from "../core/cachebust"; -import { globalConfig } from "../core/config"; +import { globalConfig, IS_DEBUG, IS_DEMO, THIRDPARTY_URLS } from "../core/config"; +import { + makeDiv, + formatSecondsToTimeAgo, + generateFileDownload, + waitNextFrame, + isSupportedBrowser, +} from "../core/utils"; +import { ReadWriteProxy } from "../core/read_write_proxy"; +import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; +import { T } from "../translations"; +import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; export class MainMenuState extends GameState { constructor() { @@ -8,45 +19,392 @@ export class MainMenuState extends GameState { } getInnerHTML() { + const bannerHtml = ` +

${T.demoBanners.title}

+ +

${T.demoBanners.intro}

+ +
    + ${T.demoBanners.advantages.map(advantage => `
  • ${advantage}
  • `).join("")} +
+ + Get the shapez.io standalone! + `; + return ` + + + + ${ + G_IS_STANDALONE + ? ` + + ` + : "" + } + + ${ + G_IS_STANDALONE + ? "" + : `` + } + + -
- +
+ + ${IS_DEMO ? `
${bannerHtml}
` : ""} + +
+ ${ + isSupportedBrowser() + ? "" + : `
${T.mainMenu.browserWarning}
` + } + + +
+ + +
+ + `; } + requestImportSavegame() { + if ( + IS_DEMO && + this.app.savegameMgr.getSavegamesMetaData().length > 0 && + !this.app.platformWrapper.getHasUnlimitedSavegames() + ) { + this.app.analytics.trackUiClick("importgame_slot_limit_show"); + this.dialogs.showWarning(T.dialogs.oneSavegameLimit.title, T.dialogs.oneSavegameLimit.desc); + return; + } + + var input = document.createElement("input"); + input.type = "file"; + input.accept = ".bin"; + + input.onchange = e => { + const file = input.files[0]; + if (file) { + waitNextFrame().then(() => { + this.app.analytics.trackUiClick("import_savegame"); + const closeLoader = this.dialogs.showLoadingDialog(); + const reader = new FileReader(); + reader.addEventListener("load", event => { + // @ts-ignore + const contents = event.target.result; + let realContent; + + try { + realContent = ReadWriteProxy.deserializeObject(contents); + } catch (err) { + closeLoader(); + this.dialogs.showWarning( + T.dialogs.importSavegameError.title, + T.dialogs.importSavegameError.text + "

" + err + ); + return; + } + + this.app.savegameMgr.importSavegame(realContent).then( + () => { + closeLoader(); + this.dialogs.showWarning( + T.dialogs.importSavegameSuccess.title, + T.dialogs.importSavegameSuccess.text + ); + + this.renderSavegames(); + }, + err => { + closeLoader(); + this.dialogs.showWarning( + T.dialogs.importSavegameError.title, + T.dialogs.importSavegameError.text + ":

" + err + ); + } + ); + }); + reader.addEventListener("error", error => { + this.dialogs.showWarning( + T.dialogs.importSavegameError.title, + T.dialogs.importSavegameError.text + ":

" + error + ); + }); + reader.readAsText(file, "utf-8"); + }); + } + }; + input.click(); + } + onBackButton() { this.app.platformWrapper.exitApp(); } onEnter(payload) { + this.dialogs = new HUDModalDialogs(null, this.app); + const dialogsElement = document.body.querySelector(".modalDialogParent"); + this.dialogs.initializeToElement(dialogsElement); + if (payload.loadError) { - alert("Error while loading game: " + payload.loadError); + this.dialogs.showWarning( + T.dialogs.gameLoadFailure.title, + T.dialogs.gameLoadFailure.text + "

" + payload.loadError + ); } const qs = this.htmlElement.querySelector.bind(this.htmlElement); this.trackClicks(qs(".mainContainer .playButton"), this.onPlayButtonClicked); + this.trackClicks(qs(".mainContainer .importButton"), this.requestImportSavegame); if (G_IS_DEV && globalConfig.debug.fastGameEnter) { + // // const games = this.app.savegameMgr.getSavegamesMetaData(); + // if (games.length > 0) { + // this.resumeGame(games[0]); + // } else { this.onPlayButtonClicked(); + // } + } + + // Initialize video + this.videoElement = this.htmlElement.querySelector("video"); + if (this.videoElement) { + this.videoElement.playbackRate = 0.9; + this.videoElement.addEventListener("canplay", () => { + if (this.videoElement) { + this.videoElement.classList.add("loaded"); + } + }); + } + + this.trackClicks(qs(".settingsButton"), this.onSettingsButtonClicked); + this.trackClicks(qs(".changelog"), this.onChangelogClicked); + + if (G_IS_STANDALONE) { + this.trackClicks(qs(".exitAppButton"), this.onExitAppButtonClicked); + } + + this.renderSavegames(); + + const steamLink = this.htmlElement.querySelector(".steamLink"); + if (steamLink) { + this.trackClicks(steamLink, () => this.onSteamLinkClicked(), { preventClick: true }); + } + + const discordLink = this.htmlElement.querySelector(".discordLink"); + this.trackClicks( + discordLink, + () => this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.discord), + { preventClick: true } + ); + + const githubLink = this.htmlElement.querySelector(".githubLink"); + this.trackClicks( + githubLink, + () => this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.github), + { preventClick: true } + ); + + const producerLink = this.htmlElement.querySelector(".producerLink"); + this.trackClicks( + producerLink, + () => this.app.platformWrapper.openExternalLink("https://tobspr.com"), + { preventClick: true } + ); + } + + onSteamLinkClicked() { + this.app.analytics.trackUiClick("main_menu_steam_link"); + this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneStorePage); + return false; + } + + onExitAppButtonClicked() { + this.app.platformWrapper.exitApp(); + } + + onChangelogClicked() { + this.moveToState("ChangelogState"); + } + + renderSavegames() { + const oldContainer = this.htmlElement.querySelector(".mainContainer .savegames"); + if (oldContainer) { + oldContainer.remove(); + } + const games = this.app.savegameMgr.getSavegamesMetaData(); + if (games.length > 0) { + const parent = makeDiv(this.htmlElement.querySelector(".mainContainer"), null, ["savegames"]); + + for (let i = 0; i < games.length; ++i) { + const elem = makeDiv(parent, null, ["savegame"]); + + makeDiv(elem, null, ["internalId"], games[i].internalId.substr(0, 6)); + makeDiv( + elem, + null, + ["updateTime"], + formatSecondsToTimeAgo((new Date().getTime() - games[i].lastUpdate) / 1000.0) + ); + + const deleteButton = document.createElement("button"); + deleteButton.classList.add("styledButton", "deleteGame"); + elem.appendChild(deleteButton); + + const downloadButton = document.createElement("button"); + downloadButton.classList.add("styledButton", "downloadGame"); + elem.appendChild(downloadButton); + + const resumeBtn = document.createElement("button"); + resumeBtn.classList.add("styledButton", "resumeGame"); + elem.appendChild(resumeBtn); + + this.trackClicks(deleteButton, () => this.deleteGame(games[i])); + this.trackClicks(downloadButton, () => this.downloadGame(games[i])); + this.trackClicks(resumeBtn, () => this.resumeGame(games[i])); + } } } - onPlayButtonClicked() { - const savegame = this.app.savegameMgr.createNewSavegame(); + /** + * @param {object} game + */ + resumeGame(game) { + this.app.analytics.trackUiClick("resume_game"); - this.app.analytics.trackUiClick("startgame"); - this.moveToState("InGameState", { - savegame, + this.app.adProvider.showVideoAd().then(() => { + this.app.analytics.trackUiClick("resume_game_adcomplete"); + const savegame = this.app.savegameMgr.getSavegameById(game.internalId); + savegame.readAsync().then(() => { + this.moveToState("InGameState", { + savegame, + }); + }); }); } + /** + * @param {object} game + */ + deleteGame(game) { + this.app.analytics.trackUiClick("delete_game"); + + const signals = this.dialogs.showWarning( + T.dialogs.confirmSavegameDelete.title, + T.dialogs.confirmSavegameDelete.text, + ["delete:bad", "cancel:good"] + ); + + signals.delete.add(() => { + this.app.savegameMgr.deleteSavegame(game).then( + () => { + this.renderSavegames(); + }, + err => { + this.dialogs.showWarning( + T.dialogs.savegameDeletionError.title, + T.dialogs.savegameDeletionError.text + "

" + err + ); + } + ); + }); + } + + /** + * @param {object} game + */ + downloadGame(game) { + this.app.analytics.trackUiClick("download_game"); + + const savegame = this.app.savegameMgr.getSavegameById(game.internalId); + savegame.readAsync().then(() => { + const data = ReadWriteProxy.serializeObject(savegame.currentData); + generateFileDownload(savegame.filename, data); + }); + } + + onSettingsButtonClicked() { + this.moveToState("SettingsState"); + } + + doStartNewGame() { + this.app.analytics.trackUiClick("startgame"); + + this.app.adProvider.showVideoAd().then(() => { + const savegame = this.app.savegameMgr.createNewSavegame(); + + this.moveToState("InGameState", { + savegame, + }); + this.app.analytics.trackUiClick("startgame_adcomplete"); + }); + } + + onPlayButtonClicked() { + if ( + IS_DEMO && + this.app.savegameMgr.getSavegamesMetaData().length > 0 && + !this.app.platformWrapper.getHasUnlimitedSavegames() + ) { + this.app.analytics.trackUiClick("startgame_slot_limit_show"); + this.dialogs.showWarning(T.dialogs.oneSavegameLimit.title, T.dialogs.oneSavegameLimit.desc); + return; + } + + if (IS_DEMO) { + this.app.analytics.trackUiClick("startgame_pre_show"); + const { ok } = this.dialogs.showWarning( + T.dialogs.demoExplanation.title, + T.dialogs.demoExplanation.desc + ); + ok.add(() => this.doStartNewGame()); + return; + } + + this.doStartNewGame(); + } + onLeave() { - // this.dialogs.cleanup(); + this.dialogs.cleanup(); } } diff --git a/src/js/states/mobile_warning.js b/src/js/states/mobile_warning.js new file mode 100644 index 00000000..c6903164 --- /dev/null +++ b/src/js/states/mobile_warning.js @@ -0,0 +1,52 @@ +import { GameState } from "../core/game_state"; +import { cachebust } from "../core/cachebust"; +import { THIRDPARTY_URLS } from "../core/config"; + +export class MobileWarningState extends GameState { + constructor() { + super("MobileWarningState"); + } + + getInnerHTML() { + return ` + + + +

+ I'm sorry, but shapez.io is not yet available on mobile devices! + (There is also no estimate when this will change, but feel to make a contribution! It's +  open source!)

+ +

If you want to play on your computer, you can also get the standalone on itch.io:

+ + + Get the shapez.io standalone! + `; + } + + getThemeMusic() { + return null; + } + + getHasFadeIn() { + return false; + } + + onEnter() { + try { + if (window.gtag) { + window.gtag("event", "click", { + event_category: "ui", + event_label: "mobile_warning", + }); + } + } catch (ex) { + console.warn("Failed to track mobile click:", ex); + } + } + onLeave() { + // this.dialogs.cleanup(); + } +} diff --git a/src/js/states/preload.js b/src/js/states/preload.js index ba3b515f..43c84eb0 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -3,6 +3,9 @@ import { createLogger } from "../core/logging"; import { findNiceValue, waitNextFrame } from "../core/utils"; import { cachebust } from "../core/cachebust"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; +import { T } from "../translations"; +import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; +import { CHANGELOG } from "../changelog"; const logger = createLogger("state/preload"); @@ -44,9 +47,9 @@ export class PreloadState extends GameState { } } - // this.dialogs = new HUDModalDialogs(null, this.app); - // const dialogsElement = document.body.querySelector(".modalDialogParent"); - // this.dialogs.initializeToElement(dialogsElement); + this.dialogs = new HUDModalDialogs(null, this.app); + const dialogsElement = document.body.querySelector(".modalDialogParent"); + this.dialogs.initializeToElement(dialogsElement); this.statusText = this.htmlElement.querySelector(".loadingStatus > .desc"); this.statusBar = this.htmlElement.querySelector(".loadingStatus > .bar > .inner"); @@ -64,13 +67,45 @@ export class PreloadState extends GameState { startLoading() { this.setStatus("Booting") + .then(() => this.setStatus("Checking for updates")) + .then(() => { + if (G_IS_STANDALONE) { + return Promise.race([ + new Promise(resolve => setTimeout(resolve, 10000)), + fetch( + "https://itch.io/api/1/x/wharf/latest?target=tobspr/shapezio&channel_name=windows", + { + cache: "no-cache", + } + ) + .then(res => res.json()) + .then(({ latest }) => { + if (latest !== G_BUILD_VERSION) { + const { ok, viewUpdate } = this.dialogs.showInfo( + T.dialogs.newUpdate.title, + T.dialogs.newUpdate.desc, + ["ok:good", "viewUpdate:good"] + ); + + return new Promise(resolve => { + ok.add(resolve); + viewUpdate.add(() => { + window.open("https://tobspr.itch.io/shapezio", "_blank"); + resolve(); + }); + }); + } + }) + .catch(err => { + logger.log("Failed to fetch version:", err); + }), + ]); + } + }) + .then(() => this.setStatus("Creating platform wrapper")) .then(() => this.app.platformWrapper.initialize()) - .then(() => this.setStatus("Initializing libraries")) - .then(() => this.app.analytics.initialize()) - .then(() => this.app.gameAnalytics.initialize()) - .then(() => this.setStatus("Initializing local storage")) .then(() => { const wrapper = this.app.platformWrapper; @@ -95,14 +130,14 @@ export class PreloadState extends GameState { return this.app.storage.initialize(); }) + .then(() => this.setStatus("Initializing libraries")) + .then(() => this.app.analytics.initialize()) + .then(() => this.app.gameAnalytics.initialize()) + .then(() => this.setStatus("Initializing settings")) .then(() => { return this.app.settings.initialize(); }) - .then(() => { - // Make sure the app pickups the right size - this.app.updateAfterUiScaleChanged(); - }) .then(() => { // Initialize fullscreen @@ -142,6 +177,53 @@ export class PreloadState extends GameState { return this.app.backgroundResourceLoader.getPromiseForBareGame(); }) + .then(() => this.setStatus("Checking changelog")) + .then(() => { + return this.app.storage + .readFileAsync("lastversion.bin") + .catch(err => { + logger.warn("Failed to read lastversion:", err); + return G_BUILD_VERSION; + }) + .then(version => { + logger.log("Last version:", version, "App version:", G_BUILD_VERSION); + this.app.storage.writeFileAsync("lastversion.bin", G_BUILD_VERSION); + return version; + }) + .then(version => { + let changelogEntries = []; + logger.log("Last seen version:", version); + + for (let i = 0; i < CHANGELOG.length; ++i) { + if (CHANGELOG[i].version === version) { + break; + } + changelogEntries.push(CHANGELOG[i]); + } + if (changelogEntries.length === 0) { + return; + } + + let dialogHtml = T.dialogs.updateSummary.desc; + for (let i = 0; i < changelogEntries.length; ++i) { + const entry = changelogEntries[i]; + dialogHtml += ` +
+ ${entry.version} + ${entry.date} +
    + ${entry.entries.map(text => `
  • ${text}
  • `).join("")} +
+
+ `; + } + + return new Promise(resolve => { + this.dialogs.showInfo(T.dialogs.updateSummary.title, dialogHtml).ok.add(resolve); + }); + }); + }) + .then(() => this.setStatus("Launching")) .then( () => { @@ -168,7 +250,8 @@ export class PreloadState extends GameState { if (G_IS_DEV) { return Promise.resolve(); } - return waitNextFrame(); + return Promise.resolve(); + // return waitNextFrame(); } showFailMessage(text) { diff --git a/src/js/states/settings.js b/src/js/states/settings.js new file mode 100644 index 00000000..e092c717 --- /dev/null +++ b/src/js/states/settings.js @@ -0,0 +1,117 @@ +import { TextualGameState } from "../core/textual_game_state"; +import { formatSecondsToTimeAgo } from "../core/utils"; +import { allApplicationSettings } from "../profile/application_settings"; +import { T } from "../translations"; + +export class SettingsState extends TextualGameState { + constructor() { + super("SettingsState"); + } + + getStateHeaderTitle() { + return T.settings.title; + } + + getMainContentHTML() { + return ` + + + + + ${this.getSettingsHtml()} +
+
${T.global.loading} ...
+
+ + + `; + } + + getSettingsHtml() { + let lastCategory = null; + let html = ""; + for (let i = 0; i < allApplicationSettings.length; ++i) { + const setting = allApplicationSettings[i]; + + if (setting.categoryId !== lastCategory) { + lastCategory = setting.categoryId; + if (i !== 0) { + html += "
"; + } + html += `${T.settings.categories[lastCategory]}`; + html += "
"; + } + + html += setting.getHtml(); + } + if (lastCategory) { + html += "
"; + } + + return html; + } + + renderBuildText() { + const labelVersion = this.htmlElement.querySelector(".buildVersion"); + const lastBuildMs = new Date().getTime() - G_BUILD_TIME; + const lastBuildText = formatSecondsToTimeAgo(lastBuildMs / 1000.0); + + const version = T.settings.versionBadges[G_APP_ENVIRONMENT]; + + labelVersion.innerHTML = ` + + ${G_BUILD_VERSION} @ ${version} @ ${G_BUILD_COMMIT_HASH} + + + ${T.settings.buildDate.replace("", lastBuildText)}
+
`; + } + + onEnter(payload) { + this.renderBuildText(); + this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, { + preventDefault: false, + }); + + const keybindingsButton = this.htmlElement.querySelector(".editKeybindings"); + + if (keybindingsButton) { + this.trackClicks(keybindingsButton, this.onKeybindingsClicked, { preventDefault: false }); + } + + this.initSettings(); + } + + initSettings() { + allApplicationSettings.forEach(setting => { + const element = this.htmlElement.querySelector("[data-setting='" + setting.id + "']"); + setting.bind(this.app, element, this.dialogs); + setting.syncValueToElement(); + this.trackClicks( + element, + () => { + setting.modify(); + }, + { preventDefault: false } + ); + }); + } + + onAboutClicked() { + this.moveToStateAddGoBack("AboutState"); + } + + onKeybindingsClicked() { + this.moveToStateAddGoBack("KeybindingsState"); + } +} diff --git a/src/js/translations.js b/src/js/translations.js new file mode 100644 index 00000000..e91f3a4c --- /dev/null +++ b/src/js/translations.js @@ -0,0 +1,21 @@ +import { globalConfig } from "./core/config"; + +// @ts-ignore +const baseTranslations = require("./built-temp/base-en.json"); + +export const T = baseTranslations; + +if (G_IS_DEV && globalConfig.debug.testTranslations) { + // Replaces all translations by fake translations to see whats translated and what not + const mapTranslations = obj => { + for (const key in obj) { + const value = obj[key]; + if (typeof value === "string") { + obj[key] = value.replace(/[a-z]/gi, "x"); + } else { + mapTranslations(value); + } + } + }; + mapTranslations(T); +} diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json new file mode 100644 index 00000000..57c0a852 --- /dev/null +++ b/src/js/tsconfig.json @@ -0,0 +1,59 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + "allowJs": true /* Allow javascript files to be compiled. */, + "checkJs": true /* Report errors in .js files. */, + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./typedefs_gen", /* Concatenate and emit output to single file. */ + // "outDir": "./typedefs_gen", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "incremental": true, /* Enable incremental compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true /* Do not emit outputs. */, + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + // "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true /* Enable strict checking of function types. */, + "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, + "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, + /* Module Resolution Options */ + "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "resolveJsonModule": true + }, + "exclude": ["backend/shared/gameserver_base_impl", "backend/shared/sentry_logger.js"] +} diff --git a/tslint.json b/src/js/tslint.json similarity index 60% rename from tslint.json rename to src/js/tslint.json index 01942cdc..c89e7770 100644 --- a/tslint.json +++ b/src/js/tslint.json @@ -1,23 +1,16 @@ { "defaultSeverity": "error", - "extends": [ - "tslint:recommended" - ], + "extends": ["tslint:recommended"], "jsRules": { "trailing-comma": false, - "comma-dangle": [ - "error", - "never" - ], + "comma-dangle": ["error", "never"], "object-literal-sort-keys": false, "member-ordering": false, "max-line-length": false, "no-console": false, "forin": false, "no-empty": false, - "space-before-function-paren": [ - "always" - ] + "space-before-function-paren": ["always"] }, "rulesDirectory": [] -} \ No newline at end of file +} diff --git a/src/js/webworkers/background_animation_frame_emittter.worker.js b/src/js/webworkers/background_animation_frame_emittter.worker.js index 5a3fe5c9..c927b2d3 100644 --- a/src/js/webworkers/background_animation_frame_emittter.worker.js +++ b/src/js/webworkers/background_animation_frame_emittter.worker.js @@ -1,12 +1,12 @@ // We clamp high deltas so 30 fps is fairly ok -const bgFps = 30; -const desiredMsDelay = 1000 / bgFps; +var bgFps = 30; +var desiredMsDelay = 1000 / bgFps; let lastTick = 0; function tick() { - const now = performance.now(); - const delta = now - lastTick; + var now = performance.now(); + var delta = now - lastTick; lastTick = now; // @ts-ignore diff --git a/src/js/webworkers/compression.worker.js b/src/js/webworkers/compression.worker.js index 69e7a817..0bcb0ea6 100644 --- a/src/js/webworkers/compression.worker.js +++ b/src/js/webworkers/compression.worker.js @@ -10,10 +10,7 @@ function accessNestedPropertyReverse(obj, keys) { return result; } -const rusha = require("rusha"); - const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]); -const encryptKey = globalConfig.info.sgSalt; onmessage = function (event) { const { jobId, job, data } = event.data; @@ -31,13 +28,6 @@ function performJob(job, data) { case "compressX64": { return compressX64(data); } - case "compressWithChecksum": { - const checksum = rusha - .createHash() - .update(data + encryptKey) - .digest("hex"); - return compressX64(checksum + data); - } case "compressFile": { const checksum = sha1(data.text + salt); return data.compressionPrefix + compressX64(checksum + data.text); diff --git a/translations/base-en.yaml b/translations/base-en.yaml new file mode 100644 index 00000000..2e5cfd4c --- /dev/null +++ b/translations/base-en.yaml @@ -0,0 +1,579 @@ +# +# GAME TRANSLATIONS +# +# Contributing: +# +# If you want to contribute, please make a pull request on this respository +# and I will have a look. +# +# Placeholders: +# +# Do *not* replace placeholders! Placeholders have a special syntax like +# `Hotkey: `. They are encapsulated within angle brackets. The correct +# translation for this one in German for example would be: `Taste: ` (notice +# how the placeholder stayed '' and was not replaced!) +# +# Adding a new language: +# +# If you want to add a new language, ask me in the discord and I will setup +# the basic structure so the game also detects it. +# + +global: + loading: Loading + error: Error + + # How big numbers are rendered, e.g. "10,000" + thousandsDivider: "," + + # Shown for infinitely big numbers + infinite: inf + + time: + # Used for formatting past time dates + oneSecondAgo: one second ago + xSecondsAgo: seconds ago + oneMinuteAgo: one minute ago + xMinutesAgo: minutes ago + oneHourAgo: one hour ago + xHoursAgo: hours ago + oneDayAgo: one day ago + xDaysAgo: days ago + + # Short formats for times, e.g. '5h 23m' + secondsShort: s + minutesAndSecondsShort: m s + hoursAndMinutesShort: h s + + xMinutes: minutes + + keys: + tab: TAB + control: CTRL + alt: ALT + escape: ESC + shift: SHIFT + space: SPACE + +demoBanners: + # This is the "advertisement" shown in the main menu and other various places + title: Hey! + intro: >- + If you enjoy this game, please consider buying the full version! + advantages: + - No advertisements + - Unlimited savegames + - Dark mode & more + - >- + Allow me to further develop shapez.io ❤️ + +mainMenu: + play: Play + changelog: Changelog + importSavegame: Import + openSourceHint: This game is open source! + discordLink: Official Discord Server + + # This is shown when using firefox and other browsers which are not supported. + browserWarning: >- + Sorry, but the game is known to run slow on your browser! Get the standalone version or download chrome for the full experience. + +dialogs: + buttons: + ok: OK + delete: Delete + cancel: Cancel + later: Later + restart: Restart + reset: Reset + getStandalone: Get Standalone + deleteGame: I know what I do + viewUpdate: View Update + showUpgrades: Show Upgrades + + importSavegameError: + title: Import Error + text: >- + Failed to import your savegame: + + importSavegameSuccess: + title: Savegame Imported + text: >- + Your savegame has been successfully imported. + + gameLoadFailure: + title: Game is broken + text: >- + Failed to load your savegame: + + confirmSavegameDelete: + title: Confirm deletion + text: >- + Are you sure you want to delete the game? + + savegameDeletionError: + title: Failed to delete + text: >- + Failed to delete the savegame: + + restartRequired: + title: Restart required + text: >- + You need to restart the game to apply the settings. + + editKeybinding: + title: Change Keybinding + desc: Press the key you want to assign, or escape to cancel. + + resetKeybindingsConfirmation: + title: Reset keybindings + desc: This will reset all keybindings to their default values. Please confirm. + + keybindingsResetOk: + title: Keybindings reset + desc: The keybindings have been reset to their respective defaults! + + featureRestriction: + title: Demo Version + desc: You tried to access a feature () which is not available in the demo. Consider to get the standalone for the full experience! + + saveNotPossibleInDemo: + desc: Your game has been saved, but restoring it is only possible in the standalone version. Consider to get the standalone for the full experience! + + leaveNotPossibleInDemo: + title: Demo version + desc: Your game was saved but you will not be able to restore it in the demo. Restoring your savegames is only possible in the full version. Are you sure? + + newUpdate: + title: Update available + desc: There is an update for this game available! + + demoExplanation: + title: Notice from the Developer + desc: I am developing this game in my free time, and I hope you enjoy it! If you do, please consider to buy the standalone version! + + oneSavegameLimit: + title: Limited savegames + desc: You can only have one savegame at a time in the demo version. Please remove the existing one or get the standalone! + + updateSummary: + title: New update! + desc: >- + Here are the changes since you last played: + + hintDescription: + title: Tutorial + desc: >- + Whenever you need help or are stuck, check out the 'Show hint' button in the lower left and I'll give my best to help you! + + upgradesIntroduction: + title: Unlock Upgrades + desc: >- + All shapes you produce can be used to unlock upgrades - Don't destroy your old factories! + The upgrades tab can be found on the top right corner of the screen. + +ingame: + # This is shown in the top left corner and displays useful keybindings in + # every situation + keybindingsOverlay: + centerMap: Center + moveMap: Move + removeBuildings: Delete + stopPlacement: Stop placement + rotateBuilding: Rotate building + placeMultiple: Place multiple + reverseOrientation: Reverse orientation + disableAutoOrientation: Disable auto orientation + toggleHud: Toggle HUD + placeBuilding: Place building + + # Everything related to placing buildings (I.e. as soon as you selected a building + # from the toolbar) + buildingPlacement: + # Buildings can have different variants which are unlocked at later levels, + # and this is the hint shown when there are multiple variants available. + cycleBuildingVariants: Press to cycle variants. + + # Shows the hotkey in the ui, e.g. "Hotkey: Q" + hotkeyLabel: >- + Hotkey: + + infoTexts: + speed: Speed + range: Range + storage: Storage + oneItemPerSecond: 1 item / second + itemsPerSecond: items / s + itemsPerSecondDouble: (x2) + + tiles: tiles + + # The notification when completing a level + levelCompleteNotification: + # is replaced by the actual level, so this gets 'Level 03' for example. + levelTitle: Level + completed: Completed + unlockText: Unlocked ! + buttonNextLevel: Next Level + + # Notifications on the lower right + notifications: + newUpgrade: A new upgrade is available! + gameSaved: Your game has been saved. + + # Mass delete information, this is when you hold CTRL and then drag with your mouse + # to select multiple buildings to delete + massDelete: + infoText: Press to remove selected buildings and to cancel. + + # The "Upgrades" window + shop: + title: Upgrades + buttonUnlock: Upgrade + + # Gets replaced to e.g. "Tier IX" + tier: Tier + + # The roman number for each tier + tierLabels: [I, II, III, IV, V, VI, VII, VIII, IX, X] + + maximumLevel: Maximum level + + # The "Statistics" window + statistics: + title: Statistics + dataSources: + stored: + title: Stored + description: Displaying amount of stored shapes in your central building. + produced: + title: Produced + description: Displaying all shapes your whole factory produces, including intermediate products. + delivered: + title: Delivered + description: Displaying shapes which are delivered to your central building. + noShapesProduced: No shapes have been produced so far. + + # Displays the shapes per minute, e.g. '523 / m' + shapesPerMinute: / m + + # Settings menu, when you press "ESC" + settingsMenu: + playtime: Playtime + + buildingsPlaced: Buildings + beltsPlaced: Belts + + buttons: + continue: Continue + settings: Settings + menu: Return to menu + + # Bottom left tutorial hints + tutorialHints: + title: Need help? + showHint: Show hint + hideHint: Close + +# All shop upgrades +shopUpgrades: + belt: + name: Belts, Distributor & Tunnels + description: Speed +% + miner: + name: Extraction + description: Speed +% + processors: + name: Cutting, Rotating & Stacking + description: Speed +% + painting: + name: Mixing & Painting + description: Speed +% + +# Buildings and their name / description +buildings: + belt: + default: + name: &belt Conveyor Belt + description: Transports items, hold and drag to place multiple. + + miner: # Internal name for the Extractor + default: + name: &miner Extractor + description: Place over a shape or color to extract it. + + chainable: + name: Extractor (Chain) + description: Place over a shape or color to extract it. Can be chained. + + underground_belt: # Internal name for the Tunnel + default: + name: &underground_belt Tunnel + description: Allows to tunnel resources under buildings and belts. + + tier2: + name: Tunnel Tier II + description: Allows to tunnel resources under buildings and belts. + + splitter: # Internal name for the Balancer + default: + name: &splitter Balancer + description: Multifunctional - Evenly distributes all inputs onto all outputs. + + compact: + name: Merger (compact) + description: Merges two conveyor belts into one. + + compact-inverse: + name: Merger (compact) + description: Merges two conveyor belts into one. + + cutter: + default: + name: &cutter Cutter + description: Cuts shapes from top to bottom and outputs both halfs. If you use only one part, be sure to destroy the other part or it will stall! + quad: + name: Cutter (Quad) + description: Cuts shapes into four parts. If you use only one part, be sure to destroy the other part or it will stall! + + rotater: + default: + name: &rotater Rotate + description: Rotates shapes clockwise by 90 degrees. + ccw: + name: Rotate (CCW) + description: Rotates shapes counter clockwise by 90 degrees. + + stacker: + default: + name: &stacker Stacker + description: Stacks both items. If they can not be merged, the right item is placed above the left item. + + mixer: + default: + name: &mixer Color Mixer + description: Mixes two colors using additive blending. + + painter: + default: + name: &painter Painter + description: Colors the whole shape on the left input with the color from the right input. + double: + name: Painter (Double) + description: Colors the both shapes on the left input with the color from the right input. + quad: + name: Painter (Quad) + description: Allows to color each quadrant of the shape with a different color. + + trash: + default: + name: &trash Trash + description: Accepts inputs from all sides and destroys them. Forever. + + storage: + name: Storage + description: Stores excess items, up to a given capacity. Can be used as an overflow gate. + +storyRewards: + # Those are the rewards gained from completing the store + reward_cutter_and_trash: + title: Cutting Shapes + desc: You just unlocked the cutter - it cuts shapes half from top to bottom regardless of its orientation!

Be sure to get rid of the waste, or otherwise it will stall - For this purpose I gave you a trash, which destroys everything you put into it! + + reward_rotater: + title: Rotating + desc: The rotater has been unlocked! It rotates shapes clockwise by 90 degrees. + + reward_painter: + title: Painting + desc: >- + The painter has been unlocked - Extract some color veins (just as you do with shapes) and combine it with a shape in the painter to color them!

PS: If you are colorblind, I'm working on a solution already! + + reward_mixer: + title: Color Mixing + desc: The mixer has been unlocked - Combine two colors using additive blending with this building! + + reward_stacker: + title: Combiner + desc: You can now combine shapes with the combiner! Both inputs are combined, and if they can be put next to each other, they will be fused. If not, the right input is stacked on top of the left input! + + reward_splitter: + title: Splitter/Merger + desc: The multifunctional balancer has been unlocked - It can be used to build bigger factories by splitting and merging items onto multiple belts!

+ + reward_tunnel: + title: Tunnel + desc: The tunnel has been unlocked - You can now pipe items through belts and buildings with it! + + reward_rotater_ccw: + title: CCW Rotating + desc: You have unlocked a variant of the rotater - It allows to rotate counter clockwise! To build it, select the rotater and press 'T' to cycle its variants! + + reward_miner_chainable: + title: Chaining Extractor + desc: You have unlocked the chaining extractor! It can forward its resources to other extractors so you can more efficiently extract resources! + + reward_underground_belt_tier_2: + title: Tunnel Tier II + desc: You have unlocked a new variant of the tunnel - It has a bigger range, and you can also mix-n-match those tunnels now! + + reward_splitter_compact: + title: Compact Balancer + desc: >- + You have unlocked a compact variant of the balancer - It accepts two inputs and merges them into one! + + reward_cutter_quad: + title: Quad Cutting + desc: You have unlocked a variant of the cutter - It allows you to cut shapes in four parts instead of just two! + + reward_painter_double: + title: Double Painting + desc: You have unlocked a variant of the painter - It works as the regular painter but processes two shapes at once consuming just one color instead of two! + + reward_painter_quad: + title: Quad Painting + desc: You have unlocked a variant of the painter - It allows to paint each part of the shape individually! + + reward_storage: + title: Storage Buffer + desc: You have unlocked a variant of the trash - It allows to store items up to a given capacity! + + reward_freeplay: + title: Freeplay + desc: You did it! You unlocked the free-play mode! This means that shapes are now randomly generated! (No worries, more content is planned for the standalone!) + + # Special reward, which is shown when there is no reward actually + no_reward: + title: Next level + desc: >- + This level gave you no reward, but the next one will!

PS: Better don't destroy your existing factory - You need all those shapes later again to unlock upgrades! + + no_reward_freeplay: + title: Next level + desc: >- + Congratulations! By the way, more content is planned for the standalone! + +settings: + title: Settings + categories: + game: Game + app: Application + + versionBadges: + dev: Development + staging: Staging + prod: Production + buildDate: Built + + labels: + uiScale: + title: Interface scale + description: >- + Changes the size of the user interface. The interface will still scale based on your device resolution, but this setting controls the amount of scale. + + fullscreen: + title: Fullscreen + description: >- + It is recommended to play the game in fullscreen to get the best experience. Only available in the standalone. + + soundsMuted: + title: Mute Sounds + description: >- + If enabled, mutes all sound effects. + + musicMuted: + title: Mute Music + description: >- + If enabled, mutes all music. + + theme: + title: Game theme + description: >- + Choose the game theme (light / dark). + + refreshRate: + title: Simulation Target + description: >- + If you have a 144hz monitor, change the refresh rate here so the game will properly simulate at higher refresh rates. This might actually decrease the FPS if your computer is too slow. + + alwaysMultiplace: + title: Multiplace + description: >- + If enabled, all buildings will stay selected after placement until you cancel it. This is equivalent to holding SHIFT permanently. + + offerHints: + title: Hints + description: >- + Whether to show the 'Show hints' button in the lower left. + +keybindings: + title: Keybindings + hint: >- + Tip: Be sure to make use of CTRL, SHIFT and ALT! They enable different placement options. + + resetKeybindings: Reset Keyinbindings + + categoryLabels: + general: Appplication + ingame: Game + placement: Placement + massSelect: Mass Delete + buildings: Building Shortcuts + placementModifiers: Placement Modifiers + + mappings: + confirm: Confirm + back: Back + mapMoveUp: Move Up + mapMoveRight: Move Right + mapMoveDown: Move Down + mapMoveLeft: Move Left + + centerMap: Center Map + + mapZoomIn: Zoom in + mapZoomOut: Zoom out + + menuOpenShop: Upgrades + menuOpenStats: Statistics + + toggleHud: Toggle HUD + toggleFPSInfo: Toggle FPS and Debug Info + belt: *belt + splitter: *splitter + underground_belt: *underground_belt + miner: *miner + cutter: *cutter + rotater: *rotater + stacker: *stacker + mixer: *mixer + painter: *painter + trash: *trash + + abortBuildingPlacement: Abort Placement + rotateWhilePlacing: Rotate + cycleBuildingVariants: Cycle Variants + confirmMassDelete: Confirm Mass Delete + cycleBuildings: Cycle Buildings + + massSelectStart: Hold and drag to start + massSelectSelectMultiple: Select multiple areas + + placementDisableAutoOrientation: Disable automatic orientation + placeMultiple: Stay in placement mode + placeInverse: Invert automatic orientation + +about: + title: About this Game + +changelog: + title: Changelog + +demo: + features: + restoringGames: Restoring savegames + importingGames: Importing savegames + oneGameLimit: Limited to one savegame + customizeKeybindings: Customizing Keybindings + + settingNotAvailable: Not available in the demo. diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 17d22902..00000000 --- a/tsconfig.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "compilerOptions": { - /* Basic Options */ - "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ - "allowJs": true, /* Allow javascript files to be compiled. */ - "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./typedefs_gen", /* Concatenate and emit output to single file. */ - // "outDir": "./typedefs_gen", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "incremental": true, /* Enable incremental compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - /* Strict Type-Checking Options */ - // "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - "strictFunctionTypes": true, /* Enable strict checking of function types. */ - "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - /* Module Resolution Options */ - "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - "resolveJsonModule": true - }, - "exclude": [ - "backend/shared/gameserver_base_impl", - "backend/shared/sentry_logger.js" - ] -} \ No newline at end of file diff --git a/version b/version index 8a9ecc2e..a6a3a43c 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.0.1 \ No newline at end of file +1.0.4 \ No newline at end of file