From b412eb8ac40a6e75a6c17e8fa29763f2e42b4591 Mon Sep 17 00:00:00 2001 From: AliasAlreadyTaken Date: Tue, 30 Mar 2021 22:11:37 +0200 Subject: [PATCH] https://github.com/ElCeejo/mob_core/commit/9481b22f8564e13963db1d59487233d5a5373557 --- LICENSE | 674 +++++++++ api.lua | 1668 +++++++++++++++++++++++ craftitems.lua | 35 + hq_lq.lua | 1350 ++++++++++++++++++ init.lua | 18 + logic.lua | 201 +++ mob_core_api.txt | 203 +++ mod.conf | 4 + mount.lua | 168 +++ pathfinder.lua | 404 ++++++ settingtypes.txt | 11 + textures/mob_core_blue_particle.png | Bin 0 -> 1971 bytes textures/mob_core_green_particle.png | Bin 0 -> 1942 bytes textures/mob_core_nametag.png | Bin 0 -> 1485 bytes textures/mob_core_protection_gem.png | Bin 0 -> 1957 bytes textures/mob_core_red_particle.png | Bin 0 -> 1916 bytes textures/mob_core_spawn_egg_base.png | Bin 0 -> 235 bytes textures/mob_core_spawn_egg_overlay.png | Bin 0 -> 1757 bytes 18 files changed, 4736 insertions(+) create mode 100644 LICENSE create mode 100644 api.lua create mode 100644 craftitems.lua create mode 100644 hq_lq.lua create mode 100644 init.lua create mode 100644 logic.lua create mode 100644 mob_core_api.txt create mode 100644 mod.conf create mode 100644 mount.lua create mode 100644 pathfinder.lua create mode 100644 settingtypes.txt create mode 100644 textures/mob_core_blue_particle.png create mode 100644 textures/mob_core_green_particle.png create mode 100644 textures/mob_core_nametag.png create mode 100644 textures/mob_core_protection_gem.png create mode 100644 textures/mob_core_red_particle.png create mode 100644 textures/mob_core_spawn_egg_base.png create mode 100644 textures/mob_core_spawn_egg_overlay.png diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /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 +. diff --git a/api.lua b/api.lua new file mode 100644 index 0000000..237e415 --- /dev/null +++ b/api.lua @@ -0,0 +1,1668 @@ +------------------ +-- Mob Core API -- +------------------ +----- Ver 0.1 ---- + +--------------------- +-- Local Variables -- +--------------------- + +local abs = math.abs +local floor = math.floor +local random = math.random +local min = math.min + +local creative = minetest.settings:get_bool("creative_mode") + +mob_core.walkable_nodes = {} + +minetest.register_on_mods_loaded(function() + for name in pairs(minetest.registered_nodes) do + if name ~= "air" and name ~= "ignore" then + if minetest.registered_nodes[name].walkable then + table.insert(mob_core.walkable_nodes, name) + end + end + end +end) + +---------------------- +-- Helper Functions -- +---------------------- + +local function all_first_to_upper(str) + str = string.gsub(" "..str, "%W%l", string.upper):sub(2) + return str +end + +local function underscore_to_space(str) + return (str:gsub("_", " ")) +end + +function mob_core.get_name_proper(str) + if str then + if str:match(":") then + str = str:split(":")[2] + end + str = all_first_to_upper(str) + str = underscore_to_space(str) + return str + end +end + +function mob_core.is_mobkit_mob(object) + if type(object) == 'userdata' then + object = object:get_luaentity() + end + if type(object) == 'table' then + if (object.logic or object.brainfunc) then + return true + else + return false + end + else + return false + end +end + +function mob_core.shared_owner(self, object) + if object:is_player() then return false end + if type(object) == 'userdata' then + object = object:get_luaentity() + end + if not self.tamed then return false end + if self.owner and object.owner then + if self.owner == object.owner then + return true + else + return false + end + end + return false +end + +function mob_core.find_val(tbl, val) + for _, v in ipairs(tbl) do + if v == val then + return true + end + end + return false +end + +-- Follow Holding? -- + +function mob_core.follow_holding(self, player) + local item = player:get_wielded_item() + local t = type(self.follow) + if t == "string" + and item:get_name() == self.follow then + return true + elseif t == "table" then + for no = 1, #self.follow do + if self.follow[no] == item:get_name() then + return true + end + end + end + return false +end + +----------------------- +-- Mob Item Handling -- +----------------------- + +function mob_core.register_spawn_egg(mob, col1, col2, inventory_image) + if col1 and col2 then + local len1 = string.len(col1) + local len2 = string.len(col2) + if len1 == 6 then + col1 = col1 .. "d9" + end + if len2 == 6 then + col2 = col2 .. "d9" + end + local base = "mob_core_spawn_egg_base.png^(mob_core_spawn_egg_base.png^[colorize:#"..col1..")" + local spots = "mob_core_spawn_egg_overlay.png^(mob_core_spawn_egg_overlay.png^[colorize:#"..col2..")" + inventory_image = base .. "^" .. spots + end + minetest.register_craftitem(mob:split(":")[1]..":spawn_"..mob:split(":")[2], { + description = "Spawn "..mob_core.get_name_proper(mob), + inventory_image = inventory_image, + stack_max = 99, + on_place = function(itemstack, _, pointed_thing) + local mobdef = minetest.registered_entities[mob] + local spawn_offset = math.abs(mobdef.collisionbox[2]) + local pos = minetest.get_pointed_thing_position(pointed_thing, true) + pos.y = pos.y+spawn_offset + minetest.add_entity(pos, mob) + if not creative then + itemstack:take_item() + return itemstack + end + end, + }) +end + +function mob_core.register_set(mob, background, mask) + local invimg = background + if mask then + invimg = "mob_core_spawn_egg_base.png^(" .. invimg .. + "^[mask:mob_core_spawn_egg_overlay.png)" + end + if not minetest.registered_entities[mob] then + return + end + -- register new spawn egg containing mob information + minetest.register_craftitem(mob .. "_set", { + description = mob_core.get_name_proper(mob).." (Captured)", + inventory_image = invimg, + groups = {not_in_creative_inventory = 1}, + stack_max = 1, + on_place = function(itemstack, placer, pointed_thing) + local pos = pointed_thing.above + -- am I clicking on something with existing on_rightclick function? + local under = minetest.get_node(pointed_thing.under) + local node = minetest.registered_nodes[under.name] + if node and node.on_rightclick then + return node.on_rightclick(pointed_thing.under, under, placer, itemstack) + end + if pos + and not minetest.is_protected(pos, placer:get_player_name()) then + pos.y = pos.y + 1 + local staticdata = itemstack:get_meta():get_string("staticdata") + minetest.add_entity(pos, mob, staticdata) + itemstack:take_item() + end + return itemstack + end, + }) +end + +----------------------- +-- Utility Functions -- +----------------------- + +------------ +-- Sounds -- +------------ + +function mob_core.make_sound(self, sound) + local spec = self.sounds and self.sounds[sound] + local parameters = {object = self.object} + + if type(spec) == 'table' then + if #spec > 0 then spec = spec[random(#spec)] end + + local function in_range(value) + return type(value) == 'table' and value[1]+random()*(value[2]-value[1]) or value + end + + local pitch = 1.0 + + pitch = pitch + random(-10, 10) * 0.005 + + if self.child + and self.sounds.alter_child_pitch then + parameters.pitch = 2.0 + end + + minetest.sound_play(spec, parameters) + + if not spec.gain then spec.gain = 1.0 end + if not spec.distance then spec.distance = 16 end + + --pick random values within a range if they're a table + parameters.gain = in_range(spec.gain) + parameters.max_hear_distance = in_range(spec.distance) + parameters.fade = in_range(spec.fade) + parameters.pitch = pitch + return minetest.sound_play(spec.name, parameters) + end + return minetest.sound_play(spec, parameters) +end + +function mob_core.random_sound(self, chance) -- Random Sound + if not chance then chance = 150 end + if math.random(1, chance) == 1 then + mob_core.make_sound(self, "random") + end +end + +---------------- +-- Drop Items -- +---------------- + +function mob_core.item_drop(self) -- Drop Items + if not self.drops or #self.drops == 0 then + return + end + local obj, item, num + local pos = mobkit.get_stand_pos(self) + for n = 1, #self.drops do + if math.random(1, self.drops[n].chance) == 1 then + num = math.random(self.drops[n].min or 0, self.drops[n].max or 1) + item = self.drops[n].name + if self.drops[n].min ~= 0 then + obj = minetest.add_item(pos, ItemStack(item .. " " .. num)) + if obj then + local v = math.random(-1, 1) + obj:add_velocity({x = v, y = 1, z = v}) + end + elseif obj then + obj:remove() + end + end + end + self.drops = {} +end + +------------------------ +-- Damage and Vitals -- +------------------------ + +-- Damage Indication -- + +local function flash_red(self) + minetest.after(0.0, function() + self.object:settexturemod("^[colorize:#FF000040") + core.after(0.2, function() + if mobkit.is_alive(self) then + self.object:settexturemod("") + end + end) + end) +end + +-- Death -- + +local pi = math.pi + +function mob_core.lq_fallover(self) + local zrot = 0 + local init = true + local func=function(self) + if init then + local vel = self.object:get_velocity() + self.object:set_velocity(mobkit.pos_shift(vel,{y=1})) + mobkit.animate(self,'stand') + init = false + end + zrot=zrot+pi*0.05 + local rot = self.object:get_rotation() + if rot then self.object:set_rotation({x=rot.x,y=rot.y,z=zrot}) end + if zrot >= pi*0.5 then return true end + end + mobkit.queue_low(self,func) +end + +function mob_core.on_die(self) + mobkit.clear_queue_high(self) + mobkit.clear_queue_low(self) + local pos = mobkit.get_stand_pos(self) + if self.driver then + mob_core.force_detach(self.driver) + end + if self.owner then + self.owner = nil + end + if self.sounds and self.sounds["death"] then + mob_core.make_sound(self, "death") + end + self.object:set_velocity({x=0,y=0,z=0}) + self.object:settexturemod("^[colorize:#FF000040") + local timer = 1 + local start = true + local func = function() + if not mobkit.exists(self) then return true end + if start then + if self.animation + and self.animation["death"] then + mobkit.animate(self,"death") + else + mob_core.lq_fallover(self) + end + self.logic = function() end -- brain dead as well + start = false + end + timer = timer-self.dtime + if timer <= 0 then + if self.driver then + mob_core.force_detach(self.driver) + end + mob_core.item_drop(self) + minetest.add_particlespawner({ + amount = 12, + time = 0.1, + minpos = { + x = pos.x - self.collisionbox[4]*0.75, + y = pos.y, + z = pos.z - self.collisionbox[4]*0.75, + }, + maxpos = { + x = pos.x + self.collisionbox[4]*0.75, + y = pos.y + self.collisionbox[4]*0.75, + z = pos.z + self.collisionbox[4]*0.75, + }, + minvel = {x=-0.2, y=-0.1, z=-0.2}, + maxvel = {x=0.2, y=-0.1, z=0.2}, + minacc = {x=0, y=0.25, z=0}, + maxacc = {x=0, y=0.45, z=0}, + minexptime = 1.5, + maxexptime = 2, + minsize = 2, + maxsize = 3, + collisiondetection = true, + vertical = false, + texture = "mob_core_red_particle.png" + }) + self.object:remove() + end + minetest.after(2, function() -- fail safe + if not mobkit.exists(self) then return true end + if self.driver then + mob_core.force_detach(self.driver) + end + mob_core.item_drop(self) + minetest.add_particlespawner({ + amount = self.collisionbox[4]*4, + time = 0.25, + minpos = { + x = pos.x - self.collisionbox[4]*0.5, + y = pos.y, + z = pos.z - self.collisionbox[4]*0.5, + }, + maxpos = { + x = pos.x + self.collisionbox[4]*0.5, + y = pos.y + self.collisionbox[4]*0.5, + z = pos.z + self.collisionbox[4]*0.5, + }, + minacc = {x = -0.25, y = 0.5, z = -0.25}, + maxacc = {x = 0.25, y = 0.25, z = 0.25}, + minexptime = 0.75, + maxexptime = 1, + minsize = 4, + maxsize = 4, + texture = "mob_core_red_particle.png", + glow = 16, + }) + self.object:remove() + end) + end + mobkit.queue_high(self, func, 100) +end + +-- Vitals -- + +function mob_core.vitals(self) + if not mobkit.is_alive(self) then return end + -- Fall Damage + if self.fall_damage == nil then + self.fall_damage = true + end + if self.fall_damage then + if not self.isonground + and not self.isinliquid + and not self.fall_start then + self.fall_start = mobkit.get_stand_pos(self).y + end + if self.fall_start then + local fall_distance = self.fall_start - mobkit.get_stand_pos(self).y + if not self.max_fall then + self.max_fall = 3 + end + if self.isonground + and fall_distance > self.max_fall then + flash_red(self) + mob_core.make_sound(self, "hurt") + mobkit.hurt(self, fall_distance) + self.fall_start = nil + end + end + end + + -- Lava/Fire Damage + if self.igniter_damage == nil then + self.igniter_damage = true + end + + if self.igniter_damage then + local pos = mobkit.get_stand_pos(self) + local node = minetest.get_node(pos) + if node and minetest.registered_nodes[node.name].groups.igniter then + if mobkit.timer(self,1) then + flash_red(self) + mob_core.make_sound(self, "hurt") + mobkit.hurt(self, self.max_hp/16) + end + end + end + + -- Drowning + if self.lung_capacity then + local colbox = self.object:get_properties().collisionbox + local headnode = mobkit.nodeatpos(mobkit.pos_shift(self.object:get_pos(),{y=colbox[5]})) -- node at hitbox top + if headnode and headnode.drawtype == 'liquid' then + self.oxygen = self.oxygen - self.dtime + else + self.oxygen = self.lung_capacity + end + + if self.oxygen <= 0 then + if mobkit.timer(self,2) then + flash_red(self) + mobkit.hurt(self,self.max_hp/self.lung_capacity) + end + end + end +end + +-- Basic Damage -- Flash red, Knockback, Make sound. + +function mob_core.on_punch_basic(self, puncher, tool_capabilities, dir) + local item = puncher:get_wielded_item() + if mobkit.is_alive(self) then + if self.immune_to then + for i = 1, #self.immune_to do + if item:get_name() == self.immune_to[i] then + return + end + end + end + if self.protected == true and puncher:get_player_name() ~= self.owner then + return + else + flash_red(self) + if self.isonground then + local hvel = vector.multiply(vector.normalize({x=dir.x,y=0,z=dir.z}),4) + self.object:add_velocity({x=hvel.x,y=2,z=hvel.z}) + end + mobkit.hurt(self,tool_capabilities.damage_groups.fleshy or 1) + mob_core.make_sound(self, "hurt") + end + end +end + +-- Retaliate -- + +function mob_core.on_punch_retaliate(self, puncher, water, group) + if mobkit.is_alive(self) then + local pos = self.object:get_pos() + if (not water) or (water and self.semiaquatic) then + mob_core.hq_hunt(self, 10, puncher) + if group then + local objs = minetest.get_objects_inside_radius(pos, self.view_range) + for n = 1, #objs do + local luaent = objs[n]:get_luaentity() + if luaent and luaent.name == self.name and luaent.owner == self.owner and mobkit.is_alive(luaent) then + mob_core.hq_hunt(luaent, 10, puncher) + end + end + end + elseif water and self.isinliquid then + mob_core.hq_aqua_attack(self, 10, puncher, 1) + if group then + local objs = minetest.get_objects_inside_radius(pos, self.view_range) + for n = 1, #objs do + local luaent = objs[n]:get_luaentity() + if luaent and luaent.name == self.name and luaent.owner == self.owner and mobkit.is_alive(luaent) then + mob_core.hq_aqua_attack(luaent, 10, puncher, 1) + end + end + end + end + end +end + +-- Runaway -- + +function mob_core.on_punch_runaway(self, puncher, water, group) + if mobkit.is_alive(self) then + local pos = self.object:get_pos() + if (not water) or (water and not self.isinliquid) then + mobkit.hq_runfrom(self, 10, puncher) + if group then + local objs = minetest.get_objects_inside_radius(pos, self.view_range) + for n = 1, #objs do + local luaent = objs[n]:get_luaentity() + if luaent and luaent.name == self.name and luaent.owner == self.owner and mobkit.is_alive(luaent) then + mobkit.hq_runfrom(self, 10, puncher) + end + end + end + elseif water and self.isinliquid then + mob_core.hq_swimfrom(self, 10, puncher, 1) + if group then + local objs = minetest.get_objects_inside_radius(pos, self.view_range) + for n = 1, #objs do + local luaent = objs[n]:get_luaentity() + if luaent and luaent.name == self.name and luaent.owner == self.owner and mobkit.is_alive(luaent) then + mob_core.hq_swimfrom(luaent, 10, puncher, 1) + end + end + end + end + end +end + +-- Default On Punch -- Likely won't be used, more of a demo + +function mob_core.on_punch(self, puncher, time_from_last_punch, tool_capabilities, dir) + mob_core.on_punch_basic(self, puncher, time_from_last_punch, tool_capabilities, dir) +end + +----------------- +-- On Activate -- +----------------- + +local function set_gender(self) + self.gender = mobkit.recall(self, "gender") or nil + if not self.gender then + if math.random(1, 2) == 1 then + self.gender = mobkit.remember(self, "gender", "female") + else + self.gender = mobkit.remember(self, "gender", "male") + end + end +end + +function mob_core.activate_nametag(self) + if not self.nametag then + return + end + self.object:set_properties({ + nametag = self.nametag, + nametag_color = "#FFFFFF" + }) +end + +function mob_core.set_textures(self) + if not self.texture_no then + if self.gender == "male" and self.male_textures then + if #self.male_textures > 1 then + self.texture_no = random(#self.male_textures) + else + self.texture_no = 1 + end + end + if self.gender == "female" and self.female_textures then + if #self.female_textures > 1 then + self.texture_no = random(#self.female_textures) + else + self.texture_no = 1 + end + end + end + if self.textures and self.texture_no then + local texture_no = self.texture_no + local props = {} + if self.gender == "female" then + if self.female_textures then + props.textures = {self.female_textures[texture_no]} + else + props.textures = {self.textures[texture_no]} + end + elseif self.gender == "male" then + if self.male_textures then + props.textures = {self.male_textures[texture_no]} + else + props.textures = {self.textures[texture_no]} + end + end + if self.child and self.child_textures then + if self.gender == "female" then + if self.child_female_textures then + if texture_no > #self.child_female_textures then + texture_no = 1 + end + props.textures = {self.child_female_textures[texture_no]} + else + if texture_no > #self.child_textures then + texture_no = 1 + end + props.textures = {self.child_textures[texture_no]} + end + elseif self.gender == "male" then + if self.child_male_textures then + if texture_no > #self.child_male_textures then + texture_no = 1 + end + props.textures = {self.child_male_textures[texture_no]} + else + if texture_no > #self.child_textures then + texture_no = 1 + end + props.textures = {self.child_textures[texture_no]} + end + end + end + self.object:set_properties(props) + end +end + +function mob_core.on_activate(self, staticdata, dtime_s) -- On Activate + local init_props = {} + if not self.textures then + if self.female_textures then + init_props.textures = {self.female_textures[1]} + elseif self.male_textures then + init_props.textures = {self.male_textures[1]} + end + self.object:set_properties(init_props) + end + if not self.textures then self.textures = {} end + mobkit.actfunc(self, staticdata, dtime_s) + self.tamed = mobkit.recall(self, "tamed") or false + self.owner = mobkit.recall(self, "owner") or nil + self.protected = mobkit.recall(self, "protected") or false + self.food = mobkit.recall(self, "food") or 0 + self.breed_mode = mobkit.recall(self, "breed_mode") or false + self.breed_timer = mobkit.recall(self, "breed_timer") or 0 + self.growth_stage = mobkit.recall(self, "growth_stage") or 4 + self.growth_timer = mobkit.recall(self, "growth_timer") or 1801 + self.child = mobkit.recall(self, "child") or false + self.status = mobkit.recall(self, "status") or "" + self.nametag = mobkit.recall(self, "nametag") or "" + set_gender(self) + mob_core.activate_nametag(self) + mob_core.set_textures(self) + if self.protected then + self.timeout = nil + end + if self.growth_stage == 1 + and self.scale_stage1 then + mob_core.set_scale(self, self.scale_stage1) + elseif self.growth_stage == 2 + and self.scale_stage2 then + mob_core.set_scale(self, self.scale_stage2) + elseif self.growth_stage == 3 + and self.scale_stage3 then + mob_core.set_scale(self, self.scale_stage3) + elseif self.growth_stage == 4 then + mob_core.set_scale(self, 1) + end +end + +----------------------- +-- Utility Functions -- +----------------------- + +-- Set Scale -- + +function mob_core.set_scale(self, scale) + self.base_size = self.visual_size + self.base_colbox = self.collisionbox + self.base_selbox = self.selectionbox + + self.object:set_properties({ + visual_size = { + x = self.base_size.x*scale, + y = self.base_size.y*scale + }, + collisionbox = { + self.base_colbox[1]*scale, + self.base_colbox[2]*scale, + self.base_colbox[3]*scale, + self.base_colbox[4]*scale, + self.base_colbox[5]*scale, + self.base_colbox[6]*scale + }, + }) +end + +-- Set Owner -- + +function mob_core.set_owner(self, name) + self.tamed = mobkit.remember(self, "tamed", true) + self.owner = mobkit.remember(self, "owner", name) +end + +-- Spawn Child Mob -- + +function mob_core.spawn_child(pos, mob) + local obj = minetest.add_entity(pos, mob) + local luaent = obj:get_luaentity() + luaent.child = mobkit.remember(luaent,"child",true) + luaent.growth_timer = mobkit.remember(luaent,"growth_timer",1) + luaent.growth_stage = mobkit.remember(luaent,"growth_stage",1) + mob_core.set_scale(luaent, luaent.scale_stage1 or 0.25) + mob_core.set_textures(luaent) + return +end + +-- Force Tame Command -- + +minetest.register_chatcommand("force_tame", { + params = "", + description = "tame pointed mobkit mob", + privs = {server = true, creative = true}, + func = function(name) + local player = minetest.get_player_by_name(name) + if not player then return false end + local dir = player:get_look_dir() + local pos = player:get_pos() + pos.y = pos.y + player:get_properties().eye_height or 1.625 + local dest = vector.add(pos, vector.multiply(dir, 40)) + local ray = minetest.raycast(pos, dest, true, false) + for pointed_thing in ray do + if pointed_thing.type == "object" then + local pointedobject = pointed_thing.ref + if pointedobject:get_luaentity() then + pointedobject = pointedobject:get_luaentity() + local mob_name = mob_core.get_name_proper(pointedobject.name) + if not pointedobject.tamed then + if not pointedobject.logic or pointedobject.brainfunc then + minetest.chat_send_player(name, "This command only works on mobkit mobs") + return + end + mob_core.set_owner(pointedobject, name) + minetest.chat_send_player(name, mob_name.." has been tamed!") + mobkit.clear_queue_high(pointedobject) + pos = pointedobject.object:get_pos() + minetest.add_particlespawner({ + amount = 16, + time = 0.25, + minpos = { + x = pos.x - pointedobject.collisionbox[4], + y = pos.y - pointedobject.collisionbox[4], + z = pos.z - pointedobject.collisionbox[4], + }, + maxpos = { + x = pos.x + pointedobject.collisionbox[4], + y = pos.y + pointedobject.collisionbox[4], + z = pos.z + pointedobject.collisionbox[4], + }, + minacc = {x = 0, y = 0.25, z = 0}, + maxacc = {x = 0, y = -0.25, z = 0}, + minexptime = 0.75, + maxexptime = 1, + minsize = 4, + maxsize = 4, + texture = "mob_core_green_particle.png", + glow = 16, + }) + return + else + minetest.chat_send_player(name, mob_name.." is already tamed.") + return + end + end + else + minetest.chat_send_player(name, "You must be pointing at a mob.") + return + end + end + end +}) + +-- Spawning -- + +function mob_core.get_biome_name(pos) + if not pos then return end + return minetest.get_biome_name(minetest.get_biome_data(pos).biome) +end + +local find_node_height = 32 + +local block_protected_spawn = minetest.settings:get_bool("block_protected_spawn") or true +local mob_limit = tonumber(minetest.settings:get("mob_limit") or 6) + +function mob_core.spawn(name, nodes, min_light, max_light, min_height, + max_height, min_rad, max_rad, group, optional) + group = group or 1 + if minetest.registered_entities[name] then + for _, player in ipairs(minetest.get_connected_players()) do + local mobs_amount = 0 + --use this instead of 'return' + local allow_spawn_mob = true + + + for _, entity in pairs(minetest.luaentities) do + if entity.name == name then + local ent_pos = entity.object:get_pos() + if ent_pos and vector.distance(player:get_pos(), ent_pos) <= + max_rad then + mobs_amount = mobs_amount + 1 + end + end + end + + local mob_spawned = false + + local spawned_pos = nil + + if mobs_amount >= mob_limit then allow_spawn_mob = false end + + local reliability = 3 + + if optional and optional.reliability then + reliability = optional.reliability + end + + for _ = 1, reliability do -- 3 attempts + local int = {-1, 1} + local pos = vector.floor(vector.add(player:get_pos(), 0.5)) + + local x, z + + -- this is used to determine the axis buffer from the player + local axis = math.random(0, 1) + + -- cast towards the direction + if axis == 0 then -- x + x = pos.x + math.random(min_rad, max_rad) * + int[random(1, 2)] + z = pos.z + math.random(-max_rad, max_rad) + else -- z + z = pos.z + math.random(min_rad, max_rad) * + int[random(1, 2)] + x = pos.x + math.random(-max_rad, max_rad) + end + + local spawner = minetest.find_nodes_in_area_under_air( + vector.new(x - 1, pos.y - find_node_height, + z - 1), vector.new(x + 1, + pos.y + + find_node_height, + z + 1), nodes) + + if table.getn(spawner) > 0 then + local mob_pos = spawner[1] + + if block_protected_spawn and + minetest.is_protected(mob_pos, "") then + allow_spawn_mob = false + end + + if optional then + if optional.biomes then + if not mob_core.find_val(optional.biomes, + mob_core.get_biome_name(pos)) then + allow_spawn_mob = false + end + end + end + + if mob_pos.y > max_height or mob_pos.y < min_height then + allow_spawn_mob = false + end + + local light = minetest.get_node_light(mob_pos) + if not light or light > max_light or light < min_light then + allow_spawn_mob = false + end + + + if allow_spawn_mob == true then + mob_spawned = true + + spawned_pos = mob_pos + + minetest.add_entity(mob_pos, name) + + if group then + + local spawned = 0 + + local attempts = 0 + + while spawned < group and attempts < group * 2 do + local mobdef = minetest.registered_entities[name] + local side = mobdef.collisionbox[4] + local group_pos = + vector.new( + mob_pos.x + (random(-group, group) * side), + mob_pos.y, + mob_pos.z + (random(-group, group) * side)) + local spawn_pos = + minetest.find_nodes_in_area_under_air( + vector.new(group_pos.x, group_pos.y - 8, + group_pos.z), vector.new( + group_pos.x, group_pos.y + 8, + group_pos.z), nodes) + if spawn_pos[1] then + minetest.add_entity( + vector.new(spawn_pos[1].x, spawn_pos[1].y + + math.abs( + mobdef.collisionbox[2]), + spawn_pos[1].z), name) + spawned = spawned + 1 + end + attempts = attempts + 1 + end + end + end + break + end + end + --return mob_spawned, spawned_pos + --moved the execution of registered_on_spawns here + if mob_spawned == true and mob_core.registered_on_spawns[name] then + mob_core.registered_spawns[name].last_pos = spawned_pos + local on_spawn = mob_core.registered_on_spawns[name] + on_spawn.func(unpack(on_spawn.args)) + end + end + end +end + +mob_core.registered_on_spawns = {} + +mob_core.registered_spawns = {} + +function mob_core.register_spawn(def, interval, chance) + local spawn_timer = 0 + mob_core.registered_spawns[def.name] = {func = nil, last_pos = {}} + mob_core.registered_spawns[def.name].func = + minetest.register_globalstep(function(dtime) + spawn_timer = spawn_timer + dtime + if spawn_timer > interval then + if random(1, chance) == 1 then + -- local spawned, last_pos = + mob_core.spawn( + def.name, + def.nodes or {"group:soil", "group:stone"}, + def.min_light or 0, def.max_light or 15, + def.min_height or -31000, + def.max_height or 31000, + def.min_rad or 24, def.max_rad or 256, + def.group or 1, + def.optional or nil + ) + -- moved into mob_core.spawn() + -- if spawned and mob_core.registered_on_spawns[def.name] then + -- mob_core.registered_spawns[def.name].last_pos = last_pos + -- local on_spawn = mob_core.registered_on_spawns[def.name] + -- on_spawn.func(unpack(on_spawn.args)) + -- end + end + spawn_timer = 0 + end + end) +end + +function mob_core.register_on_spawn(name, func, ...) + mob_core.registered_on_spawns[name] = {args = {...}, func = func} +end + + +------------- +-- On Step -- +------------- + +-- Push on entity collision -- + +function mob_core.collision_detection(self) + if not mobkit.is_alive(self) then return end + local pos = self.object:get_pos() + local hitbox = self.object:get_properties().collisionbox + local width = -hitbox[1] + hitbox[4] + 0.5 + for _, object in ipairs(minetest.get_objects_inside_radius(pos, width)) do + if (object and object ~= self.object) + and (object:is_player() or (object:get_luaentity() and object:get_luaentity().logic)) + and (not object:get_attach() or (object:get_attach() and object:get_attach() ~= self.object)) + and (not self.object:get_attach() or (self.object:get_attach() and self.object:get_attach() ~= object)) then + local pos2 = object:get_pos() + local dir = vector.direction(pos,pos2) + dir.y = 0 + if dir.x == 0 and dir.z == 0 then + dir = vector.new(math.random(-1,1)*math.random(),0,math.random(-1,1)*math.random()) + end + local velocity = vector.multiply(dir,1.1) + local vel1 = vector.multiply(velocity, -1) + local vel2 = velocity + self.object:add_velocity(vel1) + if object:is_player() then + object:add_player_velocity(vel2) + else + object:add_velocity(vel2) + end + end + end +end + +-- 4 Stage Growth -- + +function mob_core.growth(self,interval) + if not mobkit.is_alive(self) then return end + if self.growth_stage == 4 then return end + if not self.base_hp then + self.base_hp = mobkit.remember(self,"base_hp",self.max_hp) + end + local pos = self.object:get_pos() + local reach = minetest.registered_entities[self.name].reach + local speed = minetest.registered_entities[self.name].max_speed + interval = interval or 400 + if not self.scale_stage1 then + self.scale_stage1 = 0.25 + self.scale_stage2 = 0.5 + self.scale_stage3 = 0.75 + end + if self.growth_stage < 4 then + self.growth_timer = self.growth_timer + 1 + end + if self.growth_timer > interval then + self.growth_timer = 1 + self.max_speed = speed/4 + if reach then + self.reach = reach/4 + end + if self.growth_stage == 1 then + self.growth_stage = mobkit.remember(self,"growth_stage",2) + self.object:set_pos({x=pos.x,y=pos.y+math.abs(self.collisionbox[2]),z=pos.z}) + mob_core.set_scale(self, self.scale_stage2) + self.max_speed = speed/2 + if reach then + self.reach = reach/2 + end + elseif self.growth_stage == 2 then + self.growth_stage = mobkit.remember(self,"growth_stage",3) + self.object:set_pos({x=pos.x,y=pos.y+math.abs(self.collisionbox[2]),z=pos.z}) + mob_core.set_scale(self, self.scale_stage3) + self.child = mobkit.remember(self,"child",false) + mob_core.set_textures(self) + self.max_speed = speed/1.5 + if reach then + self.reach = reach/1.5 + end + elseif self.growth_stage == 3 then + self.growth_stage = mobkit.remember(self,"growth_stage",4) + self.object:set_pos({x=pos.x,y=pos.y+math.abs(self.collisionbox[2]),z=pos.z}) + mob_core.set_scale(self, 1) + self.max_speed = speed + if reach then + self.reach = reach + end + end + end + if self.growth_stage == 1 + and self.hp > self.max_hp/4 then + self.hp = self.max_hp/4 + end + if self.growth_stage == 2 + and self.hp > self.max_hp/2 then + self.hp = self.max_hp/2 + end + if self.growth_stage == 3 + and self.hp > self.max_hp/1.5 then + self.hp = self.max_hp/1.5 + end + self.growth_timer = mobkit.remember(self,"growth_timer",self.growth_timer) +end + +-- Breeding -- + +function mob_core.breed(self) + if not mobkit.is_alive(self) then return end + if self.breed_timer > 0 then + self.breed_timer = self.breed_timer - self.dtime + else + self.breed_timer = 0 + end + if self.gender == "female" then + local pos = self.object:get_pos() + local objs = minetest.get_objects_inside_radius(pos, self.collisionbox[4]*4) + for i = 1, #objs do + local luaent = objs[i]:get_luaentity() + if luaent and luaent.name == self.name then + if luaent.breed_mode == true and luaent.gender == "male" then + self.breed_mode = false + self.breed_timer = 300 + luaent.breed_mode = false + luaent.breed_timer = 300 + minetest.after(2.5,function() + mob_core.spawn_child(pos,self.name) + minetest.add_particlespawner({ + amount = 16, + time = 0.25, + minpos = { + x = pos.x - self.collisionbox[4], + y = pos.y - self.collisionbox[4], + z = pos.z - self.collisionbox[4], + }, + maxpos = { + x = pos.x + self.collisionbox[4], + y = pos.y + self.collisionbox[4], + z = pos.z + self.collisionbox[4], + }, + minacc = {x = 0, y = 0.25, z = 0}, + maxacc = {x = 0, y = -0.25, z = 0}, + minexptime = 0.75, + maxexptime = 1, + minsize = 4, + maxsize = 4, + texture = "heart.png", + glow = 16, + }) + end) + end + end + end + end +end + +-- Step Function -- + +function mob_core.on_step(self, dtime, moveresult) + mobkit.stepfunc(self, dtime) + self.moveresult = moveresult + if self.owner_target + and not mobkit.exists(self.owner_target) then + self.owner_target = nil + end + if self.custom_punch_target + and not mobkit.exists(self.custom_punch_target) then + self.custom_punch_target = nil + end + + if self.core_growth then + mob_core.growth(self) + end + if self.core_breeding then + mob_core.breed(self) + end + if self.push_on_collide then + mob_core.collision_detection(self) + end +end + + +-------------------------- +-- Rightclick Functions -- +-------------------------- + +-- Mount -- + +function mob_core.mount(self, clicker) + if not self.driver and self.child == false then + mobkit.clear_queue_high(self) + self.status = mobkit.remember(self,"status","ridden") + mob_core.attach(self, clicker) + return false + else + return true + end +end + +-- Capture Mob -- + +function mob_core.capture_mob(self, clicker, capture_tool, capture_chance, wear, force_take) + if not clicker:is_player() + or not clicker:get_inventory() then + return false + end + local mobname = self.name + local catcher = clicker:get_player_name() + local tool = clicker:get_wielded_item() + if tool:get_name() ~= capture_tool then + return false + end + if self.tamed == false then + minetest.chat_send_player(catcher, "Mob is not tamed.") + return false + end + if self.owner ~= catcher + and force_take == false then + minetest.chat_send_player(catcher, "Mob is owned by @1"..self.owner) + return false + end + if clicker:get_inventory():room_for_item("main", mobname) then + local chance = 0 + if tool:get_name() == capture_tool then + if capture_tool == "" then + chance = capture_chance + else + chance = capture_chance + tool:add_wear(wear) + clicker:set_wielded_item(tool) + end + end + if chance and chance > 0 and random(1, 100) <= chance then + local new_stack = ItemStack(mobname .. "_set") + new_stack:get_meta():set_string("staticdata", self:get_staticdata()) + local inv = clicker:get_inventory() + if inv:room_for_item("main", new_stack) then + inv:add_item("main", new_stack) + else + minetest.add_item(clicker:get_pos(), new_stack) + end + self.object:remove() + return new_stack + elseif chance and chance ~= 0 then + return false + elseif not chance then + return false + end + end + + return true +end + +-- Feed/Tame/Breed -- + +function mob_core.feed_tame(self, clicker, feed_count, tame, breed) + local item = clicker:get_wielded_item() + local pos = self.object:get_pos() + local mob_name = mob_core.get_name_proper(self.name) + if mob_core.follow_holding(self, clicker) then + if creative == false then + item:take_item() + clicker:set_wielded_item(item) + end + mobkit.heal(self, self.max_hp/feed_count) + if self.hp >= self.max_hp then + self.hp = self.max_hp + end + self.food = mobkit.remember(self, "food", self.food + 1) + if self.food >= feed_count then + self.food = mobkit.remember(self, "food", 0) + if tame and not self.tamed then + mob_core.set_owner(self, clicker:get_player_name()) + minetest.chat_send_player(clicker:get_player_name(), mob_name.." has been tamed!") + mobkit.clear_queue_high(self) + minetest.add_particlespawner({ + amount = 16, + time = 0.25, + minpos = { + x = pos.x - self.collisionbox[4], + y = pos.y - self.collisionbox[4], + z = pos.z - self.collisionbox[4], + }, + maxpos = { + x = pos.x + self.collisionbox[4], + y = pos.y + self.collisionbox[4], + z = pos.z + self.collisionbox[4], + }, + minacc = {x = 0, y = 0.25, z = 0}, + maxacc = {x = 0, y = -0.25, z = 0}, + minexptime = 0.75, + maxexptime = 1, + minsize = 4, + maxsize = 4, + texture = "mob_core_green_particle.png", + glow = 16, + }) + end + if breed then + if self.child then return false end + if self.breed_mode then return false end + if self.breed_timer == 0 and self.breed_mode == false then + self.breed_mode = true + minetest.add_particlespawner({ + amount = 16, + time = 0.25, + minpos = { + x = pos.x - self.collisionbox[4], + y = pos.y - self.collisionbox[4], + z = pos.z - self.collisionbox[4], + }, + maxpos = { + x = pos.x + self.collisionbox[4], + y = pos.y + self.collisionbox[4], + z = pos.z + self.collisionbox[4], + }, + minacc = {x = 0, y = 0.25, z = 0}, + maxacc = {x = 0, y = -0.25, z = 0}, + minexptime = 0.75, + maxexptime = 1, + minsize = 4, + maxsize = 4, + texture = "heart.png", + glow = 16, + }) + end + end + end + end + return false +end + +-- Protection -- + +function mob_core.protect(self, clicker, force_protect) + local name = clicker:get_player_name() + local item = clicker:get_wielded_item() + local mob_name = mob_core.get_name_proper(self.name) + if item:get_name() ~= "mob_core:protection_gem" then + return false + end + if self.tamed == false and not force_protect then + minetest.chat_send_player(name, mob_name.." is not tamed") + return true + end + if self.protected == true then + minetest.chat_send_player(name, mob_name.." is already protected") + return true + end + if not creative then + item:take_item() + clicker:set_wielded_item(item) + end + self.protected = true + mobkit.remember(self, "protected", self.protected) + self.timeout = nil + local pos = self.object:get_pos() + pos.y = pos.y + self.collisionbox[2] + 1/1 + minetest.add_particlespawner({ + amount = 16, + time = 0.25, + minpos = { + x = pos.x - self.collisionbox[4], + y = pos.y - self.collisionbox[4], + z = pos.z - self.collisionbox[4], + }, + maxpos = { + x = pos.x + self.collisionbox[4], + y = pos.y + self.collisionbox[4], + z = pos.z + self.collisionbox[4], + }, + minacc = {x = 0, y = 0.25, z = 0}, + maxacc = {x = 0, y = -0.25, z = 0}, + minexptime = 0.75, + maxexptime = 1, + minsize = 4, + maxsize = 4, + texture = "mob_core_green_particle.png", + glow = 16, + }) + return true +end + +-- Set Nametag -- + +local nametag_obj = {} +local nametag_item = {} + +function mob_core.nametag(self, clicker, force_name) + if not force_name + and clicker:get_player_name() ~= self.owner then + return + end + local item = clicker:get_wielded_item() + if item:get_name() == "mob_core:nametag" then + + local name = clicker:get_player_name() + + nametag_obj[name] = self + nametag_item[name] = item + + local tag = self.nametag or "" + + minetest.show_formspec(name, "mob_core_nametag", "size[8,4]" + .. "field[0.5,1;7.5,0;name;" + .. minetest.formspec_escape("Enter name:") .. ";" .. tag .. "]" + .. "button_exit[2.5,3.5;3,1;mob_rename;" + .. minetest.formspec_escape("Rename") .. "]") + end +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + + if formname == "mob_core_nametag" + and fields.name then + + local name = player:get_player_name() + + if not nametag_obj[name] + or not nametag_obj[name].object then + return + end + + local item = player:get_wielded_item() + + if item:get_name() ~= "mob_core:nametag" then + return + end + + if string.len(fields.name) > 64 then + fields.name = string.sub(fields.name, 1, 64) + end + + nametag_obj[name].nametag = mobkit.remember(nametag_obj[name], "nametag", fields.name) + + mob_core.activate_nametag(nametag_obj[name]) + + if fields.name ~= "" + and not creative then + nametag_item[name]:take_item() + player:set_wielded_item(nametag_item[name]) + end + + nametag_obj[name] = nil + nametag_item[name] = nil + end +end) + +----------------- +-- Pathfinding -- +----------------- + +----------------- +-- Pathfinding -- +----------------- + +local function can_fit(pos, width, single_plane) + height = height or 0 + local pos1 = vector.new(pos.x - width, pos.y, pos.z - width) + local pos2 = vector.new(pos.x + width, pos.y + height, pos.z + width) + for x = pos1.x, pos2.x do + for y = pos1.y, pos2.y do + for z = pos1.z, pos2.z do + local p2 = vector.new(x, y, z) + local node = minetest.get_node(p2) + if minetest.registered_nodes[node.name].walkable then + local p3 = vector.new(p2.x, p2.y + 1, p2.z) + local node2 = minetest.get_node(p3) + if minetest.registered_nodes[node2.name].walkable then + return false + end + if single_plane then + return false + end + end + end + end + end + return true +end + +local function move_from_wall(pos, width) + local pos1 = vector.new(pos.x - width, pos.y, pos.z - width) + local pos2 = vector.new(pos.x + width, pos.y, pos.z + width) + for x = pos1.x, pos2.x do + for y = pos1.y, pos2.y do + for z = pos1.z, pos2.z do + local p2 = vector.new(x, y, z) + if can_fit(p2, width) + and vector.distance(pos, p2) < width then + return p2 + end + end + end + end + return pos +end + +function mob_core.find_path_lite(pos, tpos, width) + + local raw + + if not minetest.registered_nodes[minetest.get_node( + vector.new(pos.x, pos.y - 1, pos.z)) + .name].walkable then + local min = vector.subtract(pos, width+1) + local max = vector.add(pos, width+1) + + local index_table = minetest.find_nodes_in_area_under_air( min, max, mob_core.walkable_nodes) + for _, i_pos in pairs(index_table) do + if minetest.registered_nodes[minetest.get_node(i_pos) + .name].walkable then + pos = vector.new(i_pos.x, i_pos.y + 1, i_pos.z) + break + end + end + end + + if not minetest.registered_nodes[minetest.get_node( + vector.new(tpos.x, tpos.y - 1, tpos.z)) + .name].walkable then + local min = vector.subtract(tpos, width) + local max = vector.add(tpos, width) + + local index_table = minetest.find_nodes_in_area_under_air( min, max, mob_core.walkable_nodes) + for _, i_pos in pairs(index_table) do + if minetest.registered_nodes[minetest.get_node(i_pos) + .name].walkable then + tpos = vector.new(i_pos.x, i_pos.y + 1, i_pos.z) + break + end + end + end + + local path = minetest.find_path(pos, tpos, 32, 2, 2, "A*_noprefetch") + + if not path then return end + + table.remove(path, 1) + + for i = #path, 1, -1 do + if not path then return end + if vector.distance(pos, path[i]) <= width + 1 then + for i = 3, #path do + path[i - 1] = path[i] + end + end + + if not can_fit(path[i], width + 1) then + local clear = move_from_wall(path[i], width + 1) + if clear and can_fit(clear, width) then + path[i] = clear + end + end + + if minetest.get_node(path[i]).name == "default:snow" then + path[i] = vector.new(path[i].x, path[i].y + 1, path[i].z) + end + + + raw = path + if #path > 3 then + + if vector.distance(pos, path[i]) < width then + table.remove(path, i) + end + + local pos1 = path[i - 2] + local pos2 = path[i] + -- Handle Diagonals + if pos1 + and pos2 + and pos1.x ~= pos2.x + and pos1.z ~= pos2.z then + if minetest.line_of_sight(pos1, pos2) then + local pos3 = vector.divide(vector.add(pos1, pos2), 2) + if can_fit(pos, width) then + table.remove(path, i - 1) + end + end + end + -- Reduce Straight Lines + if pos1 + and pos2 + and pos1.x == pos2.x + and pos1.z ~= pos2.z + and pos1.y == pos2.y then + if minetest.line_of_sight(pos1, pos2) then + local pos3 = vector.divide(vector.add(pos1, pos2), 2) + if can_fit(pos, width) then + table.remove(path, i - 1) + end + end + elseif pos1 + and pos2 + and pos1.x ~= pos2.x + and pos1.z == pos2.z + and pos1.y == pos2.y then + if minetest.line_of_sight(pos1, pos2) then + local pos3 = vector.divide(vector.add(pos1, pos2), 2) + if can_fit(pos, width) then + table.remove(path, i - 1) + end + end + end + end + end + + return path, raw +end + +function mob_core.find_path(self, tpos) + + if not mobkit.is_alive(self) then return end + + local pos = mobkit.get_stand_pos(self) + + local pos_above = function(v) return vector.new(v.x, v.y + 1, v.z) end + + local pos_under = function(v) return vector.new(v.x, v.y - 1, v.z) end + + local width = self.object:get_properties().collisionbox[4] + 1 + + if not minetest.registered_nodes[minetest.get_node(pos_under(pos)).name].walkable then + + local min = vector.subtract(pos, width + 1) + local max = vector.add(pos, width + 1) + + local index_table = minetest.find_nodes_in_area_under_air(min, max, mob_core.walkable_nodes) + for _, i_pos in pairs(index_table) do + if can_fit(i_pos, width) then + pos = vector.new(i_pos.x, i_pos.y + 0.6, i_pos.z) + break + end + end + end + + if not minetest.registered_nodes[minetest.get_node(pos_under(tpos)).name].walkable then + local min = vector.subtract(tpos, width) + local max = vector.add(tpos, width) + + local index_table = minetest.find_nodes_in_area_under_air(min, max, mob_core.walkable_nodes) + for _, i_pos in pairs(index_table) do + if minetest.registered_nodes[minetest.get_node(i_pos).name].walkable then + tpos = vector.new(i_pos.x, i_pos.y + 1, i_pos.z) + break + end + end + end + + local path = pathfinder.find_path(self, pos, tpos, 64, self.dtime) + + if not path then return end + + for i = #path, 1, -1 do + + local under = minetest.get_node(pos_under(path[i])) + + if minetest.registered_nodes[under.name] + and not minetest.registered_nodes[under.name].walkable then + table.remove(path, i) + end + + local above = pos_above(path[i]) + + if not can_fit(path[i], width, true) then + local clear = move_from_wall(path[i], width + 1) + if clear and can_fit(clear, width) + and (path[i + 1] + and path[i].y <= path[i + 1].y) then + path[i] = clear + end + end + + if vector.distance(pos, path[i]) <= width then + table.remove(path, i) + end + end + + return path +end diff --git a/craftitems.lua b/craftitems.lua new file mode 100644 index 0000000..fc7f6c6 --- /dev/null +++ b/craftitems.lua @@ -0,0 +1,35 @@ +-------------------- +-- Mob Core Items -- +-------------------- +------ Ver 0.1 ----- + +-- Protection Gem -- + +minetest.register_craftitem("mob_core:protection_gem", { + description = "Protection Gem", + inventory_image = "mob_core_protection_gem.png", +}) + +-- Nametag -- + +minetest.register_craftitem("mob_core:nametag", { + description = "Name Tag", + inventory_image = "mob_core_nametag.png", + groups = {flammable = 2} +}) + +-- Crafting -- + +minetest.register_craft({ + type = "shapeless", + output = "mob_core:protection_gem", + recipe = {"default:diamond"} +}) + +if minetest.get_modpath("dye") and minetest.get_modpath("farming") then + minetest.register_craft({ + type = "shapeless", + output = "mob_core:nametag", + recipe = {"default:paper", "dye:black", "farming:string"} + }) +end diff --git a/hq_lq.lua b/hq_lq.lua new file mode 100644 index 0000000..e051139 --- /dev/null +++ b/hq_lq.lua @@ -0,0 +1,1350 @@ +------------------------------ +-- Mob Core HQ/LQ Functions -- +------------------------------ +---------- Ver 0.1 ----------- + +------------ +-- Locals -- +------------ + +local random = math.random +local pi = math.pi +local abs = math.abs +local ceil = math.ceil + +local vec_dist = vector.distance + +local abr = minetest.get_mapgen_setting('active_block_range') +local legacy_jump = minetest.settings:get_bool("legacy_jump") + +local neighbors = { + {x = 1, z = 0}, {x = 1, z = 1}, {x = 0, z = 1}, {x = -1, z = 1}, + {x = -1, z = 0}, {x = -1, z = -1}, {x = 0, z = -1}, {x = 1, z = -1} +} + +--------------------- +-- Quick Callbacks -- +--------------------- + +-- Current Collisionbox -- + +function mob_core.get_hitbox(object) + if type(object) == "table" then + object = object.object + end + return object:get_properties().collisionbox +end + +local hitbox = mob_core.get_hitbox -- Recommended use for cleaner code + +local function dist_2d(pos1, pos2) + local a = vector.new(pos1.x, 0, pos1.z) + local b = vector.new(pos2.x, 0, pos2.z) + return vec_dist(a, b) +end + +-------------------- +-- Object Control -- +-------------------- + +-- Set Vertical Velocity -- + +local function set_lift(self, val) + local vel = self.object:get_velocity() + vel.y = val + self.object:set_velocity(vel) +end + +------------- +-- Sensors -- +------------- + +local function index_collisions(self, pos, no_air) + local width = self.object:get_properties().collisionbox[4] + 1 + local pos1 = vector.subtract(pos, width) + local pos2 = vector.add(pos, width) + local collisions = {} + for x = pos1.x, pos2.x do + for y = pos1.y, pos2.y do + for z = pos1.z, pos2.z do + local npos = vector.new(x, y, z) + local name = minetest.get_node(npos).name + if minetest.registered_nodes[name].walkable or + (no_air and name == "air") then + table.insert(collisions, npos) + end + end + end + end + return collisions +end + +-- Can Fit -- + +function mob_core.can_fit(self, pos, no_air) + local width = hitbox(self)[4] + 1 + local height = self.height * 0.5 + local pos1 = vector.new(pos.x - width, pos.y - height, pos.z - width) + local pos2 = vector.new(pos.x + width, pos.y + height, pos.z + width) + for x = pos1.x, pos2.x do + for y = pos1.y, pos2.y do + for z = pos1.z, pos2.z do + local npos = vector.new(x, y, z) + local name = minetest.get_node(npos).name + if minetest.registered_nodes[name].walkable or + (no_air and name == "air") then + return false + end + end + end + end + return true +end + +-- Obstacle Avoidance Calculation -- + +local function find_closest_pos(tbl, pos) + local iter = 2 + if #tbl < 2 then return end + local closest = tbl[1] + while iter < #tbl do + if vec_dist(pos, closest) < vec_dist(pos, tbl[iter + 1]) then + iter = iter + 1 + else + closest = tbl[iter] + iter = iter + 1 + end + end + if iter >= #tbl and closest then return closest end +end + +function mob_core.collision_avoidance(self) + local box = hitbox(self) + local width = abs(box[3]) + abs(box[6]) + local pos = self.object:get_pos() + local yaw = self.object:get_yaw() + local outset = self.obstacle_avoidance_range or 1 + local ahead = vector.add(pos, vector.multiply(minetest.yaw_to_dir(yaw), + width * outset)) + local can_fit = mob_core.can_fit(self, ahead) + local collisions = index_collisions(self, ahead) + local obstacle = find_closest_pos(collisions, pos) + if not can_fit and obstacle then + local avoidance_path = + vector.normalize((vector.subtract(pos, obstacle))) + local magnitude = (width * 2) - vec_dist(pos, obstacle) + return avoidance_path, magnitude + end +end + +-- Find Water Surface -- + +local function sensor_surface(self, range) + local pos = self.object:get_pos() + local node = minetest.get_node(pos) + local dist = 0 + while node.name == self.isinliquid and dist <= range do + pos.y = pos.y + 1 + node = minetest.get_node(pos) + dist = dist + 1 + end + if node.name ~= self.isinliquid then return dist end + return range +end + +-- Find First Solid Node Above Object -- + +local function sensor_ceil(self, range) + local pos = self.object:get_pos() + local node = minetest.get_node(pos) + local dist = 0 + while not minetest.registered_nodes[node.name].walkable and dist <= range do + pos.y = pos.y + 1 + node = minetest.get_node(pos) + dist = dist + 1 + end + if minetest.registered_nodes[node.name].walkable then return dist end + return range +end + +-- Find First Solid/Liquid Node Below Object -- + +local function sensor_floor(self, range, water) + water = water or false + local pos = self.object:get_pos() + local node = minetest.get_node(pos) + local dist = 0 + while not minetest.registered_nodes[node.name].walkable and abs(dist) <= + range do + pos.y = pos.y - 1 + node = minetest.get_node(pos) + dist = dist - 1 + end + if water then + if minetest.registered_nodes[node.name].walkable or + minetest.registered_nodes[node.name].drawtype == "liquid" then + return abs(dist) + end + else + if minetest.registered_nodes[node.name].walkable then + return abs(dist) + end + end + return range +end + +-- Find Nearest Node From Input Table -- + +function mob_core.find_node_expanding(self, input) + input = input or mob_core.walkable_nodes + local pos = self.object:get_pos() + local radius = 0 + local height = 0 + local input_index = {} + while radius <= self.view_range do + radius = radius + 1 + height = height + 0.25 + input_index = minetest.find_nodes_in_area_under_air( + vector.new(pos.x - radius, pos.y - 1, pos.z - radius), + vector.new(pos.x + radius, pos.y + height, + pos.z + radius), input) + end + if #input_index > 1 then return input_index[1] end +end + +--------------------- +-- Basic Functions -- +--------------------- + +-- Check for a Step -- + +local function step_check(self) -- This function is only used to find a step for pre 5.3 mobs + if not legacy_jump then return end + local pos = mobkit.get_stand_pos(self) + local width = hitbox(self)[4] + 0.3 + local pos1 = {x = pos.x + width, y = pos.y + 1.1, z = pos.z + width} + local pos2 = {x = pos.x - width, y = pos.y, z = pos.z - width} + local area = minetest.find_nodes_in_area_under_air(pos1, pos2, + mob_core.walkable_nodes) + if #area < 1 then return end + for i = 1, #area do + local yaw = self.object:get_yaw() + local yaw_to_node = minetest.dir_to_yaw(vector.direction(pos, area[i])) + if abs(yaw - yaw_to_node) <= 1.5 then + local center = self.object:get_pos() + center.y = center.y + 1.1 + center = vector.add(center, + vector.multiply(minetest.yaw_to_dir(yaw), 0.25)) + self.object:set_pos(center) + break + end + end +end + +-- Check for Fall -- + +local function line_of_sight(pos1, pos2) -- from mobs_redo, by Astrobe + local ray = minetest.raycast(pos1, pos2, true, true) + local thing = ray:next() + while thing do + if thing.type == "node" then + local name = minetest.get_node(thing.under).name + if minetest.registered_items[name] and + (minetest.registered_items[name].walkable or + minetest.registered_items[name].groups.liquid) then + return false + end + end + + thing = ray:next() + end + return true +end + +function mob_core.fall_check(self, pos, height) -- Partially taken from mobs_redo + if not mobkit.is_alive(self) then return false end + if height == 0 then return false end + local yaw = self.object:get_yaw() + local dir_x = -math.sin(yaw) * (self.collisionbox[4] + 0.5) + local dir_z = math.cos(yaw) * (self.collisionbox[4] + 0.5) + pos = pos or self.object:get_pos() + local ypos = pos.y + self.collisionbox[2] + if line_of_sight({x = pos.x + dir_x, y = ypos, z = pos.z + dir_z}, + {x = pos.x + dir_x, y = ypos - height, z = pos.z + dir_z}) then + return true + end + return false +end + +------------------------ +-- API Object Control -- +------------------------ + +function mob_core.knockback(self, target) + if not self.knockback then return end + local pos = mobkit.get_stand_pos(self) + local pos2 = target:get_pos() + if not pos2 then return end + local dir = vector.direction(pos, pos2) + if target:is_player() then + local vel = vector.multiply(dir, self.knockback) + vel.y = self.knockback * 0.5 + target:add_player_velocity(vel) + else + local vel = vector.multiply(dir, self.knockback) + vel.y = self.knockback * 0.5 + target:add_velocity(vel) + end +end + +-- Punch Timer -- + +function mob_core.punch_timer(self, new_val) + self.punch_timer = self.punch_timer or 0 + if new_val and new_val > 0 then self.punch_timer = new_val end + if self.punch_timer > 0 then + self.punch_timer = self.punch_timer - self.dtime + else + self.punch_timer = 0 + end + self.punch_timer = mobkit.remember(self, "punch_timer", self.punch_timer) +end + +------------------ +-- LQ Functions -- +------------------ + +function mob_core.lq_dumb_punch(self, target) + local func = function(self) + local vel = self.object:get_velocity() + self.object:set_velocity({x = 0, y = vel.y, z = 0}) + local pos = self.object:get_pos() + local yaw = self.object:get_yaw() + local tpos = target:get_pos() + local tyaw = minetest.dir_to_yaw(vector.direction(pos, tpos)) + if abs(tyaw - yaw) > 0.1 then mobkit.turn2yaw(self, tyaw, 4) end + local dist = dist_2d(pos, tpos) - hitbox(target)[4] + if dist < hitbox(self)[4] + self.reach and self.punch_timer <= + 0 then + mobkit.animate(self, "punch") + target:punch(self.object, 1.0, { + full_punch_interval = 0.1, + damage_groups = {fleshy = self.damage} + }, nil) + mob_core.punch_timer(self, self.punch_cooldown) + mob_core.knockback(self, target) + self.custom_punch_target = target + if self.custom_punch and self.custom_punch_target then + self.custom_punch(self) + end + mobkit.clear_queue_low(self) + return true + else + return true + end + end + mobkit.queue_low(self, func) +end + + +------------ +-- Aerial -- +------------ + +-- Takeoff -- + +function mob_core.hq_takeoff(self, prty, lift_force) + lift_force = lift_force or 2 + local tyaw = 0 + local lift = 0 + local init = false + local func = function(self) + if not init then + mobkit.animate(self, "stand") + init = true + end + local yaw = self.object:get_yaw() + local vel = self.object:get_velocity() + local steer_to, turn_intensity = mob_core.collision_avoidance(self) + local ceiling = sensor_ceil(self, self.view_range) + local floor = sensor_floor(self, self.view_range, true) + + if vel.y > 0 then mobkit.animate(self, "fly") end + + if self.isonground or self.isinliquid then + if steer_to then + tyaw = minetest.dir_to_yaw(steer_to) + else + local dir = minetest.yaw_to_dir(yaw) + dir.y = dir.y + self.height + self.object:set_velocity(vector.multiply(dir, lift_force)) + end + end + + if ceiling > self.height then lift = lift_force end + + if steer_to then tyaw = minetest.dir_to_yaw(steer_to) end + + if ceiling < self.height and floor < self.height then + mob_core.hq_land(self, prty + 1) + return false + end + + if floor >= self.soar_height then + mob_core.hq_aerial_roam(self, prty + 1, 1) + return true + else + lift = lift_force + end + + self.object:set_acceleration({x = 0, y = 0, z = 0}) + + if not turn_intensity or turn_intensity < 1 then + turn_intensity = 1 + end + + mobkit.turn2yaw(self, tyaw, (self.turn_rate or 2) * turn_intensity) + set_lift(self, lift) + mobkit.go_forward_horizontal(self, self.max_speed) + end + mobkit.queue_high(self, func, prty) +end + +-- Roam -- + +function mob_core.hq_aerial_roam(self, prty, speed_factor) + local tyaw = 0 + local lift = 0 + local center = self.object:get_pos() + local init = false + local func = function(self) + if not init then + mobkit.animate(self, 'fly') + init = true + end + local pos = mobkit.get_stand_pos(self) + local steer_to, turn_intensity = mob_core.collision_avoidance(self) + local ceiling = sensor_ceil(self, self.view_range) + local floor = sensor_floor(self, self.view_range, true) + + if floor and floor <= self.soar_height then + if lift < 1 then lift = lift + 0.2 end + end + if ceiling and ceiling <= math.abs(self.view_range / 4) then + if lift > -1 then lift = lift - 0.2 end + end + + if mobkit.timer(self, 1) then + if vec_dist(pos, center) > abr * 16 * 0.5 then + tyaw = minetest.dir_to_yaw( + vector.direction(pos, { + x = center.x + random() * 10 - 5, + y = center.y, + z = center.z + random() * 10 - 5 + })) + else + if random(10) >= 9 then + tyaw = tyaw + random() * pi - pi * 0.5 + end + end + if floor and floor > self.soar_height then lift = 0.1 end + end + + if steer_to then tyaw = minetest.dir_to_yaw(steer_to) end + + if mobkit.timer(self, random(1, 16)) then + if floor and floor > self.soar_height then lift = -0.2 end + end + + self.object:set_acceleration({x = 0, y = 0, z = 0}) + + if not turn_intensity or turn_intensity < 1 then + turn_intensity = 1 + end + + mobkit.turn2yaw(self, tyaw, (self.turn_rate or 2) * turn_intensity) + set_lift(self, lift) + mobkit.go_forward_horizontal(self, self.max_speed * speed_factor) + end + mobkit.queue_high(self, func, prty) +end + +-- Land -- + +function mob_core.hq_land(self, prty, pos2) + local init = false + local func = function(self) + if not init then + mobkit.animate(self, 'fly') + init = true + end + local floor = sensor_floor(self, self.view_range, true) + self.object:set_acceleration{x = 0, y = 0, z = 0} + if pos2 then + local pos = self.object:get_pos() + if vec_dist(pos, pos2) > self.height + 1 then + mobkit.turn2yaw(self, minetest.dir_to_yaw( + vector.direction(pos, pos2))) + set_lift(self, -self.max_speed / 2) + mobkit.go_forward_horizontal(self, self.max_speed) + end + if floor <= self.height + 1 then + mobkit.animate(self, "land") + mobkit.hq_roam(self, prty + 1) + return true + end + else + if floor > self.height + 1 then + mobkit.turn2yaw(self, + minetest.dir_to_yaw(self.object:get_velocity())) + set_lift(self, -self.max_speed / 2) + end + if floor <= self.height + 1 then + mobkit.animate(self, "land") + mobkit.hq_roam(self, prty + 1) + return true + end + end + end + mobkit.queue_high(self, func, prty) +end + +-- Follow Holding -- + +function mob_core.hq_aerial_follow_holding(self, prty, player) -- Follow Player + local tyaw = 0 + local lift = 0 + local init = false + if not player then return end + if not mob_core.follow_holding(self, player) then return end + local func = function(self) + if mobkit.is_queue_empty_low(self) then + if mob_core.follow_holding(self, player) then + if not init then + mobkit.animate(self, "fast" or "fly") + end + self.status = mobkit.remember(self, "status", "following") + local pos = mobkit.get_stand_pos(self) + local tpos = player:get_pos() + + local steer_to, turn_intensity = mob_core.collision_avoidance(self) + local ceiling = sensor_ceil(self, self.view_range) + local floor = sensor_floor(self, self.view_range, true) + + local dir = vector.direction(pos, tpos) + + lift = dir.y + + if floor and floor <= self.soar_height then + if lift < 1 then lift = lift + 0.2 end + end + if ceiling and ceiling <= math.abs(self.view_range / 4) then + if lift > -1 then lift = lift - 0.2 end + end + + tyaw = minetest.dir_to_yaw(dir) + + if steer_to then + tyaw = minetest.dir_to_yaw(steer_to) + end + + if lift < 0 then + self.object:set_acceleration({x = 0, y = 0.1, z = 0}) + else + self.object:set_acceleration({x = 0, y = 0, z = 0}) + end + + if not turn_intensity or turn_intensity < 1 then + turn_intensity = 1 + end + + mobkit.turn2yaw(self, tyaw, + (self.turn_rate or 2) * turn_intensity) + set_lift(self, lift) + mobkit.go_forward_horizontal(self, self.max_speed) + end + end + if (self.status == "following" and + not mob_core.follow_holding(self, player)) then + self.status = mobkit.remember(self, "status", "") + return true + end + end + mobkit.queue_high(self, func, prty) +end + +------------- +-- Aquatic -- +------------- + +local function aqua_radar_dumb(pos, yaw, range, reverse) -- Ported from mobkit + range = range or 4 + local function okpos(p) + local node = mobkit.nodeatpos(p) + if node then + if node.drawtype == 'liquid' then + local nodeu = mobkit.nodeatpos(mobkit.pos_shift(p, {y = 1})) + local noded = mobkit.nodeatpos(mobkit.pos_shift(p, {y = -1})) + if (nodeu and nodeu.drawtype == 'liquid') or + (noded and noded.drawtype == 'liquid') then + return true + else + return false + end + else + local h = mobkit.get_terrain_height(p) + if h then + local node2 = mobkit.nodeatpos( + {x = p.x, y = h + 1.99, z = p.z}) + if node2 and node2.drawtype == 'liquid' then + return true, h + end + else + return false + end + end + else + return false + end + end + local fpos = mobkit.pos_translate2d(pos, yaw, range) + local ok, h = okpos(fpos) + if not ok then + local ffrom, fto, fstep + if reverse then + ffrom, fto, fstep = 3, 1, -1 + else + ffrom, fto, fstep = 1, 3, 1 + end + for i = ffrom, fto, fstep do + ok, h = okpos(mobkit.pos_translate2d(pos, yaw + i, range)) + if ok then return yaw + i, h end + ok, h = okpos(mobkit.pos_translate2d(pos, yaw - i, range)) + if ok then return yaw - i, h end + end + return yaw + pi, h + else + return yaw, h + end +end + +function mob_core.hq_aqua_roam(self, prty, speed_factor) + local tyaw = 0 + local lift = 0 + local center = self.object:get_pos() + local init = false + local func = function(self) + if not self.isinliquid then return true end + if not init then + mobkit.animate(self, "swim") + init = true + end + local pos = self.object:get_pos() + local steer_to, turn_intensity = mob_core.collision_avoidance(self) + local surface = sensor_surface(self, self.view_range) + local floor = sensor_floor(self, self.view_range) + + if floor <= self.floor_avoidance_range then + if lift < 1 then lift = lift + 0.2 end + end + + if surface <= self.surface_avoidance_range then + if lift > -1 then lift = lift - 0.2 end + end + + if mobkit.timer(self, 1) then + if vec_dist(pos, center) > abr * 16 * 0.5 then + tyaw = minetest.dir_to_yaw( + vector.direction(pos, { + x = center.x + random() * 10 - 5, + y = center.y, + z = center.z + random() * 10 - 5 + })) + else + if random(10) >= 9 then + tyaw = tyaw + random() * pi - pi * 0.5 + end + end + if floor > self.height then + if math.abs(lift) > 0.5 then lift = 0.1 end + end + end + + if steer_to then tyaw = minetest.dir_to_yaw(steer_to) end + + if mobkit.timer(self, random(3, 6)) then -- Ocassionally go down + if floor > self.floor_avoidance_range and surface > + self.surface_avoidance_range then + if math.random(1, 2) == 1 then + lift = -0.5 + else + lift = 0.5 + end + end + end + + if lift < 0 then + self.object:set_acceleration({x = 0, y = 0.1, z = 0}) + else + self.object:set_acceleration({x = 0, y = 0, z = 0}) + end + + if not turn_intensity or turn_intensity < 1 then + turn_intensity = 1 + end + + mobkit.turn2yaw(self, tyaw, (self.turn_rate or 2) * turn_intensity) + set_lift(self, lift) + mobkit.go_forward_horizontal(self, self.max_speed * speed_factor) + end + mobkit.queue_high(self, func, prty) +end + +-- Aquatic Attack -- + +function mob_core.hq_aqua_attack(self, prty, target) + local tyaw = 0 + local lift = 0 + local init = false + local func = function(self) + if not self.isinliquid or not mobkit.is_alive(target) or + not mob_core.can_fit(self, target:get_pos(), true) then + return true + end + if not init then + mobkit.animate(self, "swim") + init = true + end + local pos = self.object:get_pos() + local tpos = target:get_pos() + local yaw = self.object:get_yaw() + local steer_to, turn_intensity = mob_core.collision_avoidance(self) + local surface = sensor_surface(self, self.view_range) + local floor = sensor_floor(self, self.view_range) + + local dir = vector.direction(pos, tpos) + + lift = dir.y + + if floor <= self.floor_avoidance_range then + if lift < 1 then lift = lift + 0.2 end + end + + if surface <= self.surface_avoidance_range then + if lift > -1 then lift = lift - 0.2 end + end + + tyaw = minetest.dir_to_yaw(dir) + + if steer_to then tyaw = minetest.dir_to_yaw(steer_to) end + + if lift < 0 then + self.object:set_acceleration({x = 0, y = 0.1, z = 0}) + else + self.object:set_acceleration({x = 0, y = 0, z = 0}) + end + + local target_side = abs(target:get_properties().collisionbox[4]) + + if vec_dist(pos, tpos) < self.reach + target_side then + target:punch(self.object, 1.0, { + full_punch_interval = 0.1, + damage_groups = {fleshy = self.damage} + }, nil) + mobkit.animate(self, "punch_swim" or "punch") + mob_core.knockback(self, target) + self.custom_punch_target = target + if self.custom_punch and self.custom_punch_target then + self.custom_punch(self) + end + mobkit.hq_aqua_turn(self, prty, yaw - pi, self.max_speed) + return true + end + + if not turn_intensity or turn_intensity < 1 then + turn_intensity = 1 + end + + mobkit.turn2yaw(self, tyaw, (self.turn_rate or 2) * turn_intensity) + set_lift(self, lift) + mobkit.go_forward_horizontal(self, self.max_speed) + end + mobkit.queue_high(self, func, prty) +end + +-- Swim from/Runaway -- + +function mob_core.hq_swimfrom(self, prty, target, speed) + local init = false + local timer = 6 + local func = function(self) + if not mobkit.is_alive(target) then return true end + if not self.isinliquid then return true end + if not init then + timer = timer - self.dtime + if timer <= 0 or vec_dist(self.object:get_pos(), target:get_pos()) < + 8 then + mobkit.make_sound(self, 'scared') + init = true + end + return + end + local pos = mobkit.get_stand_pos(self) + local chase_pos = target:get_pos() + local dir = vector.direction(pos, chase_pos) + local yaw = minetest.dir_to_yaw(dir) - (pi / 2) + local dist = vec_dist(pos, chase_pos) + if (dist / 1.5) < self.view_range then + local swimto, height = aqua_radar_dumb(pos, yaw, 3) + if height and height > pos.y then + local vel = self.object:get_velocity() + vel.y = vel.y + 0.1 + self.object:set_velocity(vel) + end + mobkit.hq_aqua_turn(self, prty + 1, swimto, speed) + else + return true + end + timer = timer - 1 + end + mobkit.queue_high(self, func, prty) +end + +-- Aquatic Follow -- + +function mob_core.hq_aqua_follow_holding(self, prty, player) -- Follow Player + local tyaw = 0 + local lift = 0 + local init = false + if not player then return end + if not mob_core.follow_holding(self, player) then return end + local func = function(self) + if mobkit.is_queue_empty_low(self) then + if mob_core.follow_holding(self, player) then + if not init then + mobkit.animate(self, "fast" or "swim") + end + local pos = mobkit.get_stand_pos(self) + local tpos = player:get_pos() + + self.status = mobkit.remember(self, "status", "following") + local steer_to, turn_intensity = mob_core.collision_avoidance(self) + local surface = sensor_surface(self, self.view_range) + local floor = sensor_floor(self, self.view_range) + + local dir = vector.direction(pos, tpos) + + lift = dir.y + + if floor <= self.floor_avoidance_range then + if lift < 1 then lift = lift + 0.2 end + end + + if surface <= self.surface_avoidance_range then + if lift > -1 then lift = lift - 0.2 end + end + + tyaw = minetest.dir_to_yaw(dir) + + if steer_to then + tyaw = minetest.dir_to_yaw(steer_to) + end + + if lift < 0 then + self.object:set_acceleration({x = 0, y = 0.1, z = 0}) + else + self.object:set_acceleration({x = 0, y = 0, z = 0}) + end + + if not turn_intensity or turn_intensity < 1 then + turn_intensity = 1 + end + + mobkit.turn2yaw(self, tyaw, + (self.turn_rate or 2) * turn_intensity) + set_lift(self, lift) + mobkit.go_forward_horizontal(self, self.max_speed) + end + end + if (self.status == "following" and + not mob_core.follow_holding(self, player)) then + self.status = mobkit.remember(self, "status", "") + return true + end + end + mobkit.queue_high(self, func, prty) +end + +----------- +-- Basic -- +----------- + +function mob_core.hq_follow_holding(self, prty, player, stop_threshold) -- Follow Player + if not player then return end + if not mob_core.follow_holding(self, player) then return end + stop_threshold = stop_threshold or 2.5 + local func = function(self) + if mobkit.is_queue_empty_low(self) then + if mob_core.follow_holding(self, player) then + local pos = mobkit.get_stand_pos(self) + local tpos = player:get_pos() + if vec_dist(pos, tpos) <= self.collisionbox[4] + stop_threshold then + mobkit.lq_idle(self, 0.1, "stand") + else + if self.animation["run"] then + mobkit.animate(self, "run") + else + mobkit.animate(self, "walk") + end + self.status = mobkit.remember(self, "status", "following") + mobkit.clear_queue_low(self) + mob_core.goto_next_waypoint(self, tpos) + end + end + end + if (self.status == "following" and + not mob_core.follow_holding(self, player)) then + self.status = mobkit.remember(self, "status", "") + mobkit.lq_idle(self, 1, "stand") + return true + end + end + mobkit.queue_high(self, func, prty) +end + +------------------------------- +-- Modified Mobkit Functions -- +------------------------------- + +-- Is Neighbor Node Reachable -- Modified to add variable to ignore liquidflag + +function mob_core.is_neighbor_node_reachable(self, neighbor) + local fall = self.max_fall or self.jump_height + local offset = neighbors[neighbor] + local pos = mobkit.get_stand_pos(self) + local tpos = mobkit.get_node_pos(mobkit.pos_shift(pos, offset)) + local recursteps = ceil(fall) + 1 + local height, liquidflag = mobkit.get_terrain_height(tpos, recursteps) + if height and abs(height - pos.y) <= fall then + tpos.y = height + height = height - pos.y + if neighbor % 2 == 0 then + local n2 = neighbor - 1 + offset = neighbors[n2] + local t2 = mobkit.get_node_pos(mobkit.pos_shift(pos, offset)) + local h2 = mobkit.get_terrain_height(t2, recursteps) + if h2 and h2 - pos.y > 0.02 then return end + n2 = (neighbor + 1) % 8 + offset = neighbors[n2] + t2 = mobkit.get_node_pos(mobkit.pos_shift(pos, offset)) + h2 = mobkit.get_terrain_height(t2, recursteps) + if h2 and h2 - pos.y > 0.02 then return end + end + if tpos.y + self.height - pos.y > 1 then + local snpos = mobkit.get_node_pos(pos) + local pos1 = {x = pos.x, y = snpos.y + 1, z = pos.z} + local pos2 = {x = tpos.x, y = tpos.y + self.height, z = tpos.z} + local nodes = mobkit.get_nodes_in_area(pos1, pos2, true) + for p, node in pairs(nodes) do + if snpos.x == p.x and snpos.z == p.z then + if node.name == 'ignore' or node.walkable then + return + end + else + if node.name == 'ignore' or + (node.walkable and mobkit.get_node_height(p) > tpos.y + + 0.001) then return end + end + end + end + if self.ignore_liquidflag then liquidflag = false end + return height, tpos, liquidflag + else + if self.ignore_liquidflag and not mob_core.fall_check(self, pos, fall) then + return 0.1, tpos, false + end + return + end +end + +-- Get next Waypoint -- Modified to make use of mob_core.is_neighbor_node_reachable() + +function mob_core.get_next_waypoint(self, tpos) + local pos = mobkit.get_stand_pos(self) + local dir = vector.direction(pos, tpos) + local neighbor = mobkit.dir2neighbor(dir) + local function update_pos_history(self, pos) + table.insert(self.pos_history, 1, pos) + if #self.pos_history > 2 then + table.remove(self.pos_history, #self.pos_history) + end + end + local nogopos = self.pos_history[2] + local height, pos2, liquidflag = mob_core.is_neighbor_node_reachable(self, + neighbor) + if height and not liquidflag and + not (nogopos and mobkit.isnear2d(pos2, nogopos, 0.1)) then + local heightl = mob_core.is_neighbor_node_reachable(self, + mobkit.neighbor_shift( + neighbor, -1)) + if heightl and abs(heightl - height) < 0.001 then + local heightr = mob_core.is_neighbor_node_reachable(self, + mobkit.neighbor_shift( + neighbor, 1)) + if heightr and abs(heightr - height) < 0.001 then + dir.y = 0 + local dirn = vector.normalize(dir) + local npos = mobkit.get_node_pos( + mobkit.pos_shift(pos, neighbors[neighbor])) + local factor = + abs(dirn.x) > abs(dirn.z) and abs(npos.x - pos.x) or + abs(npos.z - pos.z) + pos2 = mobkit.pos_shift(pos, { + x = dirn.x * factor, + z = dirn.z * factor + }) + end + end + update_pos_history(self, pos2) + return height, pos2 + else + for i = 1, 3 do + local height, pos2, liq = mob_core.is_neighbor_node_reachable(self, + mobkit.neighbor_shift( + neighbor, + -i * + self.path_dir)) + if height and not liq and + not (nogopos and mobkit.isnear2d(pos2, nogopos, 0.1)) then + update_pos_history(self, pos2) + return height, pos2 + end + height, pos2, liq = mob_core.is_neighbor_node_reachable(self, + mobkit.neighbor_shift( + neighbor, + i * + self.path_dir)) + if height and not liq and + not (nogopos and mobkit.isnear2d(pos2, nogopos, 0.1)) then + update_pos_history(self, pos2) + return height, pos2 + end + end + height, pos2, liquidflag = mob_core.is_neighbor_node_reachable(self, + mobkit.neighbor_shift( + neighbor, + 4)) + if height and not liquidflag and + not (nogopos and mobkit.isnear2d(pos2, nogopos, 0.1)) then + update_pos_history(self, pos2) + return height, pos2 + end + end + table.remove(self.pos_history, 2) + self.path_dir = self.path_dir * -1 +end + +------------------------ +-- Built-in behaviors -- +------------------------ + +function mob_core.goto_next_waypoint(self, tpos, speed_factor) + speed_factor = speed_factor or 1 + local _, pos2 = mob_core.get_next_waypoint(self, tpos) + if pos2 then + local yaw = self.object:get_yaw() + local tyaw = minetest.dir_to_yaw( + vector.direction(self.object:get_pos(), pos2)) + if abs(tyaw - yaw) > 1 then mobkit.lq_turn2pos(self, pos2) end + mobkit.lq_dumbwalk(self, pos2, speed_factor) + step_check(self) + return true + end +end + +-- Dumbstep -- Modified to use new jump mechanic + +function mob_core.lq_dumbwalk(self,dest,speed_factor) + local timer = 3 -- failsafe + speed_factor = speed_factor or 1 + local func = function(self) + mobkit.animate(self, "walk") + timer = timer - self.dtime + if timer < 0 then return true end + + local pos = mobkit.get_stand_pos(self) + local y = self.object:get_velocity().y + + if mobkit.is_there_yet2d(pos,minetest.yaw_to_dir(self.object:get_yaw()), dest) then +-- if mobkit.isnear2d(pos,dest,0.25) then + if (not self.isonground + and not self.isinliquid) + or abs(dest.y-pos.y) > 0.1 then -- prevent uncontrolled fall when velocity too high +-- if abs(dest.y-pos.y) > 0.1 then -- isonground too slow for speeds > 4 + self.object:set_velocity({x=0,y=y,z=0}) + end + return true + end + + if self.isonground + or self.isinliquid then + local dir = vector.normalize(vector.direction({x=pos.x,y=0,z=pos.z}, + {x=dest.x,y=0,z=dest.z})) + dir = vector.multiply(dir,self.max_speed*speed_factor) +-- self.object:set_yaw(minetest.dir_to_yaw(dir)) + mobkit.turn2yaw(self,minetest.dir_to_yaw(dir)) + dir.y = y + self.object:set_velocity(dir) + end + end + mobkit.queue_low(self, func) +end + +function mob_core.dumbstep(self, tpos, speed_factor, idle_duration) + mobkit.lq_turn2pos(self, tpos) + mob_core.lq_dumbwalk(self, tpos, speed_factor) + step_check(self) + idle_duration = idle_duration or 6 + mobkit.lq_idle(self, random(ceil(idle_duration * 0.5), idle_duration)) +end + +-- Roam -- not finished + +function mob_core.hq_roam(self, prty) + local func = function(self) + local fall = self.max_fall or self.jump_height + self.status = mobkit.remember(self, "status", "") + local pos = mobkit.get_stand_pos(self) + if mobkit.is_queue_empty_low(self) and + (self.isonground or not mob_core.fall_check(self, pos, fall)) then + local neighbor = random(8) + local _, tpos, liquidflag = mob_core.is_neighbor_node_reachable( + self, neighbor) + if tpos and not mob_core.fall_check(self, tpos, fall) and + not liquidflag then + mob_core.dumbstep(self, tpos, 0.3, random(4, 8)) + end + end + end + mobkit.queue_high(self, func, prty) +end + +------------------------------ +-- Recoded Mobkit Functions -- +------------------------------ + +-- Run From -- + +function mob_core.hq_runfrom(self, prty, tgtobj) + local init = false + local timer = 6 + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if not init then + timer = timer - self.dtime + if timer <= 0 or vec_dist(self.object:get_pos(), tgtobj:get_pos()) < + 8 then + mobkit.make_sound(self, 'scared') + init = true + end + end + mobkit.animate(self, "run") + self.status = mobkit.remember(self, "status", "fleeing") + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + if vec_dist(pos, opos) < self.view_range * 1.1 then + local tpos = { + x = 2 * pos.x - opos.x, + y = opos.y, + z = 2 * pos.z - opos.z + } + mob_core.goto_next_waypoint(self, tpos) + else + mobkit.lq_idle(self, 1) + self.object:set_velocity({x = 0, y = 0, z = 0}) + return true + end + end + end + mobkit.queue_high(self, func, prty) +end + +-- Liquid Recovery -- Recoded to be smoother and more reliably find land + +function mob_core.hq_liquid_recovery(self, prty, anim) + local tpos + local init = false + anim = anim or "walk" + local func = function(self) + if not init then + mobkit.animate(self, anim) + init = true + end + if self.isonground and not self.isinliquid then + mobkit.lq_idle(self, 0.1) + return true + end + local pos = mobkit.get_stand_pos(self) + local scan = mob_core.find_node_expanding(self) + if not tpos then + tpos = scan + elseif (scan and tpos) and (scan.y < tpos.y) then -- This eliminates issue of mobs swimming to walls + tpos = scan + end + if tpos then + local dist = vec_dist(pos, tpos) + mobkit.drive_to_pos(self, tpos, self.max_speed * 0.75, 1, + self.collisionbox[4] * 1.25) + if dist < self.collisionbox[4] * 1.75 then + mobkit.clear_queue_low(self) + mobkit.lq_turn2pos(self, tpos) + local vel = self.object:get_velocity() + vel.y = self.jump_height + self.object:set_velocity(vel) + minetest.after(0.3, function(self, vel) + if self.object:get_luaentity() then + self.object:set_acceleration( + {x = vel.x * 2, y = 0, z = vel.z * 2}) + end + end, self, vel) + return true + end + end + end + mobkit.queue_high(self, func, prty) +end + +-- Hunt -- Recoded to use various Mob Core functions + +function mob_core.hq_hunt(self, prty, target) + local scan_pos = target:get_pos() + scan_pos.y = scan_pos.y + 1 + if not line_of_sight(self.object:get_pos(), scan_pos) then return true end + local func = function(self) + if not mobkit.is_alive(target) then + mobkit.clear_queue_high(self) + return true + end + local pos = mobkit.get_stand_pos(self) + local tpos = target:get_pos() + mob_core.punch_timer(self) + if mobkit.is_queue_empty_low(self) then + self.status = mobkit.remember(self, "status", "hunting") + local dist = vec_dist(pos, tpos) + local yaw = self.object:get_yaw() + local tyaw = minetest.dir_to_yaw(vector.direction(pos, tpos)) + if abs(tyaw - yaw) > 0.1 then + mobkit.lq_turn2pos(self, tpos) + end + if dist > self.view_range then + self.status = mobkit.remember(self, "status", "") + return true + end + local target_side = abs(target:get_properties().collisionbox[4]) + mob_core.goto_next_waypoint(self, tpos) + if vec_dist(pos, tpos) < self.reach + target_side then + self.status = mobkit.remember(self, "status", "") + mob_core.lq_dumb_punch(self, target, "stand") + end + end + end + mobkit.queue_high(self, func, prty) +end + +------------------------ +-- Built-in Behaviors -- +------------------------ + +function mob_core.fly_to_next_waypoint(self, pos2, speed_factor) + speed_factor = speed_factor or 0.75 + + local lift + local tyaw + + mobkit.animate(self, "fly") + + local pos = mobkit.get_stand_pos(self) + + local steer_to, turn_intensity = mob_core.collision_avoidance(self) + + local ceiling = sensor_ceil(self, self.view_range) + + -- Basic Movement + if self.isonground or self.isinliquid then return end + + lift = 0 + + tyaw = minetest.dir_to_yaw(vector.direction(pos, pos2)) + + if ceiling <= self.height * 2 then + if lift > -1 then + lift = lift - 0.2 + end + end + + if steer_to then tyaw = minetest.dir_to_yaw(steer_to) end + + local dir = vector.direction(pos, pos2) + if dir.y > 0 then + lift = dir.y * self.max_speed + else + lift = dir.y * self.max_speed + end + + local dist = vec_dist(pos, pos2) + if dist < self.collisionbox[4] then return true end + + if not turn_intensity or turn_intensity < 1 then turn_intensity = 1 end + + mobkit.turn2yaw(self, tyaw, (self.turn_rate or 2) * turn_intensity) + set_lift(self, lift) + mobkit.go_forward_horizontal(self, self.max_speed * speed_factor) +end + + +function mob_core.swim_to_next_waypoint(self, pos2, speed_factor) + speed_factor = speed_factor or 0.75 + + local lift + local tyaw + + mobkit.animate(self, "swim") + + local pos = mobkit.get_stand_pos(self) + + local steer_to, turn_intensity = mob_core.collision_avoidance(self) + + local ceiling = sensor_ceil(self, self.view_range) + + -- Basic Movement + if self.isonground or not self.isinliquid then return end + + lift = 0 + + tyaw = minetest.dir_to_yaw(vector.direction(pos, pos2)) + + if ceiling <= self.height * 2 then + if lift > -1 then + lift = lift - 0.2 + end + end + + if steer_to then tyaw = minetest.dir_to_yaw(steer_to) end + + local dir = vector.direction(pos, pos2) + if dir.y > 0 then + lift = dir.y * self.max_speed + else + lift = dir.y * self.max_speed + end + + local dist = vec_dist(pos, pos2) + if dist < self.collisionbox[4] then return true end + + if not turn_intensity or turn_intensity < 1 then turn_intensity = 1 end + + mobkit.turn2yaw(self, tyaw, (self.turn_rate or 2) * turn_intensity) + set_lift(self, lift) + mobkit.go_forward_horizontal(self, self.max_speed * speed_factor) +end diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..4dbbba9 --- /dev/null +++ b/init.lua @@ -0,0 +1,18 @@ +mob_core = {} + +-------------- +-- Mob Core -- +-------------- +--- Ver 0.1 -- + +local path = minetest.get_modpath("mob_core") + +dofile(path.."/api.lua") +dofile(path.."/hq_lq.lua") +dofile(path.."/logic.lua") +dofile(path.."/craftitems.lua") +dofile(path.."/pathfinder.lua") + +if minetest.get_modpath("default") then + dofile(path.."/mount.lua") +end \ No newline at end of file diff --git a/logic.lua b/logic.lua new file mode 100644 index 0000000..58d9c35 --- /dev/null +++ b/logic.lua @@ -0,0 +1,201 @@ +-------------------- +-- Mob Core Logic -- +-------------------- +------ Ver 0.1 ----- + +-- Defend Owner -- + +minetest.register_on_mods_loaded(function() + for name, def in pairs(minetest.registered_entities) do + if minetest.registered_entities[name].logic + or minetest.registered_entities[name].brainfunc then + local old_punch = def.on_punch + if not old_punch then + old_punch = function() end + end + local on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir) + old_punch(self, puncher, time_from_last_punch, tool_capabilities, dir) + local pos = self.object:get_pos() + if not pos then + return + end + local objects = minetest.get_objects_inside_radius(pos, 32) + for _, object in ipairs(objects) do + if object:get_luaentity() + and mob_core.is_mobkit_mob(object) then + local entity = object:get_luaentity() + if object ~= self.object + and entity.defend_owner + and entity.owner + and entity.owner == puncher:get_player_name() then + entity.owner_target = self.object + end + end + end + end + def.on_punch = on_punch + minetest.register_entity(":"..name, def) + end + end +end) + +------------- +-- Runaway -- +------------- + +function mob_core.logic_runaway_player(self, prty) -- Runaway from player + local player = mobkit.get_nearby_player(self) + if player and vector.distance(self.object:get_pos(), player:get_pos()) < self.view_range then + if player:get_player_name() ~= self.owner then + mobkit.hq_runfrom(self,prty,player) + return + end + end +end + +function mob_core.logic_runaway_mob(self, prty, tbl) -- Runaway from specified mobs + tbl = tbl or self.runaway_from + if tbl then + for i = 1, #tbl do + local runaway_mob = mobkit.get_closest_entity(self, tbl[i]) + if runaway_mob and vector.distance(self.object:get_pos(), runaway_mob:get_pos()) < self.view_range then + mobkit.hq_runfrom(self, prty, runaway_mob) + return + end + end + end +end + +------------ +-- Attack -- +------------ + +function mob_core.logic_attack_player(self, prty, player) -- Attack player + player = player or mobkit.get_nearby_player(self) + if player + and vector.distance(self.object:get_pos(), player:get_pos()) < self.view_range + and mobkit.is_alive(player) then + mob_core.hq_hunt(self,prty,player) + return + end + return +end + +function mob_core.logic_attack_mob(self, prty, target) -- Attack specified mobs + if not mobkit.exists(target) then return true end + if target + and vector.distance(self.object:get_pos(), target:get_pos()) < self.view_range + and mobkit.is_alive(target) then + if not mob_core.shared_owner(self, target) then + mob_core.hq_hunt(self, prty, target) + return + end + end +end + +function mob_core.logic_attack_mobs(self, prty, tbl) -- Attack specified mobs + tbl = tbl or self.targets + if tbl then + for i = 1, #tbl do + local target = mobkit.get_closest_entity(self, tbl[i]) + if target + and vector.distance(self.object:get_pos(), target:get_pos()) < self.view_range + and mobkit.is_alive(target) then + if (self.tamed == true and target:get_luaentity().owner ~= self.owner) + or not self.tamed then + mob_core.hq_hunt(self,prty,target) + return + end + end + end + end +end + +function mob_core.logic_aqua_attack_player(self, prty, player) -- Attack player + player = player or mobkit.get_nearby_player(self) + if player + and vector.distance(self.object:get_pos(), player:get_pos()) < self.view_range + and mobkit.is_alive(player) + and mobkit.is_in_deep(player) then + mob_core.hq_aqua_attack(self,prty,player,self.max_speed) + return + end +end + +function mob_core.logic_aqua_attack_mob(self, prty, target) -- Attack specified mobs + if not mobkit.exists(target) then return true end + if target + and vector.distance(self.object:get_pos(), target:get_pos()) < self.view_range + and mobkit.is_alive(target) then + if not mob_core.shared_owner(self, target) then + mob_core.hq_aqua_attack(self, prty, target, 1) + return + end + end +end + +function mob_core.logic_aqua_attack_mobs(self, prty, tbl) -- Attack specified mobs + tbl = tbl or self.targets + if tbl then + for i = 1, #tbl do + local target = mobkit.get_closest_entity(self, tbl[i]) + if target + and vector.distance(self.object:get_pos(), target:get_pos()) < self.view_range + and mobkit.is_alive(target) + and mobkit.is_in_deep(target) then + if (self.tamed == true and target:get_luaentity().owner ~= self.owner) + or not self.tamed then + mob_core.hq_aqua_attack(self,prty,target,self.max_speed) + return + end + end + end + end +end + +-------------- +-- Run From -- +-------------- + +function mob_core.logic_aerial_takeoff_flee_mobs(self, prty, lift_force) -- Attack specified mobs + if self.runaway_from then + for i = 1, #self.runaway_from do + local runfrom = mobkit.get_closest_entity(self, self.runaway_from[i]) + if runfrom and runfrom.owner ~= self.owner then + return + end + if runfrom and vector.distance(self.object:get_pos(), runfrom:get_pos()) < 8 then + mob_core.hq_takeoff(self, prty, lift_force) + return + end + end + end +end + +function mob_core.logic_aerial_takeoff_flee_player(self, prty, lift_force) -- Attack specified mobs + local player = mobkit.get_nearby_player(self) + if player and vector.distance(self.object:get_pos(), player:get_pos()) < 8 then + mob_core.hq_takeoff(self, prty, lift_force) + return + end +end + +------------------------ +-- Randomly drop item -- +------------------------ + +function mob_core.random_drop(self, interval, chance, item) + self.drop_timer = (self.drop_timer or 0) + 1 + if self.drop_timer >= interval then + self.droptimer = 0 + if math.random(1, chance) == 1 then + local pos = self.object:get_pos() + minetest.add_item(pos, item) + minetest.sound_play("default_place_node_hard", { + pos = pos, + gain = 1.0, + max_hear_distance = 5, + }) + end + end +end \ No newline at end of file diff --git a/mob_core_api.txt b/mob_core_api.txt new file mode 100644 index 0000000..5f7a7bf --- /dev/null +++ b/mob_core_api.txt @@ -0,0 +1,203 @@ +Mob Core API Documentation + +Entity definition +--------------------- + +minetest.register_entity("mod:name",{ + + -- required minetest api props + + initial_properties = { + physical = true, + collide_with_objects = true, + collisionbox = {...}, + visual = "mesh", + mesh = "...", + textures = {...}, + }, + + -- required mob_core props (refer to mobkits documentation for required mobkit props) + + fall_damage = [bool] If true, the mob will take fall damage (true by default) + igniter_damage = [bool] If true, the mob will take fire/lava damage (true by default) + follow = [table/string] Table of items mob should follow (will also accept a single string) + immune_to = [table] Table of items mob can't take damage from + reach = [num] The distance from the center of the mobs hitbox in which it can hit another mob + damage = [num] The amount of damage the mob does in the fleshy group (2 damage = 1 player heart) + knockback = [num] How much knockback the mob deals to other mobs or players + defend_owner = [bool] If true, when the mobs owner punches another mob, the mob will be stored in the self.owner_target variable + drops = [table] Table of items mob can drop. Example: + { + {name = "my_mob:meat_raw", chance = 1, min = 1, max = 3} + } + + -- Obstacle Avoidance + obstacle_avoidance_range = [num] Multiplier for how far ahead the mob checks for obstacles. + surface_avoidance_range = [num] How close (in nodes) from the center of the mobs hitbox it will get to the water surface + floor_avoidance_range = [num] How close (in nodes) from the center of the mobs hitbox it will get to the water floor + + -- For mobs that use mob_core.growth() + scale_stage1 = [num] Multiplier for how big the mob will be at growth stage 1 (default: 0.25) + scale_stage2 = [num] Multiplier for how big the mob will be at growth stage 2 (default: 0.5) + scale_stage3 = [num] Multiplier for how big the mob will be at growth stage 3 (default: 0.75) + + -- For mobs that have unique textures for males, females, and children + female_textures = [table] Texture/Textures for female mobs + male_textures = [table] Texture/Textures for male mobs + child_textures = [table] Texture/Textures for child mobs +}) + +------------ +Misc Functions (not specific to mobs but useful for new functions) +------------ + +function mob_core.find_val(tbl, val) + -- tbl: table + -- val: any value + -- return true if tbl contains val + + +------------ +1. Required Functions +------------ + +function mob_core.on_step(self, dtime, moveresult) + -- REQUIRED, many functions will not work if this is not used + +function mob_core.on_activate(self, staticdata, dtime_s) + -- REQUIRED, many functions will not work if this is not used + +------------ +2. Utility Functions +------------ + +These functions are used to get/set attributes quickly, as well as easily manage mobs + +function mob_core.set_scale(self, scale) + -- Sets mobs scale (visual and collisionbox) + -- self: luaentity + -- scale: multiplier for moba current scale + +function mob_core.set_owner(self, name) + -- Sets mobs owner + -- self: luaentity + -- name: name of player to set as owner + +function mob_core.spawn_child(pos, mob) + -- Spawns a child mob + -- pos: position + -- mob: name of the mob to be spawned + +mob_core.follow_holding(self, player) + -- Check if player is holding an item the mob follows + -- self: luaentity + -- player: player + +function mob_core.shared_owner(self, object) + -- self: luaentity + -- object: luaentity or userdata + -- returns true if self and object have the same owner + +function mob_core.is_mobkit_mob(object) + -- object: luaentity or userdata + -- returns true if object uses mobkit + +function mob_core.register_spawn({ + name = [string] mob name + nodes = [table] list of nodes to spawn mob on + min_light = [number] minimum light level + max_light = [number] maximum ligth level + min_height = [number] minimum world heigh + max_height = [number] maximum world heigh + min_rad = [number] minimum radius around player + max_rad = [number] maximum radius around player + group = [number] amount of mobs to spawn +}, interval, chance) + -- interval: how often (in seconds) to attempt spawning + -- chance: chance to attempt spawning + -- mob_core.registered_spawns[name].last_pos can be used to find the last position the mob/mobs were spawned + +function mob_core.register_on_spawn(name, func, ...) + -- name: name of a mob + -- func: function + -- ...: params of func + -- func is called when 'name' is spawned + -- Ex: mob_core.register_on_spawn("my_mobs:alligator", minetest.chat_send_all, "an alligator has spawned") + +------------ +3. Interaction Functions +------------ + +These functions are used for player -> mob interactions, like right-clicking and punches + +function mob_core.mount(self, clicker) + -- self: luaentity + -- clicker: player + +function mob_core.mount(self, clicker, capture_tool, capture_chance, wear, force_take) + -- self: luaentity + -- clicker: player + -- capture_tool: itemstring + -- capture_chance: 1 / x chance + -- wear: amount of wear to be added to tool + -- force_take: boolean, true means any player can catch mob + +function mob_core.feed_tame(self, clicker, feed_count, tame, breed) + -- self: luaentity + -- clicker: player + -- feed_count: amount of feeds to reach full hp + -- tame: bool, if true mob will be tamed when feed_count is met + -- breed: bool, if true self.breed_mode variable will be set to true when feed_count is met + +function mob_core.protect(self, clicker, force_protect) + -- self: luaentity + -- clicker: player + -- force_protect: bool, if true any player can protect mob + + +function mob_core.nametag(self, clicker, force_name) + -- self: luaentity + -- clicker: player + -- force_protect: bool, if true any player can name mob + +------------ +4. Built-in behaviors +------------ + +function mob_core.item_drop(self) + -- self: luaentity + -- Mob drops items based on self.drops + +function mob_core.on_die(self) + -- self: luaentity + -- Basic mob death (falls over, drops items, disappears) + +function mob_core.fly_to_next_waypoint(self, tpos, speed_factor) + -- mob flies to tpos while avoiding obstacles + -- speed_factor: multiplier for self.max_speed + +function mob_core.swim_to_next_waypoint(self, tpos, speed_factor) + -- mob swims to tpos while avoiding obstacles + -- speed_factor: multiplier for self.max_speed + +function mob_core.goto_next_waypoint(self, tpos, speed_factor) + -- same was mobkit.goto_next_waypoint, but allows for walking into water + -- speed_factor: multiplier for self.max_speed + + +------------ +5. Pathfinding +------------ + +function mob_core.find_path_lite(pos, tpos, width) + -- pos: position + -- tpos: position + -- wdith: number + -- Finds a path from pos to tpos, and adjusts away from walls to account for width + -- Note: This doesn't always return a path with appropriate width, but is faster than mob_core.find_path() + +function mob_core.find_path(self, tpos) + -- self: luaentity + -- tpos: position + -- Finds a path to tpos, accounting for mob width + -- Note: This may not find a path 100% of the time \ No newline at end of file diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..4300769 --- /dev/null +++ b/mod.conf @@ -0,0 +1,4 @@ +name = mob_core +depends = mobkit +optional_depends = default +description = A Mob API meant to expand upon mobkit and bring easy to use features for creating new Mobs. \ No newline at end of file diff --git a/mount.lua b/mount.lua new file mode 100644 index 0000000..3e751ba --- /dev/null +++ b/mount.lua @@ -0,0 +1,168 @@ +------------------------ +-- Mob Core Mount API -- +------------------------ +-------- Ver 1.0 ------- + +local player_attached = {} +local animate_player = {} + +if minetest.get_modpath("default") then + player_attached = default.player_attached + animate_player = default.player_set_animation +elseif minetest.get_modpath("mcl_player") then + player_attached = mcl_player.player_attached + animate_player = mcl_player.player_set_animation +end + +---------------------- +-- Helper functions -- +---------------------- + +local function detach(name) + local player = minetest.get_player_by_name(name) + if not player then + return + end + local attached_to = player:get_attach() + if not attached_to then + return + end + local entity = attached_to:get_luaentity() + if entity.driver and entity.driver == player then + entity.driver = nil + end + mobkit.clear_queue_high(entity) + entity.status = mobkit.remember(entity,"status","") + player:set_detach() + if player_attached ~= nil then + player_attached[player:get_player_name()] = false + end + player:set_eye_offset({x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0}) + animate_player(player, "stand" , 30) + player:set_properties({visual_size = {x = 1, y = 1}, pointable = true }) +end + +function mob_core.force_detach(player) + minetest.after(0, detach, player:get_player_name()) +end + +local function reverse_animation(self,anim,output_name) + if self.animation and self.animation[anim] then + local frame_x = self.animation[anim].range.x + local frame_y = self.animation[anim].range.y + local loop = self.animation[anim].loop + local speed = self.animation[anim].speed + self.animation[output_name] = {range={x=frame_x,y=frame_y},speed=-speed,loop=loop} + end +end + +minetest.register_on_leaveplayer(function(player) + mob_core.force_detach(player) +end) + +minetest.register_on_shutdown(function() + local players = minetest.get_connected_players() + for i = 1, #players do + mob_core.force_detach(players[i]) + end +end) + +minetest.register_on_dieplayer(function(player) + mob_core.force_detach(player) + return true +end) + +function mob_core.attach(entity, player) + entity.player_rotation = entity.player_rotation or {x = 0, y = 0, z = 0} + entity.driver_attach_at = entity.driver_attach_at or {x = 0, y = 0, z = 0} + entity.driver_eye_offset = entity.driver_eye_offset or {{x = 0, y = 0, z = 0},{x = 0, y = 0, z = 0}} + entity.driver_scale = entity.driver_scale or {x = 1, y = 1} + local rot_view = 0 + if entity.player_rotation.y == 90 then + rot_view = math.pi/2 + end + local attach_at = entity.driver_attach_at + local eye_offset = entity.driver_eye_offset[1] or {x = 0, y = 0, z = 0} + local eye_offset_3p = entity.driver_eye_offset[2] or {x = 0, y = 0, z = 0} + entity.driver = player + player:set_attach(entity.object, "", attach_at, entity.player_rotation) + if player_attached ~= nil then + player_attached[player:get_player_name()] = true + end + player:set_eye_offset(eye_offset,eye_offset_3p) + player:set_properties({ + visual_size = { + x = entity.driver_scale.x, + y = entity.driver_scale.y + }, + pointable = false + }) + minetest.after(0.2, function() + animate_player(player, "sit" , 30) + end) + player:set_look_horizontal(entity.object:get_yaw() - rot_view) +end + +function mob_core.detach(player, offset) + mob_core.force_detach(player) + animate_player(player, "stand" , 30) + local pos = player:get_pos() + pos = {x = pos.x + offset.x, y = pos.y + 0.2 + offset.y, z = pos.z + offset.z} + minetest.after(0.1, function() + player:set_pos(pos) + end) +end + +local function go_forward(self,tvel) + local y = self.object:get_velocity().y + local yaw = self.object:get_yaw() + local vel = vector.multiply(minetest.yaw_to_dir(yaw),tvel) + vel.y = y + self.object:set_velocity(vel) +end + +function mob_core.hq_mount_logic(self,prty) + local tvel = 0 + local func = function(self) + if not self.driver then return true end + local vel = self.object:get_velocity() + local ctrl = self.driver:get_player_control() + if ctrl.up then + tvel = self.max_speed_forward + elseif ctrl.down and self.isonground then -- move backwards + if self.max_speed_reverse == 0 and vel == 0 then + return + end + tvel = -self.max_speed_reverse + reverse_animation(self, "walk", "walk_reverse") + mobkit.animate(self, "walk_reverse") + elseif tvel < 0.25 or tvel == 0 then + tvel = 0 + self.object:set_velocity({x=0,y=vel.y,z=0}) + mobkit.animate(self, "stand") + end + -- jump + if self.isonground then + if ctrl.jump then + vel.y = (self.jump_height)+4 + end + end + --stand + if tvel ~= 0 and not ctrl.up or ctrl.down then + tvel = tvel*0.75 + end + if tvel > 0 then + mobkit.animate(self,"walk") + end + local tyaw = self.driver:get_look_horizontal() or 0 + self.object:set_yaw(tyaw) + self.object:set_velocity({x=vel.x,y=vel.y,z=vel.y}) + go_forward(self,tvel) + if ctrl.sneak then + mobkit.clear_queue_low(self) + mobkit.clear_queue_high(self) + mob_core.detach(self.driver, {x = 1, y = 0, z = 1}) + end + end + mobkit.queue_high(self,func,prty) +end diff --git a/pathfinder.lua b/pathfinder.lua new file mode 100644 index 0000000..4ef8ea3 --- /dev/null +++ b/pathfinder.lua @@ -0,0 +1,404 @@ +pathfinder = {} + +local openSet = {} +local closedSet = {} +local random = math.random + +local function get_distance(start_pos, end_pos) + local distX = math.abs(start_pos.x - end_pos.x) + local distZ = math.abs(start_pos.z - end_pos.z) + + if distX > distZ then + return 14 * distZ + 10 * (distX - distZ) + else + return 14 * distX + 10 * (distZ - distX) + end +end + +local function get_distance_to_neighbor(start_pos, end_pos) + local distX = math.abs(start_pos.x - end_pos.x) + local distY = math.abs(start_pos.y - end_pos.y) + local distZ = math.abs(start_pos.z - end_pos.z) + + if distX > distZ then + return (14 * distZ + 10 * (distX - distZ)) * (distY + 1) + else + return (14 * distX + 10 * (distZ - distX)) * (distY + 1) + end +end + +local function walkable(node, pos, current_pos) + if string.find(node.name, "doors:door") then + if (node.param2 == 0 or node.param2 == 2) and + math.abs(pos.z - current_pos.z) > 0 and pos.x == current_pos.x then + return true + elseif (node.param2 == 1 or node.param2 == 3) and + math.abs(pos.z - current_pos.z) > 0 and pos.x == current_pos.x then + return false + elseif (node.param2 == 0 or node.param2 == 2) and + math.abs(pos.x - current_pos.x) > 0 and pos.z == current_pos.z then + return false + elseif (node.param2 == 1 or node.param2 == 3) and + math.abs(pos.x - current_pos.x) > 0 and pos.z == current_pos.z then + return true + end + elseif string.find(node.name, "doors:hidden") then + local node_door = minetest.get_node( + {x = pos.x, y = pos.y - 1, z = pos.z}) + if (node_door.param2 == 0 or node_door.param2 == 2) and + math.abs(pos.z - current_pos.z) > 0 and pos.x == current_pos.x then + return true + elseif (node_door.param2 == 1 or node_door.param2 == 3) and + math.abs(pos.z - current_pos.z) > 0 and pos.x == current_pos.x then + return false + elseif (node_door.param2 == 0 or node_door.param2 == 2) and + math.abs(pos.x - current_pos.x) > 0 and pos.z == current_pos.z then + return false + elseif (node_door.param2 == 1 or node_door.param2 == 3) and + math.abs(pos.x - current_pos.x) > 0 and pos.z == current_pos.z then + return true + end + + end + if minetest.registered_nodes[node.name] and + minetest.registered_nodes[node.name].walkable then + return true + else + return false + end +end + +local function can_fit(pos, width) + local pos1 = vector.new(pos.x - width, pos.y, pos.z - width) + local pos2 = vector.new(pos.x + width, pos.y, pos.z + width) + for x = pos1.x, pos2.x do + for y = pos1.y, pos2.y do + for z = pos1.z, pos2.z do + local p2 = vector.new(x, y, z) + local node = minetest.get_node(p2) + if minetest.registered_nodes[node.name] + and minetest.registered_nodes[node.name].walkable then + local p3 = vector.new(p2.x, p2.y + 1, p2.z) + local node2 = minetest.get_node(p3) + if minetest.registered_nodes[node2.name] + and minetest.registered_nodes[node2.name].walkable then + return false + end + end + end + end + end + return true +end + +local function move_from_wall(pos, width) + local pos1 = vector.new(pos.x - width, pos.y, pos.z - width) + local pos2 = vector.new(pos.x + width, pos.y, pos.z + width) + for x = pos1.x, pos2.x do + for y = pos1.y, pos2.y do + for z = pos1.z, pos2.z do + local p2 = vector.new(x, y, z) + if can_fit(p2, width) then + return p2 + end + end + end + end + return pos +end + + +local function get_neighbor_ground_level(pos, jump_height, fall_height, + current_pos) + local node = minetest.get_node(pos) + local height = 0 + if walkable(node, pos, current_pos) then + repeat + height = height + 1 + if height > jump_height then return nil end + pos.y = pos.y + 1 + node = minetest.get_node(pos) + until not walkable(node, pos, current_pos) + return pos + else + repeat + height = height + 1 + if height > fall_height then return nil end + pos.y = pos.y - 1 + node = minetest.get_node(pos) + until walkable(node, pos, current_pos) + return {x = pos.x, y = pos.y + 1, z = pos.z} + end +end + +-- local function dot(a, b) +-- return a.x * b.x + a.y * b.y + a.z * b.z +-- end +-- +-- local function len(a) +-- return math.sqrt(a.x * a.x + a.y * a.y + a.z * a.z) +-- end +-- +-- local function lensq(a) +-- return a.x * a.x + a.y * a.y + a.z * a.z +-- end +-- +-- local function normalize(a) +-- local l = len(a) +-- a.x = a.x / l +-- a.y = a.y / l +-- a.z = a.z / l +-- return a +-- end + +function pathfinder.find_path(self, pos, endpos, max_length, dtime) + -- if dtime > 0.1 then + -- return + -- end + -- round positions if not done by former functions + pos = { + x = math.floor(pos.x + 0.5), + y = math.floor(pos.y + 0.5), + z = math.floor(pos.z + 0.5) + } + + endpos = { + x = math.floor(endpos.x + 0.5), + y = math.floor(endpos.y + 0.5), + z = math.floor(endpos.z + 0.5) + } + + local target_node = minetest.get_node(endpos) + if walkable(target_node, endpos, endpos) then endpos.y = endpos.y + 1 end + + local start_node = minetest.get_node(pos) + if string.find(start_node.name, "doors:door") then + if start_node.param2 == 0 then + pos.z = pos.z + 1 + elseif start_node.param2 == 1 then + pos.x = pos.x + 1 + elseif start_node.param2 == 2 then + pos.z = pos.z - 1 + elseif start_node.param2 == 3 then + pos.x = pos.x - 1 + end + end + + local start_time = minetest.get_us_time() + local start_index = minetest.hash_node_position(pos) + local target_index = minetest.hash_node_position(endpos) + local count = 1 + + openSet = {} + closedSet = {} + -- minetest.set_node(pos, {name = "default:glass"}) + -- minetest.set_node(endpos, {name = "default:glass"}) + -- print(dump(pos)) + -- print(endpos) + + local h_start = get_distance(pos, endpos) + openSet[start_index] = { + hCost = h_start, + gCost = 0, + fCost = h_start, + parent = nil, + pos = pos + } + + -- self values + local self_height = math.ceil(self.collisionbox[5] - + self.collisionbox[2]) or 2 + local self_width = math.ceil(self.collisionbox[4]) or 1 + local self_fear_height = self.max_fall or 3 + local self_jump_height = self.jump_height or 1 + local neighbors_cache = {} + + repeat + local current_index + local current_values + + -- Get one index as reference from openSet + for i, v in pairs(openSet) do + current_index = i + current_values = v + break + end + + -- Search for lowest fCost + for i, v in pairs(openSet) do + if v.fCost < openSet[current_index].fCost or v.fCost == + current_values.fCost and v.hCost < current_values.hCost then + current_index = i + current_values = v + end + end + + openSet[current_index] = nil + closedSet[current_index] = current_values + count = count - 1 + + if current_index == target_index then + -- ~ minetest.chat_send_all("Found path in " .. (minetest.get_us_time() - start_time) / 1000 .. "ms") + local path = {} + repeat + if not closedSet[current_index] then return end + table.insert(path, closedSet[current_index].pos) + current_index = closedSet[current_index].parent + until start_index == current_index + table.insert(path, closedSet[current_index].pos) + local reverse_path = {} + repeat table.insert(reverse_path, table.remove(path)) until #path == + 0 + -- minetest.chat_send_all("Found path in " .. (minetest.get_us_time() - start_time) / 1000 .. "ms. " .. "Path length: " .. #reverse_path) + return reverse_path + end + + local current_pos = current_values.pos + + local neighbors = {} + local neighbors_index = 1 + for z = -1, 1 do + for x = -1, 1 do + local neighbor_pos = { + x = current_pos.x + x, + y = current_pos.y, + z = current_pos.z + z + } + local neighbor = minetest.get_node(neighbor_pos) + local neighbor_ground_level = + get_neighbor_ground_level(neighbor_pos, self_jump_height, + self_fear_height, current_pos) + local neighbor_clearance = false + local above_neighbor_pos = { + x = neighbor_pos.x, + y = neighbor_pos.y + 1, + z = neighbor_pos.z + } + local above_neighbor = minetest.get_node(above_neighbor_pos) + if neighbor_ground_level + and can_fit(current_pos, self_width) then + local neighbor_hash = + minetest.hash_node_position(neighbor_ground_level) + local pos_above_head = + { + x = current_pos.x, + y = current_pos.y + self_height, + z = current_pos.z + } + local node_above_head = minetest.get_node(pos_above_head) + if neighbor_ground_level.y - current_pos.y > 0 + and not walkable(node_above_head, pos_above_head, current_pos) then + local height = -1 + repeat + height = height + 1 + local pos = { + x = neighbor_ground_level.x, + y = neighbor_ground_level.y + height, + z = neighbor_ground_level.z + } + local node = minetest.get_node(pos) + until walkable(node, pos, current_pos) or height > + self_height + if height >= self_height then + neighbor_clearance = true + end + elseif neighbor_ground_level.y - current_pos.y > 0 and + walkable(node_above_head, pos_above_head, current_pos) then + neighbors[neighbors_index] = {hash = nil, pos = nil, clear = nil, walkable = nil} + else + local height = -1 + repeat + height = height + 1 + local pos = { + x = neighbor_ground_level.x, + y = current_pos.y + height, + z = neighbor_ground_level.z + } + local node = minetest.get_node(pos) + until walkable(node, pos, current_pos) or height > + self_height + if height >= self_height then + neighbor_clearance = true + end + end + + neighbors[neighbors_index] = + { + hash = minetest.hash_node_position( + neighbor_ground_level), + pos = neighbor_ground_level, + clear = neighbor_clearance, + walkable = walkable(neighbor, neighbor_pos, + current_pos) + } + else + neighbors[neighbors_index] = + {hash = nil, pos = nil, clear = nil, walkable = nil} + end + + neighbors_index = neighbors_index + 1 + end + end + + for id, neighbor in pairs(neighbors) do + -- don't cut corners + local cut_corner = false + if id == 1 then + if not neighbors[id + 1].clear or not neighbors[id + 3].clear or + neighbors[id + 1].walkable or neighbors[id + 3].walkable then + cut_corner = true + end + elseif id == 3 then + if not neighbors[id - 1].clear or not neighbors[id + 3].clear or + neighbors[id - 1].walkable or neighbors[id + 3].walkable then + cut_corner = true + end + elseif id == 7 then + if not neighbors[id + 1].clear or not neighbors[id - 3].clear or + neighbors[id + 1].walkable or neighbors[id - 3].walkable then + cut_corner = true + end + elseif id == 9 then + if not neighbors[id - 1].clear or not neighbors[id - 3].clear or + neighbors[id - 1].walkable or neighbors[id - 3].walkable then + cut_corner = true + end + end + if neighbor.hash ~= current_index and not closedSet[neighbor.hash] and + neighbor.clear and not cut_corner then + local move_cost_to_neighbor = + current_values.gCost + + get_distance_to_neighbor(current_values.pos, + neighbor.pos) + local gCost = 0 + if openSet[neighbor.hash] then + gCost = openSet[neighbor.hash].gCost + end + if move_cost_to_neighbor < gCost or not openSet[neighbor.hash] then + if not openSet[neighbor.hash] then + count = count + 1 + end + local hCost = get_distance(neighbor.pos, endpos) + openSet[neighbor.hash] = + { + gCost = move_cost_to_neighbor, + hCost = hCost, + fCost = move_cost_to_neighbor + hCost, + parent = current_index, + pos = neighbor.pos + } + end + end + end + if count > max_length then + -- minetest.chat_send_all("Path fail") + return + end + if (minetest.get_us_time() - start_time) / 1000 > 100 - dtime * 50 then + -- minetest.chat_send_all("Path timeout") + return + end + until count < 1 + -- minetest.chat_send_all("count < 1") + return {pos} +end diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..9b643c8 --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,11 @@ +# Blocks mobs from spawning in protected areas +block_protected_spawn (Block Spawning in Protected Areas) bool true + +# Sets the limit for amount of mobs in area (of a single name) +mob_limit (Mob Limit) float 4 + +# Allows for large mobs to jump on pre-5.3 clients +legacy_jump (Legacy node jumping. For pre-5.3 clients) bool false + +# Mod used to set player animations (default for minetest game, mcl_player for MineClone 2) +mount_mod (Mod used to set player animation) string default \ No newline at end of file diff --git a/textures/mob_core_blue_particle.png b/textures/mob_core_blue_particle.png new file mode 100644 index 0000000000000000000000000000000000000000..8752e6046959bf7e54851e92095aa1c862a30d59 GIT binary patch literal 1971 zcmV;k2Tb^hP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O=0(mh31F{AU%j1SB4UGBP2nZ@!!9u`42wKWQ`A!Vvf<`&|T)I=;Xjo(baBUf=CTn)*rV=<=)%-CCK^(Q!!~IX-sMdy{ou@h`8VUnkqeJ^16j z7m(2djE)TtN6zARO=NezWw+~>FvvQGK5e^)vroA*clQH%ckcstwy52`Q(0r3Mz zYNzlycblOPs$DC;JxJAxef_*}H)l_Cj`McKM=%wQzSQ+9yN7`lRJuJa;u3rV`m(Q# zE8!IzNbgXy;%bI6F?gKhsZ*gzW4DbOAbK7wXqg+kZA(r#b6YZicw97DTz1{K1brG* zb1?Ibh4#96uh%GH;J(VQ!EiEO&L0xn(w2pCJb^@~vugr;&D zHoyBaFL7gEgmq9bY?>2Zu-JNB)Id*cxvjIqxM01pC)`Q8IsipjI{*whC}1PGK#{C4 z3lVrs(5C=7JI@sgPzc<~Olad=ggA-U+L$A>$8z9fMrnZ%!9^#KCc6T$ybp*U@d+_h zf(zb+5Mq#!LJlRm=wpa6N?bLnh!ZbCqKG8PQj$wPg%p#dlyWMWvqK3BIc70A<(x~w zq6MxCW*3YorE+!E*HB}Xnrg14A$*!|p~WUGwcJYQ?mE)H$1bL)o_iTI5K6<3FyfGr zMjmA%)~1_&h8d^KH1jOCYO3nHYV<+5->XKd+EUE#aq_4Jv&iLwRyhevGbqNMptvjr z5YW6dvx&UtrQFiYW=2v3hH4a+IGvVaP|y$JFzMFrQSP_8nc%;r8-JyoSb9+}!n)T& z>>7DQIsJhWFSB2B>^*%vqfJty{bozy&^>E(ExKUhxeb2iP6xiXF&vm9hH&GI7UcoaUW{;{a$)xcHxLIR`~OE|EwZQ>kqprb5nZ(>(1Mt(9ep{yhruSe^2o{lRFr2C^a$nhtkF;N|HrAIjd%XYF42VD$g0c?6zNK zK-?S+h&>(>YgMAXogw`pNb{!m1unnpeTS*=Nf~K4MPUQ0M*&n_9DK}yhgyteJ3tsJ z+i}cgipLr3Rw15*OQ`E-aB2WiVKjRMpGQl7mQ)va0m6(MEN@3^1sv!e*gb=%1G@+` zzesJ9`9?h&%KC%aqyszAGyIA^%A;eeJA)I>iTTUY+pAHh>juzSr}d6%@Qm|nhKx9X zYhh|5XrQ8mM$>LhyaK5O3Ti+BUR8K_>GB-{!@=-crBDE{ zMUqk9D}^$T6G(kX;w#AG-MJ%+>nSiTN{w3fjMlyHZUKlM4S$QRI7N-@&OT?QXe30c z%ahxI*9Ok15L&~dOCBD_i|uf9)Uk^r%1ZtK6Vb{yMCbu-P{Qy}#!8Nd!089hXHh|}LyYDDwibEvD^=1=(DX7t+SHyM68qR; z*5D+DKi*pA1pl2*HTLl_X03g*E)B|Mf8XxRcXj`&{l!r#SLaX~t)>pILVK{#C$0JF z!FSt#*O5#dI#ET;{{Zz3EKg$OjSc_+00v@9M??Vs0RI60puMM)00009a7bBm000XU z000XU0RWnu7ytkO2XskIMF->p0SgK+Qy%d40004^Nkl+s^Muj)hko$opKoO91tMn|&z@7vz3BF`nL>>L6hBHYKMl0daaNo$RgR(1|4 z=3mHJV8$flXf))YAQNLkHRsfI>cpszyo@6`&Se0Kn?P6~(d6(05D8lL~5$ zlA7M35Udu{mW!KLvDsLNwMIz+%-av)_TwLb1HTL!;GLZSaQbIl3i(`I zLFjoHoc#-4DT8$|_Y?d{%19jde1>9W^Ii`u08dpucEvn% zwW$AkP9(T{&1OIlSlZfNl1fQpSs!g;CX*(UNk8kC-U4KbxzK*D&h7vJ002ovPDHLk FV1i`stfK$` literal 0 HcmV?d00001 diff --git a/textures/mob_core_green_particle.png b/textures/mob_core_green_particle.png new file mode 100644 index 0000000000000000000000000000000000000000..4a9e873f6653eb3a4ac08df3f9effa94e7e1e6b7 GIT binary patch literal 1942 zcmV;H2Wj|;P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1bvmgFc5{O1&N1cX?m z_M@L6504L$Kl1V_H>n;watvLTk3pWE9Xdx(jv;niPG@ZNe3$*xbk2{{GI1+^9QyDj->&fMJ(;N86s*x7vK{0fD!@hHR(7#%r< z_t-6jKCpIqetDs4lYPFPup6Vv7=6z!`v|0>Ig`4kvU*S~fRfi|5qH8bpr6*QxCK+J z0KI|Dg1Z`0a=>wrr$(6u_0?9Y2+_Ol5?be~<0TVLPF>|mfuA=Gmsq(O7VuQT8bj>v zSZMjx<*$;!b0cUvf-%AuKiuY>@OSRp&=@myiLBgMAucx)5MfN_<`Y^VgvP-%EPU5( zp7O>%5zAmOESLoo%(iS3CD0?cc=6283)UOE)16W~1^^L!2LwY525dxPQzV<5OGLm? zz)wcxj=d{jfI@JM%m6pc5#)%QzVS_>rR##*Vw4sT5jb)JHOd)ec^{BJ$qRBQ2M*qZ z5MoF{f`%M9`WRx2Db5;|6DOZSiYX;YnzFEX2@)lVh)Pzcj1m?!YBDOSnlqecn9le* zLnG(n3Kw5Oi6s>&T5?7DR9{1lHC3rvb90RwmVb*anI=tJ?$nV=-S^OAPhGn9JdkUH zhaX|Yk%kN%`DX2~`pO!8VD4+y=&&{!^Gh#1Sc5pF%LL7G5+-M0%y|OiX)=I>c9SzJ zgw8ymcV)xWlQP}3d1|31S zV!;+$EiEk^hjQH31f}!H-nQn=i+Na?y;K3H!?2uT(^@$$)FQJUCc6MhHqBoobS#I+ z^$C0NSR|LW@BF~}1^AU4=F8;x8TVb|85YATae?D&yu!9812U=bo)$K%qFv7j#|gxp zL=PLOqxicUn)XD*a6bq61xw?ew=GeFLod3dzHt<|iQSjcC$*rD#pXxK!+cjB3i(g{|k)%1Gmgg&QJyyiQ$@}jI~4_v4`UZ z;9Rk**eiDIQCH~nCUo;Y_L2^cbJ)#Q^< zNl#FirO+-TaQi)!9}$PcDE`=B*K7wP1U|^6s#_6|l?+Emmrugc4_kA8`Ce&w&g>DdQf0AEpIi>jdDXGQ=_08za;NThwF3+77i+23x!Pkn z)R5{)SS+@)pO1vY|FEwMuRS&#Ykqa(X#Fh6isMCuZQ8cYz~Q(iZWkFLU6vHSdz-+Rqe+-FoM7^~a~;-_k7{M`#2|KjfCb z?1Gorexo(tRa}1P?p$T=6hChb=U+UH{Iz4vUo$jtAXS`){s)M3DnT~~>QVpz00v@9 zM??U@0J#9IPu^hO00009a7bBm000XU000XU0RWnu7ytkO2XskIMF->r3Kj}13!v^h z0004@NklzFNE|OPG@qRPkZ~n;Q(|P{) z?d~5Vw@uL6I|4vN_*)YciIO-t$w{OK&!^=4V$A#NH8F`( z1(=w1=;i}pAzXJPopuDk>B^Zj>NTGriBp}NbHYpUn{}`j-hYH}4aTYloB2Hnu6WKJ zHr9!4=3;WNIjHsR4hX(YRb=5uqyXa&4B<0Z84F?4iMwacLiLNQ?j^YBSyt|C%j-vT z%xp(`XT}h&^($x4HIXO0vPeULu+-V$)%UNoSB-Sq5t{WYn~3(Rk?STlNKhFq@nSQl z@#`hc)@Q#bz)dUfjdAPUZvYN|o&)f9RP zaB^>EX>4U6ba`-PAZ2)IW&i+q+Rav3lI182{O1&N1SA0x$H6QSbAvhlB!=?ax~gmG zi5L^lU?HSLDa3RApTCFu3l9?o7ou9qDS130hs*^7^XpkX`Eb7XhkqM9$lW!;5D9u& zpQ&&C0(rbG(6HslqugoTc5EA>%kcTg^gPE~&$c1qC8z!E@?KZM-FEhm>o&WMKYs^B zFj|B?VaHuaJl;0F$oiEj!y9;@L=rHbPr+>bb?n?VdwYw4$B9+|U)Z13h5fl%EQqE-wP4SBu>)A4z zVT%=vHL!W0RY#c^ksTDMQK3Qo>?>734BT1KGgp>VW}=+A%Ax>`2CHOgP7F*`uqF`m zg@sPP`Se%G;JFbraTqH6@@sRy5`L#$hQ^qwOXTf}6>#y)X~3Aw%{R0_2#p=nc*gf^ z?k;cq9kBui;~8ed28%zIi6hc2x8&qmpcj2`ejN?cwhbUe@Erk$1Pu5@5-E{aW<|LW zHt}D>O_l<(LWsy84Tc;lQDSg0 z#uPPL%&{a%A;pxErdgwkiUy6ERMj+VF|lCLlBtWgypv zj4>PI5+`S1j01sjoeUtMd2;5B zLg2~VGW!_F-?A>=4{#S;Y%WGSuRzFJV@8}KdYpMrP ze=fl#aNoOU!54Z-X&FCIn_Mr)XV-OEsz>&%K4W&@4azu(FFf$&mNUO1U>xesg@ktw zkpk{|)inw@_tY9j6sx69dw%xrZSa*I8N4iO%toejjI&0pwTv|(x$(=gqs0?MtLY{* z-pF_%`z9jUjHPccHGk`FN9wNsJWjX!JO~BqtEZ8+J_l|gM4yRU6t~Z6Fsj? z8)^fEE|vA=!w9GW`(Teyd$=q%B;>-=k)3rc9h6~OJ3)Wg0`L8Yy_>la#$uo+YUzqa z9dQ{e6~P)|)6_$5ui?kmw(F9VslTnA1-tJw#C>44E0(>UjorJ~+%LM)d?4SC_rt9F z2(%=*j^b#?l|#3iR`Od*B>UcW32E{^IQQYf{1DtDC|7T*J^}9in&5s@EIy6NAyR(? z)29pDBeoaP$lYOho&5s9pI%27Q1=kUA7FAnz8W_scSY~P&F^S`#pM3Pz5i_Jnc+|7 z^baq{|BoU3XUpzi4k-H6aW;fk00006VoOIv0OSD30E}Oy(vtuH010qNS#tmYE+YT{ zE+YYWr9XB6000McNliruHzgVxR=D5UY?++JIs8 z>eX<*yOk;f0|NsC6A7CCGcYi~FvHGcw~2QFR5QcFXZIN1zWC1Y_3PLFwzg)B#2D~@ z=ds%i51-v*NDFaigbC(VPyfGl>z@A%3=E9;yzqbhyebqvGH2i3-FU+QuKC-I`3&+> zN(@l`e{_XR_%!ENEr%O|?EHOucQaf$v5pa&0VvL%*cJ&l1lj2@%?u0-42(z#2}LtZ z@pz00000NkvXXu0mjfeu22y literal 0 HcmV?d00001 diff --git a/textures/mob_core_protection_gem.png b/textures/mob_core_protection_gem.png new file mode 100644 index 0000000000000000000000000000000000000000..644014b72249ac340ac27199fdd246431b912242 GIT binary patch literal 1957 zcmV;W2U_@vP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1b>k~1d^{Ld+J1dsrUpSmDR(<0w{Sci+BXzfWE9J;Ze9) z0eSA40z7VzaN=fI!BYim3^Cod zxV3Lu`&FmdxjD=v2W1W`K0W3u;eUB=H6H%Zq21YTa3~IA_7MwiGraFvb+z7AMuJ9%7KG7 zA%qwtXh`L8FSQCe4WwqlASNlbEC_rJS*8hU$#f z88dP&RJiyON-R>eq>?Mbr}`RdtWve6nj1B4NdFd_n5HeY++9Z~b>BmeUAp$v^FXW( z9)5%ohYTHQPObx3v=&TbBDFbm|yec!5YLOmkKw_Ntm30G4c+K%VYonO_MXL z&U>27P0nm0Bt;;w=E5YWxycw9^xaVy?&j{n+*{sE@W0}XzGKcz>i!Sr%%tux_nEgZ zSZjHmD7*-oSvWDZJW$x6Kr?Bc6=SwIn10%_?4?j`e!KS8QR`OHs&Yda`K>Wbc`wnX zdfC%Bn>ma^2xzu`tK7R=t4j9O(wRH-(e-tYTU6j9I~Lh{(x2Lzw+=!~bxr%F+d2$_ z^qVy!A(&=&lmSH8?zf&$()t*et9^TIW*=Dn8miXzno$uL0T$tE^t^4?<9}=1*`_^c zGpFN5KpfvTJAZr=Z$>HH5zNQgiR7?lzYgyhv~T|wNxW0vuAOXwyYburIGrq8q*fV5prB=zWa+e%G(z|?;?x- zX@76W`6@aZKX`eEdwPx<^RqCi82RC{k8)>^?a;h5InwEXjya5kRFJxSx3wNB8 zdypiSXW+Al8Aq`3{sF{aLM_?!?;)nN9f%R3vTfG-DK+y}XJBV=XIA184i>JX$hLU{ zI}vF8pm(*wybWZ&)WizZeQ9fRk0VVtMmQ&E`)Zz?w#!Ff=ZQ6C{2bt~0rCTy-Cs^gz$`D&liZi z0oI7K|Kk33N84q4%LfR>Whuwh-Bn4HB-tz5H*2T!+)9bt5L_@NSH7zLU7UbIowf3* zm>J$Vf=hu*x1A3&scoAme4TS=bJQbT4Pt#Y>AO}+J*I;2UR}3*xoVbU-`CRLwf}=6{2LrCnSTN3M>8>jh7=b7000JJOGiWi{{a60|De66lK=n!32;bR za{vGf6951U69E94oEQKA00(qQO+^Rf3IP=dAl=VfWB>pGk4Z#9R5;6(lR-}!K@`V- zgNtktR>guIY`A#qr5~V{PaqM`J$dQHG;N{>J@%j%k|y-znbf2gYi#-fYGOQihImp5 zHc{J>u&@-^h1Uy9mayB_mrN#^_x`{6&&(UOy7GdcNT<_ODwV+e(U%ic5TJE?jqUV! zJU2t>qC%!AC?*Q1& znz}oCMlPEffrR`VEPpx`LyxdH9b;i`hRMk=)A>VjH&99;BI6GBcT4~bJwkHo5v5;s zjHnJktzix8ymv6>U@!DhTybh3HljL-_!PNnUI0EHe+fP5?6&Fkx*Q~5E7SUoJ8BJ!;_QZUo5vfz rpXvSEpCEz=2m*t7voQaEfQQ5%#`dXt$NbW=00000NkvXXu0mjf4nwO} literal 0 HcmV?d00001 diff --git a/textures/mob_core_red_particle.png b/textures/mob_core_red_particle.png new file mode 100644 index 0000000000000000000000000000000000000000..d9151dff2e058cec37de4db39fa122d780d2991f GIT binary patch literal 1916 zcmV-?2ZQ*DP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O=0%vLh)B{O1&N1SA%T}P(4JUkYNKl1WcZloSPdK6unk3u(hhUxaq)1%1omeYAR>8#8C={V=>WU06(zrK3` z7%f2P*l>5`EPm%gcKe%GJAVn2taIoSp6oo7y^Ec>yD#9~y%+3kK63XK3S;A$5MMAl zatiNbw-9<^?b`XxfRYva{CUD|jHYAseY)%;kc#FU>N?8mVPXN4ZY_&=1mA$ZtS8}7 z9I*oQ1~vVy;5Ndth#LxaU7*Nju}Q^A^p zm~UHX?VHwql@vQS!b~P8Bdqx4G4Ba~^4yBXn667?^~MTz&2Y3ZV=^~?F$;vyIGBco z?|RIW-`FQ&9T*G?X2k)sEn7ti^zbdWcy^ceOq{sG_P#bK>NX!a|BkOwyE6&e$|V zb;jz9895g&uJ{s4EK;>c-$RdGy7tub zfUgZV{0Jis89LI)H*1I0x2(|%bDy(DhqcL=U-RU_8pI-(3YzI8OwPay^H$Y0H-+X;EqQB^>%}cu znTd7nh0LvwuCII8BIh32OKa>010S|kw$55XM7|GedM$gPWjBy2`_tybJ?tE1^v_H+ zAKLnxrWu69#$QM9ehsC>F*fpnf>MtNDRgm<%27I;HKjdPl7%Roc_vDWvL?pvXI5V$ z-cnC#i!GvO%j_quc@~h4y?0XSD8nh7tviBfjo?z3+R-nmN~0~8YBUW(0LnIZ_>Pj? z!%^65s&O9-ceOTP@byw(3^rd_{9QxU>>iR>+dx}I$vCznr+Pl7a+DfU=WuKtsrw5% z8X=hn1NCckUu?S$J#93tmoH{`EvbbE<`!;Ks$U%n*Blv^>1_SF&IM}j+o#d4jmOOxx=hU(b6_UQ<9UU=6M zk4e{Ynab)g`+PLci(EYmQyM87{S(eF%BdFf02YyQf@->~-WqFf50mxTWYo^fQg2Q^ z;c`W(z69wr66uW$?O1*PW)06!ivR`;Fb(=TEq2;Y(;5r5a<9=zZLlbhRUM|@h3g<~{0covq zH=B%xdxCu-<6ErXq?>Q#texlDm3fe6$_eq3eR6K4SOd!m>0De72gp=MrdQM3ZMnZV z$1U^$W;dr^gRfBm`qu^QgBG=a$N_Pn`ZjgdULaez1|sOxtImD^?~NO-87kFNcjv^W z_B4v5$fuUK+mf#Ver~^$Y9D})lOH>he0lgd{wPgFhy@=X(nKe;aH|!*)6Q2mnQz(h zrLD9qTD_Ay;Uqg!@K^hHS^H`2|Nj)>f|(F2{{ok-4AzoFZj%5200v@9M??Vs0RI60 zpuMM)00009a7bBm000XU000XU0RWnu7ytkO2XskIMF->p0SgN-N(I5U0004xNklJS0@P>A~X@ClkdKJ984s)nuqj^_ulX0{=RdLES8G=Z`Gq1d8k3{>mdLl zLchk(5>=yE0YX&?%L~+_7*%ylye#n?0W$>DM6`DvmKRV}>QS6+0APTG8|~-qOO}_$ zZn#BI)xikWDApMQ5)=zO;|V`sp6JfObLm`-J8zv3d(6{3#Q^U)BgJEr0twKdNq;5| zLN$tYmcS9O0I#ropou}rQ{WMKClk+QRt#W)wC^efEQ-NmQ3v5RgBS6%92I{nz!Z~g zP~am?Qs6ZO43s+5}vsb9ZRs@-{h-IW2_Vt{00008+9EttD(#nNp%w(dW;@5HI&7cQN@a_#c1J2&s%zw_wH!{;xbwJ=ZW1Zrn23GxeO zaCmkj4aiCJba4!kxIDMdQ>ev&gMqV0O^5N{|0qk9#c@xs`HHh1c&=EZ?5kF_VQ2dX z*}pc%Z^crMCjK!u>0bUy?8(HJs}9MAKM+(ia?5|+5~^^jmQ%rW?cd66wIv4P^41Iv iGVWjBeze}!E53Xi^Py?0mhA@G$l&Sf=d#Wzp$PyGxL^_h literal 0 HcmV?d00001 diff --git a/textures/mob_core_spawn_egg_overlay.png b/textures/mob_core_spawn_egg_overlay.png new file mode 100644 index 0000000000000000000000000000000000000000..8dbd72a92dd91b463c55352fdba2d2d027f4121a GIT binary patch literal 1757 zcmV<31|s>1P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+SOKDlH@21{O1&V1cVTR<6s^UbAvtpCK%i0di3;k z%+Hpq3M>l=WlF-P{rjt%fADc6(Rk4qQV13wpL}u|lW6zPK9cqGf8E$)a+90$frE%K z@_rlI($A2~wLtxnZ+E%Tvh2iV=&-yD^6>01S$c69Vzo3yafFg(#@uq8a`zrPb55VYJ7-VW*}UX@hr-yf6v7im zOHT40yNS>fYlr9C1Bq7c>$SO$YKVU7qe+if?o30=9xlV^sWSa0l}?xb}Y07URD*bK7UlMTc|fvh^G zM8GkEpNyS5_HF|MB;qQW4sMtO$U!y0H`XXkT_o(h)R?sIdJenLI@g0kIKsGRWzt-)TB9b^ig7n8e>c`Czlv4Oj1Z{QcNl345t~U zGgfD491dVnggEKJZJb`f?3?QN1 z;LJwy-VNpkXEr$$g*dQAGRUbk7z2a8k&{j*cNcSC@@B;U5pV7jb7oNYH<&Yny2acp zZ*N$ud7dad2^v*6Ftyy#*m?-r#q72mTd;GAF8HUT`RT&{zab7`jJXLIog#o;-hgWC zU)qxS(sfU5mwME}Da=bX@VS_>*;i*y@lgEPh&-xkk2v|3xaod}Bk)j6hkvg726n%l z+*)TFj!U!l*5VFeilzK0Kg|>XdiXs`An);93z+-oZmR5 zcLkir)k!#F>v_fBHcsU?P??tpH^>SdtT}gtu64SF|DwBpp@Nr)wb@YSb150UN|tKp zJpyzut#~;YH%uwk%fPYzH1uiIn07QuR8OsL%SI~g=Gz?3-+9rPXA|?9U#9{!WQJp_KBzBcSvxu$mY z>OL1rhq}2@bHze?;^E-x-mm%$IImaQ*{6}3akA=_s>SPH{i+N!KIC#bjnGy7O|DfM z{bM>m*L0|7@f|&5lna@km)c#$9IolusD!hm%B`g2HQ%Y8NSg0J`3mhTvSvaX-}QjE z9qN-pGM!I<_3E+i`#ttLj;_=2u}t3_hulOLIIbN17Vl4UYR264qoJNm+`0WgX^@o* zA>fT|O@&rq^ciAJyKzn%N&9q)biXaOZSlXuC7o>-FN4Ya2YOitwPdZ{&Hw-a32;bR za{vGf6951U69E94oEQKA00(qQO+^Rf3mgjw4B;dLzyJUNmq|oHR5;6}lh0}sQ5431 zXOc`hA=^Adp2G)lDFj5D3)YG*GDV_^6QQXi3L<19lorV%YoDNN@d<=E$?UuU{pn6pzz%UqS9LF3T9t*(f z+4=tg+AWu1Fi-%uTIcbLcaq&-y?ia}Apf*53+bIJV&JGzmB zcFU#NY*MS$s8lL+9u6h5PbL#7wwKF1viTW2d#dxuqN=EBZV!S$d%d2{Bg@Y2JuT)q z>%;+sVW?pk=H;eoN~6)>vRJ3s`fbaWijJbLWUH>4TH-NR4)oR6Ju>c^BW0EA{ zLKA{F?`7j6WqoJ*>Df86*-VTvdZ9@vUe