Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56614a7ce8 | ||
|
|
6dd60de671 | ||
|
|
208ec12e0c | ||
|
|
420b6fcc8e | ||
|
|
b6bc407902 | ||
|
|
7a25e145b3 | ||
|
|
fe4125753c | ||
|
|
60417b51eb | ||
|
|
fba383d230 | ||
|
|
d8fe5c26d8 | ||
|
|
f9cc4773c0 | ||
|
|
c6c66b1320 | ||
|
|
84f7b2409d | ||
|
|
ce9c40137a | ||
|
|
b4eeb2dee7 | ||
|
|
90e36f45be | ||
|
|
3bf449a8cf | ||
|
|
0e212d8f54 | ||
|
|
db88f6a3c4 | ||
|
|
282291f576 | ||
|
|
244681a7e7 | ||
|
|
d9e95e884e | ||
|
|
8c5226f4bf | ||
|
|
0997ddc085 | ||
|
|
26c46b600a | ||
|
|
7da7e18da8 | ||
|
|
0d30fc12e1 | ||
|
|
c446233dbd | ||
|
|
9aa5d326ca | ||
|
|
67db6bae72 | ||
|
|
e3540b34f1 | ||
|
|
b3d791ae4f | ||
|
|
3747b6fff7 | ||
|
|
664a18f83e | ||
|
|
cbf23cb493 |
@@ -1,232 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
|
||||
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.
|
||||
|
||||
Advenskalender
|
||||
Copyright (C) 2025 M_Viper
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
Advenskalender Copyright (C) 2025 M_Viper
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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 <https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 8.3 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>de.mviper</groupId>
|
||||
<artifactId>adventskalender</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.3</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
@@ -24,13 +24,17 @@
|
||||
<id>placeholderapi</id>
|
||||
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>enginehub</id>
|
||||
<url>https://maven.enginehub.org/repo/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.20.1-R0.1-SNAPSHOT</version>
|
||||
<version>1.21.10-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -39,6 +43,12 @@
|
||||
<version>2.11.5</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sk89q.worldguard</groupId>
|
||||
<artifactId>worldguard-bukkit</artifactId>
|
||||
<version>7.0.9</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 503 B |
Binary file not shown.
|
After Width: | Height: | Size: 391 B |
@@ -1,56 +0,0 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.Material;
|
||||
|
||||
public class AdminCommand implements CommandExecutor {
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 1 && args[0].equalsIgnoreCase("reload")) {
|
||||
Adventskalender.getInstance().reloadConfig();
|
||||
LanguageManager.setup();
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.reload_success"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length == 3 && args[0].equalsIgnoreCase("open")) {
|
||||
Player target = Bukkit.getPlayer(args[1]);
|
||||
if (target == null) {
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.player_not_found").replace("%player%", args[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
int day = Integer.parseInt(args[2]);
|
||||
if (day < 1 || day > 24) {
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.invalid_day"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target.getInventory().firstEmpty() == -1) {
|
||||
target.getWorld().dropItemNaturally(target.getLocation(), AdventInventory.getRewardItem(day));
|
||||
} else {
|
||||
target.getInventory().addItem(AdventInventory.getRewardItem(day));
|
||||
}
|
||||
|
||||
CalendarData.setClaimed(target, day);
|
||||
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.open_success").replace("%player%", target.getName()).replace("%day%", String.valueOf(day)));
|
||||
target.sendMessage(LanguageManager.getString("messages.reward_received").replace("%day%", String.valueOf(day)));
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.invalid_day"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
sender.sendMessage("§cUsage: /adventskalender admin reload");
|
||||
sender.sendMessage("§cUsage: /adventskalender admin open <player> <day>");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,61 +2,137 @@ package de.mviper.adventskalender;
|
||||
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.entity.Player;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Platzhalter: %ak_claimed%, %ak_day%, %ak_has_claimed_today%, %ak_remaining%, %ak_is_global%, %ak_event%
|
||||
* beziehen sich auf das laufende Event (sonst das erste aktivierte). Mit Event-Präfix, z.B.
|
||||
* %ak_halloween_claimed%, gilt der Platzhalter für genau dieses Event.
|
||||
*/
|
||||
public class AdventCalendarExpansion extends PlaceholderExpansion {
|
||||
|
||||
private static final long CACHE_MILLIS = 5000L;
|
||||
|
||||
private record Cached(Set<Integer> days, long time) {}
|
||||
|
||||
// Kurzer Cache, damit Scoreboards/Hologramme die Datenbank nicht jede Sekunde abfragen
|
||||
private final ConcurrentHashMap<String, Cached> cache = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
// Der Identifier ist jetzt nur noch "ak"
|
||||
return "ak";
|
||||
public @NotNull String getIdentifier() {
|
||||
return "ak";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return "mviper";
|
||||
public @NotNull String getAuthor() {
|
||||
return "M_Viper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "1.1.0";
|
||||
public @NotNull String getVersion() {
|
||||
return Adventskalender.getInstance().getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRegister() {
|
||||
return true;
|
||||
public boolean persist() {
|
||||
return true; // Expansion bleibt nach PAPI-Reload registriert
|
||||
}
|
||||
|
||||
private Set<Integer> claimedDays(Player player, CalendarEvent ev) {
|
||||
long now = System.currentTimeMillis();
|
||||
String key = player.getUniqueId() + ":" + ev.getId();
|
||||
Cached c = cache.get(key);
|
||||
if (c != null && now - c.time() < CACHE_MILLIS) return c.days();
|
||||
Set<Integer> days = DataHandler.getViewClaimedDays(player, ev);
|
||||
if (days == null) days = new HashSet<>();
|
||||
cache.put(key, new Cached(days, now));
|
||||
return days;
|
||||
}
|
||||
|
||||
private record CachedBirthday(Optional<DataHandler.Birthday> birthday, long time) {}
|
||||
|
||||
private final ConcurrentHashMap<UUID, CachedBirthday> birthdayCache = new ConcurrentHashMap<>();
|
||||
|
||||
private String birthdayPlaceholder(Player player, boolean activeOnly) {
|
||||
long now = System.currentTimeMillis();
|
||||
CachedBirthday c = birthdayCache.get(player.getUniqueId());
|
||||
if (c == null || now - c.time() >= CACHE_MILLIS * 6) {
|
||||
try {
|
||||
c = new CachedBirthday(DataHandler.getBirthday(player.getUniqueId()), now);
|
||||
} catch (DataHandler.DataException e) {
|
||||
return activeOnly ? "Nein" : "-";
|
||||
}
|
||||
birthdayCache.put(player.getUniqueId(), c);
|
||||
}
|
||||
if (c.birthday().isEmpty()) return activeOnly ? "Nein" : "-";
|
||||
DataHandler.Birthday b = c.birthday().get();
|
||||
if (!activeOnly) return BirthdayManager.format(b.month(), b.day());
|
||||
Integer year = BirthdayManager.activeYear(b.month(), b.day(),
|
||||
java.time.LocalDate.now(Adventskalender.getInstance().getZone()));
|
||||
return (year != null && b.claimedYear() < year) ? "Ja" : "Nein";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onPlaceholderRequest(Player player, String identifier) {
|
||||
if (player == null) {
|
||||
return "";
|
||||
if (player == null) return "";
|
||||
|
||||
String id = identifier.toLowerCase();
|
||||
|
||||
// %ak_birthday% - eingetragener Geburtstag (TT.MM.) oder "-"
|
||||
// %ak_birthday_active% - "Ja", wenn das Geburtstagsgeschenk gerade abholbar ist
|
||||
if (id.equals("birthday") || id.equals("birthday_active")) {
|
||||
return birthdayPlaceholder(player, id.equals("birthday_active"));
|
||||
}
|
||||
|
||||
// Die Platzhalter sind jetzt viel kürzer
|
||||
switch (identifier) {
|
||||
case "c": // Geöffnet (claimed)
|
||||
return String.valueOf(CalendarData.getClaimedCount(player));
|
||||
|
||||
case "n": // Nächstes (next)
|
||||
for (int day = 1; day <= 24; day++) {
|
||||
if (!CalendarData.hasClaimed(player, day)) {
|
||||
return String.valueOf(day);
|
||||
}
|
||||
}
|
||||
return "None";
|
||||
CalendarEvent ev = null;
|
||||
|
||||
case "d": // Tage (days)
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate christmas = LocalDate.of(today.getYear(), Month.DECEMBER, 25);
|
||||
if (today.isAfter(christmas)) {
|
||||
christmas = christmas.plusYears(1);
|
||||
}
|
||||
return String.valueOf((int) java.time.temporal.ChronoUnit.DAYS.between(today, christmas));
|
||||
|
||||
default:
|
||||
return null;
|
||||
// Optionales Event-Präfix, z.B. "halloween_claimed"
|
||||
for (CalendarEvent candidate : EventManager.all()) {
|
||||
if (id.startsWith(candidate.getId() + "_")) {
|
||||
ev = candidate;
|
||||
id = id.substring(candidate.getId().length() + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ev == null) ev = EventManager.defaultEvent();
|
||||
if (ev == null) return "";
|
||||
|
||||
int currentDay = ev.currentDay();
|
||||
|
||||
switch (id) {
|
||||
|
||||
// %ak_event% - ID des Events
|
||||
case "event":
|
||||
return ev.getId();
|
||||
|
||||
// %ak_claimed% - Anzahl der bereits geöffneten Türchen
|
||||
case "claimed":
|
||||
return String.valueOf(claimedDays(player, ev).size());
|
||||
|
||||
// %ak_day% - Der aktuelle Tag des Kalenders
|
||||
case "day":
|
||||
return String.valueOf(currentDay);
|
||||
|
||||
// %ak_has_claimed_today% - Gibt "Ja" oder "Nein" zurück, ob heute schon geöffnet wurde
|
||||
case "has_claimed_today":
|
||||
if (currentDay == 0) return "Keine Adventszeit";
|
||||
return claimedDays(player, ev).contains(currentDay) ? "Ja" : "Nein";
|
||||
|
||||
// %ak_remaining% - Wie viele Türchen sind noch offen?
|
||||
case "remaining":
|
||||
return String.valueOf(ev.getDays() - claimedDays(player, ev).size());
|
||||
|
||||
// %ak_is_global% - Zeigt an, ob der globale Modus aktiv ist
|
||||
case "is_global":
|
||||
return ev.isGlobal() ? "Global" : "Spieler-basiert";
|
||||
}
|
||||
|
||||
return null; // Placeholder unbekannt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +1,394 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.*;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import java.time.MonthDay;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AdventCommand implements CommandExecutor {
|
||||
public class AdventCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private static final int MAX_DAYS = 36;
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
||||
Adventskalender plugin = Adventskalender.getInstance();
|
||||
|
||||
// --- Admin-Befehle (/ak admin ...) ---
|
||||
if (args.length >= 1 && args[0].equalsIgnoreCase("admin")) {
|
||||
// Prüfen, ob der Spieler die Admin-Berechtigung hat
|
||||
if (!sender.hasPermission("adventskalender.admin")) {
|
||||
sender.sendMessage(LanguageManager.getString("messages.no_permission"));
|
||||
// Christmas+-Kurzbefehle: /ac test und /ac reset <Spieler> <Tag|all>
|
||||
boolean elevated = false;
|
||||
if (args.length == 1 && args[0].equalsIgnoreCase("test")
|
||||
&& sender instanceof Player tp && tp.hasPermission("christmasplus.adventcalendar.test")) {
|
||||
boolean on = de.mviper.adventskalender.christmas.CalendarPerks.toggle(tp);
|
||||
var xm = de.mviper.adventskalender.christmas.ChristmasManager.get();
|
||||
tp.sendMessage(xm != null ? xm.msg(on ? "calendar-test-on" : "calendar-test-off") : (on ? "Testmodus an" : "Testmodus aus"));
|
||||
return true;
|
||||
}
|
||||
if (args.length == 3 && args[0].equalsIgnoreCase("reset")
|
||||
&& sender.hasPermission("christmasplus.adventcalendar.reset")) {
|
||||
args = args[2].equalsIgnoreCase("all")
|
||||
? new String[]{"admin", "reset", args[1]}
|
||||
: new String[]{"admin", "resetday", args[1], args[2]};
|
||||
elevated = true;
|
||||
}
|
||||
|
||||
if (args.length > 0 && args[0].equalsIgnoreCase("admin")) {
|
||||
if (!elevated && !sender.hasPermission("adventskalender.admin")) {
|
||||
sender.sendMessage(LanguageManager.getMessage("messages.no_permission"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// /ak admin reload
|
||||
if (args.length == 2 && args[1].equalsIgnoreCase("reload")) {
|
||||
Adventskalender.getInstance().reloadConfig();
|
||||
plugin.reloadConfig();
|
||||
plugin.reloadSettings();
|
||||
LanguageManager.setup();
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.reload_success"));
|
||||
DataHandler.setup();
|
||||
AdventInventory.initCache();
|
||||
de.mviper.adventskalender.christmas.ChristmasManager.reload();
|
||||
sender.sendMessage(LanguageManager.getMessage("messages.admin.reload_success"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// /ak admin open <player> <day>
|
||||
if (args.length == 4 && args[1].equalsIgnoreCase("open")) {
|
||||
Player target = Bukkit.getPlayer(args[2]);
|
||||
if (target == null) {
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.player_not_found").replace("%player%", args[2]));
|
||||
// /ak admin test [Event] <Tag/reset>
|
||||
if ((args.length == 3 || args.length == 4) && args[1].equalsIgnoreCase("test")) {
|
||||
CalendarEvent ev = null;
|
||||
if (args.length == 4) {
|
||||
ev = EventManager.getAny(args[2]);
|
||||
if (ev == null) {
|
||||
sendUnknownEvent(sender, args[2]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
String value = args[args.length - 1];
|
||||
List<CalendarEvent> targets = ev != null ? List.of(ev) : EventManager.allConfigured();
|
||||
|
||||
if (value.equalsIgnoreCase("reset")) {
|
||||
for (CalendarEvent t : targets) t.setTestDay(0);
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.test_mode_off").replace("%event%", label(ev)));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
int day = Integer.parseInt(args[3]);
|
||||
if (day < 1 || day > 24) {
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.invalid_day"));
|
||||
int day = parseDay(value, ev != null ? ev.getDays() : MAX_DAYS);
|
||||
if (day == -1) {
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.invalid_day"));
|
||||
return true;
|
||||
}
|
||||
for (CalendarEvent t : targets) t.setTestDay(day);
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.test_mode_on")
|
||||
.replace("%day%", String.valueOf(day)).replace("%event%", label(ev)));
|
||||
return true;
|
||||
}
|
||||
|
||||
// /ak admin resetday [Event] <global/Spieler> <Tag>
|
||||
if ((args.length == 4 || args.length == 5) && args[1].equalsIgnoreCase("resetday")) {
|
||||
CalendarEvent ev = null;
|
||||
int shift = 0;
|
||||
if (args.length == 5) {
|
||||
ev = EventManager.getAny(args[2]);
|
||||
if (ev == null) {
|
||||
sendUnknownEvent(sender, args[2]);
|
||||
return true;
|
||||
}
|
||||
|
||||
ItemStack reward = AdventInventory.getRewardItem(day);
|
||||
if (reward != null && reward.getType() != Material.AIR) {
|
||||
if (target.getInventory().firstEmpty() == -1) {
|
||||
target.getWorld().dropItemNaturally(target.getLocation(), reward);
|
||||
} else {
|
||||
target.getInventory().addItem(reward);
|
||||
}
|
||||
}
|
||||
CalendarData.setClaimed(target, day);
|
||||
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.open_success").replace("%player%", target.getName()).replace("%day%", String.valueOf(day)));
|
||||
target.sendMessage(LanguageManager.getString("messages.reward_received").replace("%day%", String.valueOf(day)));
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
shift = 1;
|
||||
}
|
||||
String target = args[2 + shift];
|
||||
int day = parseDay(args[3 + shift], ev != null ? ev.getDays() : MAX_DAYS);
|
||||
if (day == -1) {
|
||||
sender.sendMessage(LanguageManager.getString("messages.admin.invalid_day"));
|
||||
return true;
|
||||
}
|
||||
|
||||
final CalendarEvent event = ev;
|
||||
runAsync(() -> {
|
||||
if (target.equalsIgnoreCase("global")) {
|
||||
DataHandler.resetGlobalDay(event, day);
|
||||
reply(sender, LanguageManager.getString("messages.admin.reset_day_global")
|
||||
.replace("%day%", String.valueOf(day)).replace("%event%", label(event)));
|
||||
} else {
|
||||
boolean found = DataHandler.resetPlayerDay(target, event, day);
|
||||
reply(sender, found
|
||||
? LanguageManager.getString("messages.admin.reset_day_player")
|
||||
.replace("%day%", String.valueOf(day)).replace("%player%", target).replace("%event%", label(event))
|
||||
: LanguageManager.getString("messages.admin.player_not_found").replace("%player%", target));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// /ak admin reset [Event] <global/Spieler>
|
||||
if (args.length >= 3 && args[1].equalsIgnoreCase("reset")) {
|
||||
CalendarEvent ev = null;
|
||||
String targetName = args[2];
|
||||
if (args.length >= 4) {
|
||||
ev = EventManager.getAny(args[2]);
|
||||
if (ev == null) {
|
||||
sendUnknownEvent(sender, args[2]);
|
||||
return true;
|
||||
}
|
||||
targetName = args[3];
|
||||
}
|
||||
final CalendarEvent event = ev;
|
||||
final String name = targetName;
|
||||
|
||||
if (name.equalsIgnoreCase("global")) {
|
||||
runAsync(() -> {
|
||||
DataHandler.resetAll(event);
|
||||
Bukkit.getScheduler().runTask(plugin, AdventInventory::closeAll);
|
||||
reply(sender, LanguageManager.getString("messages.admin.reset_all").replace("%event%", label(event)));
|
||||
});
|
||||
} else {
|
||||
Player target = Bukkit.getPlayerExact(name);
|
||||
if (target != null) {
|
||||
runAsync(() -> {
|
||||
DataHandler.resetPlayer(target, event);
|
||||
Bukkit.getScheduler().runTask(plugin, () -> AdventInventory.closeFor(target));
|
||||
reply(sender, LanguageManager.getString("messages.admin.reset_player")
|
||||
.replace("%player%", target.getName()).replace("%event%", label(event)));
|
||||
});
|
||||
} else {
|
||||
runAsync(() -> {
|
||||
boolean found = DataHandler.resetOfflinePlayer(name, event);
|
||||
reply(sender, found
|
||||
? LanguageManager.getString("messages.admin.reset_offline")
|
||||
.replace("%player%", name).replace("%event%", label(event))
|
||||
: LanguageManager.getString("messages.admin.player_not_found").replace("%player%", name));
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Falsche Nutzung der Admin-Befehle
|
||||
sender.sendMessage("§cUsage: /ak admin reload");
|
||||
sender.sendMessage("§cUsage: /ak admin open <player> <day>");
|
||||
// /ak admin birthday <set|remove|reset> <Spieler> [TT.MM]
|
||||
if (args.length >= 4 && args[1].equalsIgnoreCase("birthday")) {
|
||||
String action = args[2].toLowerCase();
|
||||
String targetName = args[3];
|
||||
MonthDay date = null;
|
||||
if (action.equals("set")) {
|
||||
date = args.length == 5 ? BirthdayManager.parse(args[4]) : null;
|
||||
if (date == null) {
|
||||
sender.sendMessage(LanguageManager.getMessage("birthday.invalid_date", "birthday.prefix"));
|
||||
return true;
|
||||
}
|
||||
} else if (!action.equals("remove") && !action.equals("reset")) {
|
||||
for (String line : LanguageManager.getStringList("messages.admin.help")) sender.sendMessage(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
final MonthDay md = date;
|
||||
runAsync(() -> {
|
||||
@SuppressWarnings("deprecation")
|
||||
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
|
||||
if (!target.isOnline() && !target.hasPlayedBefore()) {
|
||||
reply(sender, LanguageManager.getString("messages.admin.player_not_found").replace("%player%", targetName));
|
||||
return;
|
||||
}
|
||||
String shownName = target.getName() != null ? target.getName() : targetName;
|
||||
try {
|
||||
switch (action) {
|
||||
case "set" -> {
|
||||
DataHandler.setBirthday(target.getUniqueId(), shownName, md.getMonthValue(), md.getDayOfMonth(), false);
|
||||
reply(sender, LanguageManager.getString("birthday.admin_set")
|
||||
.replace("%player%", shownName).replace("%date%", BirthdayManager.format(md)));
|
||||
}
|
||||
case "remove" -> {
|
||||
boolean removed = DataHandler.removeBirthday(target.getUniqueId(), shownName);
|
||||
reply(sender, LanguageManager.getString(removed ? "birthday.admin_removed" : "birthday.admin_none")
|
||||
.replace("%player%", shownName));
|
||||
}
|
||||
default -> {
|
||||
boolean reset = DataHandler.resetBirthdayClaim(target.getUniqueId(), shownName);
|
||||
reply(sender, LanguageManager.getString(reset ? "birthday.admin_reset" : "birthday.admin_none")
|
||||
.replace("%player%", shownName));
|
||||
}
|
||||
}
|
||||
} catch (DataHandler.DataException e) {
|
||||
reply(sender, LanguageManager.getMessage("messages.db_error"));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String line : LanguageManager.getStringList("messages.admin.help")) {
|
||||
sender.sendMessage(line);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Hauptbefehl (/ak) ---
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage(LanguageManager.getString("messages.only_player"));
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(LanguageManager.getMessage("messages.only_player"));
|
||||
return true;
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
if (!player.hasPermission("adventskalender.use")) {
|
||||
player.sendMessage(LanguageManager.getString("messages.no_permission"));
|
||||
player.sendMessage(LanguageManager.getMessage("messages.no_permission"));
|
||||
return true;
|
||||
}
|
||||
|
||||
AdventInventory.openCalendar(player);
|
||||
// /ak birthday [set <TT.MM> | claim | remove]
|
||||
if (args.length >= 1 && args[0].equalsIgnoreCase("birthday")) {
|
||||
if (!player.hasPermission("adventskalender.birthday")) {
|
||||
player.sendMessage(LanguageManager.getMessage("messages.no_permission"));
|
||||
return true;
|
||||
}
|
||||
if (args.length == 1) {
|
||||
BirthdayManager.handleInfo(player);
|
||||
BirthdayManager.sendHelp(player);
|
||||
} else if (args[1].equalsIgnoreCase("set") && args.length == 3) {
|
||||
BirthdayManager.handleSet(player, args[2]);
|
||||
} else if (args[1].equalsIgnoreCase("claim")) {
|
||||
BirthdayManager.handleClaim(player);
|
||||
} else if (args[1].equalsIgnoreCase("remove")) {
|
||||
BirthdayManager.handleRemove(player);
|
||||
} else {
|
||||
BirthdayManager.sendHelp(player);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// /ak [Event]
|
||||
CalendarEvent event;
|
||||
if (args.length >= 1) {
|
||||
event = EventManager.get(args[0]);
|
||||
if (event == null) {
|
||||
sendUnknownEvent(player, args[0]);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
event = EventManager.defaultEvent();
|
||||
if (event == null) {
|
||||
player.sendMessage(LanguageManager.getMessage("messages.no_events"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
AdventInventory.open(player, event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendUnknownEvent(CommandSender sender, String given) {
|
||||
String ids = EventManager.allConfigured().stream().map(CalendarEvent::getId).collect(Collectors.joining(", "));
|
||||
sender.sendMessage(LanguageManager.getMessage("messages.unknown_event")
|
||||
.replace("%event%", given).replace("%events%", ids));
|
||||
}
|
||||
|
||||
/** Tag zwischen 1 und max, sonst -1. */
|
||||
private static int parseDay(String value, int max) {
|
||||
try {
|
||||
int day = Integer.parseInt(value);
|
||||
return (day >= 1 && day <= max) ? day : -1;
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static String label(CalendarEvent ev) {
|
||||
return ev != null ? ev.getId() : LanguageManager.getString("messages.admin.all_events");
|
||||
}
|
||||
|
||||
private void runAsync(Runnable task) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(Adventskalender.getInstance(), task);
|
||||
}
|
||||
|
||||
/** Sendet die Antwort im Main-Thread. */
|
||||
private void reply(CommandSender sender, String message) {
|
||||
Bukkit.getScheduler().runTask(Adventskalender.getInstance(), () -> sender.sendMessage(message));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Tab-Completion
|
||||
|
||||
private static List<String> filter(Collection<String> options, String prefix) {
|
||||
return options.stream()
|
||||
.filter(opt -> opt.toLowerCase().startsWith(prefix.toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static List<String> eventIds() {
|
||||
return EventManager.allConfigured().stream().map(CalendarEvent::getId).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static List<String> targets() {
|
||||
List<String> options = new ArrayList<>();
|
||||
options.add("global");
|
||||
Bukkit.getOnlinePlayers().forEach(p -> options.add(p.getName()));
|
||||
return options;
|
||||
}
|
||||
|
||||
private static List<String> dayOptions(int max) {
|
||||
List<String> days = new ArrayList<>();
|
||||
for (int i = 1; i <= max; i++) days.add(String.valueOf(i));
|
||||
return days;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender s, Command c, String a, String[] args) {
|
||||
boolean admin = s.hasPermission("adventskalender.admin");
|
||||
|
||||
if (args.length == 1) {
|
||||
List<String> options = new ArrayList<>(EventManager.all().stream().map(CalendarEvent::getId).toList());
|
||||
if (BirthdayManager.isEnabled() && s.hasPermission("adventskalender.birthday")) options.add("birthday");
|
||||
if (admin) options.add("admin");
|
||||
return filter(options, args[0]);
|
||||
}
|
||||
|
||||
if (args[0].equalsIgnoreCase("birthday")) {
|
||||
if (args.length == 2) return filter(Arrays.asList("set", "claim", "remove"), args[1]);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (!admin || !args[0].equalsIgnoreCase("admin")) return Collections.emptyList();
|
||||
|
||||
if (args.length == 2) return filter(Arrays.asList("reload", "test", "reset", "resetday", "birthday"), args[1]);
|
||||
|
||||
if (args[1].equalsIgnoreCase("birthday")) {
|
||||
if (args.length == 3) return filter(Arrays.asList("set", "remove", "reset"), args[2]);
|
||||
if (args.length == 4) return filter(targets().stream().filter(t -> !t.equals("global")).collect(Collectors.toList()), args[3]);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
String sub = args[1].toLowerCase();
|
||||
String last = args[args.length - 1];
|
||||
CalendarEvent ev = EventManager.getAny(args[2]);
|
||||
|
||||
switch (sub) {
|
||||
case "test": {
|
||||
if (args.length == 3) {
|
||||
List<String> options = new ArrayList<>(eventIds());
|
||||
options.add(0, "reset");
|
||||
options.addAll(dayOptions(MAX_DAYS));
|
||||
return filter(options, last);
|
||||
}
|
||||
if (args.length == 4 && ev != null) {
|
||||
List<String> options = new ArrayList<>(dayOptions(ev.getDays()));
|
||||
options.add(0, "reset");
|
||||
return filter(options, last);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "reset": {
|
||||
if (args.length == 3) {
|
||||
List<String> options = new ArrayList<>(eventIds());
|
||||
options.addAll(targets());
|
||||
return filter(options, last);
|
||||
}
|
||||
if (args.length == 4 && ev != null) return filter(targets(), last);
|
||||
break;
|
||||
}
|
||||
case "resetday": {
|
||||
if (args.length == 3) {
|
||||
List<String> options = new ArrayList<>(eventIds());
|
||||
options.addAll(targets());
|
||||
return filter(options, last);
|
||||
}
|
||||
if (args.length == 4) {
|
||||
return ev != null ? filter(targets(), last) : filter(dayOptions(MAX_DAYS), last);
|
||||
}
|
||||
if (args.length == 5 && ev != null) return filter(dayOptions(ev.getDays()), last);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +1,227 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
import org.bukkit.profile.PlayerProfile;
|
||||
import org.bukkit.profile.PlayerTextures;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class AdventInventory {
|
||||
|
||||
public static void openCalendar(org.bukkit.entity.Player player) {
|
||||
// KORREKTUR: Wir lesen den Titel direkt aus der Sprachdatei.
|
||||
// Der Pfad "gui.title" entspricht der Struktur in deiner messages_de.yml
|
||||
String title = LanguageManager.getString("gui.title");
|
||||
Inventory inv = Bukkit.createInventory(null, 54, title);
|
||||
// Textur für das geöffnete Geschenk (Wreath)
|
||||
private static final String OPEN_WREATH_URL = "http://textures.minecraft.net/texture/5e39248b341c87ce3e4294ac214c6f74468889cb1273ae7412906fd28db097e8";
|
||||
|
||||
for (int i = 0; i < 24; i++) {
|
||||
int day = i + 1;
|
||||
ItemStack item = getCalendarItem(player, day);
|
||||
inv.setItem(i, item);
|
||||
// Texturen für die geschlossenen Tage (1-24)
|
||||
private static final Map<Integer, String> CLOSED_TEXTURES = new HashMap<>();
|
||||
private static final Map<String, ItemStack> HEAD_CACHE = new HashMap<>();
|
||||
|
||||
static {
|
||||
// Hier sind die URLs für die "Christmas Calendar #01-24 (red)" Reihe
|
||||
CLOSED_TEXTURES.put(1, "http://textures.minecraft.net/texture/105b356be68ac56cfe0611b95027adf1ee67050e4cd66a0a99678fc2e25b6b0f");
|
||||
CLOSED_TEXTURES.put(2, "http://textures.minecraft.net/texture/40ec45e8ffb9cb714d1412bb9ee54fd1e7a3140c02e35ae0d3f308f1666ba126");
|
||||
CLOSED_TEXTURES.put(3, "http://textures.minecraft.net/texture/2466629a31aae22dba15a7b710a4517308656c19ba39a89ac2de2915a46e2823");
|
||||
CLOSED_TEXTURES.put(4, "http://textures.minecraft.net/texture/97a1956ed6f32e18b139f2d709355ac6945d0ab8fecd04d4c159d08006bfbe58");
|
||||
CLOSED_TEXTURES.put(5, "http://textures.minecraft.net/texture/855fad012c8844fc313510436b4919e002d93d6a0761f7bdaf78069683eee5e6");
|
||||
CLOSED_TEXTURES.put(6, "http://textures.minecraft.net/texture/c0400111ae026101077b59d8482c84a832c74f4f2105fc13b7c94f5b8000ea3");
|
||||
CLOSED_TEXTURES.put(7, "http://textures.minecraft.net/texture/a34505ed2567ea2efbab49977f7da8f1178db9ae53a0e99737286dc56e63636a");
|
||||
CLOSED_TEXTURES.put(8, "http://textures.minecraft.net/texture/a17ddf12485fb54cab9c9ffb55feea62a2d90b825772c6ea38561ce750cb6f83");
|
||||
CLOSED_TEXTURES.put(9, "http://textures.minecraft.net/texture/f75022feb11b736e50bc7d19d26d4ee00cecd6734a2138d2579f799b119b6734");
|
||||
CLOSED_TEXTURES.put(10, "http://textures.minecraft.net/texture/6b84208e4be2233fcd96e7f02d9613d5437f1d006236277c57d40652c5be2f23");
|
||||
|
||||
CLOSED_TEXTURES.put(11, "http://textures.minecraft.net/texture/19aa8a65ba29b4b7cbb9c66cf59fb537ba2c3013a958620b976b9f06248e8dda");
|
||||
CLOSED_TEXTURES.put(12, "http://textures.minecraft.net/texture/7dc1330b302e800c31c8dfefbd1b996016c34649f3a396749d3e048637364aad");
|
||||
CLOSED_TEXTURES.put(13, "http://textures.minecraft.net/texture/7e98a3884e0eac967c73a2bcbff7df62ab2fb327b4ceb9e636c74254866b5c7c");
|
||||
CLOSED_TEXTURES.put(14, "http://textures.minecraft.net/texture/3866643decb2d148999f9768dae89fbdb196b30d55f8846b1fbbe736bb7e5042");
|
||||
CLOSED_TEXTURES.put(15, "http://textures.minecraft.net/texture/744a95611950cc7f154c9f56ad472c1941376ec171efb8ba476e6cec4f866526");
|
||||
CLOSED_TEXTURES.put(16, "http://textures.minecraft.net/texture/1546959556eff4b8827ca50d8df686e8f20d5c843da689dddc48bff0a217efbe");
|
||||
CLOSED_TEXTURES.put(17, "http://textures.minecraft.net/texture/1f4fb00e824c97d9cfba4c44def27a79513978979515048f2e69eb5b7123c159");
|
||||
CLOSED_TEXTURES.put(18, "http://textures.minecraft.net/texture/9216b42018004a86081b5c1ee87dc8f12770f1859266e2cd359b75b44b5e7680");
|
||||
CLOSED_TEXTURES.put(19, "http://textures.minecraft.net/texture/93c7097291042c394267892c56feee9ba3936826ea5b2232f46cb5474f9cd937");
|
||||
CLOSED_TEXTURES.put(20, "http://textures.minecraft.net/texture/4f85a13977d01865a9d95d3279528285872551c36fd5231c7e3ab9897a7560fd");
|
||||
|
||||
CLOSED_TEXTURES.put(21, "http://textures.minecraft.net/texture/cdb512403f3610966d10bd3ca9fc61fdc5021268382f69719ac5679f3efd4c14");
|
||||
CLOSED_TEXTURES.put(22, "http://textures.minecraft.net/texture/cadaef824771250374fc3e82efe020ab765e665340cc2393f3b4ae8d6d18529c");
|
||||
CLOSED_TEXTURES.put(23, "http://textures.minecraft.net/texture/266f896c37a9d7fb857ff8c9f6f5e3540d17e1b893419f523eb199b71e71dde3");
|
||||
CLOSED_TEXTURES.put(24, "http://textures.minecraft.net/texture/f081f9274e4ef802453ac95381c00426e462440d3bb085fb3afefa6ded6a8d01");
|
||||
// Hinweis: In der Praxis füllst du hier alle 1-24 mit den URLs von Minecraft-Heads.
|
||||
}
|
||||
|
||||
// Spieler, die gerade einen Kalender geöffnet haben
|
||||
private static final Set<UUID> VIEWERS = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/** Marker, an dem das Kalender-Inventar (und sein Event) erkannt wird statt am Titel. */
|
||||
public static class AdventHolder implements InventoryHolder {
|
||||
private final CalendarEvent event;
|
||||
private Inventory inventory;
|
||||
|
||||
public AdventHolder(CalendarEvent event) { this.event = event; }
|
||||
|
||||
public CalendarEvent getEvent() { return event; }
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() { return inventory; }
|
||||
}
|
||||
|
||||
public static void initCache() {
|
||||
HEAD_CACHE.clear();
|
||||
|
||||
// Advent: Wreath-Kopf und alle nummerierten geschlossenen Köpfe
|
||||
HEAD_CACHE.put("OPENED", createBaseSkull(OPEN_WREATH_URL));
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
if (CLOSED_TEXTURES.containsKey(i)) {
|
||||
HEAD_CACHE.put("CLOSED_" + i, createBaseSkull(CLOSED_TEXTURES.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
ItemStack filler = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
|
||||
ItemMeta fillerMeta = filler.getItemMeta();
|
||||
fillerMeta.setDisplayName(" ");
|
||||
filler.setItemMeta(fillerMeta);
|
||||
for (int i = 24; i < 54; i++) {
|
||||
inv.setItem(i, filler);
|
||||
// Andere Events: optionale eigene Kopf-Texturen aus der Config
|
||||
for (CalendarEvent ev : EventManager.allConfigured()) {
|
||||
if (ev.isAdvent()) continue;
|
||||
if (!ev.getClosedTexture().isBlank()) HEAD_CACHE.put(ev.getId() + "_CLOSED", createBaseSkull(ev.getClosedTexture()));
|
||||
if (!ev.getOpenedTexture().isBlank()) HEAD_CACHE.put(ev.getId() + "_OPENED", createBaseSkull(ev.getOpenedTexture()));
|
||||
}
|
||||
}
|
||||
|
||||
public static void markClosed(Player player) { VIEWERS.remove(player.getUniqueId()); }
|
||||
|
||||
public static void closeFor(Player player) {
|
||||
if (VIEWERS.contains(player.getUniqueId())) player.closeInventory();
|
||||
}
|
||||
|
||||
public static void closeAll() {
|
||||
for (UUID id : new ArrayList<>(VIEWERS)) {
|
||||
Player p = Bukkit.getPlayer(id);
|
||||
if (p != null) p.closeInventory();
|
||||
}
|
||||
VIEWERS.clear();
|
||||
}
|
||||
|
||||
/** Lädt die Daten asynchron und öffnet das Inventar danach im Main-Thread. */
|
||||
public static void open(Player player, CalendarEvent event) {
|
||||
Adventskalender plugin = Adventskalender.getInstance();
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
Set<Integer> claimedDays = DataHandler.getViewClaimedDays(player, event);
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
if (claimedDays == null) {
|
||||
player.sendMessage(LanguageManager.getMessage("messages.db_error", event));
|
||||
return;
|
||||
}
|
||||
build(player, event, claimedDays);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static void build(Player player, CalendarEvent event, Set<Integer> claimedDays) {
|
||||
String title = " " + LanguageManager.getString(event.titleKey());
|
||||
AdventHolder holder = new AdventHolder(event);
|
||||
Inventory inv = Bukkit.createInventory(holder, 54, title);
|
||||
holder.inventory = inv;
|
||||
|
||||
int currentDay = event.currentDay();
|
||||
|
||||
for (int i = 0; i < 54; i++) {
|
||||
int row = i / 9;
|
||||
boolean border = row == 0 || row == 5 || (event.hasSideBorder() && (i % 9 == 0 || i % 9 == 8));
|
||||
if (border) {
|
||||
Material m = (i % 2 == 0) ? Material.RED_STAINED_GLASS_PANE : Material.GREEN_STAINED_GLASS_PANE;
|
||||
inv.setItem(i, createFiller(m));
|
||||
} else {
|
||||
inv.setItem(i, createFiller(Material.WHITE_STAINED_GLASS_PANE));
|
||||
}
|
||||
}
|
||||
|
||||
for (int day = 1; day <= event.getDays(); day++) {
|
||||
inv.setItem(event.slotForDay(day), createGiftItem(event, day, claimedDays.contains(day), currentDay >= day));
|
||||
}
|
||||
|
||||
player.openInventory(inv);
|
||||
// Nach openInventory, da ein vorher offenes Kalender-Inventar dabei geschlossen wird
|
||||
VIEWERS.add(player.getUniqueId());
|
||||
}
|
||||
|
||||
public static ItemStack getCalendarItem(org.bukkit.entity.Player player, int day) {
|
||||
LocalDate today = LocalDate.now();
|
||||
String prefix = LanguageManager.getString("prefix");
|
||||
|
||||
if (today.getMonth() != Month.DECEMBER || today.getDayOfMonth() < day) {
|
||||
ItemStack item = new ItemStack(Material.BARRIER);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
meta.setDisplayName(prefix + "§cTag " + day);
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add(prefix + "§7Noch nicht verfügbar.");
|
||||
lore.add(prefix + "§7Komme am " + day + ". Dezember wieder.");
|
||||
meta.setLore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
private static ItemStack baseItem(CalendarEvent event, int day, boolean claimed) {
|
||||
if (event.isAdvent()) {
|
||||
return claimed
|
||||
? HEAD_CACHE.getOrDefault("OPENED", new ItemStack(Material.PLAYER_HEAD)).clone()
|
||||
: HEAD_CACHE.getOrDefault("CLOSED_" + day, new ItemStack(Material.PLAYER_HEAD)).clone();
|
||||
}
|
||||
ItemStack head = HEAD_CACHE.get(event.getId() + (claimed ? "_OPENED" : "_CLOSED"));
|
||||
ItemStack item = head != null ? head.clone() : new ItemStack(claimed ? event.getOpenedMaterial() : event.getClosedMaterial());
|
||||
item.setAmount(Math.max(1, Math.min(64, day))); // Die Stackgröße zeigt den Tag an
|
||||
return item;
|
||||
}
|
||||
|
||||
if (CalendarData.hasClaimed(player, day)) {
|
||||
ItemStack item = new ItemStack(Material.GREEN_STAINED_GLASS_PANE);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
meta.setDisplayName(prefix + "§aTag " + day + " - Geöffnet");
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add(prefix + LanguageManager.getString("messages.day_already_claimed"));
|
||||
meta.setLore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
private static ItemStack createGiftItem(CalendarEvent event, int day, boolean claimed, boolean isAvailable) {
|
||||
ItemStack item = baseItem(event, day, claimed);
|
||||
|
||||
ItemStack item = new ItemStack(Material.CHEST);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
meta.setDisplayName(prefix + "§eTag " + day + " - Noch zu holen!");
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add(prefix + "§7Klicke, um dein Geschenk zu erhalten.");
|
||||
lore.add(" ");
|
||||
String dayStr = String.valueOf(day);
|
||||
|
||||
String rewardName = LanguageManager.has(event.nameKey(day))
|
||||
? LanguageManager.getString(event.nameKey(day))
|
||||
: LanguageManager.getString("gui.default_name").replace("%day%", dayStr);
|
||||
|
||||
if (claimed) {
|
||||
meta.setDisplayName(rewardName);
|
||||
// Glitzer-Effekt über eine beliebige Verzauberung, die per Key gesucht wird (versionsunabhängig)
|
||||
Enchantment glow = Enchantment.getByKey(NamespacedKey.minecraft("luck_of_the_sea"));
|
||||
if (glow != null) {
|
||||
meta.addEnchant(glow, 1, true);
|
||||
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
}
|
||||
lore.add(LanguageManager.getString("gui.claimed_status"));
|
||||
lore.add(LanguageManager.getString("gui.claimed_hint"));
|
||||
} else if (!isAvailable) {
|
||||
meta.setDisplayName(LanguageManager.getString("gui.locked_name").replace("%day%", dayStr));
|
||||
lore.add(LanguageManager.getString("gui.locked_status"));
|
||||
lore.add(LanguageManager.getString("gui.locked_hint")
|
||||
.replace("%day%", dayStr).replace("%date%", event.formatDate(day)));
|
||||
} else {
|
||||
meta.setDisplayName(rewardName);
|
||||
lore.add(LanguageManager.getString("gui.click_open"));
|
||||
lore.addAll(LanguageManager.getStringList(event.loreKey(day)));
|
||||
}
|
||||
|
||||
meta.setLore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
public static ItemStack getRewardItem(int day) {
|
||||
String path = "rewards." + day;
|
||||
if (!Adventskalender.getInstance().getConfig().contains(path)) {
|
||||
return new ItemStack(Material.AIR);
|
||||
private static ItemStack createBaseSkull(String url) {
|
||||
ItemStack item = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) item.getItemMeta();
|
||||
try {
|
||||
UUID uuid = UUID.nameUUIDFromBytes(url.getBytes(StandardCharsets.UTF_8));
|
||||
PlayerProfile profile = Bukkit.createPlayerProfile(uuid, "AdventHead");
|
||||
PlayerTextures textures = profile.getTextures();
|
||||
textures.setSkin(URI.create(url).toURL());
|
||||
profile.setTextures(textures);
|
||||
meta.setOwnerProfile(profile);
|
||||
item.setItemMeta(meta);
|
||||
} catch (Exception e) {
|
||||
Adventskalender.getInstance().getLogger().warning("Kopf-Textur konnte nicht geladen werden (" + url + "): " + e.getMessage());
|
||||
}
|
||||
|
||||
String materialName = Adventskalender.getInstance().getConfig().getString(path + ".material");
|
||||
int amount = Adventskalender.getInstance().getConfig().getInt(path + ".amount");
|
||||
String key = Adventskalender.getInstance().getConfig().getString(path + ".key");
|
||||
|
||||
Material material = Material.getMaterial(materialName);
|
||||
if (material == null) {
|
||||
Adventskalender.getInstance().getLogger().warning("Material '" + materialName + "' für Tag " + day + " nicht gefunden!");
|
||||
return new ItemStack(Material.BARRIER);
|
||||
}
|
||||
|
||||
ItemStack item = new ItemStack(material, amount);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
|
||||
String namePath = key + ".name";
|
||||
String lorePath = key + ".lore";
|
||||
|
||||
meta.setDisplayName(LanguageManager.getString(namePath));
|
||||
meta.setLore(LanguageManager.getStringList(lorePath));
|
||||
|
||||
List<Map<?, ?>> enchantmentList = Adventskalender.getInstance().getConfig().getMapList(path + ".enchantments");
|
||||
if (enchantmentList != null) {
|
||||
for (Map<?, ?> enchantmentMap : enchantmentList) {
|
||||
String enchantName = (String) enchantmentMap.get("type");
|
||||
int level = (int) enchantmentMap.get("level");
|
||||
Enchantment enchantment = Enchantment.getByName(enchantName);
|
||||
if (enchantment != null) {
|
||||
meta.addEnchant(enchantment, level, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
private static ItemStack createFiller(Material material) {
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) { meta.setDisplayName(" "); item.setItemMeta(meta); }
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,179 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.*;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.chat.hover.content.Text;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class AdventListener implements Listener {
|
||||
|
||||
// Spieler, deren Klick gerade verarbeitet wird (verhindert Spam-Klicks)
|
||||
private final Set<UUID> busy = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
// KORREKTUR: Wir verwenden den gleichen Schlüssel wie in der AdventInventory.java,
|
||||
// um den Titel zu laden. So sind beide Titel identisch.
|
||||
String translatedTitle = LanguageManager.getString("gui.title");
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
|
||||
if (event.getView().getTitle().equals(translatedTitle)) {
|
||||
event.setCancelled(true); // Diese Zeile wird jetzt wieder ausgeführt!
|
||||
// Hinweis auf das Geburtstagsgeschenk (falls abholbar)
|
||||
Bukkit.getScheduler().runTaskLater(Adventskalender.getInstance(), () -> {
|
||||
if (player.isOnline()) BirthdayManager.checkOnJoin(player);
|
||||
}, 80L);
|
||||
|
||||
if (!(event.getWhoClicked() instanceof Player)) return;
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
// Nachricht an Admins senden, wenn ein Update da ist
|
||||
if (player.hasPermission("adventskalender.admin")) {
|
||||
// Delay von 2 Sekunden (40 Ticks), damit die Nachricht nicht im Join-Spam untergeht
|
||||
Bukkit.getScheduler().runTaskLater(Adventskalender.getInstance(), () -> {
|
||||
if (!player.isOnline()) return;
|
||||
if (Adventskalender.getInstance().isUpdateAvailable()) {
|
||||
String latest = Adventskalender.getInstance().getLatestVersion();
|
||||
String current = Adventskalender.getInstance().getDescription().getVersion();
|
||||
|
||||
int slot = event.getSlot();
|
||||
if (slot < 0 || slot > 23) return;
|
||||
player.sendMessage("§7 ");
|
||||
player.sendMessage("§8§m+-------------------------------------------+");
|
||||
player.sendMessage("§6§l ADVENTSKALENDER UPDATE");
|
||||
player.sendMessage("§7 ");
|
||||
player.sendMessage("§7 Eine neue Version ist verfügbar: §a§l" + latest);
|
||||
player.sendMessage("§7 Installierte Version: §c" + current);
|
||||
player.sendMessage("§7 ");
|
||||
|
||||
int day = slot + 1;
|
||||
LocalDate today = LocalDate.now();
|
||||
// Erstellung der klickbaren Nachricht
|
||||
TextComponent message = new TextComponent("§e §nKLICKE HIER ZUM DOWNLOAD");
|
||||
message.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://www.spigotmc.org/resources/130974/"));
|
||||
message.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("§7Öffnet die Spigot-Seite")));
|
||||
|
||||
if (today.getMonth() == Month.DECEMBER && today.getDayOfMonth() >= day && !CalendarData.hasClaimed(player, day)) {
|
||||
ItemStack reward = AdventInventory.getRewardItem(day);
|
||||
if (reward != null && reward.getType() != Material.AIR) {
|
||||
if (player.getInventory().firstEmpty() == -1) {
|
||||
player.getWorld().dropItemNaturally(player.getLocation(), reward);
|
||||
player.sendMessage(LanguageManager.getString("messages.inventory_full"));
|
||||
} else {
|
||||
player.getInventory().addItem(reward);
|
||||
}
|
||||
player.sendMessage(LanguageManager.getString("messages.reward_received").replace("%day%", String.valueOf(day)));
|
||||
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
|
||||
player.spawnParticle(Particle.VILLAGER_HAPPY, player.getLocation().add(0, 1, 0), 20, 0.5, 0.5, 0.5, 0);
|
||||
player.spigot().sendMessage(message);
|
||||
|
||||
player.sendMessage("§7 ");
|
||||
player.sendMessage("§8§m+-------------------------------------------+");
|
||||
player.sendMessage("§7 ");
|
||||
}
|
||||
|
||||
CalendarData.setClaimed(player, day);
|
||||
ItemStack newItem = AdventInventory.getCalendarItem(player, day);
|
||||
event.getInventory().setItem(slot, newItem);
|
||||
|
||||
} else {
|
||||
player.sendMessage(LanguageManager.getString("messages.day_not_available"));
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
|
||||
}
|
||||
}, 40L);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClose(InventoryCloseEvent event) {
|
||||
if (event.getInventory().getHolder() instanceof AdventInventory.AdventHolder
|
||||
&& event.getPlayer() instanceof Player player) {
|
||||
AdventInventory.markClosed(player);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryDrag(InventoryDragEvent event) {
|
||||
if (event.getInventory().getHolder() instanceof AdventInventory.AdventHolder) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (!(event.getInventory().getHolder() instanceof AdventInventory.AdventHolder)) return;
|
||||
|
||||
event.setCancelled(true);
|
||||
|
||||
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||
if (event.getClickedInventory() != event.getInventory()) return;
|
||||
|
||||
CalendarEvent calendar = ((AdventInventory.AdventHolder) event.getInventory().getHolder()).getEvent();
|
||||
int day = calendar.dayForSlot(event.getRawSlot());
|
||||
if (day == -1) return;
|
||||
|
||||
Adventskalender plugin = Adventskalender.getInstance();
|
||||
|
||||
boolean tester = de.mviper.adventskalender.christmas.CalendarPerks.isTester(player);
|
||||
if (calendar.currentDay() < day && !tester) {
|
||||
player.sendMessage(LanguageManager.getMessage("messages.day_not_available", calendar));
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_CHEST_LOCKED, 1f, 1f);
|
||||
return;
|
||||
}
|
||||
|
||||
// Christmas+: vergangene Tage nur mit Recht oeffnen (optional)
|
||||
var xm = de.mviper.adventskalender.christmas.ChristmasManager.get();
|
||||
if (xm != null && !tester && calendar.currentDay() > day
|
||||
&& xm.cfg().getBoolean("calendar-extras.restrict-past-days", false)
|
||||
&& !player.hasPermission("christmasplus.adventcalendar.claimpast")) {
|
||||
player.sendMessage(xm.msg("past-day-restricted"));
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_CHEST_LOCKED, 1f, 1f);
|
||||
return;
|
||||
}
|
||||
|
||||
// Belohnung vorab prüfen: Bei Konfigurationsfehlern wird der Tag nicht verbraucht
|
||||
ItemStack reward = buildReward(calendar, day);
|
||||
if (reward == null) {
|
||||
player.sendMessage(LanguageManager.getMessage("messages.reward_error", calendar).replace("%day%", String.valueOf(day)));
|
||||
return;
|
||||
}
|
||||
|
||||
UUID id = player.getUniqueId();
|
||||
if (!busy.add(id)) return;
|
||||
|
||||
// Datenbankzugriff async, Ergebnis wieder im Main-Thread verarbeiten
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
DataHandler.ClaimResult result = DataHandler.isReady()
|
||||
? DataHandler.setClaimed(player, calendar, day)
|
||||
: DataHandler.ClaimResult.ERROR;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
busy.remove(id);
|
||||
|
||||
if (result == DataHandler.ClaimResult.SUCCESS && !player.isOnline()) {
|
||||
// Spieler ist inzwischen weg: Tag wieder freigeben, damit die Belohnung nicht verloren geht
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
if (calendar.isGlobal()) {
|
||||
DataHandler.resetGlobalDay(calendar, day);
|
||||
} else {
|
||||
DataHandler.resetPlayerDay(player.getName(), calendar, day);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!player.isOnline()) return;
|
||||
|
||||
switch (result) {
|
||||
case SUCCESS -> {
|
||||
giveReward(player, calendar, day, reward);
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.5f);
|
||||
AdventInventory.open(player, calendar);
|
||||
}
|
||||
case ALREADY_CLAIMED -> {
|
||||
player.sendMessage(LanguageManager.getMessage(calendar.isGlobal() ? "messages.global_already_claimed" : "messages.day_already_claimed", calendar));
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f);
|
||||
}
|
||||
case ERROR -> player.sendMessage(LanguageManager.getMessage("messages.db_error", calendar));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Baut das Belohnungs-Item aus der Config. Gibt null zurück, wenn das Material ungültig ist. */
|
||||
private ItemStack buildReward(CalendarEvent calendar, int day) {
|
||||
return RewardUtil.build(calendar.rewardsPath() + "." + day, "Tag " + day + " (" + calendar.getId() + ")");
|
||||
}
|
||||
|
||||
private void giveReward(Player player, CalendarEvent calendar, int day, ItemStack reward) {
|
||||
if (RewardUtil.give(player, reward)) {
|
||||
player.sendMessage(LanguageManager.getMessage("messages.inventory_full", calendar));
|
||||
}
|
||||
de.mviper.adventskalender.christmas.ChristmasExtras.give(player, calendar.rewardsPath() + "." + day);
|
||||
var xm = de.mviper.adventskalender.christmas.ChristmasManager.get();
|
||||
if (xm != null && xm.cfg().getBoolean("calendar-extras.broadcast-claim", false)) {
|
||||
Bukkit.broadcastMessage(xm.msg("calendar-claimed-broadcast", "%player%", player.getName(), "%day%", String.valueOf(day)));
|
||||
}
|
||||
player.sendMessage(LanguageManager.getMessage("messages.reward_received", calendar).replace("%day%", String.valueOf(day)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,106 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.time.DateTimeException;
|
||||
import java.time.ZoneId;
|
||||
|
||||
public final class Adventskalender extends JavaPlugin {
|
||||
private static Adventskalender instance;
|
||||
private NamespacedKey dataKey;
|
||||
|
||||
// Update-Status Variablen
|
||||
private volatile boolean updateAvailable = false;
|
||||
private volatile String latestVersion = "";
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
saveDefaultConfig();
|
||||
this.dataKey = new NamespacedKey(this, "claimed_days_v2");
|
||||
|
||||
saveDefaultConfig();
|
||||
reloadSettings();
|
||||
LanguageManager.setup();
|
||||
CalendarData.setup();
|
||||
|
||||
// Initialisierung der Daten (MySQL wird hier bevorzugt behandelt)
|
||||
DataHandler.setup();
|
||||
|
||||
// Textur-Puffer füllen, um Steve-Köpfe zu vermeiden
|
||||
AdventInventory.initCache();
|
||||
|
||||
getCommand("adventskalender").setExecutor(new AdventCommand());
|
||||
getServer().getPluginManager().registerEvents(new AdventListener(), this);
|
||||
|
||||
// Prüfen, ob PlaceholderAPI vorhanden ist und unsere neue Erweiterung registrieren
|
||||
// Update Checker Integration (ID: 130974)
|
||||
new UpdateChecker(this, 130974).getVersion(version -> {
|
||||
String current = this.getDescription().getVersion();
|
||||
if (isNewer(version, current)) {
|
||||
this.updateAvailable = true;
|
||||
this.latestVersion = version;
|
||||
getLogger().warning("Eine neue Version (" + version + ") ist verfügbar!");
|
||||
getLogger().warning("Download: https://www.spigotmc.org/resources/130974/");
|
||||
} else {
|
||||
getLogger().info("Das Plugin ist auf dem neuesten Stand.");
|
||||
}
|
||||
});
|
||||
|
||||
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
|
||||
// HIER IST DIE ÄNDERUNG: Wir erstellen eine Instanz der neuen Klasse
|
||||
new AdventCalendarExpansion().register();
|
||||
getLogger().info("PlaceholderAPI expansion registered.");
|
||||
} else {
|
||||
getLogger().warning("PlaceholderAPI not found. Placeholders will not work.");
|
||||
}
|
||||
|
||||
getLogger().info("Adventskalender-Plugin wurde erfolgreich aktiviert!");
|
||||
de.mviper.adventskalender.christmas.ChristmasManager.enable(this);
|
||||
|
||||
getLogger().info("Adventskalender erfolgreich gestartet!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
getLogger().info("Adventskalender-Plugin wurde deaktiviert.");
|
||||
de.mviper.adventskalender.christmas.ChristmasManager.disable();
|
||||
MySQLManager.close();
|
||||
}
|
||||
|
||||
public static Adventskalender getInstance() {
|
||||
return instance;
|
||||
/** Liest die Werte, die aus der Config stammen, neu ein (auch nach /ak admin reload). */
|
||||
public void reloadSettings() {
|
||||
EventManager.load();
|
||||
}
|
||||
}
|
||||
|
||||
public ZoneId getZone() {
|
||||
String tz = getConfig().getString("general.timezone", "");
|
||||
if (tz != null && !tz.isBlank()) {
|
||||
try {
|
||||
return ZoneId.of(tz.trim());
|
||||
} catch (DateTimeException e) {
|
||||
getLogger().warning("Ungültige Zeitzone '" + tz + "' in der config.yml - nutze Server-Zeitzone.");
|
||||
}
|
||||
}
|
||||
return ZoneId.systemDefault();
|
||||
}
|
||||
|
||||
/** true, wenn die entfernte Version numerisch neuer ist als die lokale. */
|
||||
private static boolean isNewer(String remote, String local) {
|
||||
String[] r = remote.split("\\.");
|
||||
String[] l = local.split("\\.");
|
||||
for (int i = 0; i < Math.max(r.length, l.length); i++) {
|
||||
int a = part(r, i);
|
||||
int b = part(l, i);
|
||||
if (a != b) return a > b;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int part(String[] parts, int i) {
|
||||
if (i >= parts.length) return 0;
|
||||
String digits = parts[i].replaceAll("[^0-9]", "");
|
||||
if (digits.isEmpty() || digits.length() > 9) return 0;
|
||||
return Integer.parseInt(digits);
|
||||
}
|
||||
|
||||
public static Adventskalender getInstance() { return instance; }
|
||||
public NamespacedKey getDataKey() { return dataKey; }
|
||||
|
||||
// Getter für den Update-Status
|
||||
public boolean isUpdateAvailable() { return updateAvailable; }
|
||||
public String getLatestVersion() { return latestVersion; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.chat.hover.content.Text;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.time.DateTimeException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.MonthDay;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Spieler-Geburtstage: Der Spieler trägt sein Datum (ohne Jahr) ein und kann am Geburtstag
|
||||
* sowie bis zu birthday.grace_days Tage danach einmal pro Jahr ein Geschenk abholen.
|
||||
*/
|
||||
public final class BirthdayManager {
|
||||
|
||||
private BirthdayManager() {}
|
||||
|
||||
// ------------------------------------------------------------ Config / Datum
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return Adventskalender.getInstance().getConfig().getBoolean("birthday.enabled", true);
|
||||
}
|
||||
|
||||
public static int graceDays() {
|
||||
return Math.max(0, Adventskalender.getInstance().getConfig().getInt("birthday.grace_days", 2));
|
||||
}
|
||||
|
||||
private static boolean allowChange() {
|
||||
return Adventskalender.getInstance().getConfig().getBoolean("birthday.allow_change", false);
|
||||
}
|
||||
|
||||
/** Erwartet TT.MM (auch 5.3 oder 05.03.). Gibt null bei ungültigen Daten zurück. */
|
||||
public static MonthDay parse(String input) {
|
||||
if (input == null) return null;
|
||||
String s = input.trim();
|
||||
if (s.endsWith(".")) s = s.substring(0, s.length() - 1);
|
||||
String[] parts = s.split("\\.");
|
||||
if (parts.length != 2) return null;
|
||||
try {
|
||||
return MonthDay.of(Integer.parseInt(parts[1].trim()), Integer.parseInt(parts[0].trim()));
|
||||
} catch (NumberFormatException | DateTimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String format(int month, int day) {
|
||||
return String.format("%02d.%02d.", day, month);
|
||||
}
|
||||
|
||||
public static String format(MonthDay md) {
|
||||
return format(md.getMonthValue(), md.getDayOfMonth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Jahr des Geburtstags, dessen Zeitfenster (Geburtstag bis +grace_days) heute läuft, sonst null.
|
||||
* Berücksichtigt den Jahreswechsel (z.B. 31.12. + 2 Tage). Der 29.2. wird in Nichtschaltjahren zum 28.2.
|
||||
*/
|
||||
public static Integer activeYear(int month, int day, LocalDate today) {
|
||||
MonthDay md = MonthDay.of(month, day);
|
||||
for (int y = today.getYear() - 1; y <= today.getYear(); y++) {
|
||||
LocalDate start = md.atYear(y);
|
||||
if (!today.isBefore(start) && !today.isAfter(start.plusDays(graceDays()))) return y;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String msg(String path) {
|
||||
return LanguageManager.getMessage(path, "birthday.prefix");
|
||||
}
|
||||
|
||||
private static LocalDate today() {
|
||||
return LocalDate.now(Adventskalender.getInstance().getZone());
|
||||
}
|
||||
|
||||
private static void async(Runnable task) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(Adventskalender.getInstance(), task);
|
||||
}
|
||||
|
||||
private static void sync(Runnable task) {
|
||||
Bukkit.getScheduler().runTask(Adventskalender.getInstance(), task);
|
||||
}
|
||||
|
||||
private static boolean checkEnabled(Player player) {
|
||||
if (isEnabled()) return true;
|
||||
player.sendMessage(msg("birthday.disabled"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Spieler-Aktionen
|
||||
|
||||
public static void sendHelp(Player player) {
|
||||
for (String line : LanguageManager.getStringList("birthday.help")) {
|
||||
player.sendMessage(line.replace("%days%", String.valueOf(graceDays())));
|
||||
}
|
||||
}
|
||||
|
||||
public static void handleInfo(Player player) {
|
||||
if (!checkEnabled(player)) return;
|
||||
UUID id = player.getUniqueId();
|
||||
async(() -> {
|
||||
try {
|
||||
Optional<DataHandler.Birthday> b = DataHandler.getBirthday(id);
|
||||
sync(() -> {
|
||||
if (b.isPresent()) {
|
||||
player.sendMessage(msg("birthday.info")
|
||||
.replace("%date%", format(b.get().month(), b.get().day())));
|
||||
} else {
|
||||
player.sendMessage(msg("birthday.info_none"));
|
||||
}
|
||||
});
|
||||
} catch (DataHandler.DataException e) {
|
||||
sync(() -> player.sendMessage(msg("messages.db_error")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void handleSet(Player player, String input) {
|
||||
if (!checkEnabled(player)) return;
|
||||
MonthDay md = parse(input);
|
||||
if (md == null) {
|
||||
player.sendMessage(msg("birthday.invalid_date"));
|
||||
return;
|
||||
}
|
||||
UUID id = player.getUniqueId();
|
||||
String name = player.getName();
|
||||
async(() -> {
|
||||
try {
|
||||
boolean written = DataHandler.setBirthday(id, name, md.getMonthValue(), md.getDayOfMonth(), !allowChange());
|
||||
Optional<DataHandler.Birthday> current = written ? Optional.empty() : DataHandler.getBirthday(id);
|
||||
sync(() -> {
|
||||
if (written) {
|
||||
player.sendMessage(msg("birthday.set_success")
|
||||
.replace("%date%", format(md)).replace("%days%", String.valueOf(graceDays())));
|
||||
} else {
|
||||
player.sendMessage(msg("birthday.already_set")
|
||||
.replace("%date%", current.map(b -> format(b.month(), b.day())).orElse("?")));
|
||||
}
|
||||
});
|
||||
} catch (DataHandler.DataException e) {
|
||||
sync(() -> player.sendMessage(msg("messages.db_error")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void handleRemove(Player player) {
|
||||
if (!checkEnabled(player)) return;
|
||||
if (!allowChange()) {
|
||||
player.sendMessage(msg("birthday.change_not_allowed"));
|
||||
return;
|
||||
}
|
||||
UUID id = player.getUniqueId();
|
||||
async(() -> {
|
||||
try {
|
||||
boolean removed = DataHandler.removeBirthday(id, null);
|
||||
sync(() -> player.sendMessage(msg(removed ? "birthday.removed" : "birthday.info_none")));
|
||||
} catch (DataHandler.DataException e) {
|
||||
sync(() -> player.sendMessage(msg("messages.db_error")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void handleClaim(Player player) {
|
||||
if (!checkEnabled(player)) return;
|
||||
|
||||
// Belohnungen vorab bauen: Bei Konfigurationsfehlern wird nichts verbraucht
|
||||
List<ItemStack> items = new ArrayList<>();
|
||||
ConfigurationSection rewards = Adventskalender.getInstance().getConfig().getConfigurationSection("birthday.rewards");
|
||||
if (rewards != null) {
|
||||
for (String key : rewards.getKeys(false)) {
|
||||
ItemStack item = RewardUtil.build("birthday.rewards." + key, "Geburtstags-Belohnung " + key);
|
||||
if (item == null) {
|
||||
player.sendMessage(msg("birthday.reward_error"));
|
||||
return;
|
||||
}
|
||||
items.add(item);
|
||||
}
|
||||
}
|
||||
List<String> commands = Adventskalender.getInstance().getConfig().getStringList("birthday.commands");
|
||||
if (items.isEmpty() && commands.isEmpty()) {
|
||||
player.sendMessage(msg("birthday.reward_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
UUID id = player.getUniqueId();
|
||||
String name = player.getName();
|
||||
LocalDate today = today();
|
||||
|
||||
async(() -> {
|
||||
String messageKey = null;
|
||||
DataHandler.ClaimResult result = null;
|
||||
Integer year = null;
|
||||
try {
|
||||
Optional<DataHandler.Birthday> b = DataHandler.getBirthday(id);
|
||||
if (b.isEmpty()) {
|
||||
messageKey = "birthday.not_set";
|
||||
} else {
|
||||
year = activeYear(b.get().month(), b.get().day(), today);
|
||||
if (year == null) {
|
||||
String date = format(b.get().month(), b.get().day());
|
||||
sync(() -> player.sendMessage(msg("birthday.not_today")
|
||||
.replace("%date%", date).replace("%days%", String.valueOf(graceDays()))));
|
||||
return;
|
||||
}
|
||||
result = DataHandler.claimBirthday(id, year);
|
||||
}
|
||||
} catch (DataHandler.DataException e) {
|
||||
result = DataHandler.ClaimResult.ERROR;
|
||||
}
|
||||
|
||||
final String key = messageKey;
|
||||
final DataHandler.ClaimResult res = result;
|
||||
sync(() -> {
|
||||
if (key != null) {
|
||||
player.sendMessage(msg(key));
|
||||
return;
|
||||
}
|
||||
if (res == DataHandler.ClaimResult.SUCCESS && !player.isOnline()) {
|
||||
// Spieler ist inzwischen weg: Abholung zurücknehmen, damit das Geschenk nicht verloren geht
|
||||
async(() -> DataHandler.resetBirthdayClaim(id, null));
|
||||
return;
|
||||
}
|
||||
if (!player.isOnline()) return;
|
||||
switch (res) {
|
||||
case SUCCESS -> giveRewards(player, items, commands);
|
||||
case ALREADY_CLAIMED -> player.sendMessage(msg("birthday.already_claimed"));
|
||||
default -> player.sendMessage(msg("messages.db_error"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static void giveRewards(Player player, List<ItemStack> items, List<String> commands) {
|
||||
boolean dropped = false;
|
||||
for (ItemStack item : items) {
|
||||
dropped |= RewardUtil.give(player, item);
|
||||
}
|
||||
for (String command : commands) {
|
||||
String cmd = command.startsWith("/") ? command.substring(1) : command;
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd.replace("%player%", player.getName()));
|
||||
}
|
||||
if (dropped) player.sendMessage(msg("messages.inventory_full"));
|
||||
player.sendMessage(msg("birthday.reward_received"));
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1f, 1f);
|
||||
|
||||
if (Adventskalender.getInstance().getConfig().getBoolean("birthday.broadcast", true)) {
|
||||
Bukkit.broadcastMessage(msg("birthday.broadcast").replace("%player%", player.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Join-Hinweis
|
||||
|
||||
/** Weist beim Beitritt auf das Geschenk hin, wenn es gerade abholbar ist. */
|
||||
public static void checkOnJoin(Player player) {
|
||||
if (!isEnabled() || !player.hasPermission("adventskalender.birthday")) return;
|
||||
UUID id = player.getUniqueId();
|
||||
LocalDate today = today();
|
||||
async(() -> {
|
||||
try {
|
||||
Optional<DataHandler.Birthday> b = DataHandler.getBirthday(id);
|
||||
if (b.isEmpty()) return;
|
||||
Integer year = activeYear(b.get().month(), b.get().day(), today);
|
||||
if (year == null || b.get().claimedYear() >= year) return;
|
||||
|
||||
sync(() -> {
|
||||
if (!player.isOnline()) return;
|
||||
TextComponent message = new TextComponent(
|
||||
msg("birthday.join_notice"));
|
||||
message.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/adventskalender birthday claim"));
|
||||
message.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new Text(LanguageManager.getString("birthday.join_hover"))));
|
||||
player.spigot().sendMessage(message);
|
||||
});
|
||||
} catch (DataHandler.DataException ignored) {
|
||||
// Beim Beitritt keine Fehlermeldung an den Spieler
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class CalendarData {
|
||||
|
||||
private static File file;
|
||||
private static FileConfiguration customFile;
|
||||
|
||||
public static void setup() {
|
||||
file = new File(Adventskalender.getInstance().getDataFolder(), "calendar.yml");
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
file.createNewFile();
|
||||
} catch (IOException e) {
|
||||
Adventskalender.getInstance().getLogger().severe("Konnte calendar.yml nicht erstellen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
customFile = YamlConfiguration.loadConfiguration(file);
|
||||
}
|
||||
|
||||
public static void save() {
|
||||
try {
|
||||
customFile.save(file);
|
||||
} catch (IOException e) {
|
||||
Adventskalender.getInstance().getLogger().severe("Konnte calendar.yml nicht speichern: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasClaimed(Player player, int day) {
|
||||
boolean isGlobal = Adventskalender.getInstance().getConfig().getBoolean("calendar.use_global_calendar");
|
||||
if (isGlobal) {
|
||||
List<Integer> claimedDays = customFile.getIntegerList("global_claimed_days");
|
||||
return claimedDays.contains(day);
|
||||
} else {
|
||||
List<Integer> claimedDays = customFile.getIntegerList(player.getUniqueId().toString());
|
||||
return claimedDays.contains(day);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setClaimed(Player player, int day) {
|
||||
boolean isGlobal = Adventskalender.getInstance().getConfig().getBoolean("calendar.use_global_calendar");
|
||||
if (isGlobal) {
|
||||
List<Integer> claimedDays = customFile.getIntegerList("global_claimed_days");
|
||||
claimedDays.add(day);
|
||||
customFile.set("global_claimed_days", claimedDays);
|
||||
} else {
|
||||
List<Integer> claimedDays = customFile.getIntegerList(player.getUniqueId().toString());
|
||||
claimedDays.add(day);
|
||||
customFile.set(player.getUniqueId().toString(), claimedDays);
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
public static int getClaimedCount(Player player) {
|
||||
if (Adventskalender.getInstance().getConfig().getBoolean("calendar.use_global_calendar")) {
|
||||
return customFile.getIntegerList("global_claimed_days").size();
|
||||
} else {
|
||||
return customFile.getIntegerList(player.getUniqueId().toString()).size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.MonthDay;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Ein Kalender-Event (z.B. Advent, Halloween, Ostern), gelesen aus dem Abschnitt events.<id> der config.yml.
|
||||
*/
|
||||
public final class CalendarEvent {
|
||||
|
||||
public static final String ADVENT = "advent";
|
||||
private static final Pattern EASTER = Pattern.compile("(?i)easter\\s*([+-]\\s*\\d+)?");
|
||||
|
||||
private final String id;
|
||||
private final boolean enabled;
|
||||
private final int days;
|
||||
private final int graceDays;
|
||||
private final Boolean globalOverride;
|
||||
private final MonthDay fixedStart;
|
||||
private final Integer easterOffset;
|
||||
private final Material closedMaterial;
|
||||
private final Material openedMaterial;
|
||||
private final String closedTexture;
|
||||
private final String openedTexture;
|
||||
private final int perRow;
|
||||
private final int[] slots;
|
||||
private volatile int testDay;
|
||||
|
||||
public CalendarEvent(String id, FileConfiguration cfg, int testDay) {
|
||||
this.id = id.toLowerCase();
|
||||
String p = "events." + id + ".";
|
||||
|
||||
this.enabled = cfg.getBoolean(p + "enabled", true);
|
||||
this.days = Math.max(1, Math.min(36, cfg.getInt(p + "days", 24)));
|
||||
this.graceDays = Math.max(0, cfg.getInt(p + "grace_days", 0));
|
||||
Object g = cfg.get(p + "global");
|
||||
this.globalOverride = (g instanceof Boolean b) ? b : null;
|
||||
this.closedMaterial = material(cfg.getString(p + "closed_material", "CHEST"), Material.CHEST);
|
||||
this.openedMaterial = material(cfg.getString(p + "opened_material", "ENDER_CHEST"), Material.ENDER_CHEST);
|
||||
this.closedTexture = cfg.getString(p + "closed_texture", "");
|
||||
this.openedTexture = cfg.getString(p + "opened_texture", "");
|
||||
this.testDay = testDay;
|
||||
|
||||
String start = cfg.getString(p + "start", "12-01").trim();
|
||||
Matcher m = EASTER.matcher(start);
|
||||
if (m.matches()) {
|
||||
this.fixedStart = null;
|
||||
this.easterOffset = m.group(1) == null ? 0 : Integer.parseInt(m.group(1).replaceAll("\\s", ""));
|
||||
} else {
|
||||
try {
|
||||
this.fixedStart = MonthDay.parse("--" + start);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Ungültiger Startwert '" + start + "' (erwartet MM-dd oder easter-13)");
|
||||
}
|
||||
this.easterOffset = null;
|
||||
}
|
||||
|
||||
// Slots: bis 28 Tage mit Rand links/rechts (7 pro Reihe), darüber 9 pro Reihe. Letzte Reihe zentriert.
|
||||
this.perRow = days <= 28 ? 7 : 9;
|
||||
int colStart = perRow == 7 ? 1 : 0;
|
||||
this.slots = new int[days];
|
||||
int rows = (days + perRow - 1) / perRow;
|
||||
int d = 0;
|
||||
for (int r = 0; r < rows; r++) {
|
||||
int n = Math.min(perRow, days - d);
|
||||
int offset = (perRow - n) / 2;
|
||||
for (int i = 0; i < n; i++) {
|
||||
slots[d++] = (1 + r) * 9 + colStart + offset + i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Material material(String name, Material fallback) {
|
||||
Material m = name == null ? null : Material.matchMaterial(name);
|
||||
return (m != null && m.isItem() && !m.isAir()) ? m : fallback;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- Datum
|
||||
|
||||
/** Ostersonntag (Gaußsche Osterformel, gregorianisch). */
|
||||
public static LocalDate easter(int year) {
|
||||
int a = year % 19, b = year / 100, c = year % 100;
|
||||
int d = b / 4, e = b % 4, f = (b + 8) / 25, g = (b - f + 1) / 3;
|
||||
int h = (19 * a + b - d - g + 15) % 30;
|
||||
int i = c / 4, k = c % 4;
|
||||
int l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||
int m = (a + 11 * h + 22 * l) / 451;
|
||||
int month = (h + l - 7 * m + 114) / 31;
|
||||
int day = ((h + l - 7 * m + 114) % 31) + 1;
|
||||
return LocalDate.of(year, month, day);
|
||||
}
|
||||
|
||||
private LocalDate startFor(int year) {
|
||||
return easterOffset != null ? easter(year).plusDays(easterOffset) : fixedStart.atYear(year);
|
||||
}
|
||||
|
||||
/** Start des laufenden oder des nächsten Zeitfensters. */
|
||||
private LocalDate relevantStart(LocalDate today) {
|
||||
for (int y = today.getYear() - 1; y <= today.getYear() + 1; y++) {
|
||||
LocalDate s = startFor(y);
|
||||
if (today.isBefore(s.plusDays(days + graceDays))) return s;
|
||||
}
|
||||
return startFor(today.getYear() + 1);
|
||||
}
|
||||
|
||||
/** Aktueller Tag (1..days), oder 0 außerhalb des Zeitraums. Berücksichtigt den Testmodus. */
|
||||
public int currentDay() {
|
||||
if (testDay > 0) return Math.min(days, testDay);
|
||||
LocalDate today = LocalDate.now(Adventskalender.getInstance().getZone());
|
||||
long offset = ChronoUnit.DAYS.between(relevantStart(today), today);
|
||||
if (offset < 0) return 0;
|
||||
return (int) Math.min(days, offset + 1);
|
||||
}
|
||||
|
||||
/** Kalenderdatum eines Türchens, formatiert für die Sprache aus der Config. */
|
||||
public String formatDate(int day) {
|
||||
LocalDate today = LocalDate.now(Adventskalender.getInstance().getZone());
|
||||
LocalDate date = relevantStart(today).plusDays(day - 1);
|
||||
String lang = Adventskalender.getInstance().getConfig().getString("general.language", "de");
|
||||
Locale locale = Locale.forLanguageTag(lang);
|
||||
return date.format(DateTimeFormatter.ofPattern(lang.equalsIgnoreCase("de") ? "d. MMMM" : "MMMM d", locale));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------- Config-Pfade
|
||||
|
||||
public boolean isAdvent() { return ADVENT.equals(id); }
|
||||
|
||||
public String titleKey() { return isAdvent() ? "gui.title" : "events." + id + ".title"; }
|
||||
|
||||
public String nameKey(int day) { return rewardLangKey(day) + ".name"; }
|
||||
|
||||
public String loreKey(int day) { return rewardLangKey(day) + ".lore"; }
|
||||
|
||||
private String rewardLangKey(int day) {
|
||||
return (isAdvent() ? "rewards" : "events." + id + ".rewards") + ".day_" + day;
|
||||
}
|
||||
|
||||
public String rewardsPath() { return isAdvent() ? "rewards" : "events." + id + ".rewards"; }
|
||||
|
||||
public boolean isGlobal() {
|
||||
if (globalOverride != null) return globalOverride;
|
||||
return Adventskalender.getInstance().getConfig().getBoolean("calendar.use_global_calendar", false);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Layout
|
||||
|
||||
public int dayForSlot(int slot) {
|
||||
for (int i = 0; i < slots.length; i++) {
|
||||
if (slots[i] == slot) return i + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int slotForDay(int day) { return slots[day - 1]; }
|
||||
|
||||
/** true, wenn die Randspalten links/rechts als Dekoration frei bleiben. */
|
||||
public boolean hasSideBorder() { return perRow == 7; }
|
||||
|
||||
// ---------------------------------------------------------- Getter
|
||||
|
||||
public String getId() { return id; }
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public int getDays() { return days; }
|
||||
public Material getClosedMaterial() { return closedMaterial; }
|
||||
public Material getOpenedMaterial() { return openedMaterial; }
|
||||
public String getClosedTexture() { return closedTexture; }
|
||||
public String getOpenedTexture() { return openedTexture; }
|
||||
public int getTestDay() { return testDay; }
|
||||
public void setTestDay(int day) { this.testDay = day; }
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Alle Zugriffe sind synchronisiert, damit sie sicher aus Async-Tasks aufgerufen werden können.
|
||||
* Pro Event werden die Daten getrennt gespeichert. Das Event "advent" nutzt weiterhin die
|
||||
* bisherigen Pfade/Zeilen, damit bestehende Daten erhalten bleiben.
|
||||
*/
|
||||
public class DataHandler {
|
||||
|
||||
public enum ClaimResult { SUCCESS, ALREADY_CLAIMED, ERROR }
|
||||
|
||||
/** Geburtstag eines Spielers ohne Jahr. claimedYear = Jahr des zuletzt abgeholten Geschenks (0 = nie). */
|
||||
public record Birthday(int month, int day, int claimedYear) {}
|
||||
|
||||
/** Wird bei Datenbankfehlern geworfen, damit Aufrufer "nicht vorhanden" und "Fehler" unterscheiden können. */
|
||||
public static class DataException extends RuntimeException {
|
||||
public DataException(String message) { super(message); }
|
||||
}
|
||||
|
||||
private static File file;
|
||||
private static FileConfiguration dataConfig;
|
||||
private static boolean mysqlActive = false;
|
||||
|
||||
public static synchronized void setup() {
|
||||
mysqlActive = false;
|
||||
dataConfig = null;
|
||||
file = null;
|
||||
MySQLManager.close();
|
||||
|
||||
if (Adventskalender.getInstance().getConfig().getBoolean("mysql.enable")) {
|
||||
MySQLManager.connect();
|
||||
if (MySQLManager.isConnected()) {
|
||||
mysqlActive = true;
|
||||
Adventskalender.getInstance().getLogger().info("MySQL-Verbindung steht. data.yml wird deaktiviert.");
|
||||
return;
|
||||
} else {
|
||||
Adventskalender.getInstance().getLogger().warning("MySQL aktiviert, aber Verbindung fehlgeschlagen! Nutze data.yml als Backup.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!Adventskalender.getInstance().getDataFolder().exists()) {
|
||||
Adventskalender.getInstance().getDataFolder().mkdirs();
|
||||
}
|
||||
|
||||
file = new File(Adventskalender.getInstance().getDataFolder(), "data.yml");
|
||||
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
file.createNewFile();
|
||||
} catch (IOException e) {
|
||||
Adventskalender.getInstance().getLogger().severe("data.yml konnte nicht erstellt werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
dataConfig = YamlConfiguration.loadConfiguration(file);
|
||||
}
|
||||
|
||||
/** false, wenn der Datenspeicher gerade nicht benutzbar ist (z.B. MySQL ausgefallen). */
|
||||
public static synchronized boolean isReady() {
|
||||
if (mysqlActive) return MySQLManager.ensureConnected();
|
||||
return dataConfig != null;
|
||||
}
|
||||
|
||||
private static void logSql(SQLException e) {
|
||||
Adventskalender.getInstance().getLogger().severe("Datenbankfehler: " + e.getMessage());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Pfade / SQL-Helfer
|
||||
|
||||
private static String playersPath(CalendarEvent ev) {
|
||||
return ev.isAdvent() ? "players" : "events." + ev.getId() + ".players";
|
||||
}
|
||||
|
||||
private static String globalPath(CalendarEvent ev) {
|
||||
return ev.isAdvent() ? "global" : "events." + ev.getId() + ".global";
|
||||
}
|
||||
|
||||
/** Ein Event, oder alle konfigurierten Events bei null. */
|
||||
private static List<CalendarEvent> eventsOf(CalendarEvent ev) {
|
||||
return ev != null ? List.of(ev) : EventManager.allConfigured();
|
||||
}
|
||||
|
||||
private static int exec(String sql, Object... params) throws SQLException {
|
||||
try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement(sql)) {
|
||||
for (int i = 0; i < params.length; i++) ps.setObject(i + 1, params[i]);
|
||||
return ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<Integer> queryDays(String sql, Object... params) throws SQLException {
|
||||
Set<Integer> days = new HashSet<>();
|
||||
try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement(sql)) {
|
||||
for (int i = 0; i < params.length; i++) ps.setObject(i + 1, params[i]);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) days.add(rs.getInt("day"));
|
||||
}
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Lesen
|
||||
|
||||
/** Tage, die für den Spieler im GUI als geöffnet gelten (global oder pro Spieler). null = Fehler. */
|
||||
public static synchronized Set<Integer> getViewClaimedDays(Player player, CalendarEvent ev) {
|
||||
if (!isReady()) return null;
|
||||
try {
|
||||
return ev.isGlobal() ? loadGlobalDays(ev) : loadPlayerDays(player.getUniqueId().toString(), ev);
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<Integer> loadPlayerDays(String uuid, CalendarEvent ev) throws SQLException {
|
||||
if (mysqlActive) {
|
||||
return queryDays("SELECT day FROM advent_players WHERE uuid = ? AND event = ?", uuid, ev.getId());
|
||||
}
|
||||
return new HashSet<>(dataConfig.getIntegerList(playersPath(ev) + "." + uuid + ".claimed"));
|
||||
}
|
||||
|
||||
private static Set<Integer> loadGlobalDays(CalendarEvent ev) throws SQLException {
|
||||
if (mysqlActive) {
|
||||
return queryDays("SELECT day FROM advent_global WHERE event = ?", ev.getId());
|
||||
}
|
||||
Set<Integer> days = new HashSet<>();
|
||||
ConfigurationSection section = dataConfig.getConfigurationSection(globalPath(ev));
|
||||
if (section != null) {
|
||||
for (String key : section.getKeys(false)) {
|
||||
try {
|
||||
days.add(Integer.parseInt(key.replace("day_", "")));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Schreiben
|
||||
|
||||
/** Markiert einen Tag atomar als abgeholt. Verhindert Doppelabholung auch bei gleichzeitigen Klicks. */
|
||||
public static synchronized ClaimResult setClaimed(Player player, CalendarEvent ev, int day) {
|
||||
if (!isReady()) return ClaimResult.ERROR;
|
||||
|
||||
if (ev.isGlobal()) {
|
||||
return claimGlobal(ev, day, player.getName());
|
||||
}
|
||||
|
||||
String uuid = player.getUniqueId().toString();
|
||||
if (mysqlActive) {
|
||||
try {
|
||||
int rows = exec("INSERT IGNORE INTO advent_players (uuid, event, day, name) VALUES (?, ?, ?, ?)",
|
||||
uuid, ev.getId(), day, player.getName());
|
||||
return rows > 0 ? ClaimResult.SUCCESS : ClaimResult.ALREADY_CLAIMED;
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
return ClaimResult.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
String base = playersPath(ev) + "." + uuid;
|
||||
List<Integer> claimed = new ArrayList<>(dataConfig.getIntegerList(base + ".claimed"));
|
||||
if (claimed.contains(day)) return ClaimResult.ALREADY_CLAIMED;
|
||||
claimed.add(day);
|
||||
dataConfig.set(base + ".claimed", claimed);
|
||||
dataConfig.set(base + ".name", player.getName());
|
||||
return save() ? ClaimResult.SUCCESS : ClaimResult.ERROR;
|
||||
}
|
||||
|
||||
private static ClaimResult claimGlobal(CalendarEvent ev, int day, String playerName) {
|
||||
if (mysqlActive) {
|
||||
try {
|
||||
int rows = exec("INSERT IGNORE INTO advent_global (event, day, player_name) VALUES (?, ?, ?)",
|
||||
ev.getId(), day, playerName);
|
||||
return rows > 0 ? ClaimResult.SUCCESS : ClaimResult.ALREADY_CLAIMED;
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
return ClaimResult.ERROR;
|
||||
}
|
||||
}
|
||||
String path = globalPath(ev) + ".day_" + day;
|
||||
if (dataConfig.contains(path)) return ClaimResult.ALREADY_CLAIMED;
|
||||
dataConfig.set(path, playerName);
|
||||
return save() ? ClaimResult.SUCCESS : ClaimResult.ERROR;
|
||||
}
|
||||
|
||||
/** Setzt einen Tag für einen Spieler (per Name, auch offline) zurück. ev = null: in allen Events. */
|
||||
public static synchronized boolean resetPlayerDay(String playerName, CalendarEvent ev, int day) {
|
||||
boolean found = false;
|
||||
if (mysqlActive && MySQLManager.ensureConnected()) {
|
||||
String uuid = lookupUuid(playerName);
|
||||
try {
|
||||
if (ev != null) {
|
||||
found |= exec("DELETE FROM advent_players WHERE day = ? AND (name = ? OR uuid = ?) AND event = ?",
|
||||
day, playerName, uuid, ev.getId()) > 0;
|
||||
} else {
|
||||
found |= exec("DELETE FROM advent_players WHERE day = ? AND (name = ? OR uuid = ?)",
|
||||
day, playerName, uuid) > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
}
|
||||
}
|
||||
if (dataConfig != null) {
|
||||
for (CalendarEvent e : eventsOf(ev)) {
|
||||
ConfigurationSection section = dataConfig.getConfigurationSection(playersPath(e));
|
||||
if (section == null) continue;
|
||||
for (String uuid : section.getKeys(false)) {
|
||||
String base = playersPath(e) + "." + uuid;
|
||||
if (playerName.equalsIgnoreCase(dataConfig.getString(base + ".name"))) {
|
||||
List<Integer> claimed = new ArrayList<>(dataConfig.getIntegerList(base + ".claimed"));
|
||||
found |= claimed.remove(Integer.valueOf(day));
|
||||
dataConfig.set(base + ".claimed", claimed);
|
||||
save();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Löscht alle Daten eines (online) Spielers. ev = null: in allen Events. */
|
||||
public static synchronized void resetPlayer(Player player, CalendarEvent ev) {
|
||||
String uuid = player.getUniqueId().toString();
|
||||
if (mysqlActive && MySQLManager.ensureConnected()) {
|
||||
try {
|
||||
if (ev != null) {
|
||||
exec("DELETE FROM advent_players WHERE uuid = ? AND event = ?", uuid, ev.getId());
|
||||
} else {
|
||||
exec("DELETE FROM advent_players WHERE uuid = ?", uuid);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
}
|
||||
}
|
||||
if (dataConfig != null) {
|
||||
for (CalendarEvent e : eventsOf(ev)) {
|
||||
dataConfig.set(playersPath(e) + "." + uuid, null);
|
||||
}
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
/** Löscht alle Daten eines Spielers per Name (auch offline). ev = null: in allen Events. */
|
||||
public static synchronized boolean resetOfflinePlayer(String name, CalendarEvent ev) {
|
||||
boolean found = false;
|
||||
if (mysqlActive && MySQLManager.ensureConnected()) {
|
||||
String uuid = lookupUuid(name);
|
||||
try {
|
||||
if (ev != null) {
|
||||
found |= exec("DELETE FROM advent_players WHERE (name = ? OR uuid = ?) AND event = ?",
|
||||
name, uuid, ev.getId()) > 0;
|
||||
} else {
|
||||
found |= exec("DELETE FROM advent_players WHERE name = ? OR uuid = ?", name, uuid) > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
}
|
||||
}
|
||||
if (dataConfig != null) {
|
||||
for (CalendarEvent e : eventsOf(ev)) {
|
||||
ConfigurationSection section = dataConfig.getConfigurationSection(playersPath(e));
|
||||
if (section == null) continue;
|
||||
for (String uuid : section.getKeys(false)) {
|
||||
if (name.equalsIgnoreCase(dataConfig.getString(playersPath(e) + "." + uuid + ".name"))) {
|
||||
dataConfig.set(playersPath(e) + "." + uuid, null);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
save();
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Löscht alle Spieler- und globalen Daten. ev = null: aller Events. */
|
||||
public static synchronized void resetAll(CalendarEvent ev) {
|
||||
if (mysqlActive && MySQLManager.ensureConnected()) {
|
||||
try {
|
||||
if (ev != null) {
|
||||
exec("DELETE FROM advent_players WHERE event = ?", ev.getId());
|
||||
exec("DELETE FROM advent_global WHERE event = ?", ev.getId());
|
||||
} else {
|
||||
exec("DELETE FROM advent_players");
|
||||
exec("DELETE FROM advent_global");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
}
|
||||
}
|
||||
if (dataConfig != null) {
|
||||
for (CalendarEvent e : eventsOf(ev)) {
|
||||
dataConfig.set(playersPath(e), null);
|
||||
dataConfig.set(globalPath(e), null);
|
||||
}
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
/** Gibt einen global abgeholten Tag wieder frei. ev = null: in allen Events. */
|
||||
public static synchronized void resetGlobalDay(CalendarEvent ev, int day) {
|
||||
if (mysqlActive && MySQLManager.ensureConnected()) {
|
||||
try {
|
||||
if (ev != null) {
|
||||
exec("DELETE FROM advent_global WHERE day = ? AND event = ?", day, ev.getId());
|
||||
} else {
|
||||
exec("DELETE FROM advent_global WHERE day = ?", day);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
}
|
||||
}
|
||||
if (dataConfig != null) {
|
||||
for (CalendarEvent e : eventsOf(ev)) {
|
||||
dataConfig.set(globalPath(e) + ".day_" + day, null);
|
||||
}
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Geburtstage
|
||||
|
||||
public static synchronized Optional<Birthday> getBirthday(UUID uuid) {
|
||||
if (!isReady()) throw new DataException("Datenspeicher nicht verfügbar");
|
||||
if (mysqlActive) {
|
||||
try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement(
|
||||
"SELECT month, day, claimed_year FROM advent_birthdays WHERE uuid = ?")) {
|
||||
ps.setString(1, uuid.toString());
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return Optional.empty();
|
||||
return Optional.of(new Birthday(rs.getInt("month"), rs.getInt("day"), rs.getInt("claimed_year")));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
throw new DataException(e.getMessage());
|
||||
}
|
||||
}
|
||||
String base = "birthdays." + uuid;
|
||||
if (!dataConfig.contains(base + ".month")) return Optional.empty();
|
||||
return Optional.of(new Birthday(dataConfig.getInt(base + ".month"), dataConfig.getInt(base + ".day"),
|
||||
dataConfig.getInt(base + ".claimed_year", 0)));
|
||||
}
|
||||
|
||||
/** Speichert den Geburtstag. onlyIfAbsent: nichts überschreiben. Gibt zurück, ob geschrieben wurde. */
|
||||
public static synchronized boolean setBirthday(UUID uuid, String name, int month, int day, boolean onlyIfAbsent) {
|
||||
if (!isReady()) throw new DataException("Datenspeicher nicht verfügbar");
|
||||
if (mysqlActive) {
|
||||
try {
|
||||
if (onlyIfAbsent) {
|
||||
return exec("INSERT IGNORE INTO advent_birthdays (uuid, name, month, day) VALUES (?, ?, ?, ?)",
|
||||
uuid.toString(), name, month, day) > 0;
|
||||
}
|
||||
exec("INSERT INTO advent_birthdays (uuid, name, month, day) VALUES (?, ?, ?, ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE name = VALUES(name), month = VALUES(month), day = VALUES(day)",
|
||||
uuid.toString(), name, month, day);
|
||||
return true;
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
throw new DataException(e.getMessage());
|
||||
}
|
||||
}
|
||||
String base = "birthdays." + uuid;
|
||||
if (onlyIfAbsent && dataConfig.contains(base + ".month")) return false;
|
||||
dataConfig.set(base + ".name", name);
|
||||
dataConfig.set(base + ".month", month);
|
||||
dataConfig.set(base + ".day", day);
|
||||
if (!save()) throw new DataException("data.yml konnte nicht gespeichert werden");
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Löscht einen Geburtstag per UUID oder Name (einer von beiden darf null sein). */
|
||||
public static synchronized boolean removeBirthday(UUID uuid, String name) {
|
||||
if (!isReady()) throw new DataException("Datenspeicher nicht verfügbar");
|
||||
boolean found = false;
|
||||
if (mysqlActive) {
|
||||
try {
|
||||
found = exec("DELETE FROM advent_birthdays WHERE uuid = ? OR name = ?",
|
||||
uuid != null ? uuid.toString() : "", name != null ? name : "") > 0;
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
throw new DataException(e.getMessage());
|
||||
}
|
||||
return found;
|
||||
}
|
||||
ConfigurationSection section = dataConfig.getConfigurationSection("birthdays");
|
||||
if (section == null) return false;
|
||||
for (String key : section.getKeys(false)) {
|
||||
boolean match = (uuid != null && key.equals(uuid.toString()))
|
||||
|| (name != null && name.equalsIgnoreCase(dataConfig.getString("birthdays." + key + ".name")));
|
||||
if (match) {
|
||||
dataConfig.set("birthdays." + key, null);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) save();
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Markiert das Geschenk für das Geburtstagsjahr atomar als abgeholt. */
|
||||
public static synchronized ClaimResult claimBirthday(UUID uuid, int year) {
|
||||
if (!isReady()) return ClaimResult.ERROR;
|
||||
if (mysqlActive) {
|
||||
try {
|
||||
int rows = exec("UPDATE advent_birthdays SET claimed_year = ? WHERE uuid = ? AND claimed_year < ?",
|
||||
year, uuid.toString(), year);
|
||||
return rows > 0 ? ClaimResult.SUCCESS : ClaimResult.ALREADY_CLAIMED;
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
return ClaimResult.ERROR;
|
||||
}
|
||||
}
|
||||
String path = "birthdays." + uuid + ".claimed_year";
|
||||
if (dataConfig.getInt(path, 0) >= year) return ClaimResult.ALREADY_CLAIMED;
|
||||
dataConfig.set(path, year);
|
||||
return save() ? ClaimResult.SUCCESS : ClaimResult.ERROR;
|
||||
}
|
||||
|
||||
/** Macht die Abholung rückgängig (Admin-Reset bzw. wenn der Spieler vor der Vergabe offline ging). */
|
||||
public static synchronized boolean resetBirthdayClaim(UUID uuid, String name) {
|
||||
if (!isReady()) return false;
|
||||
if (mysqlActive) {
|
||||
try {
|
||||
return exec("UPDATE advent_birthdays SET claimed_year = 0 WHERE uuid = ? OR name = ?",
|
||||
uuid != null ? uuid.toString() : "", name != null ? name : "") > 0;
|
||||
} catch (SQLException e) {
|
||||
logSql(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ConfigurationSection section = dataConfig.getConfigurationSection("birthdays");
|
||||
if (section == null) return false;
|
||||
for (String key : section.getKeys(false)) {
|
||||
boolean match = (uuid != null && key.equals(uuid.toString()))
|
||||
|| (name != null && name.equalsIgnoreCase(dataConfig.getString("birthdays." + key + ".name")));
|
||||
if (match) {
|
||||
dataConfig.set("birthdays." + key + ".claimed_year", 0);
|
||||
return save();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static String lookupUuid(String name) {
|
||||
OfflinePlayer target = Bukkit.getOfflinePlayer(name);
|
||||
return target.getUniqueId().toString();
|
||||
}
|
||||
|
||||
private static boolean save() {
|
||||
if (dataConfig == null || file == null) return false;
|
||||
try {
|
||||
dataConfig.save(file);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Adventskalender.getInstance().getLogger().severe("data.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Verwaltet alle konfigurierten Kalender-Events. */
|
||||
public final class EventManager {
|
||||
|
||||
private static volatile Map<String, CalendarEvent> events = new LinkedHashMap<>();
|
||||
|
||||
private EventManager() {}
|
||||
|
||||
public static void load() {
|
||||
Adventskalender plugin = Adventskalender.getInstance();
|
||||
FileConfiguration cfg = plugin.getConfig();
|
||||
int startTestDay = Math.max(0, cfg.getInt("general.test_day", 0));
|
||||
|
||||
// IDs aus der Config des Servers und den mitgelieferten Standardwerten (Standard zuerst)
|
||||
Set<String> ids = new LinkedHashSet<>();
|
||||
if (cfg.getDefaults() != null) {
|
||||
ConfigurationSection def = cfg.getDefaults().getConfigurationSection("events");
|
||||
if (def != null) ids.addAll(def.getKeys(false));
|
||||
}
|
||||
ConfigurationSection user = cfg.getConfigurationSection("events");
|
||||
if (user != null) ids.addAll(user.getKeys(false));
|
||||
if (ids.isEmpty()) ids.add(CalendarEvent.ADVENT);
|
||||
|
||||
Map<String, CalendarEvent> loaded = new LinkedHashMap<>();
|
||||
for (String id : ids) {
|
||||
try {
|
||||
CalendarEvent ev = new CalendarEvent(id, cfg, startTestDay);
|
||||
loaded.put(ev.getId(), ev);
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLogger().warning("Event '" + id + "' wird übersprungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
events = loaded;
|
||||
}
|
||||
|
||||
/** Alle aktivierten Events. */
|
||||
public static List<CalendarEvent> all() {
|
||||
List<CalendarEvent> list = new ArrayList<>();
|
||||
for (CalendarEvent ev : events.values()) {
|
||||
if (ev.isEnabled()) list.add(ev);
|
||||
}
|
||||
return Collections.unmodifiableList(list);
|
||||
}
|
||||
|
||||
/** Alle Events, auch deaktivierte (z.B. für Admin-Resets). */
|
||||
public static List<CalendarEvent> allConfigured() {
|
||||
return Collections.unmodifiableList(new ArrayList<>(events.values()));
|
||||
}
|
||||
|
||||
/** Aktiviertes Event per ID, sonst null. */
|
||||
public static CalendarEvent get(String id) {
|
||||
CalendarEvent ev = id == null ? null : events.get(id.toLowerCase());
|
||||
return (ev != null && ev.isEnabled()) ? ev : null;
|
||||
}
|
||||
|
||||
/** Event per ID, auch wenn es deaktiviert ist. */
|
||||
public static CalendarEvent getAny(String id) {
|
||||
return id == null ? null : events.get(id.toLowerCase());
|
||||
}
|
||||
|
||||
/** Das Event, das gerade läuft; sonst das erste aktivierte. null, wenn keines aktiviert ist. */
|
||||
public static CalendarEvent defaultEvent() {
|
||||
CalendarEvent first = null;
|
||||
for (CalendarEvent ev : all()) {
|
||||
if (first == null) first = ev;
|
||||
if (ev.currentDay() > 0) return ev;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,76 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class LanguageManager {
|
||||
|
||||
private static FileConfiguration languageConfig;
|
||||
private static YamlConfiguration cfg;
|
||||
private static String prefix;
|
||||
|
||||
public static void setup() {
|
||||
String lang = Adventskalender.getInstance().getConfig().getString("general.language", "de");
|
||||
File languageFile = new File(Adventskalender.getInstance().getDataFolder(), "messages_" + lang + ".yml");
|
||||
Adventskalender plugin = Adventskalender.getInstance();
|
||||
String lang = plugin.getConfig().getString("general.language", "de").toLowerCase();
|
||||
if (!lang.matches("[a-z_]+")) lang = "de";
|
||||
|
||||
if (!languageFile.exists()) {
|
||||
Adventskalender.getInstance().saveResource("messages_" + lang + ".yml", false);
|
||||
String resName = "messages_" + lang + ".yml";
|
||||
File f = new File(plugin.getDataFolder(), resName);
|
||||
if (!f.exists() && plugin.getResource(resName) == null) {
|
||||
plugin.getLogger().warning("Sprachdatei '" + resName + "' existiert nicht - nutze 'de'.");
|
||||
resName = "messages_de.yml";
|
||||
f = new File(plugin.getDataFolder(), resName);
|
||||
}
|
||||
if (!f.exists()) plugin.saveResource(resName, false);
|
||||
cfg = YamlConfiguration.loadConfiguration(f);
|
||||
|
||||
// Fehlende Schlüssel (z.B. nach einem Update) aus der mitgelieferten Datei ergänzen
|
||||
try (InputStream in = plugin.getResource(resName)) {
|
||||
if (in != null) {
|
||||
cfg.setDefaults(YamlConfiguration.loadConfiguration(new InputStreamReader(in, StandardCharsets.UTF_8)));
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
languageConfig = YamlConfiguration.loadConfiguration(languageFile);
|
||||
prefix = ChatColor.translateAlternateColorCodes('&', cfg.getString("prefix", "&6[Advent] "));
|
||||
}
|
||||
|
||||
public static String getString(String path) {
|
||||
if (languageConfig.contains(path)) {
|
||||
return format(languageConfig.getString(path));
|
||||
}
|
||||
return "§cLanguage key not found: " + path;
|
||||
String s = cfg.getString(path);
|
||||
return s == null ? "§cKey error: " + path : ChatColor.translateAlternateColorCodes('&', s);
|
||||
}
|
||||
|
||||
public static boolean has(String path) {
|
||||
return cfg.contains(path);
|
||||
}
|
||||
|
||||
public static String getMessage(String path) {
|
||||
return prefix + getString(path);
|
||||
}
|
||||
|
||||
/** Nachricht mit dem Präfix aus prefixKey (z.B. "birthday.prefix"); fehlt er, gilt das Standard-Präfix. */
|
||||
public static String getMessage(String path, String prefixKey) {
|
||||
return prefixOf(prefixKey) + getString(path);
|
||||
}
|
||||
|
||||
/** Nachricht mit dem Präfix des Events (z.B. [Halloween]); der Advent nutzt das Standard-Präfix. */
|
||||
public static String getMessage(String path, CalendarEvent event) {
|
||||
return getMessage(path, event == null || event.isAdvent() ? null : "events." + event.getId() + ".prefix");
|
||||
}
|
||||
|
||||
private static String prefixOf(String prefixKey) {
|
||||
if (prefixKey == null || !cfg.contains(prefixKey)) return prefix;
|
||||
return ChatColor.translateAlternateColorCodes('&', cfg.getString(prefixKey, ""));
|
||||
}
|
||||
|
||||
public static List<String> getStringList(String path) {
|
||||
if (languageConfig.contains(path)) {
|
||||
return languageConfig.getStringList(path).stream().map(LanguageManager::format).toList();
|
||||
}
|
||||
return List.of("§cLanguage key list not found: " + path);
|
||||
return cfg.getStringList(path).stream()
|
||||
.map(s -> ChatColor.translateAlternateColorCodes('&', s))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static String format(String message) {
|
||||
return org.bukkit.ChatColor.translateAlternateColorCodes('&', message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import java.sql.*;
|
||||
|
||||
public class MySQLManager {
|
||||
|
||||
private static Connection connection;
|
||||
|
||||
/** Baut die Verbindung (neu) auf. Eine bestehende Verbindung wird vorher geschlossen. */
|
||||
public static synchronized void connect() {
|
||||
FileConfiguration config = Adventskalender.getInstance().getConfig();
|
||||
String host = config.getString("mysql.host");
|
||||
int port = config.getInt("mysql.port");
|
||||
String database = config.getString("mysql.database");
|
||||
String user = config.getString("mysql.user");
|
||||
String password = config.getString("mysql.password");
|
||||
|
||||
close();
|
||||
try {
|
||||
loadDriver();
|
||||
connection = DriverManager.getConnection(
|
||||
"jdbc:mysql://" + host + ":" + port + "/" + database + "?useSSL=false&characterEncoding=utf8", user, password);
|
||||
createTables();
|
||||
Adventskalender.getInstance().getLogger().info("MySQL-Verbindung erfolgreich hergestellt!");
|
||||
} catch (Exception e) {
|
||||
connection = null;
|
||||
Adventskalender.getInstance().getLogger().severe("MySQL-Verbindung fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadDriver() throws ClassNotFoundException {
|
||||
try {
|
||||
Class.forName("com.mysql.cj.jdbc.Driver");
|
||||
} catch (ClassNotFoundException e) {
|
||||
Class.forName("com.mysql.jdbc.Driver");
|
||||
}
|
||||
}
|
||||
|
||||
private static void createTables() {
|
||||
try (Statement s = connection.createStatement()) {
|
||||
// Tabelle für Einzelspieler-Daten
|
||||
s.executeUpdate("CREATE TABLE IF NOT EXISTS advent_players (uuid VARCHAR(36), event VARCHAR(32) NOT NULL DEFAULT 'advent', day INT, name VARCHAR(32), PRIMARY KEY(uuid, event, day))");
|
||||
// Tabelle für globalen Modus
|
||||
s.executeUpdate("CREATE TABLE IF NOT EXISTS advent_global (event VARCHAR(32) NOT NULL DEFAULT 'advent', day INT, player_name VARCHAR(32), PRIMARY KEY(event, day))");
|
||||
// Tabelle für Spieler-Geburtstage (ohne Jahr)
|
||||
s.executeUpdate("CREATE TABLE IF NOT EXISTS advent_birthdays (uuid VARCHAR(36) PRIMARY KEY, name VARCHAR(32), month INT, day INT, claimed_year INT NOT NULL DEFAULT 0)");
|
||||
} catch (SQLException e) {
|
||||
Adventskalender.getInstance().getLogger().severe("MySQL-Tabellen konnten nicht erstellt werden: " + e.getMessage());
|
||||
}
|
||||
// Migrationen für Tabellen aus älteren Versionen (Fehler = Spalte existiert schon)
|
||||
migrate("ALTER TABLE advent_players ADD COLUMN name VARCHAR(32)", null);
|
||||
migrate("ALTER TABLE advent_players ADD COLUMN event VARCHAR(32) NOT NULL DEFAULT 'advent'",
|
||||
"ALTER TABLE advent_players DROP PRIMARY KEY, ADD PRIMARY KEY (uuid, event, day)");
|
||||
migrate("ALTER TABLE advent_global ADD COLUMN event VARCHAR(32) NOT NULL DEFAULT 'advent'",
|
||||
"ALTER TABLE advent_global DROP PRIMARY KEY, ADD PRIMARY KEY (event, day)");
|
||||
}
|
||||
|
||||
/** Führt eine Migration aus; followUp läuft nur, wenn sie erfolgreich war. */
|
||||
private static void migrate(String sql, String followUp) {
|
||||
try (Statement s = connection.createStatement()) {
|
||||
s.executeUpdate(sql);
|
||||
} catch (SQLException ignored) {
|
||||
return;
|
||||
}
|
||||
if (followUp == null) return;
|
||||
try (Statement s = connection.createStatement()) {
|
||||
s.executeUpdate(followUp);
|
||||
} catch (SQLException e) {
|
||||
Adventskalender.getInstance().getLogger().severe("MySQL-Migration fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized boolean isConnected() {
|
||||
try {
|
||||
return connection != null && !connection.isClosed();
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Prüft die Verbindung und baut sie bei Bedarf neu auf. */
|
||||
public static synchronized boolean ensureConnected() {
|
||||
try {
|
||||
if (connection != null && connection.isValid(2)) return true;
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
connect();
|
||||
return connection != null;
|
||||
}
|
||||
|
||||
public static synchronized void close() {
|
||||
try {
|
||||
if (connection != null) connection.close();
|
||||
} catch (SQLException e) {
|
||||
Adventskalender.getInstance().getLogger().warning("MySQL-Verbindung konnte nicht sauber geschlossen werden: " + e.getMessage());
|
||||
}
|
||||
connection = null;
|
||||
}
|
||||
|
||||
public static synchronized Connection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Verwaltet die Spielerdaten in einer separaten YAML-Datei.
|
||||
* Speichert, welcher Spieler an welchem Tag sein Geschenk bereits geholt hat.
|
||||
*/
|
||||
public class PlayerData {
|
||||
|
||||
private static File file;
|
||||
private static FileConfiguration customFile;
|
||||
|
||||
/**
|
||||
* Erstellt die playerdata.yml-Datei, falls sie nicht existiert.
|
||||
*/
|
||||
public static void setup() {
|
||||
file = new File(Adventskalender.getInstance().getDataFolder(), "playerdata.yml");
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
file.createNewFile();
|
||||
} catch (IOException e) {
|
||||
Adventskalender.getInstance().getLogger().severe("Konnte playerdata.yml nicht erstellen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
customFile = YamlConfiguration.loadConfiguration(file);
|
||||
}
|
||||
|
||||
public static FileConfiguration get() {
|
||||
return customFile;
|
||||
}
|
||||
|
||||
public static void save() {
|
||||
try {
|
||||
customFile.save(file);
|
||||
} catch (IOException e) {
|
||||
Adventskalender.getInstance().getLogger().severe("Konnte playerdata.yml nicht speichern: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void reload() {
|
||||
customFile = YamlConfiguration.loadConfiguration(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Spieler ein Geschenk für einen bestimmten Tag bereits geholt hat.
|
||||
* @param uuid Die UUID des Spielers
|
||||
* @param day Der Tag (1-24)
|
||||
* @return true, wenn bereits geholt, sonst false
|
||||
*/
|
||||
public static boolean hasClaimed(UUID uuid, int day) {
|
||||
// Wir speichern eine Liste der geöffneten Tage für jeden Spieler
|
||||
List<Integer> claimedDays = customFile.getIntegerList(uuid.toString());
|
||||
return claimedDays.contains(day);
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert einen Tag für einen Spieler als geholt.
|
||||
* @param uuid Die UUID des Spielers
|
||||
* @param day Der Tag (1-24)
|
||||
*/
|
||||
public static void setClaimed(UUID uuid, int day) {
|
||||
List<Integer> claimedDays = customFile.getIntegerList(uuid.toString());
|
||||
claimedDays.add(day);
|
||||
customFile.set(uuid.toString(), claimedDays);
|
||||
save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Gemeinsame Logik zum Bauen und Übergeben von Belohnungs-Items aus der Config. */
|
||||
public final class RewardUtil {
|
||||
|
||||
private RewardUtil() {}
|
||||
|
||||
/**
|
||||
* Baut ein Item aus dem Config-Abschnitt (material, amount, enchantments).
|
||||
* Gibt null zurück, wenn das Material ungültig ist.
|
||||
*/
|
||||
public static ItemStack build(String path, String label) {
|
||||
Adventskalender plugin = Adventskalender.getInstance();
|
||||
String matName = plugin.getConfig().getString(path + ".material", "COOKIE");
|
||||
int amount = Math.max(1, plugin.getConfig().getInt(path + ".amount", 1));
|
||||
|
||||
Material material = Material.matchMaterial(matName);
|
||||
if (material == null || !material.isItem() || material.isAir()) {
|
||||
plugin.getLogger().warning("Ungültiges Material '" + matName + "' für " + label + " in der config.yml!");
|
||||
return null;
|
||||
}
|
||||
|
||||
ItemStack reward = new ItemStack(material, amount);
|
||||
ItemMeta meta = reward.getItemMeta();
|
||||
|
||||
if (meta != null) {
|
||||
List<Map<?, ?>> enchants = plugin.getConfig().getMapList(path + ".enchantments");
|
||||
for (Map<?, ?> entry : enchants) {
|
||||
Object typeObj = entry.get("type");
|
||||
if (typeObj == null) continue;
|
||||
String type = String.valueOf(typeObj);
|
||||
Object levelObj = entry.get("level");
|
||||
int level = (levelObj instanceof Number n) ? n.intValue() : 1;
|
||||
|
||||
Enchantment enc = findEnchantment(type);
|
||||
if (enc != null) {
|
||||
meta.addEnchant(enc, level, true);
|
||||
} else {
|
||||
plugin.getLogger().warning("Unbekannte Verzauberung '" + type + "' für " + label + " in der config.yml!");
|
||||
}
|
||||
}
|
||||
reward.setItemMeta(meta);
|
||||
}
|
||||
return reward;
|
||||
}
|
||||
|
||||
/** Sucht eine Verzauberung über ihren Minecraft-Key (z.B. SHARPNESS), mit Fallback auf alte Bukkit-Namen. */
|
||||
@SuppressWarnings("deprecation")
|
||||
private static Enchantment findEnchantment(String type) {
|
||||
String name = type.trim();
|
||||
NamespacedKey key = NamespacedKey.fromString(name.toLowerCase());
|
||||
Enchantment enc = key != null ? Enchantment.getByKey(key) : null;
|
||||
return enc != null ? enc : Enchantment.getByName(name.toUpperCase());
|
||||
}
|
||||
|
||||
/** Legt das Item ins Inventar, Überschuss wird gedroppt. Gibt true zurück, wenn etwas gedroppt wurde. */
|
||||
public static boolean give(Player player, ItemStack reward) {
|
||||
Map<Integer, ItemStack> leftover = player.getInventory().addItem(reward);
|
||||
for (ItemStack item : leftover.values()) {
|
||||
player.getWorld().dropItemNaturally(player.getLocation(), item);
|
||||
}
|
||||
return !leftover.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.mviper.adventskalender;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Scanner;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class UpdateChecker {
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final int resourceId;
|
||||
|
||||
public UpdateChecker(JavaPlugin plugin, int resourceId) {
|
||||
this.plugin = plugin;
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public void getVersion(final Consumer<String> consumer) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> {
|
||||
try {
|
||||
URLConnection connection = URI.create("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId).toURL().openConnection();
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
try (InputStream is = connection.getInputStream(); Scanner scann = new Scanner(is)) {
|
||||
if (scann.hasNext()) {
|
||||
consumer.accept(scann.next());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().info("Update-Check fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/** Persoenlicher Testmodus (/ac test): erlaubt einem Spieler, jeden Tag zu oeffnen. */
|
||||
public final class CalendarPerks {
|
||||
private static final Set<UUID> TESTERS = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private CalendarPerks() {}
|
||||
|
||||
public static boolean isTester(Player p) {
|
||||
return TESTERS.contains(p.getUniqueId()) && p.hasPermission("christmasplus.adventcalendar.test");
|
||||
}
|
||||
|
||||
/** @return true, wenn der Testmodus jetzt an ist */
|
||||
public static boolean toggle(Player p) {
|
||||
if (!TESTERS.remove(p.getUniqueId())) {
|
||||
TESTERS.add(p.getUniqueId());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.RewardUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Verarbeitet /christmas und /cracker. */
|
||||
public final class ChristmasCommand implements CommandExecutor, TabCompleter {
|
||||
private final ChristmasManager mgr;
|
||||
|
||||
ChristmasCommand(ChristmasManager mgr) { this.mgr = mgr; }
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender s, Command cmd, String label, String[] a) {
|
||||
if (cmd.getName().equalsIgnoreCase("cracker")) return cracker(s, a);
|
||||
if (a.length == 0) {
|
||||
s.sendMessage(mgr.msg("info", "%status%", mgr.raw(mgr.trees().isEnded() ? "status-ended" : "status-active"),
|
||||
"%trees%", String.valueOf(mgr.trees().treeCount())));
|
||||
return true;
|
||||
}
|
||||
switch (a[0].toLowerCase()) {
|
||||
case "help" -> mgr.rawList("help").forEach(s::sendMessage);
|
||||
case "snow" -> snow(s);
|
||||
case "santa" -> santa(s, a);
|
||||
case "snowman" -> snowman(s, a);
|
||||
case "countdown" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
|
||||
s.sendMessage(mgr.msg(mgr.countdown().toggle(p) ? "countdown-on" : "countdown-off"));
|
||||
if (!mgr.countdown().visible()) {
|
||||
s.sendMessage(mgr.msg("countdown-not-yet", "%days%", String.valueOf(mgr.countdown().daysLeft()),
|
||||
"%in%", String.valueOf(mgr.countdown().daysUntilVisible())));
|
||||
} else {
|
||||
s.sendMessage(mgr.prefixed(mgr.countdown().text()));
|
||||
}
|
||||
}
|
||||
case "cookie" -> cookie(s, a);
|
||||
case "wichteln" -> wichteln(s, a);
|
||||
case "wreath" -> wreath(s, a);
|
||||
case "snowball" -> snowball(s, a);
|
||||
case "deco" -> deco(s, a);
|
||||
case "stocking" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
|
||||
String sub = a.length > 1 ? a[1].toLowerCase() : "";
|
||||
switch (sub) {
|
||||
case "place" -> mgr.stockings().place(p);
|
||||
case "remove" -> mgr.stockings().removeNearest(p);
|
||||
case "flight" -> {
|
||||
if (!admin(s)) return true;
|
||||
s.sendMessage(mgr.msg(mgr.stockings().flight().launch(true) ? "stocking-flight-ok" : "stocking-flight-none"));
|
||||
}
|
||||
default -> mgr.stockings().use(p);
|
||||
}
|
||||
}
|
||||
case "hat" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
|
||||
mgr.hats().give(p);
|
||||
}
|
||||
case "quiz" -> quiz(s, a);
|
||||
case "wish" -> wish(s, a);
|
||||
case "wishes" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
|
||||
if (!admin(s)) return true;
|
||||
mgr.wishes().openMenu(p, 0);
|
||||
}
|
||||
case "market" -> market(s, a);
|
||||
case "portal" -> portal(s, a);
|
||||
case "give" -> giveCrystal(s, a);
|
||||
case "gifts" -> {
|
||||
if (!admin(s)) return true;
|
||||
mgr.trees().spawnPresentsEverywhere();
|
||||
Bukkit.broadcastMessage(mgr.prefixed(mgr.raw("gifts-spawned")));
|
||||
}
|
||||
case "addhand" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
|
||||
if (!admin(s)) return true;
|
||||
s.sendMessage(mgr.msg(mgr.trees().addGift(p.getInventory().getItemInMainHand()) ? "addhand-ok" : "addhand-empty"));
|
||||
}
|
||||
case "end" -> {
|
||||
if (!admin(s)) return true;
|
||||
mgr.trees().setEnded(true);
|
||||
s.sendMessage(mgr.msg("end-set"));
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!s.hasPermission("christmasplus.christmas.reload") && !s.hasPermission("xmas.admin")) {
|
||||
s.sendMessage(mgr.msg("no-permission"));
|
||||
return true;
|
||||
}
|
||||
ChristmasManager.reload();
|
||||
s.sendMessage(mgr.msg("reloaded"));
|
||||
}
|
||||
default -> s.sendMessage(mgr.msg("usage"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ---------- Plaetzchen ----------
|
||||
|
||||
private void cookie(CommandSender s, String[] a) {
|
||||
if (!admin(s)) return;
|
||||
CookieManager cm = mgr.cookies();
|
||||
if (a.length >= 2 && a[1].equalsIgnoreCase("list")) {
|
||||
s.sendMessage(mgr.prefixed(String.join(", ", cm.ids())));
|
||||
return;
|
||||
}
|
||||
if (a.length < 4 || !a[1].equalsIgnoreCase("give")) { s.sendMessage(mgr.msg("cookie-usage")); return; }
|
||||
Player t = Bukkit.getPlayerExact(a[2]);
|
||||
if (t == null) { s.sendMessage(mgr.msg("player-offline")); return; }
|
||||
int amount = a.length > 4 ? Math.max(1, Math.min(64, ChristmasManager.toInt(a[4], 1))) : 1;
|
||||
ItemStack it = cm.create(a[3], amount);
|
||||
if (it == null) { s.sendMessage(mgr.msg("cookie-unknown", "%id%", a[3])); return; }
|
||||
RewardUtil.give(t, it);
|
||||
t.sendMessage(mgr.msg("cookie-given", "%amount%", String.valueOf(amount), "%id%", a[3]));
|
||||
}
|
||||
|
||||
// ---------- Wichteln ----------
|
||||
|
||||
private void wichteln(CommandSender s, String[] a) {
|
||||
WichtelnManager w = mgr.wichteln();
|
||||
String sub = a.length > 1 ? a[1].toLowerCase() : "status";
|
||||
boolean adminCmd = sub.equals("open") || sub.equals("draw") || sub.equals("deliver") || sub.equals("reset");
|
||||
if (adminCmd && !admin(s)) return;
|
||||
if (!adminCmd && !perm(s, "xmas.use")) return;
|
||||
|
||||
switch (sub) {
|
||||
case "open" -> {
|
||||
if (!w.open()) { s.sendMessage(mgr.msg("wi-wrong-state")); return; }
|
||||
Bukkit.broadcastMessage(mgr.msg("wi-open"));
|
||||
}
|
||||
case "draw" -> {
|
||||
if (w.state() != WichtelnManager.State.OPEN) { s.sendMessage(mgr.msg("wi-wrong-state")); return; }
|
||||
if (!w.draw()) { s.sendMessage(mgr.msg("wi-too-few")); return; }
|
||||
Bukkit.broadcastMessage(mgr.msg("wi-drawn"));
|
||||
}
|
||||
case "deliver" -> {
|
||||
int n = w.deliver();
|
||||
if (n < 0) { s.sendMessage(mgr.msg("wi-wrong-state")); return; }
|
||||
Bukkit.broadcastMessage(mgr.msg("wi-delivered", "%count%", String.valueOf(n)));
|
||||
}
|
||||
case "reset" -> { w.reset(); s.sendMessage(mgr.msg("wi-reset")); }
|
||||
case "join" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
if (w.state() != WichtelnManager.State.OPEN) { s.sendMessage(mgr.msg("wi-wrong-state")); return; }
|
||||
boolean already = w.joined(p.getUniqueId());
|
||||
w.join(p);
|
||||
s.sendMessage(mgr.msg(already ? "wi-already" : "wi-joined"));
|
||||
}
|
||||
case "leave" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
s.sendMessage(mgr.msg(w.leave(p) ? "wi-left" : "wi-not-joined"));
|
||||
}
|
||||
case "who" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
String name = w.targetName(p.getUniqueId());
|
||||
s.sendMessage(name.isEmpty() ? mgr.msg("wi-not-joined") : mgr.msg("wi-who", "%name%", name));
|
||||
}
|
||||
case "gift" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
String key = w.setGift(p);
|
||||
s.sendMessage(mgr.msg(key, "%name%", w.targetName(p.getUniqueId())));
|
||||
}
|
||||
default -> s.sendMessage(mgr.msg("wi-status", "%state%", w.state().name(),
|
||||
"%count%", String.valueOf(w.participantCount()), "%gifts%", String.valueOf(w.giftCount())));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Adventskranz ----------
|
||||
|
||||
private void wreath(CommandSender s, String[] a) {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
if (!admin(s)) return;
|
||||
org.bukkit.block.Block target = p.getTargetBlockExact(8);
|
||||
if (target == null) { s.sendMessage(mgr.msg("deco-look")); return; }
|
||||
String sub = a.length > 1 ? a[1].toLowerCase() : "";
|
||||
if (sub.equals("create")) {
|
||||
mgr.wreaths().create(target);
|
||||
s.sendMessage(mgr.msg("wreath-created"));
|
||||
} else if (sub.equals("remove")) {
|
||||
s.sendMessage(mgr.msg(mgr.wreaths().removeNear(target) ? "wreath-removed" : "wreath-none"));
|
||||
} else {
|
||||
s.sendMessage(mgr.msg("wreath-usage"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Schneeball-Spiel ----------
|
||||
|
||||
private void snowball(CommandSender s, String[] a) {
|
||||
SnowballGame g = mgr.snowball();
|
||||
String sub = a.length > 1 ? a[1].toLowerCase() : "top";
|
||||
if (sub.equals("top")) {
|
||||
List<String> lines = g.topLines(5);
|
||||
if (lines.isEmpty()) s.sendMessage(mgr.msg("sb-no-scores")); else lines.forEach(s::sendMessage);
|
||||
return;
|
||||
}
|
||||
if (!admin(s)) return;
|
||||
switch (sub) {
|
||||
case "arena" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
int r = a.length > 2 ? ChristmasManager.toInt(a[2], 15) : 15;
|
||||
g.setArena(p.getLocation(), r);
|
||||
s.sendMessage(mgr.msg("sb-arena-set", "%radius%", String.valueOf(Math.max(3, r))));
|
||||
}
|
||||
case "start" -> {
|
||||
if (!g.hasArena()) { s.sendMessage(mgr.msg("sb-no-arena")); return; }
|
||||
int time = mgr.cfg().getInt("snowball.duration", 180);
|
||||
if (a.length > 2 && ChristmasManager.parseDuration(a[2]) > 0) time = ChristmasManager.parseDuration(a[2]);
|
||||
if (!g.start(time)) s.sendMessage(mgr.msg("sb-already"));
|
||||
}
|
||||
case "stop" -> {
|
||||
if (!g.isActive()) { s.sendMessage(mgr.msg("sb-not-active")); return; }
|
||||
g.stop(true);
|
||||
}
|
||||
default -> s.sendMessage(mgr.msg("sb-usage"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Deko ----------
|
||||
|
||||
private void deco(CommandSender s, String[] a) {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
if (!admin(s)) return;
|
||||
if (a.length > 1 && a[1].equalsIgnoreCase("undo")) mgr.deco().undoLast(p);
|
||||
else mgr.deco().open(p);
|
||||
}
|
||||
|
||||
|
||||
// ---------- Quiz ----------
|
||||
|
||||
private void quiz(CommandSender s, String[] a) {
|
||||
if (!admin(s)) return;
|
||||
QuizManager q = mgr.quiz();
|
||||
String sub = a.length > 1 ? a[1].toLowerCase() : "";
|
||||
switch (sub) {
|
||||
case "start" -> {
|
||||
int idx = a.length > 2 ? ChristmasManager.toInt(a[2], 0) - 1 : -1;
|
||||
if (q.isActive()) { s.sendMessage(mgr.msg("quiz-already")); return; }
|
||||
if (!q.ask(idx)) s.sendMessage(mgr.msg("quiz-none"));
|
||||
}
|
||||
case "skip", "stop" -> s.sendMessage(mgr.msg(q.skip() ? "quiz-stopped" : "quiz-not-active"));
|
||||
case "count" -> s.sendMessage(mgr.prefixed(String.valueOf(q.questionCount())));
|
||||
default -> s.sendMessage(mgr.msg("quiz-usage"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Wunschzettel ----------
|
||||
|
||||
private void wish(CommandSender s, String[] a) {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
if (!perm(s, "xmas.use")) return;
|
||||
if (a.length < 2) { s.sendMessage(mgr.msg("wish-usage")); return; }
|
||||
if (a[1].equalsIgnoreCase("list")) { mgr.wishes().list(p); return; }
|
||||
if (a[1].equalsIgnoreCase("clear")) {
|
||||
s.sendMessage(mgr.msg("wish-cleared", "%count%", String.valueOf(mgr.wishes().clearOwn(p))));
|
||||
return;
|
||||
}
|
||||
String text = String.join(" ", Arrays.copyOfRange(a, 1, a.length));
|
||||
s.sendMessage(mgr.msg(mgr.wishes().add(p, text), "%max%", String.valueOf(mgr.cfg().getInt("wishes.max-length", 120))));
|
||||
}
|
||||
|
||||
// ---------- Marktstaende ----------
|
||||
|
||||
private void market(CommandSender s, String[] a) {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
String sub = a.length > 1 ? a[1].toLowerCase() : "";
|
||||
if (sub.equals("back")) { // fuer alle Spieler
|
||||
s.sendMessage(mgr.msg(mgr.portals().back(p)));
|
||||
return;
|
||||
}
|
||||
if (!admin(s)) return;
|
||||
switch (sub) {
|
||||
case "list" -> s.sendMessage(mgr.prefixed(String.join(", ", mgr.market().stallIds())));
|
||||
case "remove" -> s.sendMessage(mgr.msg(mgr.market().removeNearest(p, 6) ? "market-removed" : "market-none"));
|
||||
case "set" -> {
|
||||
mgr.market().setSpawn(p.getLocation());
|
||||
s.sendMessage(mgr.msg("market-set"));
|
||||
}
|
||||
case "unset" -> {
|
||||
mgr.market().clearSpawn();
|
||||
s.sendMessage(mgr.msg("market-unset"));
|
||||
}
|
||||
case "tp" -> {
|
||||
Location dest = mgr.market().spawn();
|
||||
if (dest == null) { s.sendMessage(mgr.msg("portal-no-market")); return; }
|
||||
p.teleport(dest);
|
||||
}
|
||||
case "spawn" -> {
|
||||
if (a.length < 3) { s.sendMessage(mgr.msg("market-usage")); return; }
|
||||
if (!mgr.market().spawn(p.getLocation(), a[2])) { s.sendMessage(mgr.msg("market-unknown", "%id%", a[2])); return; }
|
||||
s.sendMessage(mgr.msg("market-spawned"));
|
||||
}
|
||||
default -> s.sendMessage(mgr.msg("market-usage"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Portale ----------
|
||||
|
||||
private void portal(CommandSender s, String[] a) {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
if (!perm(s, "xmas.use")) return;
|
||||
String sub = a.length > 1 ? a[1].toLowerCase() : "";
|
||||
org.bukkit.block.Block target = p.getTargetBlockExact(6);
|
||||
switch (sub) {
|
||||
case "create" -> {
|
||||
if (target == null) { s.sendMessage(mgr.msg("deco-look")); return; }
|
||||
s.sendMessage(mgr.msg(mgr.portals().create(p, target), "%cost%",
|
||||
String.join(", ", mgr.cfg().getStringList("portal.cost")),
|
||||
"%limit%", String.valueOf(mgr.cfg().getInt("portal.limit-per-player", 1))));
|
||||
}
|
||||
case "remove" -> {
|
||||
if (target == null) { s.sendMessage(mgr.msg("portal-none")); return; }
|
||||
s.sendMessage(mgr.msg(mgr.portals().remove(p, target)));
|
||||
}
|
||||
case "info" -> s.sendMessage(mgr.msg("portal-info", "%cost%", String.join(", ", mgr.cfg().getStringList("portal.cost")),
|
||||
"%count%", String.valueOf(mgr.portals().countOf(p.getUniqueId())),
|
||||
"%limit%", String.valueOf(mgr.cfg().getInt("portal.limit-per-player", 1))));
|
||||
default -> s.sendMessage(mgr.msg("portal-usage"));
|
||||
}
|
||||
}
|
||||
|
||||
private void snowman(CommandSender s, String[] a) {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
if (!admin(s)) return;
|
||||
if (a.length < 2) { s.sendMessage(mgr.msg("snowman-usage")); return; }
|
||||
switch (a[1].toLowerCase()) {
|
||||
case "spawn" -> {
|
||||
int from = 2;
|
||||
Boolean walk = null;
|
||||
if (a.length > 2) {
|
||||
String m = a[2].toLowerCase();
|
||||
if (m.equals("walk") || m.equals("laufen")) { walk = true; from = 3; }
|
||||
else if (m.equals("stand") || m.equals("stehen")) { walk = false; from = 3; }
|
||||
}
|
||||
String name = a.length > from ? String.join(" ", Arrays.copyOfRange(a, from, a.length)) : "";
|
||||
mgr.snowmen().spawnDeco(p.getLocation(), name, walk);
|
||||
s.sendMessage(mgr.msg("snowman-spawned"));
|
||||
}
|
||||
case "remove" -> s.sendMessage(mgr.msg(mgr.snowmen().removeNearestDeco(p, 6) ? "snowman-removed" : "snowman-none"));
|
||||
default -> s.sendMessage(mgr.msg("snowman-usage"));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean admin(CommandSender s) {
|
||||
return perm(s, "xmas.admin");
|
||||
}
|
||||
|
||||
private void giveCrystal(CommandSender s, String[] a) {
|
||||
if (!admin(s)) return;
|
||||
if (a.length < 2) { s.sendMessage(mgr.msg("usage")); return; }
|
||||
Player t = Bukkit.getPlayerExact(a[1]);
|
||||
if (t == null) { s.sendMessage(mgr.msg("player-offline")); return; }
|
||||
int amount = 1;
|
||||
if (a.length > 2 && a[2].matches("\\d+")) amount = Math.max(1, Math.min(64, Integer.parseInt(a[2])));
|
||||
RewardUtil.give(t, mgr.trees().crystal(amount));
|
||||
t.sendMessage(mgr.msg("crystal-given", "%amount%", String.valueOf(amount)));
|
||||
if (s != t) s.sendMessage(mgr.msg("crystal-gave", "%player%", t.getName(), "%amount%", String.valueOf(amount)));
|
||||
}
|
||||
|
||||
private boolean perm(CommandSender s, String p) {
|
||||
if (s.hasPermission(p)) return true;
|
||||
s.sendMessage(mgr.msg("no-permission"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- Snow ----------
|
||||
|
||||
private void snow(CommandSender s) {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
if (!perm(s, "christmasplus.christmas.snow")) return;
|
||||
if (!mgr.cfg().getBoolean("snow.enable", true)) { s.sendMessage(mgr.msg("snow-disabled-global")); return; }
|
||||
s.sendMessage(mgr.msg(mgr.toggleSnow(p) ? "snow-on" : "snow-off"));
|
||||
}
|
||||
|
||||
// ---------- Santa ----------
|
||||
|
||||
private void santa(CommandSender s, String[] a) {
|
||||
if (!perm(s, "christmasplus.christmas.santa")) return;
|
||||
if (a.length < 2) { s.sendMessage(mgr.msg("santa-usage")); return; }
|
||||
SantaEvent santa = mgr.santa();
|
||||
switch (a[1].toLowerCase()) {
|
||||
case "stop" -> {
|
||||
if (!santa.isActive()) s.sendMessage(mgr.msg("santa-not-active"));
|
||||
else santa.stop(true);
|
||||
}
|
||||
case "teleport" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
|
||||
if (!santa.isActive()) { s.sendMessage(mgr.msg("santa-not-active")); return; }
|
||||
p.teleport(santa.getLocation());
|
||||
p.sendMessage(mgr.msg("santa-teleported"));
|
||||
}
|
||||
case "start" -> {
|
||||
if (santa.isActive()) { s.sendMessage(mgr.msg("santa-already")); return; }
|
||||
int time = mgr.cfg().getInt("santa.duration", 300);
|
||||
int i = 2;
|
||||
if (a.length > i && ChristmasManager.parseDuration(a[i]) > 0) time = ChristmasManager.parseDuration(a[i++]);
|
||||
Location where = null;
|
||||
if (a.length > i && a[i].equalsIgnoreCase("random")) {
|
||||
where = santa.randomLocation();
|
||||
if (where == null) { s.sendMessage(mgr.msg("santa-no-locations")); return; }
|
||||
} else if (a.length >= i + 4) {
|
||||
World w = Bukkit.getWorld(a[i]);
|
||||
try {
|
||||
if (w != null) where = new Location(w, Double.parseDouble(a[i + 1]),
|
||||
Double.parseDouble(a[i + 2]), Double.parseDouble(a[i + 3]));
|
||||
} catch (NumberFormatException ignored) { }
|
||||
} else if (s instanceof Player p) {
|
||||
where = p.getLocation();
|
||||
}
|
||||
if (where == null) { s.sendMessage(mgr.msg("santa-usage")); return; }
|
||||
santa.start(time, where);
|
||||
s.sendMessage(mgr.msg("santa-started-sender", "%duration%", SantaEvent.formatDuration(time)));
|
||||
}
|
||||
default -> s.sendMessage(mgr.msg("santa-usage"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Cracker ----------
|
||||
|
||||
private boolean cracker(CommandSender s, String[] a) {
|
||||
CrackerManager cm = mgr.crackers();
|
||||
if (a.length == 0) { s.sendMessage(mgr.msg("cracker-usage")); return true; }
|
||||
switch (a[0].toLowerCase()) {
|
||||
case "list" -> {
|
||||
if (!perm(s, "christmasplus.cracker.list")) return true;
|
||||
s.sendMessage(mgr.prefixed(String.join(", ", cm.ids())));
|
||||
}
|
||||
case "get" -> {
|
||||
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
|
||||
if (!perm(s, "christmasplus.cracker.get")) return true;
|
||||
String id = cm.firstId();
|
||||
int amount = 1;
|
||||
for (int i = 1; i < a.length; i++) {
|
||||
if (a[i].matches("\\d+")) amount = Integer.parseInt(a[i]); else id = a[i];
|
||||
}
|
||||
if (amount > 1 && !perm(s, "christmasplus.cracker.get.multiple")) return true;
|
||||
giveCracker(s, p, id, amount);
|
||||
}
|
||||
case "give" -> {
|
||||
if (!perm(s, "christmasplus.cracker.give")) return true;
|
||||
if (a.length < 2) { s.sendMessage(mgr.msg("cracker-usage")); return true; }
|
||||
Player t = Bukkit.getPlayerExact(a[1]);
|
||||
if (t == null) { s.sendMessage(mgr.msg("player-not-found")); return true; }
|
||||
String id = cm.firstId();
|
||||
int amount = 1;
|
||||
for (int i = 2; i < a.length; i++) {
|
||||
if (a[i].matches("\\d+")) amount = Integer.parseInt(a[i]); else id = a[i];
|
||||
}
|
||||
giveCracker(s, t, id, amount);
|
||||
}
|
||||
default -> s.sendMessage(mgr.msg("cracker-usage"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void giveCracker(CommandSender s, Player target, String id, int amount) {
|
||||
amount = Math.max(1, Math.min(amount, 64));
|
||||
ItemStack it = id == null ? null : mgr.crackers().create(id, amount);
|
||||
if (it == null) { s.sendMessage(mgr.msg("cracker-unknown", "%id%", String.valueOf(id))); return; }
|
||||
RewardUtil.give(target, it);
|
||||
target.sendMessage(mgr.msg("cracker-given", "%amount%", String.valueOf(amount), "%id%", id));
|
||||
if (s != target) s.sendMessage(mgr.msg("cracker-gave", "%player%", target.getName(),
|
||||
"%amount%", String.valueOf(amount), "%id%", id));
|
||||
}
|
||||
|
||||
// ---------- Tab ----------
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender s, Command cmd, String label, String[] a) {
|
||||
List<String> out = new ArrayList<>();
|
||||
if (cmd.getName().equalsIgnoreCase("cracker")) {
|
||||
if (a.length == 1) out.addAll(Arrays.asList("list", "get", "give"));
|
||||
else if (a[0].equalsIgnoreCase("give") && a.length == 2)
|
||||
Bukkit.getOnlinePlayers().forEach(p -> out.add(p.getName()));
|
||||
else out.addAll(mgr.crackers().ids());
|
||||
} else if (a.length == 1) {
|
||||
out.addAll(Arrays.asList("help", "snow", "santa", "give", "gifts", "addhand", "snowman", "countdown", "cookie", "wichteln", "wreath", "snowball", "deco", "stocking", "hat", "quiz", "wish", "wishes", "market", "portal", "end", "reload"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("santa")) {
|
||||
out.addAll(Arrays.asList("start", "stop", "teleport"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("stocking")) {
|
||||
out.addAll(Arrays.asList("place", "remove", "flight"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("snowman")) {
|
||||
out.addAll(Arrays.asList("spawn", "remove"));
|
||||
} else if (a.length == 3 && a[0].equalsIgnoreCase("snowman") && a[1].equalsIgnoreCase("spawn")) {
|
||||
out.addAll(Arrays.asList("stand", "walk"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("cookie")) {
|
||||
out.addAll(Arrays.asList("give", "list"));
|
||||
} else if (a.length == 4 && a[0].equalsIgnoreCase("cookie")) {
|
||||
out.addAll(mgr.cookies().ids());
|
||||
} else if (a.length == 3 && a[0].equalsIgnoreCase("cookie")) {
|
||||
Bukkit.getOnlinePlayers().forEach(p -> out.add(p.getName()));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("wichteln")) {
|
||||
out.addAll(Arrays.asList("join", "leave", "who", "gift", "status", "open", "draw", "deliver", "reset"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("wreath")) {
|
||||
out.addAll(Arrays.asList("create", "remove"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("snowball")) {
|
||||
out.addAll(Arrays.asList("top", "arena", "start", "stop"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("deco")) {
|
||||
out.add("undo");
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("portal")) {
|
||||
out.addAll(Arrays.asList("create", "remove", "info"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("quiz")) {
|
||||
out.addAll(Arrays.asList("start", "skip", "count"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("wish")) {
|
||||
out.addAll(Arrays.asList("list", "clear"));
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("market")) {
|
||||
out.addAll(Arrays.asList("spawn", "remove", "list", "set", "unset", "tp", "back"));
|
||||
} else if (a.length == 3 && a[0].equalsIgnoreCase("market") && a[1].equalsIgnoreCase("spawn")) {
|
||||
out.addAll(mgr.market().stallIds());
|
||||
} else if (a.length == 2 && a[0].equalsIgnoreCase("give")) {
|
||||
Bukkit.getOnlinePlayers().forEach(p -> out.add(p.getName()));
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String last = a[a.length - 1].toLowerCase();
|
||||
out.removeIf(x -> !x.toLowerCase().startsWith(last));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.Adventskalender;
|
||||
import de.mviper.adventskalender.RewardUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Zusatz-Belohnungen fuer Kalendertage (Christmas+ Funktionen). Im Abschnitt rewards.<Tag> der config.yml moeglich:
|
||||
* <pre>
|
||||
* commands: ["give %player% apple 3"]
|
||||
* permissions: ["essentials.fly"] # wird ueber calendar-extras.permission-command vergeben
|
||||
* multiple: ["DIAMOND:2", "COOKIE:8"] # mehrere Geschenke am Tag
|
||||
* random: # zufaelliges Zusatzgeschenk (chance = Gewicht)
|
||||
* - { chance: 50, item: "GOLD_INGOT:4" }
|
||||
* - { chance: 50, command: "say Hallo %player%" }
|
||||
* cracker: "cracker:2" # Cracker-ID und Anzahl
|
||||
* crystal: 1 # Weihnachts-Kristall
|
||||
* groups: # Extra-Geschenke nur fuer bestimmte Gruppen (z.B. Donor)
|
||||
* donor:
|
||||
* permission: "christmasplus.donor"
|
||||
* material: EMERALD # optional, wie das normale Geschenk
|
||||
* amount: 3
|
||||
* multiple: ["DIAMOND:1"] # und alle Schluessel von oben
|
||||
* </pre>
|
||||
*/
|
||||
public final class ChristmasExtras {
|
||||
private ChristmasExtras() {}
|
||||
|
||||
public static void give(Player p, String path) {
|
||||
ConfigurationSection s = Adventskalender.getInstance().getConfig().getConfigurationSection(path);
|
||||
if (s == null) return;
|
||||
apply(p, s);
|
||||
|
||||
ConfigurationSection groups = s.getConfigurationSection("groups");
|
||||
if (groups == null) return;
|
||||
for (String name : groups.getKeys(false)) {
|
||||
ConfigurationSection g = groups.getConfigurationSection(name);
|
||||
if (g == null) continue;
|
||||
String perm = g.getString("permission", "");
|
||||
if (!perm.isBlank() && !p.hasPermission(perm)) continue;
|
||||
if (g.contains("material")) {
|
||||
ItemStack it = RewardUtil.build(path + ".groups." + name, "Gruppe " + name);
|
||||
if (it != null) RewardUtil.give(p, it);
|
||||
}
|
||||
apply(p, g);
|
||||
}
|
||||
}
|
||||
|
||||
/** Wendet commands, permissions, multiple, random, cracker und crystal eines Abschnitts an. */
|
||||
public static void apply(Player p, ConfigurationSection s) {
|
||||
for (String cmd : s.getStringList("commands")) run(p, cmd);
|
||||
for (String perm : s.getStringList("permissions")) grantPermission(p, perm);
|
||||
|
||||
for (String spec : s.getStringList("multiple")) {
|
||||
ItemStack it = ChristmasManager.parseItem(spec);
|
||||
if (it != null) RewardUtil.give(p, it);
|
||||
}
|
||||
|
||||
List<Map<?, ?>> random = s.getMapList("random");
|
||||
double sum = 0;
|
||||
for (Map<?, ?> r : random) sum += weight(r);
|
||||
if (sum > 0) {
|
||||
double roll = ChristmasManager.RANDOM.nextDouble() * sum;
|
||||
for (Map<?, ?> r : random) {
|
||||
roll -= weight(r);
|
||||
if (roll > 0) continue;
|
||||
applyMap(p, r);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ChristmasManager mgr = ChristmasManager.get();
|
||||
if (mgr == null) return;
|
||||
String cracker = s.getString("cracker");
|
||||
if (cracker != null && !cracker.isBlank()) {
|
||||
String[] parts = cracker.split(":");
|
||||
int amount = 1;
|
||||
if (parts.length > 1) {
|
||||
try { amount = Integer.parseInt(parts[1].trim()); } catch (NumberFormatException ignored) { }
|
||||
}
|
||||
ItemStack it = mgr.crackers().create(parts[0].trim(), amount);
|
||||
if (it != null) RewardUtil.give(p, it);
|
||||
}
|
||||
int crystal = s.getInt("crystal", 0);
|
||||
if (crystal > 0) RewardUtil.give(p, mgr.trees().crystal(crystal));
|
||||
}
|
||||
|
||||
/** Eintrag mit den Schluesseln item, command, permission. */
|
||||
public static void applyMap(Player p, Map<?, ?> r) {
|
||||
if (r.get("item") != null) {
|
||||
ItemStack it = ChristmasManager.parseItem(String.valueOf(r.get("item")));
|
||||
if (it != null) RewardUtil.give(p, it);
|
||||
}
|
||||
if (r.get("command") != null) run(p, String.valueOf(r.get("command")));
|
||||
if (r.get("permission") != null) grantPermission(p, String.valueOf(r.get("permission")));
|
||||
}
|
||||
|
||||
public static void grantPermission(Player p, String permission) {
|
||||
ChristmasManager mgr = ChristmasManager.get();
|
||||
String template = mgr == null ? "" : mgr.cfg().getString("permission-command", "");
|
||||
if (template == null || template.isBlank()) {
|
||||
Adventskalender.getInstance().getLogger().warning(
|
||||
"Permission '" + permission + "' soll vergeben werden, aber 'permission-command' ist in christmas.yml leer.");
|
||||
return;
|
||||
}
|
||||
run(p, template.replace("%permission%", permission));
|
||||
}
|
||||
|
||||
private static void run(Player p, String cmd) {
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd.replace("%player%", p.getName()));
|
||||
}
|
||||
|
||||
private static double weight(Map<?, ?> r) {
|
||||
Object c = r.get("chance");
|
||||
return c instanceof Number n ? Math.max(0, n.doubleValue()) : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.entity.Snowball;
|
||||
import org.bukkit.entity.Snowman;
|
||||
import org.bukkit.event.entity.ProjectileHitEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.event.player.PlayerItemConsumeEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.event.player.PlayerShearEntityEvent;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockBurnEvent;
|
||||
import org.bukkit.event.block.BlockExplodeEvent;
|
||||
import org.bukkit.event.block.BlockPistonExtendEvent;
|
||||
import org.bukkit.event.block.BlockPistonRetractEvent;
|
||||
import org.bukkit.event.block.LeavesDecayEvent;
|
||||
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.world.StructureGrowEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
public final class ChristmasListener implements Listener {
|
||||
private final ChristmasManager mgr;
|
||||
|
||||
ChristmasListener(ChristmasManager mgr) { this.mgr = mgr; }
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH)
|
||||
public void onInteract(PlayerInteractEvent e) {
|
||||
if (e.getHand() != EquipmentSlot.HAND) return;
|
||||
Action a = e.getAction();
|
||||
Player p = e.getPlayer();
|
||||
MagicTreeManager trees = mgr.trees();
|
||||
|
||||
// Cracker oeffnen
|
||||
if (a == Action.RIGHT_CLICK_AIR || a == Action.RIGHT_CLICK_BLOCK) {
|
||||
ItemStack inHand = e.getItem();
|
||||
String id = mgr.crackers().idOf(inHand);
|
||||
if (id != null) {
|
||||
e.setCancelled(true);
|
||||
inHand.setAmount(inHand.getAmount() - 1);
|
||||
mgr.crackers().open(p, id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (a != Action.RIGHT_CLICK_BLOCK || e.getClickedBlock() == null) return;
|
||||
Block b = e.getClickedBlock();
|
||||
|
||||
// Geschenk unter einer Socke oeffnen
|
||||
if (mgr.stockings().isPresentBlock(b)) {
|
||||
e.setCancelled(true);
|
||||
mgr.stockings().openPresent(p, b);
|
||||
return;
|
||||
}
|
||||
|
||||
// Geschenk oeffnen
|
||||
if (trees.isPresentBlock(b)) {
|
||||
e.setCancelled(true);
|
||||
trees.openPresent(p, trees.treeOfPresent(b), b);
|
||||
return;
|
||||
}
|
||||
|
||||
// Kristall auf Setzling
|
||||
if (trees.isCrystal(e.getItem())) {
|
||||
e.setCancelled(true);
|
||||
if (Tag.SAPLINGS.isTagged(b.getType()) && !trees.isTreeBlock(b)) {
|
||||
ItemStack inHand = e.getItem();
|
||||
if (!p.hasPermission("xmas.use")) { p.sendMessage(mgr.msg("no-permission")); return; }
|
||||
int before = trees.treeCount();
|
||||
trees.plant(p, b);
|
||||
if (trees.treeCount() > before) inHand.setAmount(inHand.getAmount() - 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Baum: Rechtsklick = wachsen / Info
|
||||
if (trees.isTreeBlock(b)) {
|
||||
e.setCancelled(true);
|
||||
MagicTreeManager.Tree t = trees.treeOf(b);
|
||||
if (p.isSneaking()) trees.info(p, t);
|
||||
else trees.click(p, t);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH)
|
||||
public void onBreak(BlockBreakEvent e) {
|
||||
MagicTreeManager trees = mgr.trees();
|
||||
Block b = e.getBlock();
|
||||
if (mgr.stockings().isPresentBlock(b)) {
|
||||
e.setCancelled(true);
|
||||
mgr.stockings().openPresent(e.getPlayer(), b);
|
||||
return;
|
||||
}
|
||||
if (trees.isPresentBlock(b)) {
|
||||
e.setCancelled(true);
|
||||
trees.openPresent(e.getPlayer(), trees.treeOfPresent(b), b);
|
||||
return;
|
||||
}
|
||||
if (trees.isTreeBlock(b)) {
|
||||
e.setCancelled(true);
|
||||
trees.breakAttempt(e.getPlayer(), trees.treeOf(b), b);
|
||||
return;
|
||||
}
|
||||
if ((mgr.wreaths().isProtected(b) || mgr.portals().isProtected(b)) && !e.getPlayer().hasPermission("xmas.admin")) {
|
||||
e.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDrop(PlayerDropItemEvent e) {
|
||||
Item item = e.getItemDrop();
|
||||
Player p = e.getPlayer();
|
||||
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
|
||||
if (item.isValid() && p.isOnline()) mgr.trees().absorb(p, item);
|
||||
}, 40L);
|
||||
}
|
||||
|
||||
// ---------- Schutz der Baeume und Geschenke ----------
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPistonExtend(BlockPistonExtendEvent e) {
|
||||
for (Block b : e.getBlocks()) if (mgr.isProtected(b)) { e.setCancelled(true); return; }
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPistonRetract(BlockPistonRetractEvent e) {
|
||||
for (Block b : e.getBlocks()) if (mgr.isProtected(b)) { e.setCancelled(true); return; }
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onExplode(EntityExplodeEvent e) {
|
||||
e.blockList().removeIf(b -> mgr.isProtected(b));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onBlockExplode(BlockExplodeEvent e) {
|
||||
e.blockList().removeIf(b -> mgr.isProtected(b));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onBurn(BlockBurnEvent e) {
|
||||
if (mgr.isProtected(e.getBlock())) e.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onDecay(LeavesDecayEvent e) {
|
||||
if (mgr.isProtected(e.getBlock())) e.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onStructureGrow(StructureGrowEvent e) {
|
||||
if (mgr.isProtected(e.getLocation().getBlock())) e.setCancelled(true);
|
||||
}
|
||||
|
||||
// ---------- Schneemaenner ----------
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onSnowmanBuilt(CreatureSpawnEvent e) {
|
||||
if (e.getSpawnReason() == CreatureSpawnEvent.SpawnReason.BUILD_SNOWMAN && e.getEntity() instanceof Snowman s) {
|
||||
// Eine Tick spaeter, damit der Schneemann sicher existiert
|
||||
Bukkit.getScheduler().runTask(mgr.plugin(), () -> { if (s.isValid()) mgr.snowmen().onBuilt(s); });
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onShear(PlayerShearEntityEvent e) {
|
||||
if (mgr.snowmen().isDeco(e.getEntity())) e.setCancelled(true);
|
||||
}
|
||||
|
||||
// ---------- Join, Plaetzchen, Schneeball, Deko-Menue ----------
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent e) {
|
||||
Player p = e.getPlayer();
|
||||
mgr.countdown().onJoin(p);
|
||||
mgr.streaks().onJoin(p);
|
||||
mgr.greetings().onJoin(p);
|
||||
mgr.hats().onJoin(p);
|
||||
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
|
||||
if (!p.isOnline()) return;
|
||||
mgr.stockings().tryDeliver(p, false);
|
||||
mgr.wishes().deliver(p);
|
||||
}, 140L);
|
||||
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> { if (p.isOnline()) mgr.wichteln().onJoin(p); }, 100L);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onConsume(PlayerItemConsumeEvent e) {
|
||||
String id = mgr.cookies().idOf(e.getItem());
|
||||
if (id != null) mgr.cookies().eat(e.getPlayer(), id);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onSnowballHit(ProjectileHitEvent e) {
|
||||
if (e.getEntity() instanceof Snowball sb && sb.getShooter() instanceof Player shooter
|
||||
&& e.getHitEntity() instanceof Player victim) {
|
||||
mgr.snowball().onHit(shooter, victim);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onMenuClick(InventoryClickEvent e) {
|
||||
if (!(e.getInventory().getHolder() instanceof DecoManager.Menu)) return;
|
||||
e.setCancelled(true);
|
||||
if (e.getWhoClicked() instanceof Player p && e.getClickedInventory() == e.getInventory()
|
||||
&& p.hasPermission("xmas.admin")) {
|
||||
mgr.deco().click(p, e.getRawSlot());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onMenuDrag(InventoryDragEvent e) {
|
||||
if (e.getInventory().getHolder() instanceof DecoManager.Menu) e.setCancelled(true);
|
||||
}
|
||||
|
||||
// ---------- Quiz, Feuerwerk, Wunschzettel-Menue ----------
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onChat(AsyncPlayerChatEvent e) {
|
||||
if (mgr.quiz().onChat(e.getPlayer(), e.getMessage())) e.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onSnowmanTrail(org.bukkit.event.block.EntityBlockFormEvent e) {
|
||||
mgr.snowmen().onSnowForm(e);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onFireworkDamage(EntityDamageByEntityEvent e) {
|
||||
if (mgr.greetings().isOurs(e.getDamager())) e.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onWishMenuClick(InventoryClickEvent e) {
|
||||
if (!(e.getInventory().getHolder() instanceof WishManager.Menu menu)) return;
|
||||
e.setCancelled(true);
|
||||
if (e.getWhoClicked() instanceof Player p && e.getClickedInventory() == e.getInventory()
|
||||
&& p.hasPermission("xmas.admin")) {
|
||||
mgr.wishes().click(p, menu, e.getRawSlot(), e.isRightClick(), e.isShiftClick());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onWishMenuDrag(InventoryDragEvent e) {
|
||||
if (e.getInventory().getHolder() instanceof WishManager.Menu) e.setCancelled(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.Adventskalender;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** Zentrale Verwaltung der Weihnachts-Module: Schnee, Santa, Cracker, Magic Tree. */
|
||||
public final class ChristmasManager {
|
||||
private static ChristmasManager instance;
|
||||
public static final Random RANDOM = new Random();
|
||||
|
||||
private final Adventskalender plugin;
|
||||
private YamlConfiguration cfg;
|
||||
private final NamespacedKey snowOffKey;
|
||||
private BukkitTask snowTask;
|
||||
|
||||
private SantaEvent santa;
|
||||
private CrackerManager crackers;
|
||||
private MagicTreeManager trees;
|
||||
private SnowmanManager snowmen;
|
||||
private CookieManager cookies;
|
||||
private CountdownManager countdown;
|
||||
private WichtelnManager wichteln;
|
||||
private WreathManager wreaths;
|
||||
private SnowballGame snowball;
|
||||
private DecoManager deco;
|
||||
private LoginStreakManager streaks;
|
||||
private GreetingManager greetings;
|
||||
private StockingManager stockings;
|
||||
private HatManager hats;
|
||||
private QuizManager quiz;
|
||||
private WishManager wishes;
|
||||
private MarketManager market;
|
||||
private PortalManager portals;
|
||||
private YamlConfiguration msgs;
|
||||
|
||||
private ChristmasManager(Adventskalender plugin) {
|
||||
this.plugin = plugin;
|
||||
this.snowOffKey = new NamespacedKey(plugin, "snow_off");
|
||||
}
|
||||
|
||||
public static void enable(Adventskalender plugin) {
|
||||
instance = new ChristmasManager(plugin);
|
||||
instance.loadConfig();
|
||||
instance.santa = new SantaEvent(instance);
|
||||
instance.crackers = new CrackerManager(instance);
|
||||
instance.trees = new MagicTreeManager(instance);
|
||||
instance.snowmen = new SnowmanManager(instance);
|
||||
instance.loadMessages();
|
||||
instance.cookies = new CookieManager(instance);
|
||||
instance.countdown = new CountdownManager(instance);
|
||||
instance.wichteln = new WichtelnManager(instance);
|
||||
instance.wreaths = new WreathManager(instance);
|
||||
instance.snowball = new SnowballGame(instance);
|
||||
instance.deco = new DecoManager(instance);
|
||||
instance.streaks = new LoginStreakManager(instance);
|
||||
instance.greetings = new GreetingManager(instance);
|
||||
instance.stockings = new StockingManager(instance);
|
||||
instance.hats = new HatManager(instance);
|
||||
instance.quiz = new QuizManager(instance);
|
||||
instance.wishes = new WishManager(instance);
|
||||
instance.market = new MarketManager(instance);
|
||||
instance.portals = new PortalManager(instance);
|
||||
instance.crackers.reload();
|
||||
instance.trees.load();
|
||||
instance.snowmen.start();
|
||||
instance.countdown.start();
|
||||
instance.wichteln.load();
|
||||
instance.wreaths.load();
|
||||
instance.snowball.load();
|
||||
instance.streaks.load();
|
||||
instance.greetings.start();
|
||||
instance.stockings.load();
|
||||
instance.quiz.start();
|
||||
instance.wishes.load();
|
||||
instance.market.load();
|
||||
instance.portals.load();
|
||||
instance.startSnow();
|
||||
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
|
||||
try {
|
||||
new XmasExpansion(instance).register();
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning("PlaceholderAPI-Platzhalter (xmas) konnten nicht registriert werden: " + t);
|
||||
}
|
||||
}
|
||||
|
||||
ChristmasCommand cmd = new ChristmasCommand(instance);
|
||||
plugin.getCommand("christmas").setExecutor(cmd);
|
||||
plugin.getCommand("christmas").setTabCompleter(cmd);
|
||||
plugin.getCommand("cracker").setExecutor(cmd);
|
||||
plugin.getCommand("cracker").setTabCompleter(cmd);
|
||||
plugin.getServer().getPluginManager().registerEvents(new ChristmasListener(instance), plugin);
|
||||
}
|
||||
|
||||
public static void disable() {
|
||||
if (instance == null) return;
|
||||
instance.santa.stop(false);
|
||||
instance.trees.save();
|
||||
instance.trees.shutdown();
|
||||
instance.snowmen.stop();
|
||||
instance.countdown.stop();
|
||||
instance.wichteln.save();
|
||||
instance.wreaths.stop();
|
||||
instance.snowball.shutdown();
|
||||
instance.streaks.save();
|
||||
instance.greetings.stop();
|
||||
instance.stockings.stop();
|
||||
instance.quiz.stop();
|
||||
instance.portals.stop();
|
||||
if (instance.snowTask != null) instance.snowTask.cancel();
|
||||
instance = null;
|
||||
}
|
||||
|
||||
public static void reload() {
|
||||
if (instance == null) return;
|
||||
instance.loadConfig();
|
||||
instance.loadMessages();
|
||||
instance.crackers.reload();
|
||||
instance.countdown.start();
|
||||
instance.quiz.start();
|
||||
instance.market.refreshAll();
|
||||
instance.startSnow();
|
||||
}
|
||||
|
||||
public static ChristmasManager get() { return instance; }
|
||||
|
||||
private void loadConfig() {
|
||||
File f = new File(plugin.getDataFolder(), "christmas.yml");
|
||||
if (!f.exists()) plugin.saveResource("christmas.yml", false);
|
||||
cfg = YamlConfiguration.loadConfiguration(f);
|
||||
try (java.io.InputStream in = plugin.getResource("christmas.yml")) {
|
||||
if (in != null) {
|
||||
cfg.setDefaults(YamlConfiguration.loadConfiguration(
|
||||
new java.io.InputStreamReader(in, java.nio.charset.StandardCharsets.UTF_8)));
|
||||
}
|
||||
} catch (java.io.IOException ignored) { }
|
||||
}
|
||||
|
||||
public Adventskalender plugin() { return plugin; }
|
||||
public YamlConfiguration cfg() { return cfg; }
|
||||
public SantaEvent santa() { return santa; }
|
||||
public CrackerManager crackers() { return crackers; }
|
||||
public MagicTreeManager trees() { return trees; }
|
||||
public SnowmanManager snowmen() { return snowmen; }
|
||||
public CookieManager cookies() { return cookies; }
|
||||
public CountdownManager countdown() { return countdown; }
|
||||
public WichtelnManager wichteln() { return wichteln; }
|
||||
public WreathManager wreaths() { return wreaths; }
|
||||
public SnowballGame snowball() { return snowball; }
|
||||
public DecoManager deco() { return deco; }
|
||||
public LoginStreakManager streaks() { return streaks; }
|
||||
public GreetingManager greetings() { return greetings; }
|
||||
public StockingManager stockings() { return stockings; }
|
||||
public HatManager hats() { return hats; }
|
||||
public QuizManager quiz() { return quiz; }
|
||||
public WishManager wishes() { return wishes; }
|
||||
public MarketManager market() { return market; }
|
||||
public PortalManager portals() { return portals; }
|
||||
|
||||
/** Baum-, Geschenk- und Kranzbloecke, die vor Zerstoerung geschuetzt sind. */
|
||||
public boolean isProtected(org.bukkit.block.Block b) {
|
||||
return trees.isProtected(b) || wreaths.isProtected(b) || portals.isProtected(b) || stockings.isProtected(b);
|
||||
}
|
||||
|
||||
// ---------- Nachrichten ----------
|
||||
|
||||
/** Laedt die Sprachdatei (christmas_messages_de.yml / _en.yml) passend zu general.language. */
|
||||
private void loadMessages() {
|
||||
String lang = plugin.getConfig().getString("general.language", "de").toLowerCase();
|
||||
if (!lang.matches("[a-z_]+")) lang = "de";
|
||||
String res = "christmas_messages_" + lang + ".yml";
|
||||
if (plugin.getResource(res) == null) res = "christmas_messages_de.yml";
|
||||
File f = new File(plugin.getDataFolder(), res);
|
||||
if (!f.exists()) plugin.saveResource(res, false);
|
||||
msgs = YamlConfiguration.loadConfiguration(f);
|
||||
try (java.io.InputStream in = plugin.getResource(res)) {
|
||||
if (in != null) {
|
||||
msgs.setDefaults(YamlConfiguration.loadConfiguration(
|
||||
new java.io.InputStreamReader(in, java.nio.charset.StandardCharsets.UTF_8)));
|
||||
}
|
||||
} catch (java.io.IOException ignored) { }
|
||||
}
|
||||
|
||||
public String raw(String key) {
|
||||
// Reihenfolge: Sprachdatei, alte Texte aus christmas.yml, sonst Fehlermeldung
|
||||
String text = msgs == null ? null : msgs.getString("messages." + key);
|
||||
if (text == null) text = cfg.getString("messages." + key);
|
||||
return ChatColor.translateAlternateColorCodes('&', text != null ? text : "&cMissing: " + key);
|
||||
}
|
||||
|
||||
public List<String> rawList(String key) {
|
||||
List<String> l = msgs == null ? List.of() : msgs.getStringList("messages." + key);
|
||||
if (l.isEmpty()) l = cfg.getStringList("messages." + key);
|
||||
List<String> out = new java.util.ArrayList<>();
|
||||
for (String x : l) out.add(ChatColor.translateAlternateColorCodes('&', x));
|
||||
return out;
|
||||
}
|
||||
|
||||
public String msg(String key, String... replacements) {
|
||||
String s = raw(key);
|
||||
for (int i = 0; i + 1 < replacements.length; i += 2) s = s.replace(replacements[i], replacements[i + 1]);
|
||||
return raw("prefix") + s;
|
||||
}
|
||||
|
||||
public String prefixed(String text) {
|
||||
return raw("prefix") + text;
|
||||
}
|
||||
|
||||
/** "300", "30s", "5m", "1h" -> Sekunden; -1 bei ungueltiger Eingabe. */
|
||||
public static int parseDuration(String s) {
|
||||
if (s == null || s.isBlank()) return -1;
|
||||
s = s.trim().toLowerCase();
|
||||
int mult = 1;
|
||||
char last = s.charAt(s.length() - 1);
|
||||
if (last == 's' || last == 'm' || last == 'h') {
|
||||
mult = last == 'm' ? 60 : last == 'h' ? 3600 : 1;
|
||||
s = s.substring(0, s.length() - 1);
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(s) * mult;
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static String color(String s) {
|
||||
return ChatColor.translateAlternateColorCodes('&', s);
|
||||
}
|
||||
|
||||
// ---------- Item-Hilfen ----------
|
||||
|
||||
/**
|
||||
* "DIAMOND:2" -> ItemStack, null wenn ungueltig. Zusaetzlich: "cracker:id:n", "cookie:id:n", "crystal:n".
|
||||
*/
|
||||
public static ItemStack parseItem(String spec) {
|
||||
if (spec == null) return null;
|
||||
String[] p = spec.trim().split(":");
|
||||
String head = p[0].trim().toLowerCase();
|
||||
ChristmasManager m = instance;
|
||||
if (m != null) {
|
||||
if ((head.equals("cracker") || head.equals("cookie")) && p.length > 1) {
|
||||
int a = p.length > 2 ? toInt(p[2], 1) : 1;
|
||||
return head.equals("cracker") ? m.crackers.create(p[1].trim(), a) : m.cookies.create(p[1].trim(), a);
|
||||
}
|
||||
if (head.equals("crystal")) return m.trees.crystal(p.length > 1 ? toInt(p[1], 1) : 1);
|
||||
if (head.equals("present")) return m.presentHead(p.length > 1 ? toInt(p[1], 1) : 1);
|
||||
}
|
||||
Material mat = Material.matchMaterial(p[0].trim());
|
||||
if (mat == null || !mat.isItem() || mat.isAir()) return null;
|
||||
return new ItemStack(mat, Math.max(1, p.length > 1 ? toInt(p[1], 1) : 1));
|
||||
}
|
||||
|
||||
/** Geschenk-Kopf (Textur aus magic-tree.presents) als Item. */
|
||||
ItemStack presentHead(int amount) {
|
||||
ItemStack it = new ItemStack(Material.PLAYER_HEAD, Math.max(1, amount));
|
||||
List<String> skins = cfg.getStringList("magic-tree.presents");
|
||||
if (skins.isEmpty()) return it;
|
||||
org.bukkit.inventory.meta.SkullMeta meta = (org.bukkit.inventory.meta.SkullMeta) it.getItemMeta();
|
||||
if (meta == null) return it;
|
||||
String url = skins.get(RANDOM.nextInt(skins.size()));
|
||||
try {
|
||||
org.bukkit.profile.PlayerProfile prof = Bukkit.createPlayerProfile(java.util.UUID.nameUUIDFromBytes(url.getBytes()), "present");
|
||||
org.bukkit.profile.PlayerTextures tex = prof.getTextures();
|
||||
tex.setSkin(new java.net.URL(url));
|
||||
prof.setTextures(tex);
|
||||
meta.setOwnerProfile(prof);
|
||||
meta.setDisplayName(raw("present-name"));
|
||||
it.setItemMeta(meta);
|
||||
} catch (java.net.MalformedURLException ignored) { }
|
||||
return it;
|
||||
}
|
||||
|
||||
static int toInt(String s, int def) {
|
||||
try { return Integer.parseInt(s.trim()); } catch (NumberFormatException e) { return def; }
|
||||
}
|
||||
|
||||
public static boolean hasAll(Player p, List<String> cost) {
|
||||
for (String c : cost) {
|
||||
ItemStack it = parseItem(c);
|
||||
if (it != null && !p.getInventory().containsAtLeast(new ItemStack(it.getType()), it.getAmount())) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void takeAll(Player p, List<String> cost) {
|
||||
for (String c : cost) {
|
||||
ItemStack it = parseItem(c);
|
||||
if (it != null) p.getInventory().removeItem(it);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Schnee ----------
|
||||
|
||||
public boolean isSnowEnabledFor(Player p) {
|
||||
Byte off = p.getPersistentDataContainer().get(snowOffKey, PersistentDataType.BYTE);
|
||||
if (off != null) return off == 0;
|
||||
return cfg.getBoolean("snow.default-on", true);
|
||||
}
|
||||
|
||||
public boolean toggleSnow(Player p) {
|
||||
boolean now = !isSnowEnabledFor(p);
|
||||
p.getPersistentDataContainer().set(snowOffKey, PersistentDataType.BYTE, (byte) (now ? 0 : 1));
|
||||
return now;
|
||||
}
|
||||
|
||||
private void startSnow() {
|
||||
if (snowTask != null) snowTask.cancel();
|
||||
snowTask = null;
|
||||
if (!cfg.getBoolean("snow.enable", true)) return;
|
||||
int interval = Math.max(1, cfg.getInt("snow.display-task-interval", 2));
|
||||
int level = Math.max(1, Math.min(5, cfg.getInt("snow.level", 3)));
|
||||
List<String> blocked = cfg.getStringList("snow.disable-in-worlds");
|
||||
boolean regionsOnly = cfg.getBoolean("snow.enable-only-in-regions", false)
|
||||
&& Bukkit.getPluginManager().getPlugin("WorldGuard") != null;
|
||||
List<String> regions = cfg.getStringList("snow.regions");
|
||||
snowTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
World w = p.getWorld();
|
||||
if (blocked.contains(w.getName()) || !isSnowEnabledFor(p)) continue;
|
||||
var loc = p.getLocation();
|
||||
if (regionsOnly && !WorldGuardHook.inAnyRegion(loc, regions)) continue;
|
||||
for (int i = 0; i < level * 4; i++) {
|
||||
double x = loc.getX() + (RANDOM.nextDouble() - 0.5) * 20;
|
||||
double z = loc.getZ() + (RANDOM.nextDouble() - 0.5) * 20;
|
||||
double y = loc.getY() + 2 + RANDOM.nextDouble() * 7;
|
||||
p.spawnParticle(Particle.SNOWFLAKE, x, y, z, 0, 0, -0.15, 0, 1);
|
||||
}
|
||||
}
|
||||
}, 20L, interval);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** Plaetzchen und Lebkuchen: Essbare Items mit Traenke-Effekten. */
|
||||
public final class CookieManager {
|
||||
private final ChristmasManager mgr;
|
||||
private final NamespacedKey key;
|
||||
|
||||
CookieManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.key = new NamespacedKey(mgr.plugin(), "cookie_id");
|
||||
}
|
||||
|
||||
public Set<String> ids() {
|
||||
ConfigurationSection s = mgr.cfg().getConfigurationSection("cookies");
|
||||
return s == null ? new LinkedHashSet<>() : s.getKeys(false);
|
||||
}
|
||||
|
||||
public ItemStack create(String id, int amount) {
|
||||
ConfigurationSection s = mgr.cfg().getConfigurationSection("cookies." + id);
|
||||
if (s == null) return null;
|
||||
Material m = Material.matchMaterial(s.getString("material", "COOKIE"));
|
||||
if (m == null || !m.isItem() || !m.isEdible()) m = Material.COOKIE;
|
||||
ItemStack it = new ItemStack(m, Math.max(1, amount));
|
||||
ItemMeta meta = it.getItemMeta();
|
||||
if (meta == null) return it;
|
||||
meta.setDisplayName(ChristmasManager.color(s.getString("name", "&6" + id)));
|
||||
List<String> lore = new ArrayList<>();
|
||||
for (String l : s.getStringList("lore")) lore.add(ChristmasManager.color(l));
|
||||
meta.setLore(lore);
|
||||
meta.setEnchantmentGlintOverride(true);
|
||||
meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, id);
|
||||
it.setItemMeta(meta);
|
||||
return it;
|
||||
}
|
||||
|
||||
public String idOf(ItemStack it) {
|
||||
if (it == null || !it.hasItemMeta()) return null;
|
||||
return it.getItemMeta().getPersistentDataContainer().get(key, PersistentDataType.STRING);
|
||||
}
|
||||
|
||||
/** Wendet die Effekte eines gegessenen Plaetzchens an. */
|
||||
public void eat(Player p, String id) {
|
||||
for (String spec : mgr.cfg().getStringList("cookies." + id + ".effects")) {
|
||||
String[] parts = spec.split(":");
|
||||
PotionEffectType type = effect(parts[0].trim());
|
||||
if (type == null) continue;
|
||||
int seconds = parts.length > 1 ? ChristmasManager.toInt(parts[1], 30) : 30;
|
||||
int amp = parts.length > 2 ? ChristmasManager.toInt(parts[2], 0) : 0;
|
||||
p.addPotionEffect(new PotionEffect(type, seconds * 20, Math.max(0, amp)));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static PotionEffectType effect(String name) {
|
||||
String n = name.toLowerCase();
|
||||
PotionEffectType t = Registry.EFFECT.get(NamespacedKey.minecraft(n));
|
||||
return t != null ? t : PotionEffectType.getByName(name.toUpperCase());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.Adventskalender;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.boss.BarColor;
|
||||
import org.bukkit.boss.BarStyle;
|
||||
import org.bukkit.boss.BossBar;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
/** Countdown bis Heiligabend: Nachricht beim Join, Bossbar und Platzhalter. */
|
||||
public final class CountdownManager {
|
||||
private final ChristmasManager mgr;
|
||||
private final NamespacedKey offKey;
|
||||
private BossBar bar;
|
||||
private BukkitTask task;
|
||||
|
||||
CountdownManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.offKey = new NamespacedKey(mgr.plugin(), "countdown_off");
|
||||
}
|
||||
|
||||
void start() {
|
||||
stop();
|
||||
if (!mgr.cfg().getBoolean("countdown.enable", true)) return;
|
||||
bar = Bukkit.createBossBar("", BarColor.RED, BarStyle.SEGMENTED_10);
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::update, 40L, 20L * 30);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
if (bar != null) { bar.removeAll(); bar = null; }
|
||||
}
|
||||
|
||||
// ---------- Berechnung ----------
|
||||
|
||||
private LocalDate today() {
|
||||
return LocalDate.now(Adventskalender.getInstance().getZone());
|
||||
}
|
||||
|
||||
private LocalDate target() {
|
||||
String t = mgr.cfg().getString("countdown.target", "12-24");
|
||||
String[] p = t.split("-");
|
||||
int month = 12, day = 24;
|
||||
try {
|
||||
month = Integer.parseInt(p[0].trim());
|
||||
day = Integer.parseInt(p[1].trim());
|
||||
} catch (Exception ignored) { }
|
||||
LocalDate today = today();
|
||||
LocalDate thisYear;
|
||||
try {
|
||||
thisYear = LocalDate.of(today.getYear(), month, day);
|
||||
} catch (Exception e) {
|
||||
thisYear = LocalDate.of(today.getYear(), 12, 24);
|
||||
}
|
||||
return today.isAfter(thisYear) ? thisYear.plusYears(1) : thisYear;
|
||||
}
|
||||
|
||||
/** Tage bis zum Zieldatum (0 = heute). */
|
||||
public int daysLeft() {
|
||||
return (int) ChronoUnit.DAYS.between(today(), target());
|
||||
}
|
||||
|
||||
/** Liegt der Countdown im Anzeigefenster (show-days-before Tage vorher)? */
|
||||
public boolean visible() {
|
||||
return mgr.cfg().getBoolean("countdown.enable", true)
|
||||
&& daysLeft() <= mgr.cfg().getInt("countdown.show-days-before", 60);
|
||||
}
|
||||
|
||||
/** Tage, bis die Anzeige beginnt (0 = schon sichtbar). */
|
||||
public int daysUntilVisible() {
|
||||
return Math.max(0, daysLeft() - mgr.cfg().getInt("countdown.show-days-before", 60));
|
||||
}
|
||||
|
||||
public String text() {
|
||||
int d = daysLeft();
|
||||
if (d == 0) return mgr.raw("countdown-today");
|
||||
return mgr.raw(d == 1 ? "countdown-bossbar-one" : "countdown-bossbar").replace("%days%", String.valueOf(d));
|
||||
}
|
||||
|
||||
// ---------- Anzeige ----------
|
||||
|
||||
private boolean hiddenFor(Player p) {
|
||||
Byte b = p.getPersistentDataContainer().get(offKey, PersistentDataType.BYTE);
|
||||
return b != null && b == 1;
|
||||
}
|
||||
|
||||
/** Schaltet die Bossbar fuer den Spieler um; true = jetzt sichtbar. */
|
||||
public boolean toggle(Player p) {
|
||||
boolean nowHidden = !hiddenFor(p);
|
||||
p.getPersistentDataContainer().set(offKey, PersistentDataType.BYTE, (byte) (nowHidden ? 1 : 0));
|
||||
update();
|
||||
return !nowHidden;
|
||||
}
|
||||
|
||||
private void update() {
|
||||
if (bar == null) return;
|
||||
boolean show = visible() && mgr.cfg().getBoolean("countdown.bossbar", true);
|
||||
if (!show) { bar.removeAll(); return; }
|
||||
int show_days = Math.max(1, mgr.cfg().getInt("countdown.show-days-before", 60));
|
||||
bar.setTitle(text());
|
||||
bar.setProgress(Math.max(0, Math.min(1, 1.0 - daysLeft() / (double) show_days)));
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (hiddenFor(p)) bar.removePlayer(p); else bar.addPlayer(p);
|
||||
}
|
||||
}
|
||||
|
||||
public void onJoin(Player p) {
|
||||
if (!visible()) return;
|
||||
if (bar != null && mgr.cfg().getBoolean("countdown.bossbar", true) && !hiddenFor(p)) bar.addPlayer(p);
|
||||
if (mgr.cfg().getBoolean("countdown.join-message", true)) {
|
||||
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
|
||||
if (p.isOnline()) p.sendMessage(mgr.prefixed(text()));
|
||||
}, 60L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.RewardUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Weihnachts-Cracker: Items, die per Rechtsklick geoeffnet werden und eine zufaellige Belohnung geben. */
|
||||
public final class CrackerManager {
|
||||
private final ChristmasManager mgr;
|
||||
private final NamespacedKey key;
|
||||
|
||||
CrackerManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.key = new NamespacedKey(mgr.plugin(), "cracker_id");
|
||||
}
|
||||
|
||||
void reload() { /* Konfiguration wird bei Bedarf direkt gelesen */ }
|
||||
|
||||
public Set<String> ids() {
|
||||
ConfigurationSection s = mgr.cfg().getConfigurationSection("crackers");
|
||||
return s == null ? new LinkedHashSet<>() : s.getKeys(false);
|
||||
}
|
||||
|
||||
public String firstId() {
|
||||
return ids().stream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public ItemStack create(String id, int amount) {
|
||||
ConfigurationSection s = mgr.cfg().getConfigurationSection("crackers." + id);
|
||||
if (s == null) return null;
|
||||
Material m = Material.matchMaterial(s.getString("material", "FIREWORK_STAR"));
|
||||
if (m == null || !m.isItem()) m = Material.FIREWORK_STAR;
|
||||
ItemStack it = new ItemStack(m, Math.max(1, amount));
|
||||
ItemMeta meta = it.getItemMeta();
|
||||
if (meta == null) return it;
|
||||
meta.setDisplayName(ChristmasManager.color(s.getString("name", "&cCracker")));
|
||||
List<String> lore = new ArrayList<>();
|
||||
for (String l : s.getStringList("lore")) lore.add(ChristmasManager.color(l));
|
||||
meta.setLore(lore);
|
||||
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ATTRIBUTES);
|
||||
meta.addEnchant(Enchantment.LURE, 1, true);
|
||||
meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, id);
|
||||
it.setItemMeta(meta);
|
||||
return it;
|
||||
}
|
||||
|
||||
/** Gibt die Cracker-ID des Items zurueck oder null. */
|
||||
public String idOf(ItemStack it) {
|
||||
if (it == null || !it.hasItemMeta()) return null;
|
||||
return it.getItemMeta().getPersistentDataContainer().get(key, PersistentDataType.STRING);
|
||||
}
|
||||
|
||||
/** Oeffnet einen Cracker (verbraucht das Item NICHT, das macht der Aufrufer). */
|
||||
public void open(Player p, String id) {
|
||||
List<Map<?, ?>> rewards = mgr.cfg().getMapList("crackers." + id + ".rewards");
|
||||
double sum = 0;
|
||||
for (Map<?, ?> r : rewards) sum += weight(r);
|
||||
if (sum <= 0) return;
|
||||
double roll = ChristmasManager.RANDOM.nextDouble() * sum;
|
||||
for (Map<?, ?> r : rewards) {
|
||||
roll -= weight(r);
|
||||
if (roll > 0) continue;
|
||||
ChristmasExtras.applyMap(p, r);
|
||||
break;
|
||||
}
|
||||
p.sendMessage(mgr.msg("cracker-opened"));
|
||||
p.getWorld().spawnParticle(Particle.END_ROD, p.getLocation().add(0, 1.5, 0), 40, 0.4, 0.4, 0.4, 0.1);
|
||||
p.getWorld().playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1f, 1f);
|
||||
}
|
||||
|
||||
private static double weight(Map<?, ?> r) {
|
||||
Object c = r.get("chance");
|
||||
return c instanceof Number n ? Math.max(0, n.doubleValue()) : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.Skull;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.Candle;
|
||||
import org.bukkit.block.data.type.Leaves;
|
||||
import org.bukkit.block.data.type.Snow;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.profile.PlayerProfile;
|
||||
import org.bukkit.profile.PlayerTextures;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Weihnachtsmarkt-Deko: Ein Menue, aus dem Admins Deko per Klick aufstellen und wieder rueckgaengig machen. */
|
||||
public final class DecoManager {
|
||||
|
||||
public static final class Menu implements InventoryHolder {
|
||||
private Inventory inv;
|
||||
@Override public Inventory getInventory() { return inv; }
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
PRESENT(Material.CHEST, 10), CANDY_CANE(Material.RED_CONCRETE, 11),
|
||||
LANTERN_POST(Material.LANTERN, 12), TREE(Material.SPRUCE_SAPLING, 13), WREATH(Material.RED_CANDLE, 14),
|
||||
SNOWMAN(Material.CARVED_PUMPKIN, 15), LIGHTS(Material.SEA_LANTERN, 16), SNOW_PILE(Material.SNOW_BLOCK, 21);
|
||||
final Material icon;
|
||||
final int slot;
|
||||
Type(Material icon, int slot) { this.icon = icon; this.slot = slot; }
|
||||
}
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private final Map<UUID, Deque<Runnable>> undo = new HashMap<>();
|
||||
|
||||
DecoManager(ChristmasManager mgr) { this.mgr = mgr; }
|
||||
|
||||
// ---------- Menue ----------
|
||||
|
||||
public void open(Player p) {
|
||||
Menu holder = new Menu();
|
||||
holder.inv = Bukkit.createInventory(holder, 27, mgr.raw("deco-menu-title"));
|
||||
for (Type t : Type.values()) {
|
||||
ItemStack it = new ItemStack(t.icon);
|
||||
ItemMeta meta = it.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(mgr.raw("deco-" + t.name().toLowerCase()));
|
||||
meta.setLore(List.of(mgr.raw("deco-click-hint")));
|
||||
it.setItemMeta(meta);
|
||||
}
|
||||
holder.inv.setItem(t.slot, it);
|
||||
}
|
||||
p.openInventory(holder.inv);
|
||||
}
|
||||
|
||||
public void click(Player p, int slot) {
|
||||
for (Type t : Type.values()) {
|
||||
if (t.slot == slot) {
|
||||
p.closeInventory();
|
||||
place(p, t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Platzieren ----------
|
||||
|
||||
private final class Session {
|
||||
final List<BlockState> before = new ArrayList<>();
|
||||
final List<Entity> entities = new ArrayList<>();
|
||||
|
||||
void set(Block b, BlockData data) {
|
||||
before.add(b.getState());
|
||||
b.setBlockData(data, false);
|
||||
}
|
||||
|
||||
void set(Block b, Material m) { set(b, m.createBlockData()); }
|
||||
|
||||
boolean free(Block b) {
|
||||
return b.getType().isAir() || (b.isPassable() && !b.isLiquid());
|
||||
}
|
||||
}
|
||||
|
||||
public void place(Player p, Type type) {
|
||||
Block target = p.getTargetBlockExact(8);
|
||||
if (target == null) { p.sendMessage(mgr.msg("deco-look")); return; }
|
||||
Block base = target.getRelative(BlockFace.UP);
|
||||
Session s = new Session();
|
||||
BlockFace facing = p.getFacing();
|
||||
|
||||
switch (type) {
|
||||
case PRESENT -> present(s, base);
|
||||
case CANDY_CANE -> candyCane(s, base, facing);
|
||||
case LANTERN_POST -> {
|
||||
for (int i = 0; i < 3; i++) if (s.free(base.getRelative(0, i, 0))) s.set(base.getRelative(0, i, 0), Material.OAK_FENCE);
|
||||
Block top = base.getRelative(0, 3, 0);
|
||||
if (s.free(top)) s.set(top, Material.LANTERN);
|
||||
}
|
||||
case TREE -> {
|
||||
Map<Vector, BlockData> st = mgr.trees().structure(MagicTreeManager.Level.TREE);
|
||||
for (var e : st.entrySet()) {
|
||||
Vector v = e.getKey();
|
||||
Block b = base.getRelative(v.getBlockX(), v.getBlockY(), v.getBlockZ());
|
||||
if (s.free(b)) s.set(b, e.getValue());
|
||||
}
|
||||
}
|
||||
case WREATH -> wreath(s, base);
|
||||
case SNOWMAN -> {
|
||||
mgr.snowmen().spawnDeco(base.getLocation().add(0.5, 0, 0.5), "");
|
||||
for (Entity e : base.getWorld().getNearbyEntities(base.getLocation().add(0.5, 0.5, 0.5), 1, 1, 1)) {
|
||||
if (e instanceof org.bukkit.entity.Snowman sm && mgr.snowmen().isDeco(sm)) s.entities.add(sm);
|
||||
}
|
||||
}
|
||||
case LIGHTS -> {
|
||||
Material[] mats = {Material.SEA_LANTERN, Material.SHROOMLIGHT, Material.GLOWSTONE, Material.SHROOMLIGHT};
|
||||
BlockFace f = facing == BlockFace.UP || facing == BlockFace.DOWN ? BlockFace.NORTH : facing;
|
||||
for (int i = 0; i < 7; i++) {
|
||||
Block b = base.getRelative(f, i);
|
||||
if (s.free(b)) s.set(b, mats[i % mats.length]);
|
||||
}
|
||||
}
|
||||
case SNOW_PILE -> {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
Block b = base.getRelative(dx, 0, dz);
|
||||
if (!s.free(b)) continue;
|
||||
Snow data = (Snow) Material.SNOW.createBlockData();
|
||||
data.setLayers(dx == 0 && dz == 0 ? 3 : (Math.abs(dx) + Math.abs(dz) == 1 ? 2 : 1));
|
||||
s.set(b, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
undo.computeIfAbsent(p.getUniqueId(), k -> new ArrayDeque<>()).push(() -> {
|
||||
for (int i = s.before.size() - 1; i >= 0; i--) s.before.get(i).update(true, false);
|
||||
for (Entity e : s.entities) if (e.isValid()) e.remove();
|
||||
});
|
||||
Deque<Runnable> stack = undo.get(p.getUniqueId());
|
||||
while (stack.size() > 30) stack.removeLast();
|
||||
p.sendMessage(mgr.msg("deco-placed"));
|
||||
}
|
||||
|
||||
public void undoLast(Player p) {
|
||||
Deque<Runnable> stack = undo.get(p.getUniqueId());
|
||||
if (stack == null || stack.isEmpty()) { p.sendMessage(mgr.msg("deco-nothing-undo")); return; }
|
||||
stack.pop().run();
|
||||
p.sendMessage(mgr.msg("deco-undone"));
|
||||
}
|
||||
|
||||
// ---------- Formen ----------
|
||||
|
||||
private void present(Session s, Block base) {
|
||||
List<String> skins = mgr.cfg().getStringList("magic-tree.presents");
|
||||
if (!s.free(base) || skins.isEmpty()) return;
|
||||
s.set(base, Material.PLAYER_HEAD);
|
||||
if (base.getState() instanceof Skull sk) {
|
||||
String url = skins.get(ChristmasManager.RANDOM.nextInt(skins.size()));
|
||||
try {
|
||||
PlayerProfile prof = Bukkit.createPlayerProfile(UUID.nameUUIDFromBytes(url.getBytes()), "present");
|
||||
PlayerTextures tex = prof.getTextures();
|
||||
tex.setSkin(new URL(url));
|
||||
prof.setTextures(tex);
|
||||
sk.setOwnerProfile(prof);
|
||||
sk.update(false, false);
|
||||
} catch (MalformedURLException ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
private void candyCane(Session s, Block base, BlockFace facing) {
|
||||
BlockFace f = facing == BlockFace.UP || facing == BlockFace.DOWN ? BlockFace.NORTH : facing;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Block b = base.getRelative(0, i, 0);
|
||||
if (s.free(b)) s.set(b, i % 2 == 0 ? Material.RED_CONCRETE : Material.WHITE_CONCRETE);
|
||||
}
|
||||
Block hook = base.getRelative(0, 3, 0).getRelative(f);
|
||||
if (s.free(hook)) s.set(hook, Material.RED_CONCRETE);
|
||||
Block hook2 = hook.getRelative(BlockFace.DOWN);
|
||||
if (s.free(hook2)) s.set(hook2, Material.WHITE_CONCRETE);
|
||||
}
|
||||
|
||||
private void wreath(Session s, Block base) {
|
||||
Leaves leaves = (Leaves) Material.SPRUCE_LEAVES.createBlockData();
|
||||
leaves.setPersistent(true);
|
||||
int[][] ring = {{-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {-1, 1}, {0, 1}, {1, 1}};
|
||||
for (int[] r : ring) {
|
||||
Block b = base.getRelative(r[0], 0, r[1]);
|
||||
if (s.free(b)) s.set(b, leaves);
|
||||
}
|
||||
int[][] candles = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};
|
||||
for (int[] c : candles) {
|
||||
Block b = base.getRelative(c[0], 1, c[1]);
|
||||
if (!s.free(b)) continue;
|
||||
Candle data = (Candle) Material.RED_CANDLE.createBlockData();
|
||||
data.setLit(true);
|
||||
s.set(b, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.FireworkEffect;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Firework;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.meta.FireworkMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/** Weihnachts- und Neujahrsgruss: Chat-Nachricht an bestimmten Tagen, auf Wunsch mit Feuerwerk beim Join. */
|
||||
public final class GreetingManager {
|
||||
private final ChristmasManager mgr;
|
||||
private final File file;
|
||||
private final NamespacedKey fireworkKey;
|
||||
private String lastDate = "";
|
||||
private BukkitTask task;
|
||||
|
||||
GreetingManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.file = new File(mgr.plugin().getDataFolder(), "greetings.yml");
|
||||
this.fireworkKey = new NamespacedKey(mgr.plugin(), "xmas_firework");
|
||||
}
|
||||
|
||||
void start() {
|
||||
stop();
|
||||
if (file.exists()) lastDate = YamlConfiguration.loadConfiguration(file).getString("last", "");
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::check, 200L, 20L * 30);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
}
|
||||
|
||||
private LocalDate today() { return LocalDate.now(mgr.plugin().getZone()); }
|
||||
|
||||
/** Nachrichten-Schluessel fuer heute oder null. */
|
||||
private String keyForToday() {
|
||||
if (!mgr.cfg().getBoolean("greetings.enable", true)) return null;
|
||||
LocalDate d = today();
|
||||
String md = String.format("%02d-%02d", d.getMonthValue(), d.getDayOfMonth());
|
||||
return mgr.cfg().getString("greetings.days." + md);
|
||||
}
|
||||
|
||||
/** Einmal pro Tag an alle Online-Spieler. */
|
||||
private void check() {
|
||||
String key = keyForToday();
|
||||
if (key == null || today().toString().equals(lastDate)) return;
|
||||
if (Bukkit.getOnlinePlayers().isEmpty()) return;
|
||||
lastDate = today().toString();
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
y.set("last", lastDate);
|
||||
try { y.save(file); } catch (IOException ignored) { }
|
||||
|
||||
Bukkit.broadcastMessage(mgr.prefixed(mgr.raw(key)));
|
||||
if (mgr.cfg().getBoolean("greetings.fireworks-on-broadcast", true)) {
|
||||
for (Player p : Bukkit.getOnlinePlayers()) fireworks(p);
|
||||
}
|
||||
}
|
||||
|
||||
public void onJoin(Player p) {
|
||||
String key = keyForToday();
|
||||
if (key == null) return;
|
||||
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
|
||||
if (!p.isOnline()) return;
|
||||
p.sendMessage(mgr.prefixed(mgr.raw(key)));
|
||||
if (mgr.cfg().getBoolean("greetings.join-fireworks", true)) fireworks(p);
|
||||
}, 80L);
|
||||
}
|
||||
|
||||
/** Ein kleines Feuerwerk hoch ueber dem Spieler (kein Schaden). */
|
||||
public void fireworks(Player p) {
|
||||
Location at = p.getLocation().add(0, 8, 0);
|
||||
Firework fw = p.getWorld().spawn(at, Firework.class, f -> {
|
||||
FireworkMeta meta = f.getFireworkMeta();
|
||||
meta.addEffect(FireworkEffect.builder().with(FireworkEffect.Type.BALL_LARGE)
|
||||
.withColor(Color.RED, Color.WHITE).withFade(Color.GREEN).flicker(true).trail(true).build());
|
||||
f.setFireworkMeta(meta);
|
||||
f.getPersistentDataContainer().set(fireworkKey, PersistentDataType.BYTE, (byte) 1);
|
||||
});
|
||||
fw.detonate();
|
||||
}
|
||||
|
||||
public boolean isOurs(Entity e) {
|
||||
return e.getPersistentDataContainer().has(fireworkKey, PersistentDataType.BYTE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.chat.hover.content.Text;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.LeatherArmorMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/** Nikolausmuetze: Rotes Kopf-Item, das beim Join angeboten (oder direkt aufgesetzt) wird. */
|
||||
public final class HatManager {
|
||||
private final ChristmasManager mgr;
|
||||
private final NamespacedKey hatKey;
|
||||
|
||||
HatManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.hatKey = new NamespacedKey(mgr.plugin(), "santa_hat");
|
||||
}
|
||||
|
||||
public boolean inSeason() {
|
||||
if (!mgr.cfg().getBoolean("santa-hat.enable", true)) return false;
|
||||
LocalDate today = LocalDate.now(mgr.plugin().getZone());
|
||||
try {
|
||||
String[] from = mgr.cfg().getString("santa-hat.from", "12-01").split("-");
|
||||
String[] until = mgr.cfg().getString("santa-hat.until", "12-31").split("-");
|
||||
LocalDate a = LocalDate.of(today.getYear(), Integer.parseInt(from[0].trim()), Integer.parseInt(from[1].trim()));
|
||||
LocalDate b = LocalDate.of(today.getYear(), Integer.parseInt(until[0].trim()), Integer.parseInt(until[1].trim()));
|
||||
return !today.isBefore(a) && !today.isAfter(b);
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public ItemStack create() {
|
||||
ItemStack it = new ItemStack(Material.LEATHER_HELMET);
|
||||
LeatherArmorMeta meta = (LeatherArmorMeta) it.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setColor(Color.fromRGB(200, 25, 30));
|
||||
meta.setDisplayName(mgr.raw("hat-name"));
|
||||
meta.setLore(mgr.rawList("hat-lore"));
|
||||
meta.setUnbreakable(true);
|
||||
meta.addItemFlags(org.bukkit.inventory.ItemFlag.HIDE_UNBREAKABLE, org.bukkit.inventory.ItemFlag.HIDE_ATTRIBUTES,
|
||||
org.bukkit.inventory.ItemFlag.HIDE_DYE);
|
||||
meta.getPersistentDataContainer().set(hatKey, PersistentDataType.BYTE, (byte) 1);
|
||||
it.setItemMeta(meta);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
private boolean isHat(ItemStack it) {
|
||||
return it != null && it.hasItemMeta()
|
||||
&& it.getItemMeta().getPersistentDataContainer().has(hatKey, PersistentDataType.BYTE);
|
||||
}
|
||||
|
||||
private boolean owns(Player p) {
|
||||
for (ItemStack it : p.getInventory().getContents()) if (isHat(it)) return true;
|
||||
for (ItemStack it : p.getInventory().getArmorContents()) if (isHat(it)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** /xmas hat und Klick auf das Angebot. */
|
||||
public void give(Player p) {
|
||||
if (!inSeason()) { p.sendMessage(mgr.msg("hat-season")); return; }
|
||||
if (owns(p)) { p.sendMessage(mgr.msg("hat-have")); return; }
|
||||
ItemStack hat = create();
|
||||
if (p.getInventory().getHelmet() == null || p.getInventory().getHelmet().getType().isAir()) {
|
||||
p.getInventory().setHelmet(hat);
|
||||
} else {
|
||||
de.mviper.adventskalender.RewardUtil.give(p, hat);
|
||||
}
|
||||
p.sendMessage(mgr.msg("hat-given"));
|
||||
}
|
||||
|
||||
public void onJoin(Player p) {
|
||||
if (!inSeason() || owns(p)) return;
|
||||
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
|
||||
if (!p.isOnline() || owns(p)) return;
|
||||
if (mgr.cfg().getBoolean("santa-hat.auto-equip", false)) {
|
||||
give(p);
|
||||
return;
|
||||
}
|
||||
if (!mgr.cfg().getBoolean("santa-hat.offer-on-join", true)) return;
|
||||
TextComponent msg = new TextComponent(mgr.prefixed(mgr.raw("hat-offer")) + " ");
|
||||
TextComponent click = new TextComponent(mgr.raw("hat-offer-click"));
|
||||
click.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/xmas hat"));
|
||||
click.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(mgr.raw("hat-offer-hover"))));
|
||||
msg.addExtra(click);
|
||||
p.spigot().sendMessage(msg);
|
||||
}, 120L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Login-Serie: Wer mehrere Tage hintereinander online kommt, bekommt steigende Belohnungen. */
|
||||
public final class LoginStreakManager {
|
||||
private static final class Data {
|
||||
LocalDate last;
|
||||
int streak;
|
||||
int best;
|
||||
}
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private final File file;
|
||||
private final Map<UUID, Data> data = new HashMap<>();
|
||||
|
||||
LoginStreakManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.file = new File(mgr.plugin().getDataFolder(), "streaks.yml");
|
||||
}
|
||||
|
||||
public int streak(UUID id) {
|
||||
Data d = data.get(id);
|
||||
return d == null ? 0 : d.streak;
|
||||
}
|
||||
|
||||
public int best(UUID id) {
|
||||
Data d = data.get(id);
|
||||
return d == null ? 0 : d.best;
|
||||
}
|
||||
|
||||
void load() {
|
||||
data.clear();
|
||||
if (!file.exists()) return;
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||
ConfigurationSection s = y.getConfigurationSection("players");
|
||||
if (s == null) return;
|
||||
for (String k : s.getKeys(false)) {
|
||||
try {
|
||||
Data d = new Data();
|
||||
String last = s.getString(k + ".last");
|
||||
d.last = last == null ? null : LocalDate.parse(last);
|
||||
d.streak = s.getInt(k + ".streak");
|
||||
d.best = s.getInt(k + ".best");
|
||||
data.put(UUID.fromString(k), d);
|
||||
} catch (Exception ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
void save() {
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
for (var e : data.entrySet()) {
|
||||
String p = "players." + e.getKey();
|
||||
y.set(p + ".last", e.getValue().last == null ? null : e.getValue().last.toString());
|
||||
y.set(p + ".streak", e.getValue().streak);
|
||||
y.set(p + ".best", e.getValue().best);
|
||||
}
|
||||
try {
|
||||
y.save(file);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("streaks.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean inWindow(LocalDate today) {
|
||||
try {
|
||||
String[] from = mgr.cfg().getString("login-streak.from", "12-01").split("-");
|
||||
String[] until = mgr.cfg().getString("login-streak.until", "12-31").split("-");
|
||||
LocalDate a = LocalDate.of(today.getYear(), Integer.parseInt(from[0].trim()), Integer.parseInt(from[1].trim()));
|
||||
LocalDate b = LocalDate.of(today.getYear(), Integer.parseInt(until[0].trim()), Integer.parseInt(until[1].trim()));
|
||||
return !today.isBefore(a) && !today.isAfter(b);
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void onJoin(Player p) {
|
||||
if (!mgr.cfg().getBoolean("login-streak.enable", true)) return;
|
||||
LocalDate today = LocalDate.now(mgr.plugin().getZone());
|
||||
if (!inWindow(today)) return;
|
||||
|
||||
Data d = data.computeIfAbsent(p.getUniqueId(), k -> new Data());
|
||||
if (today.equals(d.last)) return; // heute schon belohnt
|
||||
d.streak = (d.last != null && d.last.plusDays(1).equals(today)) ? d.streak + 1 : 1;
|
||||
d.best = Math.max(d.best, d.streak);
|
||||
d.last = today;
|
||||
save();
|
||||
|
||||
int cycle = Math.max(1, mgr.cfg().getInt("login-streak.cycle", 7));
|
||||
int day = ((d.streak - 1) % cycle) + 1;
|
||||
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
|
||||
if (!p.isOnline()) return;
|
||||
for (Map<?, ?> reward : mgr.cfg().getMapList("login-streak.rewards." + day)) {
|
||||
ChristmasExtras.applyMap(p, reward);
|
||||
}
|
||||
p.sendMessage(mgr.msg("streak-reward", "%day%", String.valueOf(d.streak), "%cycle%", String.valueOf(day)));
|
||||
}, 100L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.RewardUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.Skull;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.Leaves;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.ShapedRecipe;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.profile.PlayerProfile;
|
||||
import org.bukkit.profile.PlayerTextures;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Magic Tree: Kristall auf Setzling, Items neben den Baum werfen, Rechtsklick = wachsen, Geschenk-Koepfe. */
|
||||
public final class MagicTreeManager {
|
||||
|
||||
public enum Level {
|
||||
SAPLING("sapling", 1), SMALL_TREE("small_tree", 2), TREE("tree", 3), MAGIC_TREE("magic_tree", 4);
|
||||
final String key;
|
||||
final int radius;
|
||||
Level(String key, int radius) { this.key = key; this.radius = radius; }
|
||||
Level next() { return ordinal() + 1 < values().length ? values()[ordinal() + 1] : null; }
|
||||
static Level parse(String s) {
|
||||
try { return valueOf(s.toUpperCase()); } catch (Exception e) { return SAPLING; }
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Tree {
|
||||
final UUID id = UUID.randomUUID();
|
||||
final UUID owner;
|
||||
String ownerName;
|
||||
final String world;
|
||||
final int x, y, z;
|
||||
Level level = Level.SAPLING;
|
||||
final Map<Material, Integer> req = new EnumMap<>(Material.class);
|
||||
final Map<Material, Integer> spent = new EnumMap<>(Material.class);
|
||||
final Set<String> blocks = new HashSet<>();
|
||||
final Set<String> presents = new HashSet<>();
|
||||
boolean indexed;
|
||||
boolean readyAnnounced;
|
||||
long counter;
|
||||
|
||||
Tree(UUID owner, String ownerName, String world, int x, int y, int z) {
|
||||
this.owner = owner; this.ownerName = ownerName; this.world = world;
|
||||
this.x = x; this.y = y; this.z = z;
|
||||
}
|
||||
|
||||
public UUID owner() { return owner; }
|
||||
public String ownerName() { return ownerName; }
|
||||
public Level level() { return level; }
|
||||
Location loc() {
|
||||
World w = Bukkit.getWorld(world);
|
||||
return w == null ? null : new Location(w, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private final List<Tree> trees = new ArrayList<>();
|
||||
private final Map<String, Tree> blockMap = new HashMap<>();
|
||||
private final Map<String, Tree> presentMap = new HashMap<>();
|
||||
private final Map<UUID, Long> destroyers = new HashMap<>();
|
||||
private final File file;
|
||||
private final File giftFile;
|
||||
private final NamespacedKey crystalKey;
|
||||
private final List<ItemStack> customGifts = new ArrayList<>();
|
||||
private boolean endedFlag;
|
||||
private boolean endAnnounced;
|
||||
private BukkitTask task;
|
||||
private BukkitTask fxTask;
|
||||
private int fxTick;
|
||||
|
||||
MagicTreeManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.file = new File(mgr.plugin().getDataFolder(), "trees.yml");
|
||||
this.giftFile = new File(mgr.plugin().getDataFolder(), "gifts.yml");
|
||||
this.crystalKey = new NamespacedKey(mgr.plugin(), "xmas_crystal");
|
||||
}
|
||||
|
||||
// ================= Status =================
|
||||
|
||||
public boolean enabled() { return mgr.cfg().getBoolean("magic-tree.enable", true); }
|
||||
public int treeCount() { return trees.size(); }
|
||||
|
||||
public long countOf(UUID owner) {
|
||||
return trees.stream().filter(t -> t.owner.equals(owner)).count();
|
||||
}
|
||||
|
||||
public boolean isEnded() {
|
||||
if (endedFlag) return true;
|
||||
if (!mgr.cfg().getBoolean("magic-tree.holiday-ends.enabled", true)) return false;
|
||||
try {
|
||||
LocalDateTime end = LocalDateTime.parse(mgr.cfg().getString("magic-tree.holiday-ends.date", ""),
|
||||
DateTimeFormatter.ofPattern("dd-MM-yyyy HH-mm-ss"));
|
||||
return LocalDateTime.now().isAfter(end);
|
||||
} catch (DateTimeParseException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setEnded(boolean ended) {
|
||||
endedFlag = ended;
|
||||
save();
|
||||
if (ended) announceEnd();
|
||||
}
|
||||
|
||||
private void announceEnd() {
|
||||
if (endAnnounced) return;
|
||||
endAnnounced = true;
|
||||
Bukkit.broadcastMessage(mgr.prefixed(mgr.raw("timeout")));
|
||||
}
|
||||
|
||||
// ================= Keys =================
|
||||
|
||||
private static String key(Block b) {
|
||||
return b.getWorld().getName() + ";" + b.getX() + ";" + b.getY() + ";" + b.getZ();
|
||||
}
|
||||
|
||||
public boolean isProtected(Block b) {
|
||||
String k = key(b);
|
||||
return blockMap.containsKey(k) || presentMap.containsKey(k);
|
||||
}
|
||||
|
||||
public boolean isTreeBlock(Block b) { return blockMap.containsKey(key(b)); }
|
||||
public boolean isPresentBlock(Block b) { return presentMap.containsKey(key(b)); }
|
||||
public Tree treeOf(Block b) { return blockMap.get(key(b)); }
|
||||
public Tree treeOfPresent(Block b) { return presentMap.get(key(b)); }
|
||||
|
||||
// ================= Kristall =================
|
||||
|
||||
public ItemStack crystal(int amount) {
|
||||
ConfigurationSection s = mgr.cfg().getConfigurationSection("crystal");
|
||||
Material m = Material.matchMaterial(s == null ? "NETHER_STAR" : s.getString("material", "NETHER_STAR"));
|
||||
if (m == null || !m.isItem()) m = Material.NETHER_STAR;
|
||||
ItemStack it = new ItemStack(m, Math.max(1, amount));
|
||||
ItemMeta meta = it.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ChristmasManager.color(s == null ? "&bWeihnachts-Kristall" : s.getString("name", "&bWeihnachts-Kristall")));
|
||||
List<String> lore = new ArrayList<>();
|
||||
if (s != null) for (String l : s.getStringList("lore")) lore.add(ChristmasManager.color(l));
|
||||
meta.setLore(lore);
|
||||
meta.getPersistentDataContainer().set(crystalKey, PersistentDataType.BYTE, (byte) 1);
|
||||
it.setItemMeta(meta);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
public boolean isCrystal(ItemStack it) {
|
||||
return it != null && it.hasItemMeta()
|
||||
&& it.getItemMeta().getPersistentDataContainer().has(crystalKey, PersistentDataType.BYTE);
|
||||
}
|
||||
|
||||
// ================= Persistenz =================
|
||||
|
||||
void load() {
|
||||
trees.clear();
|
||||
blockMap.clear();
|
||||
presentMap.clear();
|
||||
customGifts.clear();
|
||||
|
||||
if (giftFile.exists()) {
|
||||
YamlConfiguration g = YamlConfiguration.loadConfiguration(giftFile);
|
||||
for (Object o : g.getList("items", new ArrayList<>())) if (o instanceof ItemStack i) customGifts.add(i);
|
||||
}
|
||||
|
||||
if (file.exists()) {
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||
endedFlag = y.getBoolean("ended", false);
|
||||
ConfigurationSection s = y.getConfigurationSection("trees");
|
||||
if (s != null) {
|
||||
for (String k : s.getKeys(false)) {
|
||||
ConfigurationSection t = s.getConfigurationSection(k);
|
||||
if (t == null || !t.isString("level")) continue;
|
||||
try {
|
||||
Tree tr = new Tree(UUID.fromString(t.getString("owner", "")), t.getString("owner-name", "?"),
|
||||
t.getString("world", "world"), t.getInt("x"), t.getInt("y"), t.getInt("z"));
|
||||
tr.level = Level.parse(t.getString("level", "SAPLING"));
|
||||
readMap(t.getConfigurationSection("req"), tr.req);
|
||||
readMap(t.getConfigurationSection("spent"), tr.spent);
|
||||
tr.counter = t.getLong("counter", 0);
|
||||
for (String p : t.getStringList("presents")) {
|
||||
tr.presents.add(p);
|
||||
presentMap.put(p, tr);
|
||||
}
|
||||
trees.add(tr);
|
||||
} catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (task != null) task.cancel();
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 100L, 20L);
|
||||
registerRecipe();
|
||||
if (fxTask != null) fxTask.cancel();
|
||||
fxTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::fx, 60L, 3L);
|
||||
}
|
||||
|
||||
// ---------- Rezept: Diamanten im Kreuz, Smaragd in der Mitte ----------
|
||||
|
||||
private NamespacedKey recipeKey() { return new NamespacedKey(mgr.plugin(), "xmas_crystal"); }
|
||||
|
||||
void registerRecipe() {
|
||||
unregisterRecipe();
|
||||
if (!mgr.cfg().getBoolean("magic-tree.crystal-recipe", true)) return;
|
||||
ShapedRecipe r = new ShapedRecipe(recipeKey(), crystal(1));
|
||||
r.shape(" D ", "DED", " D ");
|
||||
r.setIngredient('D', Material.DIAMOND);
|
||||
r.setIngredient('E', Material.EMERALD);
|
||||
Bukkit.addRecipe(r);
|
||||
}
|
||||
|
||||
void unregisterRecipe() {
|
||||
Bukkit.removeRecipe(recipeKey());
|
||||
}
|
||||
|
||||
private static void readMap(ConfigurationSection s, Map<Material, Integer> out) {
|
||||
if (s == null) return;
|
||||
for (String k : s.getKeys(false)) {
|
||||
Material m = Material.matchMaterial(k);
|
||||
if (m != null && s.getInt(k) > 0) out.put(m, s.getInt(k));
|
||||
}
|
||||
}
|
||||
|
||||
void save() {
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
y.set("ended", endedFlag);
|
||||
for (Tree t : trees) {
|
||||
String p = "trees." + t.id;
|
||||
y.set(p + ".owner", t.owner.toString());
|
||||
y.set(p + ".owner-name", t.ownerName);
|
||||
y.set(p + ".world", t.world);
|
||||
y.set(p + ".x", t.x);
|
||||
y.set(p + ".y", t.y);
|
||||
y.set(p + ".z", t.z);
|
||||
y.set(p + ".level", t.level.name());
|
||||
y.set(p + ".counter", t.counter);
|
||||
for (var e : t.req.entrySet()) y.set(p + ".req." + e.getKey().name(), e.getValue());
|
||||
for (var e : t.spent.entrySet()) y.set(p + ".spent." + e.getKey().name(), e.getValue());
|
||||
y.set(p + ".presents", new ArrayList<>(t.presents));
|
||||
}
|
||||
try {
|
||||
y.save(file);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("trees.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveGifts() {
|
||||
YamlConfiguration g = new YamlConfiguration();
|
||||
g.set("items", customGifts);
|
||||
try {
|
||||
g.save(giftFile);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("gifts.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
if (fxTask != null) fxTask.cancel();
|
||||
fxTask = null;
|
||||
unregisterRecipe();
|
||||
}
|
||||
|
||||
public boolean addGift(ItemStack hand) {
|
||||
if (hand == null || hand.getType().isAir()) return false;
|
||||
customGifts.add(hand.clone());
|
||||
saveGifts();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ================= Struktur =================
|
||||
|
||||
Map<Vector, BlockData> structure(Level lv) {
|
||||
Map<Vector, BlockData> m = new LinkedHashMap<>();
|
||||
if (lv == Level.SAPLING) {
|
||||
m.put(new Vector(0, 0, 0), Material.SPRUCE_SAPLING.createBlockData());
|
||||
return m;
|
||||
}
|
||||
int trunk;
|
||||
int[] radii;
|
||||
switch (lv) {
|
||||
case SMALL_TREE -> { trunk = 2; radii = new int[]{2, 1, 0}; }
|
||||
case TREE -> { trunk = 3; radii = new int[]{3, 2, 2, 1, 1, 0}; }
|
||||
default -> { trunk = 3; radii = new int[]{4, 3, 3, 2, 2, 1, 1, 0}; }
|
||||
}
|
||||
Leaves leaves = (Leaves) Material.SPRUCE_LEAVES.createBlockData();
|
||||
leaves.setPersistent(true);
|
||||
Material[] swag = {Material.RED_CONCRETE, Material.GOLD_BLOCK, Material.SEA_LANTERN, Material.BLUE_CONCRETE};
|
||||
|
||||
for (int y = 0; y < trunk; y++) m.put(new Vector(0, y, 0), Material.SPRUCE_LOG.createBlockData());
|
||||
for (int i = 0; i < radii.length; i++) {
|
||||
int r = radii[i];
|
||||
int y = trunk + i;
|
||||
if (r == 0) {
|
||||
m.put(new Vector(0, y, 0), Material.GLOWSTONE.createBlockData());
|
||||
continue;
|
||||
}
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
for (int dz = -r; dz <= r; dz++) {
|
||||
int d2 = dx * dx + dz * dz;
|
||||
if (d2 > r * r + 1) continue;
|
||||
if (dx == 0 && dz == 0) { m.put(new Vector(0, y, 0), Material.SPRUCE_LOG.createBlockData()); continue; }
|
||||
boolean edge = d2 >= r * r;
|
||||
if (lv != Level.SMALL_TREE && edge && Math.floorMod(dx * 7 + dz * 13 + y * 5, 5) == 0) {
|
||||
m.put(new Vector(dx, y, dz), swag[Math.floorMod(dx + dz + y, swag.length)].createBlockData());
|
||||
} else {
|
||||
m.put(new Vector(dx, y, dz), leaves);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
private boolean canGrow(Tree t, Level lv) {
|
||||
Location base = t.loc();
|
||||
if (base == null) return false;
|
||||
for (Vector v : structure(lv).keySet()) {
|
||||
Block b = base.getWorld().getBlockAt(t.x + v.getBlockX(), t.y + v.getBlockY(), t.z + v.getBlockZ());
|
||||
if (b.getType().isAir() || t.blocks.contains(key(b)) || (b.isPassable() && !b.isLiquid())) continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void clearBlocks(Tree t) {
|
||||
World w = Bukkit.getWorld(t.world);
|
||||
for (String k : t.blocks) {
|
||||
blockMap.remove(k);
|
||||
if (w == null) continue;
|
||||
String[] p = k.split(";");
|
||||
w.getBlockAt(Integer.parseInt(p[1]), Integer.parseInt(p[2]), Integer.parseInt(p[3])).setType(Material.AIR, false);
|
||||
}
|
||||
t.blocks.clear();
|
||||
}
|
||||
|
||||
private void build(Tree t) {
|
||||
World w = Bukkit.getWorld(t.world);
|
||||
if (w == null) return;
|
||||
clearBlocks(t);
|
||||
for (var e : structure(t.level).entrySet()) {
|
||||
Vector v = e.getKey();
|
||||
Block b = w.getBlockAt(t.x + v.getBlockX(), t.y + v.getBlockY(), t.z + v.getBlockZ());
|
||||
b.setBlockData(e.getValue(), false);
|
||||
t.blocks.add(key(b));
|
||||
blockMap.put(key(b), t);
|
||||
}
|
||||
t.indexed = true;
|
||||
}
|
||||
|
||||
/** Merkt sich nach dem Laden, welche Bloecke zum Baum gehoeren (ohne etwas zu platzieren). */
|
||||
private void index(Tree t) {
|
||||
World w = Bukkit.getWorld(t.world);
|
||||
if (w == null || !w.isChunkLoaded(t.x >> 4, t.z >> 4)) return;
|
||||
t.blocks.clear();
|
||||
for (var e : structure(t.level).entrySet()) {
|
||||
Vector v = e.getKey();
|
||||
Block b = w.getBlockAt(t.x + v.getBlockX(), t.y + v.getBlockY(), t.z + v.getBlockZ());
|
||||
if (b.getType() == e.getValue().getMaterial()) {
|
||||
t.blocks.add(key(b));
|
||||
blockMap.put(key(b), t);
|
||||
}
|
||||
}
|
||||
t.indexed = true;
|
||||
}
|
||||
|
||||
private int height(Level lv) {
|
||||
int max = 0;
|
||||
for (Vector v : structure(lv).keySet()) max = Math.max(max, v.getBlockY());
|
||||
return max;
|
||||
}
|
||||
|
||||
// ================= Aktionen =================
|
||||
|
||||
private void loadRequirements(Tree t) {
|
||||
t.req.clear();
|
||||
ConfigurationSection s = mgr.cfg().getConfigurationSection("magic-tree.levels." + t.level.key + ".lvlup");
|
||||
if (s == null) return;
|
||||
for (String k : s.getKeys(false)) {
|
||||
Material m = Material.matchMaterial(k);
|
||||
if (m != null && s.getInt(k) > 0) t.req.put(m, s.getInt(k));
|
||||
}
|
||||
t.readyAnnounced = false;
|
||||
}
|
||||
|
||||
public void plant(Player p, Block sapling) {
|
||||
if (!enabled() || isEnded()) { p.sendMessage(mgr.msg("tree.tree-disabled")); return; }
|
||||
int limit = mgr.cfg().getInt("magic-tree.tree-limit", 3);
|
||||
if (!p.hasPermission("xmas.admin") && trees.stream().filter(t -> t.owner.equals(p.getUniqueId())).count() >= limit) {
|
||||
p.sendMessage(mgr.msg("tree.tree-limit"));
|
||||
return;
|
||||
}
|
||||
Tree t = new Tree(p.getUniqueId(), p.getName(), sapling.getWorld().getName(),
|
||||
sapling.getX(), sapling.getY(), sapling.getZ());
|
||||
loadRequirements(t);
|
||||
t.counter = cooldown(t.level);
|
||||
trees.add(t);
|
||||
build(t);
|
||||
save();
|
||||
p.getWorld().spawnParticle(Particle.END_ROD, sapling.getLocation().add(0.5, 0.5, 0.5), 30, 0.4, 0.4, 0.4, 0.05);
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1f, 1.2f);
|
||||
p.sendMessage(mgr.msg("tree.tree-planted"));
|
||||
}
|
||||
|
||||
private String reqText(Tree t) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
for (var e : t.req.entrySet()) parts.add(e.getValue() + "x " + e.getKey().name());
|
||||
return String.join(", ", parts);
|
||||
}
|
||||
|
||||
/** Rechtsklick auf einen Baumblock. */
|
||||
public void click(Player p, Tree t) {
|
||||
if (!t.owner.equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) {
|
||||
p.sendMessage(mgr.msg("tree.not-owner"));
|
||||
return;
|
||||
}
|
||||
if (isEnded()) {
|
||||
p.sendMessage(mgr.msg("tree.tree-disabled"));
|
||||
return;
|
||||
}
|
||||
Level next = t.level.next();
|
||||
if (next == null) { p.sendMessage(mgr.msg("tree.grow-lvl-max")); return; }
|
||||
if (!t.req.isEmpty()) {
|
||||
p.sendMessage(mgr.raw("tree.grow-req-list-title"));
|
||||
p.sendMessage("§f" + reqText(t));
|
||||
return;
|
||||
}
|
||||
if (!canGrow(t, next)) { p.sendMessage(mgr.msg("tree.grow-not-enough-place")); return; }
|
||||
t.level = next;
|
||||
build(t);
|
||||
loadRequirements(t);
|
||||
t.counter = cooldown(t.level);
|
||||
save();
|
||||
Location top = t.loc().add(0.5, height(t.level) + 1, 0.5);
|
||||
t.loc().getWorld().spawnParticle(Particle.END_ROD, top, 60, 1, 1, 1, 0.1);
|
||||
t.loc().getWorld().playSound(top, Sound.ENTITY_PLAYER_LEVELUP, 1f, 1f);
|
||||
if (t.level.next() == null) p.sendMessage(mgr.msg("tree.grow-lvl-max"));
|
||||
}
|
||||
|
||||
/** Geworfenes Item nahe am Baum aufnehmen. */
|
||||
public void absorb(Player p, Item item) {
|
||||
ItemStack stack = item.getItemStack();
|
||||
for (Tree t : trees) {
|
||||
if (!t.owner.equals(p.getUniqueId())) continue;
|
||||
Location base = t.loc();
|
||||
if (base == null || !base.getWorld().equals(item.getWorld())) continue;
|
||||
Location il = item.getLocation();
|
||||
double dx = il.getX() - (t.x + 0.5), dz = il.getZ() - (t.z + 0.5), dy = il.getY() - t.y;
|
||||
if (dx * dx + dz * dz > Math.pow(t.level.radius + 3, 2) || dy < -2 || dy > height(t.level) + 4) continue;
|
||||
Integer need = t.req.get(stack.getType());
|
||||
if (need == null || need <= 0) continue;
|
||||
|
||||
int take = Math.min(need, stack.getAmount());
|
||||
if (take >= need) t.req.remove(stack.getType()); else t.req.put(stack.getType(), need - take);
|
||||
t.spent.merge(stack.getType(), take, Integer::sum);
|
||||
if (stack.getAmount() - take <= 0) item.remove();
|
||||
else { stack.setAmount(stack.getAmount() - take); item.setItemStack(stack); }
|
||||
|
||||
il.getWorld().spawnParticle(Particle.END_ROD, il, 12, 0.2, 0.2, 0.2, 0.05);
|
||||
p.sendMessage(mgr.msg("tree.grow-fed", "%amount%", String.valueOf(take), "%item%", stack.getType().name()));
|
||||
if (t.req.isEmpty() && t.level.next() != null && !t.readyAnnounced) {
|
||||
t.readyAnnounced = true;
|
||||
p.sendMessage(mgr.msg("tree.grow-lvl-ready"));
|
||||
}
|
||||
save();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** Abbauversuch auf einen Baumblock. */
|
||||
public void breakAttempt(Player p, Tree t, Block b) {
|
||||
boolean trunkLike = b.getType() == Material.SPRUCE_LOG || Tag_isSapling(b.getType());
|
||||
if (!trunkLike) {
|
||||
p.sendMessage(mgr.msg(t.owner.equals(p.getUniqueId()) ? "tree.destroy-leaves-tut" : "tree.destroy-leaves-santa"));
|
||||
return;
|
||||
}
|
||||
if (!t.owner.equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) {
|
||||
p.sendMessage(mgr.msg("tree.destroy-fail-owner"));
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = destroyers.get(p.getUniqueId());
|
||||
if (last == null || now - last > 10_000) {
|
||||
destroyers.put(p.getUniqueId(), now);
|
||||
p.sendMessage(mgr.msg(t.level == Level.SAPLING ? "tree.destroy-sapling" : "tree.destroy-warning"));
|
||||
return;
|
||||
}
|
||||
destroyers.remove(p.getUniqueId());
|
||||
destroy(p, t);
|
||||
}
|
||||
|
||||
private static boolean Tag_isSapling(Material m) {
|
||||
return org.bukkit.Tag.SAPLINGS.isTagged(m);
|
||||
}
|
||||
|
||||
private void destroy(Player p, Tree t) {
|
||||
boolean refund = isEnded() && mgr.cfg().getBoolean("magic-tree.holiday-ends.resource-back", true);
|
||||
removeTree(t);
|
||||
if (refund) {
|
||||
for (var e : t.spent.entrySet()) RewardUtil.give(p, new ItemStack(e.getKey(), e.getValue()));
|
||||
p.sendMessage(mgr.msg("tree.destroy-refund"));
|
||||
} else {
|
||||
p.sendMessage(mgr.msg("tree.destroy-complete"));
|
||||
}
|
||||
}
|
||||
|
||||
private void removeTree(Tree t) {
|
||||
World w = Bukkit.getWorld(t.world);
|
||||
for (String k : t.presents) {
|
||||
presentMap.remove(k);
|
||||
if (w == null) continue;
|
||||
String[] pp = k.split(";");
|
||||
w.getBlockAt(Integer.parseInt(pp[1]), Integer.parseInt(pp[2]), Integer.parseInt(pp[3])).setType(Material.AIR, false);
|
||||
}
|
||||
t.presents.clear();
|
||||
clearBlocks(t);
|
||||
trees.remove(t);
|
||||
save();
|
||||
}
|
||||
|
||||
public void info(Player p, Tree t) {
|
||||
p.sendMessage(mgr.msg("tree.info", "%owner%", t.ownerName, "%level%", t.level.name()));
|
||||
}
|
||||
|
||||
// ================= Geschenke =================
|
||||
|
||||
private long cooldown(Level lv) {
|
||||
return mgr.cfg().getLong("magic-tree.levels." + lv.key + ".gift-cooldown", -1);
|
||||
}
|
||||
|
||||
public int spawnPresentsEverywhere() {
|
||||
int n = 0;
|
||||
for (Tree t : new ArrayList<>(trees)) if (t.level != Level.SAPLING && spawnPresent(t, true)) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
private boolean spawnPresent(Tree t, boolean ignoreCap) {
|
||||
Location base = t.loc();
|
||||
if (base == null || !base.getWorld().isChunkLoaded(t.x >> 4, t.z >> 4)) return false;
|
||||
int cap = mgr.cfg().getInt("magic-tree.max-presents-per-tree", 5);
|
||||
if (!ignoreCap && t.presents.size() >= cap) return false;
|
||||
List<String> skins = mgr.cfg().getStringList("magic-tree.presents");
|
||||
if (skins.isEmpty()) return false;
|
||||
|
||||
World w = base.getWorld();
|
||||
for (int attempt = 0; attempt < 12; attempt++) {
|
||||
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
|
||||
double dist = t.level.radius + 1 + ChristmasManager.RANDOM.nextInt(2);
|
||||
int bx = t.x + (int) Math.round(Math.cos(ang) * dist);
|
||||
int bz = t.z + (int) Math.round(Math.sin(ang) * dist);
|
||||
for (int by = t.y + 2; by >= t.y - 3; by--) {
|
||||
Block b = w.getBlockAt(bx, by, bz);
|
||||
if (b.getType().isAir() && w.getBlockAt(bx, by - 1, bz).getType().isSolid() && !isProtected(b)) {
|
||||
setSkin(b, skins.get(ChristmasManager.RANDOM.nextInt(skins.size())));
|
||||
t.presents.add(key(b));
|
||||
presentMap.put(key(b), t);
|
||||
w.spawnParticle(Particle.SNOWFLAKE, b.getLocation().add(0.5, 0.8, 0.5), 15, 0.3, 0.3, 0.3, 0.02);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void setSkin(Block b, String url) {
|
||||
b.setType(Material.PLAYER_HEAD, false);
|
||||
if (!(b.getState() instanceof Skull sk)) return;
|
||||
try {
|
||||
PlayerProfile prof = Bukkit.createPlayerProfile(UUID.nameUUIDFromBytes(url.getBytes()), "present");
|
||||
PlayerTextures tex = prof.getTextures();
|
||||
tex.setSkin(new URL(url));
|
||||
prof.setTextures(tex);
|
||||
sk.setOwnerProfile(prof);
|
||||
sk.update(false, false);
|
||||
} catch (MalformedURLException ignored) { }
|
||||
}
|
||||
|
||||
public void openPresent(Player p, Tree t, Block b) {
|
||||
String k = key(b);
|
||||
presentMap.remove(k);
|
||||
t.presents.remove(k);
|
||||
b.setType(Material.AIR, false);
|
||||
b.getWorld().spawnParticle(Particle.END_ROD, b.getLocation().add(0.5, 0.5, 0.5), 20, 0.3, 0.3, 0.3, 0.05);
|
||||
b.getWorld().playSound(b.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.4f);
|
||||
save();
|
||||
|
||||
if (mgr.cfg().getBoolean("magic-tree.luck.enabled", false)
|
||||
&& ChristmasManager.RANDOM.nextInt(100) >= mgr.cfg().getInt("magic-tree.luck.chance", 75)) {
|
||||
p.sendMessage(mgr.msg("gift.unluck-message"));
|
||||
return;
|
||||
}
|
||||
ItemStack gift = randomGift();
|
||||
if (gift != null) {
|
||||
RewardUtil.give(p, gift);
|
||||
p.sendMessage(mgr.msg("gift.opened"));
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack randomGift() {
|
||||
List<String> mats = mgr.cfg().getStringList("magic-tree.gifts");
|
||||
int total = mats.size() + customGifts.size();
|
||||
if (total == 0) return null;
|
||||
int i = ChristmasManager.RANDOM.nextInt(total);
|
||||
if (i < mats.size()) return ChristmasManager.parseItem(mats.get(i));
|
||||
return customGifts.get(i - mats.size()).clone();
|
||||
}
|
||||
|
||||
// ================= Partikel-Show =================
|
||||
|
||||
private static final Particle DUST = lookup("DUST", "REDSTONE");
|
||||
private static final Color[] SPIRAL = {Color.RED, Color.fromRGB(255, 200, 50), Color.LIME};
|
||||
|
||||
private static Particle lookup(String... names) {
|
||||
for (String n : names) {
|
||||
try { return Particle.valueOf(n); } catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void dust(World w, double x, double y, double z, Color c, float size) {
|
||||
if (DUST == null) return;
|
||||
w.spawnParticle(DUST, x, y, z, 1, 0, 0, 0, 0, new Particle.DustOptions(c, size));
|
||||
}
|
||||
|
||||
private boolean playerNear(Location l, double dist) {
|
||||
for (Player p : l.getWorld().getPlayers()) {
|
||||
if (p.getLocation().distanceSquared(l) <= dist * dist) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Leuchtende Spirale, Lichter, Stern an der Spitze und Glitzer ueber Geschenken. */
|
||||
private void fx() {
|
||||
if (!enabled() || isEnded() || !mgr.cfg().getBoolean("magic-tree.particles.enable", true)) return;
|
||||
fxTick++;
|
||||
double intensity = Math.max(0.2, Math.min(5.0, mgr.cfg().getDouble("magic-tree.particles.intensity", 2.0)));
|
||||
|
||||
for (Tree t : trees) {
|
||||
Location base = t.loc();
|
||||
if (base == null || !base.getWorld().isChunkLoaded(t.x >> 4, t.z >> 4) || !playerNear(base, 40)) continue;
|
||||
World w = base.getWorld();
|
||||
double cx = t.x + 0.5, cz = t.z + 0.5;
|
||||
int h = height(t.level);
|
||||
|
||||
if (t.level == Level.SAPLING) {
|
||||
if (fxTick % 4 == 0) {
|
||||
double a = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
|
||||
w.spawnParticle(Particle.END_ROD, cx + Math.cos(a) * 0.6, t.y + 0.3 + ChristmasManager.RANDOM.nextDouble() * 0.6,
|
||||
cz + Math.sin(a) * 0.6, 1, 0, 0, 0, 0.002);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
double yStart = t.y + 2.2;
|
||||
double yEnd = t.y + h + 0.5;
|
||||
int strands = t.level.ordinal() + 1; // 2, 3, 4 Straenge je nach Stufe
|
||||
int trail = Math.max(3, (int) Math.round(5 * intensity)); // Punkte pro Strang (laengerer Schweif)
|
||||
|
||||
// Spirale in Rot, Gold und Gruen mit leuchtendem Schweif, die nach oben wandert
|
||||
for (int sIdx = 0; sIdx < strands; sIdx++) {
|
||||
double f0 = ((fxTick * 0.012) + sIdx / (double) strands) % 1.0;
|
||||
double phase0 = fxTick * 0.30 + sIdx * (Math.PI * 2 / strands);
|
||||
Color c = SPIRAL[(sIdx + fxTick / 8) % SPIRAL.length];
|
||||
for (int k = 0; k < trail; k++) {
|
||||
double f = Math.max(0, f0 - k * 0.012);
|
||||
double phase = phase0 - k * 0.22;
|
||||
double rad = Math.max(0.3, t.level.radius * (1 - f) + 0.35);
|
||||
dust(w, cx + Math.cos(phase) * rad, yStart + f * (yEnd - yStart), cz + Math.sin(phase) * rad,
|
||||
c, k == 0 ? 1.5f : 1.1f);
|
||||
}
|
||||
}
|
||||
|
||||
// Leuchtende Ringe, die ueber den Baum pulsieren
|
||||
if (fxTick % 6 == 0) {
|
||||
int rings = 3;
|
||||
int pointsPerRing = Math.max(8, (int) Math.round(10 * intensity));
|
||||
for (int r = 0; r < rings; r++) {
|
||||
double f = ((fxTick * 0.01) + r / (double) rings) % 1.0;
|
||||
double rad = Math.max(0.3, t.level.radius * (1 - f) + 0.45);
|
||||
double y = yStart + f * (yEnd - yStart);
|
||||
Color c = SPIRAL[(r + fxTick / 12) % SPIRAL.length];
|
||||
for (int i = 0; i < pointsPerRing; i++) {
|
||||
double a = i * Math.PI * 2 / pointsPerRing;
|
||||
dust(w, cx + Math.cos(a) * rad, y, cz + Math.sin(a) * rad, c, 0.9f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Funkelnde Lichter auf der Baumoberflaeche
|
||||
int twinkles = Math.max(1, (int) Math.round(3 * intensity));
|
||||
for (int i = 0; i < twinkles; i++) {
|
||||
double f = ChristmasManager.RANDOM.nextDouble();
|
||||
double a = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
|
||||
double rad = Math.max(0.3, t.level.radius * (1 - f) + 0.2);
|
||||
double x = cx + Math.cos(a) * rad, y = yStart + f * (yEnd - yStart), z = cz + Math.sin(a) * rad;
|
||||
if (ChristmasManager.RANDOM.nextInt(3) == 0) {
|
||||
dust(w, x, y, z, Color.fromRGB(255, 240, 170), 1.3f);
|
||||
} else {
|
||||
w.spawnParticle(Particle.END_ROD, x, y, z, 1, 0, 0, 0, 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
// Schneeflocken, die von den Aesten nach unten fallen
|
||||
int flakes = Math.max(1, (int) Math.round(2 * intensity));
|
||||
for (int i = 0; i < flakes; i++) {
|
||||
double f = ChristmasManager.RANDOM.nextDouble() * 0.8;
|
||||
double a = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
|
||||
double rad = Math.max(0.3, t.level.radius * (1 - f) + 0.3);
|
||||
w.spawnParticle(Particle.SNOWFLAKE, cx + Math.cos(a) * rad, yStart + f * (yEnd - yStart), cz + Math.sin(a) * rad,
|
||||
0, 0, -1, 0, 0.12);
|
||||
}
|
||||
|
||||
// Stern an der Spitze
|
||||
w.spawnParticle(Particle.END_ROD, cx, t.y + h + 1.3, cz, Math.max(2, (int) Math.round(3 * intensity)), 0.2, 0.2, 0.2, 0.004);
|
||||
if (fxTick % 12 == 0) {
|
||||
// Strahlenkranz um den Stern
|
||||
for (int i = 0; i < 8; i++) {
|
||||
double a = i * Math.PI / 4 + fxTick * 0.05;
|
||||
w.spawnParticle(Particle.END_ROD, cx + Math.cos(a) * 0.5, t.y + h + 1.3, cz + Math.sin(a) * 0.5, 1, 0, 0, 0, 0.01);
|
||||
}
|
||||
}
|
||||
if (t.level == Level.MAGIC_TREE && fxTick % 20 == 0) {
|
||||
w.spawnParticle(Particle.END_ROD, cx, t.y + h + 1.3, cz, (int) (24 * intensity), 0.1, 0.1, 0.1, 0.15);
|
||||
w.spawnParticle(Particle.SNOWFLAKE, cx, t.y + h + 0.5, cz, (int) (20 * intensity), 1.6, 0.8, 1.6, 0.02);
|
||||
}
|
||||
|
||||
// Bereit zum Wachsen: leuchtender Ring am Boden
|
||||
if (t.req.isEmpty() && t.level.next() != null && fxTick % 10 == 0) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
double a = i * Math.PI * 2 / 16;
|
||||
w.spawnParticle(Particle.END_ROD, cx + Math.cos(a) * (t.level.radius + 0.8), t.y + 0.2,
|
||||
cz + Math.sin(a) * (t.level.radius + 0.8), 1, 0, 0, 0, 0.004);
|
||||
}
|
||||
}
|
||||
|
||||
// Glitzer ueber den Geschenken
|
||||
if (fxTick % 8 == 0) {
|
||||
for (String key : t.presents) {
|
||||
String[] pp = key.split(";");
|
||||
if (pp.length < 4) continue;
|
||||
try {
|
||||
dust(w, Integer.parseInt(pp[1]) + 0.5, Integer.parseInt(pp[2]) + 1.0, Integer.parseInt(pp[3]) + 0.5,
|
||||
Color.fromRGB(255, 215, 0), 0.9f);
|
||||
} catch (NumberFormatException ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================= Tick =================
|
||||
|
||||
private void tick() {
|
||||
if (!enabled()) return;
|
||||
if (isEnded()) {
|
||||
if (!endAnnounced && !trees.isEmpty()) announceEnd();
|
||||
return;
|
||||
}
|
||||
for (Tree t : trees) {
|
||||
Location base = t.loc();
|
||||
if (base == null || !base.getWorld().isChunkLoaded(t.x >> 4, t.z >> 4)) continue;
|
||||
if (!t.indexed) index(t);
|
||||
|
||||
Location top = base.clone().add(0.5, height(t.level) + 0.5, 0.5);
|
||||
int r = t.level.radius;
|
||||
base.getWorld().spawnParticle(Particle.SNOWFLAKE, top, 6, r, 1, r, 0.01);
|
||||
if (t.req.isEmpty() && t.level.next() != null) {
|
||||
base.getWorld().spawnParticle(Particle.END_ROD, top, 3, 0.3, 0.3, 0.3, 0.02);
|
||||
}
|
||||
|
||||
long cd = cooldown(t.level);
|
||||
if (cd > 0) {
|
||||
t.counter--;
|
||||
if (t.counter <= 0) {
|
||||
spawnPresent(t, false);
|
||||
t.counter = cd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Mannequin;
|
||||
import org.bukkit.profile.PlayerProfile;
|
||||
import org.bukkit.profile.PlayerTextures;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Spielermodell mit Skin ueber das Mannequin (Minecraft 1.21.9+), ohne Zusatz-Plugin.
|
||||
* Diese Klasse darf nur geladen werden, wenn {@link #supported()} true ergibt.
|
||||
*/
|
||||
final class MannequinSupport {
|
||||
private MannequinSupport() {}
|
||||
|
||||
static boolean supported() {
|
||||
try {
|
||||
EntityType.valueOf("MANNEQUIN");
|
||||
Class.forName("org.bukkit.entity.Mannequin");
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static LivingEntity spawn(Location loc, String name, String skinUrl, String skinPlayer) {
|
||||
return loc.getWorld().spawn(loc, Mannequin.class, m -> {
|
||||
m.setPersistent(false);
|
||||
m.setInvulnerable(true);
|
||||
m.setGravity(false);
|
||||
m.setSilent(true);
|
||||
m.setCustomName(name);
|
||||
m.setCustomNameVisible(true);
|
||||
// Diese Methoden heissen je nach Version anders oder fehlen: einzeln und fehlertolerant aufrufen
|
||||
optional(m, "setImmovable", true);
|
||||
optional(m, "setHideDescription", true);
|
||||
applySkin(m, skinUrl, skinPlayer);
|
||||
});
|
||||
}
|
||||
|
||||
private static void optional(Object target, String method, boolean value) {
|
||||
try {
|
||||
target.getClass().getMethod(method, boolean.class).invoke(target, value);
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
|
||||
private static void applySkin(Mannequin m, String skinUrl, String skinPlayer) {
|
||||
if (skinUrl != null && !skinUrl.isBlank()) {
|
||||
try {
|
||||
PlayerProfile prof = Bukkit.createPlayerProfile(UUID.nameUUIDFromBytes(skinUrl.getBytes()), "santa");
|
||||
PlayerTextures tex = prof.getTextures();
|
||||
tex.setSkin(new URL(skinUrl));
|
||||
prof.setTextures(tex);
|
||||
m.setPlayerProfile(prof);
|
||||
} catch (MalformedURLException ignored) { }
|
||||
} else if (skinPlayer != null && !skinPlayer.isBlank()) {
|
||||
PlayerProfile prof = Bukkit.createPlayerProfile(skinPlayer);
|
||||
prof.update().thenAccept(updated -> Bukkit.getScheduler().runTask(
|
||||
ChristmasManager.get().plugin(), () -> { if (m.isValid()) m.setPlayerProfile(updated); }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.MerchantRecipe;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Weihnachtsmarkt-Staende: Haendler (Villager), bei denen Spieler Plaetzchen und Deko gegen Items tauschen. */
|
||||
public final class MarketManager {
|
||||
private final ChristmasManager mgr;
|
||||
private final NamespacedKey stallKey;
|
||||
|
||||
private final File spawnFile;
|
||||
private Location spawn;
|
||||
|
||||
MarketManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.spawnFile = new File(mgr.plugin().getDataFolder(), "market.yml");
|
||||
this.stallKey = new NamespacedKey(mgr.plugin(), "market_stall");
|
||||
}
|
||||
|
||||
// ---------- Marktpunkt (Ziel der Portale) ----------
|
||||
|
||||
void load() {
|
||||
spawn = null;
|
||||
if (!spawnFile.exists()) return;
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(spawnFile);
|
||||
World w = Bukkit.getWorld(y.getString("spawn.world", ""));
|
||||
if (w == null) return;
|
||||
spawn = new Location(w, y.getDouble("spawn.x"), y.getDouble("spawn.y"), y.getDouble("spawn.z"),
|
||||
(float) y.getDouble("spawn.yaw"), (float) y.getDouble("spawn.pitch"));
|
||||
}
|
||||
|
||||
/** Ziel der Portale oder null, wenn noch nicht gesetzt. */
|
||||
public Location spawn() {
|
||||
if (spawn == null || spawn.getWorld() == null) load();
|
||||
return spawn == null ? null : spawn.clone();
|
||||
}
|
||||
|
||||
/** Hebt den Marktpunkt auf: Portale fuehren dann nirgendwohin, bis ein Admin ihn neu setzt. */
|
||||
public void clearSpawn() {
|
||||
spawn = null;
|
||||
if (spawnFile.exists()) spawnFile.delete();
|
||||
}
|
||||
|
||||
public void setSpawn(Location l) {
|
||||
spawn = l.clone();
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
y.set("spawn.world", l.getWorld().getName());
|
||||
y.set("spawn.x", l.getX());
|
||||
y.set("spawn.y", l.getY());
|
||||
y.set("spawn.z", l.getZ());
|
||||
y.set("spawn.yaw", l.getYaw());
|
||||
y.set("spawn.pitch", l.getPitch());
|
||||
try {
|
||||
y.save(spawnFile);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("market.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> stallIds() {
|
||||
ConfigurationSection s = mgr.cfg().getConfigurationSection("market.stalls");
|
||||
return s == null ? new LinkedHashSet<>() : s.getKeys(false);
|
||||
}
|
||||
|
||||
public boolean isStall(Entity e) {
|
||||
return e.getPersistentDataContainer().has(stallKey, PersistentDataType.STRING);
|
||||
}
|
||||
|
||||
public boolean spawn(Location loc, String id) {
|
||||
ConfigurationSection s = mgr.cfg().getConfigurationSection("market.stalls." + id);
|
||||
if (s == null) return false;
|
||||
World w = loc.getWorld();
|
||||
Villager v = w.spawn(loc, Villager.class, vill -> {
|
||||
vill.setAI(false);
|
||||
vill.setInvulnerable(true);
|
||||
vill.setPersistent(true);
|
||||
vill.setRemoveWhenFarAway(false);
|
||||
vill.setSilent(true);
|
||||
vill.setCustomName(ChristmasManager.color(s.getString("name", "&6Markt")));
|
||||
vill.setCustomNameVisible(true);
|
||||
vill.setVillagerType(Villager.Type.SNOW);
|
||||
vill.setProfession(profession(s.getString("profession", "LIBRARIAN")));
|
||||
vill.setVillagerLevel(5);
|
||||
vill.getPersistentDataContainer().set(stallKey, PersistentDataType.STRING, id);
|
||||
});
|
||||
applyTrades(v, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Villager.Profession profession(String name) {
|
||||
Villager.Profession p = Registry.VILLAGER_PROFESSION.get(NamespacedKey.minecraft(name.toLowerCase()));
|
||||
return p != null ? p : Villager.Profession.LIBRARIAN;
|
||||
}
|
||||
|
||||
private void applyTrades(Villager v, String id) {
|
||||
List<MerchantRecipe> recipes = new ArrayList<>();
|
||||
for (Map<?, ?> t : mgr.cfg().getMapList("market.stalls." + id + ".trades")) {
|
||||
ItemStack give = ChristmasManager.parseItem(String.valueOf(t.get("give")));
|
||||
if (give == null) {
|
||||
mgr.plugin().getLogger().warning("Markt '" + id + "': ungueltiges Item '" + t.get("give") + "'");
|
||||
continue;
|
||||
}
|
||||
MerchantRecipe r = new MerchantRecipe(give, 0, 100000, false, 0, 0f);
|
||||
Object price = t.get("price");
|
||||
int added = 0;
|
||||
if (price instanceof List<?> list) {
|
||||
for (Object o : list) {
|
||||
ItemStack p = ChristmasManager.parseItem(String.valueOf(o));
|
||||
if (p != null && added < 2) { r.addIngredient(p); added++; }
|
||||
}
|
||||
} else if (price != null) {
|
||||
ItemStack p = ChristmasManager.parseItem(String.valueOf(price));
|
||||
if (p != null) { r.addIngredient(p); added++; }
|
||||
}
|
||||
if (added == 0) continue;
|
||||
// Rabatte durch Ruf/Nachfrage abschalten (nur auf Paper vorhanden, sonst ignorieren)
|
||||
try {
|
||||
r.getClass().getMethod("setIgnoreDiscounts", boolean.class).invoke(r, true);
|
||||
} catch (Throwable ignored) { }
|
||||
recipes.add(r);
|
||||
}
|
||||
v.setRecipes(recipes);
|
||||
}
|
||||
|
||||
/** Wendet nach /xmas reload die Angebote auf alle geladenen Staende an. */
|
||||
public void refreshAll() {
|
||||
for (World w : Bukkit.getWorlds()) {
|
||||
for (Villager v : w.getEntitiesByClass(Villager.class)) {
|
||||
String id = v.getPersistentDataContainer().get(stallKey, PersistentDataType.STRING);
|
||||
if (id != null && mgr.cfg().getConfigurationSection("market.stalls." + id) != null) applyTrades(v, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeNearest(Player p, double radius) {
|
||||
Villager best = null;
|
||||
double bestDist = Double.MAX_VALUE;
|
||||
for (Entity e : p.getNearbyEntities(radius, radius, radius)) {
|
||||
if (e instanceof Villager v && isStall(v)) {
|
||||
double d = v.getLocation().distanceSquared(p.getLocation());
|
||||
if (d < bestDist) { bestDist = d; best = v; }
|
||||
}
|
||||
}
|
||||
if (best == null) return false;
|
||||
best.remove();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.RewardUtil;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.type.Candle;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Portale zum Weihnachtsmarkt: Spieler bauen sie per Befehl, der Markt wird vom Admin gesetzt. */
|
||||
public final class PortalManager {
|
||||
|
||||
private record Portal(UUID owner, String ownerName, String world, int x, int y, int z, boolean axisX) {}
|
||||
|
||||
private record Part(int u, int v, Material material) {}
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private final File file;
|
||||
private final List<Portal> portals = new ArrayList<>();
|
||||
private final Set<String> blocks = new HashSet<>();
|
||||
private final Map<UUID, Long> entering = new HashMap<>();
|
||||
private final Map<UUID, Long> cooldown = new HashMap<>();
|
||||
private final Map<UUID, Location> origin = new HashMap<>();
|
||||
private BukkitTask task;
|
||||
private int tickCount;
|
||||
|
||||
PortalManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.file = new File(mgr.plugin().getDataFolder(), "portals.yml");
|
||||
}
|
||||
|
||||
// ---------- Form ----------
|
||||
|
||||
/** Rahmen aus Schneebloecken: zwei Pfosten (u=-2/+2), Sturz, Seelaterne in der Mitte, Schneeschicht und Kerzen oben. */
|
||||
private static List<Part> parts() {
|
||||
List<Part> list = new ArrayList<>();
|
||||
for (int v = 0; v <= 3; v++) {
|
||||
list.add(new Part(-2, v, Material.SNOW_BLOCK));
|
||||
list.add(new Part(2, v, Material.SNOW_BLOCK));
|
||||
}
|
||||
for (int u = -1; u <= 1; u++) {
|
||||
list.add(new Part(u, 3, u == 0 ? Material.SEA_LANTERN : Material.SNOW_BLOCK));
|
||||
list.add(new Part(u, 4, Material.SNOW));
|
||||
}
|
||||
list.add(new Part(-2, 4, Material.RED_CANDLE));
|
||||
list.add(new Part(2, 4, Material.RED_CANDLE));
|
||||
return list;
|
||||
}
|
||||
|
||||
private static int bx(Portal p, int u) { return p.axisX() ? p.x() + u : p.x(); }
|
||||
private static int bz(Portal p, int u) { return p.axisX() ? p.z() : p.z() + u; }
|
||||
|
||||
private static String key(String world, int x, int y, int z) { return world + ";" + x + ";" + y + ";" + z; }
|
||||
|
||||
// ---------- Persistenz ----------
|
||||
|
||||
void load() {
|
||||
portals.clear();
|
||||
blocks.clear();
|
||||
if (file.exists()) {
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||
ConfigurationSection s = y.getConfigurationSection("portals");
|
||||
if (s != null) {
|
||||
for (String k : s.getKeys(false)) {
|
||||
ConfigurationSection c = s.getConfigurationSection(k);
|
||||
if (c == null) continue;
|
||||
try {
|
||||
Portal p = new Portal(UUID.fromString(c.getString("owner", "")), c.getString("owner-name", "?"),
|
||||
c.getString("world", "world"), c.getInt("x"), c.getInt("y"), c.getInt("z"),
|
||||
c.getBoolean("axis-x", true));
|
||||
portals.add(p);
|
||||
index(p);
|
||||
} catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
stop();
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 100L, 5L);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
}
|
||||
|
||||
private void save() {
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
int i = 0;
|
||||
for (Portal p : portals) {
|
||||
String k = "portals." + (i++);
|
||||
y.set(k + ".owner", p.owner().toString());
|
||||
y.set(k + ".owner-name", p.ownerName());
|
||||
y.set(k + ".world", p.world());
|
||||
y.set(k + ".x", p.x());
|
||||
y.set(k + ".y", p.y());
|
||||
y.set(k + ".z", p.z());
|
||||
y.set(k + ".axis-x", p.axisX());
|
||||
}
|
||||
try {
|
||||
y.save(file);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("portals.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void index(Portal p) {
|
||||
for (Part part : parts()) blocks.add(key(p.world(), bx(p, part.u()), p.y() + part.v(), bz(p, part.u())));
|
||||
}
|
||||
|
||||
public boolean isProtected(Block b) {
|
||||
return blocks.contains(key(b.getWorld().getName(), b.getX(), b.getY(), b.getZ()));
|
||||
}
|
||||
|
||||
public int countOf(UUID owner) {
|
||||
int n = 0;
|
||||
for (Portal p : portals) if (p.owner().equals(owner)) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
// ---------- Bauen und Entfernen ----------
|
||||
|
||||
private List<String> cost() { return mgr.cfg().getStringList("portal.cost"); }
|
||||
|
||||
/** @return Nachrichten-Schluessel */
|
||||
public String create(Player p, Block ground) {
|
||||
if (!mgr.cfg().getBoolean("portal.enable", true)) return "portal-disabled";
|
||||
if (mgr.market().spawn() == null) return "portal-no-market";
|
||||
List<String> worlds = mgr.cfg().getStringList("portal.allowed-worlds");
|
||||
if (!worlds.isEmpty() && !worlds.contains(p.getWorld().getName())) return "portal-world";
|
||||
int limit = Math.max(1, mgr.cfg().getInt("portal.limit-per-player", 1));
|
||||
if (!p.hasPermission("xmas.admin") && countOf(p.getUniqueId()) >= limit) return "portal-limit";
|
||||
|
||||
BlockFace f = p.getFacing();
|
||||
boolean axisX = f == BlockFace.NORTH || f == BlockFace.SOUTH;
|
||||
Block base = ground.getRelative(BlockFace.UP);
|
||||
Portal portal = new Portal(p.getUniqueId(), p.getName(), p.getWorld().getName(),
|
||||
base.getX(), base.getY(), base.getZ(), axisX);
|
||||
World w = p.getWorld();
|
||||
|
||||
// Platz pruefen: Rahmen und Oeffnung muessen frei sein
|
||||
for (Part part : parts()) {
|
||||
if (!free(w.getBlockAt(bx(portal, part.u()), portal.y() + part.v(), bz(portal, part.u())))) return "portal-no-space";
|
||||
}
|
||||
for (int u = -1; u <= 1; u++) {
|
||||
for (int v = 0; v <= 2; v++) {
|
||||
if (!free(w.getBlockAt(bx(portal, u), portal.y() + v, bz(portal, u)))) return "portal-no-space";
|
||||
}
|
||||
}
|
||||
// Boden unter dem Rahmen muss fest sein
|
||||
for (int u = -2; u <= 2; u++) {
|
||||
if (!w.getBlockAt(bx(portal, u), portal.y() - 1, bz(portal, u)).getType().isSolid()) return "portal-no-space";
|
||||
}
|
||||
|
||||
List<String> cost = cost();
|
||||
if (!p.hasPermission("xmas.admin")) {
|
||||
if (!ChristmasManager.hasAll(p, cost)) return "portal-need";
|
||||
ChristmasManager.takeAll(p, cost);
|
||||
}
|
||||
|
||||
for (Part part : parts()) {
|
||||
Block b = w.getBlockAt(bx(portal, part.u()), portal.y() + part.v(), bz(portal, part.u()));
|
||||
if (part.material() == Material.RED_CANDLE) {
|
||||
Candle c = (Candle) Material.RED_CANDLE.createBlockData();
|
||||
c.setLit(true);
|
||||
b.setBlockData(c, false);
|
||||
} else {
|
||||
b.setType(part.material(), false);
|
||||
}
|
||||
}
|
||||
portals.add(portal);
|
||||
index(portal);
|
||||
save();
|
||||
w.spawnParticle(Particle.END_ROD, bx(portal, 0) + 0.5, portal.y() + 1.5, bz(portal, 0) + 0.5, 60, 1, 1, 1, 0.08);
|
||||
w.playSound(p.getLocation(), Sound.BLOCK_BEACON_ACTIVATE, 1f, 1.3f);
|
||||
return "portal-created";
|
||||
}
|
||||
|
||||
private static boolean free(Block b) {
|
||||
return b.getType().isAir() || (b.isPassable() && !b.isLiquid());
|
||||
}
|
||||
|
||||
/** Entfernt das Portal, zu dem der Block gehoert. */
|
||||
public String remove(Player p, Block target) {
|
||||
Portal found = null;
|
||||
for (Portal portal : portals) {
|
||||
if (!portal.world().equals(target.getWorld().getName())) continue;
|
||||
for (Part part : parts()) {
|
||||
if (bx(portal, part.u()) == target.getX() && portal.y() + part.v() == target.getY()
|
||||
&& bz(portal, part.u()) == target.getZ()) { found = portal; break; }
|
||||
}
|
||||
if (found != null) break;
|
||||
}
|
||||
if (found == null) return "portal-none";
|
||||
if (!found.owner().equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) return "portal-not-owner";
|
||||
|
||||
World w = Bukkit.getWorld(found.world());
|
||||
if (w != null) {
|
||||
for (Part part : parts()) {
|
||||
blocks.remove(key(found.world(), bx(found, part.u()), found.y() + part.v(), bz(found, part.u())));
|
||||
w.getBlockAt(bx(found, part.u()), found.y() + part.v(), bz(found, part.u())).setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
portals.remove(found);
|
||||
save();
|
||||
if (mgr.cfg().getBoolean("portal.refund", true) && found.owner().equals(p.getUniqueId())) {
|
||||
for (String c : cost()) {
|
||||
ItemStack it = ChristmasManager.parseItem(c);
|
||||
if (it != null) RewardUtil.give(p, it);
|
||||
}
|
||||
}
|
||||
return "portal-removed";
|
||||
}
|
||||
|
||||
// ---------- Ruecksprung ----------
|
||||
|
||||
public String back(Player p) {
|
||||
Location l = origin.get(p.getUniqueId());
|
||||
if (l == null) return "portal-back-none";
|
||||
p.teleport(l);
|
||||
return "portal-back-ok";
|
||||
}
|
||||
|
||||
// ---------- Tick: Partikel und Teleport ----------
|
||||
|
||||
private void tick() {
|
||||
tickCount++;
|
||||
long now = System.currentTimeMillis();
|
||||
long delay = Math.max(1, mgr.cfg().getInt("portal.teleport-delay-seconds", 3)) * 1000L;
|
||||
long cd = Math.max(0, mgr.cfg().getInt("portal.cooldown-seconds", 10)) * 1000L;
|
||||
Set<UUID> inside = new HashSet<>();
|
||||
|
||||
for (Portal portal : portals) {
|
||||
World w = Bukkit.getWorld(portal.world());
|
||||
if (w == null || !w.isChunkLoaded(portal.x() >> 4, portal.z() >> 4)) continue;
|
||||
boolean near = false;
|
||||
for (Player p : w.getPlayers()) {
|
||||
if (p.getLocation().distanceSquared(new Location(w, portal.x(), portal.y(), portal.z())) <= 40 * 40) near = true;
|
||||
if (p.getLocation().distanceSquared(new Location(w, portal.x(), portal.y(), portal.z())) > 6 * 6) continue;
|
||||
if (!inOpening(portal, p.getLocation())) continue;
|
||||
inside.add(p.getUniqueId());
|
||||
|
||||
if (cooldown.getOrDefault(p.getUniqueId(), 0L) > now) continue;
|
||||
long start = entering.computeIfAbsent(p.getUniqueId(), k -> now);
|
||||
long left = delay - (now - start);
|
||||
if (left > 0) {
|
||||
p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(
|
||||
mgr.raw("portal-countdown").replace("%s%", String.valueOf((left + 999) / 1000))));
|
||||
w.spawnParticle(Particle.END_ROD, p.getLocation().add(0, 1, 0), 3, 0.3, 0.6, 0.3, 0.02);
|
||||
} else {
|
||||
teleport(p, portal);
|
||||
entering.remove(p.getUniqueId());
|
||||
cooldown.put(p.getUniqueId(), now + cd);
|
||||
}
|
||||
}
|
||||
if (!near) continue;
|
||||
|
||||
// Schimmernde Oeffnung
|
||||
if (tickCount % 2 == 0) {
|
||||
double cx = bx(portal, 0) + 0.5, cz = bz(portal, 0) + 0.5;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
double u = (ChristmasManager.RANDOM.nextDouble() * 2 - 1) * 1.4;
|
||||
double v = ChristmasManager.RANDOM.nextDouble() * 3;
|
||||
double x = portal.axisX() ? cx + u : cx;
|
||||
double z = portal.axisX() ? cz : cz + u;
|
||||
Color c = ChristmasManager.RANDOM.nextBoolean() ? Color.fromRGB(120, 200, 255) : Color.WHITE;
|
||||
spawnDust(w, x, portal.y() + v, z, c);
|
||||
}
|
||||
w.spawnParticle(Particle.SNOWFLAKE, cx, portal.y() + 1.5, cz, 6, 0.8, 1.2, 0.8, 0.01);
|
||||
w.spawnParticle(Particle.END_ROD, cx, portal.y() + 1.5, cz, 2, 0.9, 1.3, 0.9, 0.01);
|
||||
}
|
||||
}
|
||||
entering.keySet().removeIf(id -> !inside.contains(id));
|
||||
}
|
||||
|
||||
private void spawnDust(World w, double x, double y, double z, Color c) {
|
||||
Particle dust;
|
||||
try { dust = Particle.valueOf("DUST"); } catch (IllegalArgumentException e) {
|
||||
try { dust = Particle.valueOf("REDSTONE"); } catch (IllegalArgumentException ex) { return; }
|
||||
}
|
||||
w.spawnParticle(dust, x, y, z, 1, 0, 0, 0, 0, new Particle.DustOptions(c, 1.2f));
|
||||
}
|
||||
|
||||
private boolean inOpening(Portal p, Location l) {
|
||||
double x = l.getX(), y = l.getY(), z = l.getZ();
|
||||
if (y < p.y() - 0.2 || y > p.y() + 3) return false;
|
||||
if (p.axisX()) {
|
||||
return x >= p.x() - 1 && x < p.x() + 2 && z >= p.z() - 0.4 && z < p.z() + 1.4;
|
||||
}
|
||||
return z >= p.z() - 1 && z < p.z() + 2 && x >= p.x() - 0.4 && x < p.x() + 1.4;
|
||||
}
|
||||
|
||||
private void teleport(Player p, Portal portal) {
|
||||
Location dest = mgr.market().spawn();
|
||||
if (dest == null) { p.sendMessage(mgr.msg("portal-no-market")); return; }
|
||||
|
||||
// Rueckweg: etwas vor dem Portal, auf der Seite, von der der Spieler kam
|
||||
double side = portal.axisX() ? Math.signum(p.getLocation().getZ() - (portal.z() + 0.5))
|
||||
: Math.signum(p.getLocation().getX() - (portal.x() + 0.5));
|
||||
if (side == 0) side = 1;
|
||||
double ox = portal.axisX() ? portal.x() + 0.5 : portal.x() + 0.5 + side * 2.5;
|
||||
double oz = portal.axisX() ? portal.z() + 0.5 + side * 2.5 : portal.z() + 0.5;
|
||||
Location back = new Location(Bukkit.getWorld(portal.world()), ox, portal.y(), oz,
|
||||
portal.axisX() ? (side > 0 ? 180f : 0f) : (side > 0 ? 90f : -90f), 0f);
|
||||
origin.put(p.getUniqueId(), back);
|
||||
|
||||
p.getWorld().spawnParticle(Particle.END_ROD, p.getLocation().add(0, 1, 0), 30, 0.4, 0.8, 0.4, 0.05);
|
||||
p.teleport(dest);
|
||||
p.getWorld().playSound(dest, Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1.4f);
|
||||
p.getWorld().spawnParticle(Particle.SNOWFLAKE, dest.clone().add(0, 1, 0), 40, 0.6, 0.8, 0.6, 0.03);
|
||||
|
||||
net.md_5.bungee.api.chat.TextComponent msg = new net.md_5.bungee.api.chat.TextComponent(
|
||||
mgr.prefixed(mgr.raw("portal-teleported")) + " ");
|
||||
net.md_5.bungee.api.chat.TextComponent click = new net.md_5.bungee.api.chat.TextComponent(mgr.raw("portal-back-click"));
|
||||
click.setClickEvent(new net.md_5.bungee.api.chat.ClickEvent(net.md_5.bungee.api.chat.ClickEvent.Action.RUN_COMMAND, "/xmas market back"));
|
||||
msg.addExtra(click);
|
||||
p.spigot().sendMessage(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.text.Normalizer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/** Weihnachtsquiz: Frage im Chat, die schnellste richtige Antwort gewinnt. */
|
||||
public final class QuizManager {
|
||||
private final ChristmasManager mgr;
|
||||
private volatile boolean active;
|
||||
private volatile List<String> answers = List.of();
|
||||
private volatile List<Map<?, ?>> rewards = List.of();
|
||||
private String shownAnswer = "";
|
||||
private final AtomicBoolean answered = new AtomicBoolean(false);
|
||||
private BukkitTask timeoutTask;
|
||||
private BukkitTask autoTask;
|
||||
private int lastIndex = -1;
|
||||
|
||||
QuizManager(ChristmasManager mgr) { this.mgr = mgr; }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
|
||||
void start() {
|
||||
stop();
|
||||
if (!mgr.cfg().getBoolean("quiz.auto.enable", false)) return;
|
||||
long ticks = 20L * 60 * Math.max(1, mgr.cfg().getInt("quiz.auto.interval-minutes", 20));
|
||||
autoTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
|
||||
int min = mgr.cfg().getInt("quiz.auto.min-players", 2);
|
||||
if (!active && Bukkit.getOnlinePlayers().size() >= min) ask(-1);
|
||||
}, ticks, ticks);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (autoTask != null) autoTask.cancel();
|
||||
autoTask = null;
|
||||
cancelRound();
|
||||
}
|
||||
|
||||
private void cancelRound() {
|
||||
active = false;
|
||||
if (timeoutTask != null) timeoutTask.cancel();
|
||||
timeoutTask = null;
|
||||
}
|
||||
|
||||
public int questionCount() { return mgr.cfg().getMapList("quiz.questions").size(); }
|
||||
|
||||
/** Startet eine Frage (index -1 = zufaellig). false, wenn schon aktiv oder keine Fragen vorhanden. */
|
||||
public boolean ask(int index) {
|
||||
if (active) return false;
|
||||
List<Map<?, ?>> questions = mgr.cfg().getMapList("quiz.questions");
|
||||
if (questions.isEmpty()) return false;
|
||||
int i = index;
|
||||
if (i < 0 || i >= questions.size()) {
|
||||
do {
|
||||
i = ChristmasManager.RANDOM.nextInt(questions.size());
|
||||
} while (questions.size() > 1 && i == lastIndex);
|
||||
}
|
||||
lastIndex = i;
|
||||
Map<?, ?> q = questions.get(i);
|
||||
|
||||
List<String> ans = new ArrayList<>();
|
||||
Object a = q.get("a");
|
||||
if (a instanceof List<?> list) for (Object o : list) ans.add(normalize(String.valueOf(o)));
|
||||
else if (a != null) ans.add(normalize(String.valueOf(a)));
|
||||
if (ans.isEmpty()) return false;
|
||||
|
||||
List<Map<?, ?>> rw = new ArrayList<>();
|
||||
Object r = q.get("reward");
|
||||
if (r instanceof List<?> rl) for (Object o : rl) if (o instanceof Map<?, ?> m) rw.add(m);
|
||||
if (rw.isEmpty()) rw = mgr.cfg().getMapList("quiz.default-reward");
|
||||
|
||||
String shown = a instanceof List<?> l && !l.isEmpty() ? String.valueOf(l.get(0)) : String.valueOf(a);
|
||||
answers = ans;
|
||||
rewards = rw;
|
||||
shownAnswer = shown;
|
||||
answered.set(false);
|
||||
active = true;
|
||||
|
||||
int seconds = Math.max(5, mgr.cfg().getInt("quiz.answer-seconds", 30));
|
||||
Bukkit.broadcastMessage(mgr.msg("quiz-question", "%question%", String.valueOf(q.get("q")),
|
||||
"%time%", String.valueOf(seconds)));
|
||||
timeoutTask = Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
|
||||
if (!active) return;
|
||||
cancelRound();
|
||||
Bukkit.broadcastMessage(mgr.msg("quiz-timeout", "%answer%", shownAnswer));
|
||||
}, seconds * 20L);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean skip() {
|
||||
if (!active) return false;
|
||||
cancelRound();
|
||||
Bukkit.broadcastMessage(mgr.msg("quiz-timeout", "%answer%", shownAnswer));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String normalize(String s) {
|
||||
// Umlaute wie ae/oe/ue behandeln, damit "Türchen" und "Tuerchen" beide richtig sind
|
||||
String lower = s.trim().toLowerCase().replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss");
|
||||
String n = Normalizer.normalize(lower, Normalizer.Form.NFD).replaceAll("\\p{M}", "");
|
||||
return n.replaceAll("[^a-z0-9 ]", "").replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wird aus dem (asynchronen) Chat-Ereignis aufgerufen. true = richtige Antwort, Nachricht sollte unterdrueckt werden.
|
||||
*/
|
||||
public boolean onChat(Player p, String message) {
|
||||
if (!active) return false;
|
||||
if (!answers.contains(normalize(message))) return false;
|
||||
if (!answered.compareAndSet(false, true)) return true; // jemand war schneller
|
||||
Bukkit.getScheduler().runTask(mgr.plugin(), () -> win(p));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void win(Player p) {
|
||||
cancelRound();
|
||||
Bukkit.broadcastMessage(mgr.msg("quiz-win", "%player%", p.getName(), "%answer%", shownAnswer));
|
||||
for (Map<?, ?> reward : rewards) ChristmasExtras.applyMap(p, reward);
|
||||
p.sendMessage(mgr.msg("quiz-reward"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.boss.BarColor;
|
||||
import org.bukkit.boss.BarStyle;
|
||||
import org.bukkit.boss.BossBar;
|
||||
import org.bukkit.entity.ArmorStand;
|
||||
import org.bukkit.entity.Horse;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.LeatherArmorMeta;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
import org.bukkit.profile.PlayerProfile;
|
||||
import org.bukkit.profile.PlayerTextures;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Santa-Claus-Event: Ein Weihnachtsmann steht (oder laeuft) an einem Ort, hinterlaesst Schnee und wirft Geschenke ab. */
|
||||
public final class SantaEvent {
|
||||
private final ChristmasManager mgr;
|
||||
private boolean active;
|
||||
private Location origin;
|
||||
private Location loc;
|
||||
private LivingEntity stand;
|
||||
private BossBar bar;
|
||||
private BukkitTask task;
|
||||
private int total;
|
||||
private int left;
|
||||
private final Map<Block, Long> trail = new HashMap<>();
|
||||
private final SantaMelody melody;
|
||||
private long lastDropMsg;
|
||||
|
||||
// Schlitten-Anflug
|
||||
private boolean flying;
|
||||
private BukkitTask flightTask;
|
||||
private final List<LivingEntity> flightEntities = new java.util.ArrayList<>();
|
||||
private ArmorStand sleighStand;
|
||||
private int flightStep;
|
||||
|
||||
SantaEvent(ChristmasManager mgr) { this.mgr = mgr; this.melody = new SantaMelody(mgr); }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public Location getLocation() { return loc == null ? null : loc.clone(); }
|
||||
|
||||
/** Waehlt zufaellig einen konfigurierten Ort, null wenn keiner vorhanden. */
|
||||
public Location randomLocation() {
|
||||
List<Map<?, ?>> list = mgr.cfg().getMapList("santa.locations");
|
||||
for (int tries = 0; tries < 10 && !list.isEmpty(); tries++) {
|
||||
Map<?, ?> m = list.get(ChristmasManager.RANDOM.nextInt(list.size()));
|
||||
World w = Bukkit.getWorld(String.valueOf(m.get("world")));
|
||||
if (w == null) continue;
|
||||
return new Location(w, num(m.get("x")), num(m.get("y")), num(m.get("z")));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static double num(Object o) { return o instanceof Number n ? n.doubleValue() : 0; }
|
||||
|
||||
public static String formatDuration(int sec) {
|
||||
if (sec % 3600 == 0 && sec >= 3600) return (sec / 3600) + "h";
|
||||
if (sec % 60 == 0 && sec >= 60) return (sec / 60) + " min";
|
||||
return sec + "s";
|
||||
}
|
||||
|
||||
public void start(int seconds, Location where) {
|
||||
if (active || where == null || where.getWorld() == null) return;
|
||||
active = true;
|
||||
origin = where.clone();
|
||||
loc = where.clone();
|
||||
total = left = Math.max(5, seconds);
|
||||
|
||||
stand = createModel(loc);
|
||||
|
||||
bar = Bukkit.createBossBar(barTitle(), BarColor.RED, BarStyle.SOLID);
|
||||
if (mgr.cfg().getBoolean("santa.sleigh.enable", true)) beginFlight();
|
||||
Bukkit.broadcastMessage(mgr.msg("santa-started",
|
||||
"%x%", String.valueOf(loc.getBlockX()), "%y%", String.valueOf(loc.getBlockY()),
|
||||
"%z%", String.valueOf(loc.getBlockZ()), "%world%", loc.getWorld().getName(),
|
||||
"%duration%", formatDuration(total)));
|
||||
|
||||
lastDropMsg = 0;
|
||||
melody.play(loc.getWorld());
|
||||
startWalking();
|
||||
|
||||
int dropEvery = Math.max(1, mgr.cfg().getInt("santa.drop-interval", 3));
|
||||
double chance = mgr.cfg().getDouble("santa.drop-chance", 100);
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
|
||||
left--;
|
||||
fadeTrail(false);
|
||||
if (left <= 0) { stop(true); return; }
|
||||
bar.setTitle(barTitle());
|
||||
bar.setProgress(Math.max(0, Math.min(1, left / (double) total)));
|
||||
for (Player p : Bukkit.getOnlinePlayers()) bar.addPlayer(p);
|
||||
loc.getWorld().spawnParticle(Particle.SNOWFLAKE, loc.clone().add(0, 2, 0), 20, 1, 1, 1, 0.02);
|
||||
if ((total - left) % dropEvery == 0 && ChristmasManager.RANDOM.nextDouble() * 100 < chance) dropGift();
|
||||
}, 20L, 20L);
|
||||
}
|
||||
|
||||
/** Erzeugt Santas Modell (Spielermodell oder Ruestungsstaender) an der Stelle. */
|
||||
LivingEntity createModel(Location loc) {
|
||||
LivingEntity stand = null;
|
||||
String name = ChristmasManager.color(mgr.cfg().getString("santa.name", "&cSanta Claus"));
|
||||
String skin = mgr.cfg().getString("santa.skin-url", "");
|
||||
String model = mgr.cfg().getString("santa.model", "AUTO").toUpperCase();
|
||||
String skinPlayer = mgr.cfg().getString("santa.skin-player", "");
|
||||
boolean mannequin = !model.equals("ARMOR_STAND") && MannequinSupport.supported();
|
||||
if (mannequin) {
|
||||
try {
|
||||
stand = MannequinSupport.spawn(loc, name, skin, skinPlayer);
|
||||
if (skin.isBlank() && skinPlayer.isBlank()) {
|
||||
// Ohne eigenen Skin: rote Lederruestung, damit er wie Santa aussieht
|
||||
var eq = stand.getEquipment();
|
||||
eq.setHelmet(dyed(Material.LEATHER_HELMET));
|
||||
eq.setChestplate(dyed(Material.LEATHER_CHESTPLATE));
|
||||
eq.setLeggings(dyed(Material.LEATHER_LEGGINGS));
|
||||
eq.setBoots(dyed(Material.LEATHER_BOOTS));
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
mgr.plugin().getLogger().warning("Mannequin konnte nicht erzeugt werden, nutze Rüstungsständer: " + t);
|
||||
if (stand != null) stand.remove();
|
||||
stand = null;
|
||||
}
|
||||
}
|
||||
if (stand == null) {
|
||||
stand = loc.getWorld().spawn(loc, ArmorStand.class, s -> {
|
||||
s.setPersistent(false);
|
||||
s.setInvulnerable(true);
|
||||
s.setGravity(false);
|
||||
s.setArms(true);
|
||||
s.setCustomName(name);
|
||||
s.setCustomNameVisible(true);
|
||||
ItemStack head = skin.isBlank() ? dyed(Material.LEATHER_HELMET) : skull(skin);
|
||||
s.getEquipment().setHelmet(head);
|
||||
s.getEquipment().setChestplate(dyed(Material.LEATHER_CHESTPLATE));
|
||||
s.getEquipment().setLeggings(dyed(Material.LEATHER_LEGGINGS));
|
||||
s.getEquipment().setBoots(dyed(Material.LEATHER_BOOTS));
|
||||
});
|
||||
}
|
||||
return stand;
|
||||
}
|
||||
|
||||
private String barTitle() {
|
||||
return mgr.raw("santa-bossbar").replace("%time%", String.valueOf(left));
|
||||
}
|
||||
|
||||
private static ItemStack dyed(Material m) {
|
||||
ItemStack it = new ItemStack(m);
|
||||
LeatherArmorMeta meta = (LeatherArmorMeta) it.getItemMeta();
|
||||
if (meta != null) { meta.setColor(Color.RED); it.setItemMeta(meta); }
|
||||
return it;
|
||||
}
|
||||
|
||||
private static ItemStack skull(String url) {
|
||||
ItemStack it = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) it.getItemMeta();
|
||||
if (meta == null) return it;
|
||||
try {
|
||||
PlayerProfile prof = Bukkit.createPlayerProfile(UUID.nameUUIDFromBytes(url.getBytes()), "santa");
|
||||
PlayerTextures tex = prof.getTextures();
|
||||
tex.setSkin(new URL(url));
|
||||
prof.setTextures(tex);
|
||||
meta.setOwnerProfile(prof);
|
||||
it.setItemMeta(meta);
|
||||
} catch (MalformedURLException ignored) { }
|
||||
return it;
|
||||
}
|
||||
|
||||
// ---------- Laufen und Schneespur ----------
|
||||
|
||||
// Fluessiges Laufen: jeden Tick ein kleiner Schritt zu einem Zufallsziel, sanftes Drehen, kurze Pausen.
|
||||
private BukkitTask walkTask;
|
||||
private Location walkTarget;
|
||||
private int walkPause;
|
||||
private float walkYaw;
|
||||
private Block lastStep;
|
||||
|
||||
private void startWalking() {
|
||||
stopWalking();
|
||||
if (mgr.cfg().getDouble("santa.walk-radius", 0) <= 0) return;
|
||||
walkYaw = loc.getYaw();
|
||||
walkTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::walkStep, 20L, 1L);
|
||||
}
|
||||
|
||||
private void stopWalking() {
|
||||
if (walkTask != null) walkTask.cancel();
|
||||
walkTask = null;
|
||||
walkTarget = null;
|
||||
}
|
||||
|
||||
/** Sucht die Standhoehe an x/z (Fuss-Block frei, darunter fest, 2 Bloecke Platz); NaN = nicht begehbar. */
|
||||
private double groundAt(World w, double x, double z, double fromY) {
|
||||
int bx = (int) Math.floor(x), bz = (int) Math.floor(z);
|
||||
for (int by = (int) Math.floor(fromY) + 1; by >= (int) Math.floor(fromY) - 3; by--) {
|
||||
Block feet = w.getBlockAt(bx, by, bz);
|
||||
if (!feet.getType().isSolid() && !feet.isLiquid()
|
||||
&& w.getBlockAt(bx, by + 1, bz).getType().isAir()
|
||||
&& w.getBlockAt(bx, by - 1, bz).getType().isSolid()) {
|
||||
return by;
|
||||
}
|
||||
}
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
private void walkStep() {
|
||||
if (!active || flying || stand == null || stand.isDead()) return;
|
||||
double radius = mgr.cfg().getDouble("santa.walk-radius", 0);
|
||||
if (radius <= 0) return;
|
||||
if (walkPause > 0) { walkPause--; return; }
|
||||
World w = loc.getWorld();
|
||||
|
||||
if (walkTarget == null) {
|
||||
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
|
||||
double dist = 2 + ChristmasManager.RANDOM.nextDouble() * Math.max(1, radius - 1);
|
||||
double tx = origin.getX() + Math.cos(ang) * Math.min(dist, radius);
|
||||
double tz = origin.getZ() + Math.sin(ang) * Math.min(dist, radius);
|
||||
double gy = groundAt(w, tx, tz, loc.getY());
|
||||
if (Double.isNaN(gy)) { walkPause = 20; return; }
|
||||
walkTarget = new Location(w, tx, gy, tz);
|
||||
}
|
||||
|
||||
double dx = walkTarget.getX() - loc.getX(), dz = walkTarget.getZ() - loc.getZ();
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist < 0.25) {
|
||||
walkTarget = null;
|
||||
walkPause = 30 + ChristmasManager.RANDOM.nextInt(80); // stehen bleiben und umsehen
|
||||
return;
|
||||
}
|
||||
|
||||
// sanft zum Ziel drehen
|
||||
float want = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
float diff = ((want - walkYaw + 540f) % 360f) - 180f;
|
||||
walkYaw += Math.max(-10f, Math.min(10f, diff));
|
||||
if (Math.abs(diff) > 45f) return; // erst drehen, dann loslaufen
|
||||
|
||||
double speed = Math.max(0.02, mgr.cfg().getDouble("santa.walk-speed", 0.07));
|
||||
double rad = Math.toRadians(walkYaw);
|
||||
double nx = loc.getX() - Math.sin(rad) * speed;
|
||||
double nz = loc.getZ() + Math.cos(rad) * speed;
|
||||
double ox = nx - origin.getX(), oz = nz - origin.getZ();
|
||||
double gy = (ox * ox + oz * oz > radius * radius) ? Double.NaN : groundAt(w, nx, nz, loc.getY());
|
||||
if (Double.isNaN(gy) || Math.abs(gy - loc.getY()) > 1.01) {
|
||||
walkTarget = null; // Hindernis oder Rand: neues Ziel nach kurzer Pause
|
||||
walkPause = 10 + ChristmasManager.RANDOM.nextInt(20);
|
||||
return;
|
||||
}
|
||||
double ny = loc.getY() + Math.max(-0.25, Math.min(0.25, gy - loc.getY()));
|
||||
Location next = new Location(w, nx, ny, nz, walkYaw, 0f);
|
||||
stand.teleport(next);
|
||||
loc = next;
|
||||
|
||||
// Schritte: Geraeusch und kurze Schneespur
|
||||
if (stand.getTicksLived() % 9 == 0) w.playSound(next, Sound.BLOCK_SNOW_STEP, 0.6f, 1f);
|
||||
Block feet = w.getBlockAt(next.getBlockX(), (int) Math.floor(gy), next.getBlockZ());
|
||||
if (!feet.equals(lastStep)) {
|
||||
lastStep = feet;
|
||||
if (mgr.cfg().getBoolean("santa.snow-trail.enabled", true) && feet.getType() == Material.AIR) {
|
||||
feet.setType(Material.SNOW, false);
|
||||
long fade = Math.max(1, mgr.cfg().getLong("santa.snow-trail.fade-seconds", 5)) * 1000L;
|
||||
trail.put(feet, System.currentTimeMillis() + fade);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fadeTrail(boolean all) {
|
||||
long now = System.currentTimeMillis();
|
||||
Iterator<Map.Entry<Block, Long>> it = trail.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
var e = it.next();
|
||||
if (all || e.getValue() <= now) {
|
||||
if (e.getKey().getType() == Material.SNOW) e.getKey().setType(Material.AIR, false);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Schlitten-Anflug ----------
|
||||
|
||||
private void beginFlight() {
|
||||
int seconds = Math.max(5, Math.min(total - 2, mgr.cfg().getInt("santa.sleigh.flight-seconds", 15)));
|
||||
double dist = Math.max(20, mgr.cfg().getDouble("santa.sleigh.distance", 70));
|
||||
double height = Math.max(8, mgr.cfg().getDouble("santa.sleigh.height", 30));
|
||||
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
|
||||
Vector dir = new Vector(Math.cos(ang), 0, Math.sin(ang));
|
||||
Location from = origin.clone().add(dir.clone().multiply(-dist));
|
||||
from.setY(origin.getY() + height);
|
||||
final int steps = seconds * 10;
|
||||
|
||||
flying = true;
|
||||
flightStep = 0;
|
||||
World w = origin.getWorld();
|
||||
|
||||
sleighStand = w.spawn(from, ArmorStand.class, s -> {
|
||||
s.setPersistent(false);
|
||||
s.setInvulnerable(true);
|
||||
s.setGravity(false);
|
||||
s.setVisible(false);
|
||||
s.getEquipment().setHelmet(new ItemStack(Material.RED_SHULKER_BOX));
|
||||
});
|
||||
List<String> names = mgr.cfg().getStringList("santa.sleigh.reindeer-names");
|
||||
for (int i = 0; i < 2; i++) {
|
||||
final String n = names.size() > i ? names.get(i) : "Rudolph";
|
||||
Horse horse = w.spawn(from, Horse.class, h -> {
|
||||
h.setPersistent(false);
|
||||
h.setInvulnerable(true);
|
||||
h.setGravity(false);
|
||||
h.setAI(false);
|
||||
h.setSilent(true);
|
||||
h.setColor(Horse.Color.BROWN);
|
||||
h.setCustomName(ChristmasManager.color("&6" + n));
|
||||
h.setCustomNameVisible(true);
|
||||
});
|
||||
flightEntities.add(horse);
|
||||
}
|
||||
|
||||
flightTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
|
||||
if (stand == null || !stand.isValid()) { endFlight(); return; }
|
||||
double t = Math.min(1.0, flightStep / (double) steps);
|
||||
// Sinkflug: erst waagerecht, dann weich landen
|
||||
double eased = t * t * (3 - 2 * t);
|
||||
Location pos = from.clone().add(origin.clone().subtract(from).toVector().multiply(t));
|
||||
pos.setY(from.getY() + (origin.getY() - from.getY()) * eased);
|
||||
pos.setDirection(dir);
|
||||
loc = pos.clone();
|
||||
|
||||
stand.teleport(pos);
|
||||
if (sleighStand != null) {
|
||||
sleighStand.teleport(pos.clone().subtract(dir.clone().multiply(3)).subtract(0, 1.5, 0));
|
||||
}
|
||||
for (int i = 0; i < flightEntities.size(); i++) {
|
||||
flightEntities.get(i).teleport(pos.clone().add(dir.clone().multiply(3 + i * 2.5)));
|
||||
}
|
||||
w.spawnParticle(Particle.END_ROD, pos.clone().subtract(dir.clone().multiply(4)), 6, 0.3, 0.3, 0.3, 0.02);
|
||||
w.spawnParticle(Particle.SNOWFLAKE, pos.clone().subtract(dir.clone().multiply(2)), 10, 0.6, 0.4, 0.6, 0.02);
|
||||
if (flightStep % 10 == 0) w.playSound(pos, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 2f, 1.5f);
|
||||
|
||||
flightStep++;
|
||||
if (flightStep > steps) endFlight();
|
||||
}, 0L, 2L);
|
||||
}
|
||||
|
||||
private void endFlight() {
|
||||
if (flightTask != null) flightTask.cancel();
|
||||
flightTask = null;
|
||||
flying = false;
|
||||
if (sleighStand != null) { sleighStand.remove(); sleighStand = null; }
|
||||
for (LivingEntity e : flightEntities) e.remove();
|
||||
flightEntities.clear();
|
||||
if (active && stand != null && stand.isValid() && origin != null) {
|
||||
loc = origin.clone();
|
||||
stand.teleport(origin);
|
||||
origin.getWorld().spawnParticle(Particle.CLOUD, origin.clone().add(0, 1, 0), 40, 1, 0.5, 1, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Geschenke ----------
|
||||
|
||||
private void dropGift() {
|
||||
List<String> gifts = mgr.cfg().getStringList("santa.gifts");
|
||||
if (gifts.isEmpty()) return;
|
||||
String pick = gifts.get(ChristmasManager.RANDOM.nextInt(gifts.size()));
|
||||
ItemStack it;
|
||||
if (pick.toLowerCase().startsWith("cracker:")) {
|
||||
String[] p = pick.split(":");
|
||||
int amount = 1;
|
||||
if (p.length > 2) {
|
||||
try { amount = Integer.parseInt(p[2].trim()); } catch (NumberFormatException ignored) { }
|
||||
}
|
||||
it = mgr.crackers().create(p[1].trim(), amount);
|
||||
} else {
|
||||
it = ChristmasManager.parseItem(pick);
|
||||
}
|
||||
if (it == null) return;
|
||||
double r = mgr.cfg().getDouble("santa.radius", 6);
|
||||
Location at = loc.clone().add((ChristmasManager.RANDOM.nextDouble() - 0.5) * 2 * r, 3,
|
||||
(ChristmasManager.RANDOM.nextDouble() - 0.5) * 2 * r);
|
||||
loc.getWorld().dropItem(at, it);
|
||||
loc.getWorld().spawnParticle(Particle.END_ROD, at, 10, 0.2, 0.2, 0.2, 0.02);
|
||||
loc.getWorld().playSound(at, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.6f, 1.2f);
|
||||
// Nachricht nur selten, sonst wird der Chat bei jedem Geschenk zugespammt
|
||||
long every = mgr.cfg().getLong("santa.drop-message-interval", 60) * 1000L;
|
||||
long now = System.currentTimeMillis();
|
||||
if (every > 0 && now - lastDropMsg >= every) {
|
||||
lastDropMsg = now;
|
||||
for (Player p : loc.getWorld().getPlayers()) {
|
||||
if (p.getLocation().distanceSquared(loc) <= 64 * 64) p.sendMessage(mgr.msg("santa-dropped"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop(boolean announce) {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
fadeTrail(true);
|
||||
melody.stop();
|
||||
stopWalking();
|
||||
endFlightSilently();
|
||||
if (stand != null) stand.remove();
|
||||
stand = null;
|
||||
if (bar != null) { bar.removeAll(); bar = null; }
|
||||
if (announce) Bukkit.broadcastMessage(mgr.msg("santa-stopped"));
|
||||
}
|
||||
|
||||
private void endFlightSilently() {
|
||||
if (flightTask != null) flightTask.cancel();
|
||||
flightTask = null;
|
||||
flying = false;
|
||||
if (sleighStand != null) { sleighStand.remove(); sleighStand = null; }
|
||||
for (LivingEntity e : flightEntities) e.remove();
|
||||
flightEntities.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.SoundCategory;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
/** Spielt beim Santa-Event eine Weihnachtsmelodie mit Glocken (Notenblock-Klaenge, keine Dateien noetig). */
|
||||
final class SantaMelody {
|
||||
// Melodie: {Halbtoene ueber C4 (-1 = Pause), Dauer in Viertelschlag-Achteln (1 = Achtel)}
|
||||
private static final int[][] JINGLE = {
|
||||
{4, 2}, {4, 2}, {4, 4}, {4, 2}, {4, 2}, {4, 4}, {4, 2}, {7, 2}, {0, 3}, {2, 1}, {4, 8},
|
||||
{5, 2}, {5, 2}, {5, 3}, {5, 1}, {5, 2}, {4, 2}, {4, 2}, {4, 1}, {4, 1}, {4, 2}, {2, 2}, {2, 2}, {4, 2}, {2, 4}, {7, 4},
|
||||
{4, 2}, {4, 2}, {4, 4}, {4, 2}, {4, 2}, {4, 4}, {4, 2}, {7, 2}, {0, 3}, {2, 1}, {4, 8},
|
||||
{5, 2}, {5, 2}, {5, 3}, {5, 1}, {5, 2}, {4, 2}, {4, 2}, {4, 1}, {4, 1}, {7, 2}, {7, 2}, {5, 2}, {2, 2}, {0, 8},
|
||||
};
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private BukkitTask task;
|
||||
|
||||
SantaMelody(ChristmasManager mgr) { this.mgr = mgr; }
|
||||
|
||||
void play(World world) {
|
||||
stop();
|
||||
if (!mgr.cfg().getBoolean("santa.melody.enable", true)) return;
|
||||
final float volume = (float) mgr.cfg().getDouble("santa.melody.volume", 1.0);
|
||||
final boolean everyone = mgr.cfg().getBoolean("santa.melody.everyone", false);
|
||||
final int tickPerUnit = Math.max(1, mgr.cfg().getInt("santa.melody.tempo", 3));
|
||||
final int[] pos = {0};
|
||||
final int[] wait = {0};
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
|
||||
if (wait[0]-- > 0) return;
|
||||
if (pos[0] >= JINGLE.length) { stop(); return; }
|
||||
int[] n = JINGLE[pos[0]++];
|
||||
float pitch = (float) Math.pow(2.0, (n[0] + 6 - 12) / 12.0); // C4 = Notenblock-Ton 6
|
||||
for (Player p : everyone ? Bukkit.getOnlinePlayers() : world.getPlayers()) {
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BELL, SoundCategory.RECORDS, volume, pitch);
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_CHIME, SoundCategory.RECORDS, volume * 0.5f, pitch);
|
||||
}
|
||||
wait[0] = n[1] * tickPerUnit - 1;
|
||||
}, 10L, 1L);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Schneeball-Minispiel: In einer Arena zaehlen Treffer mit Schneebaellen, die Besten bekommen Belohnungen. */
|
||||
public final class SnowballGame {
|
||||
private final ChristmasManager mgr;
|
||||
private final File file;
|
||||
private String arenaWorld;
|
||||
private double ax, ay, az;
|
||||
private int radius;
|
||||
private boolean active;
|
||||
private int left;
|
||||
private BukkitTask task;
|
||||
private BukkitTask arenaTask;
|
||||
private final Map<UUID, Integer> points = new HashMap<>();
|
||||
private final Map<UUID, String> names = new HashMap<>();
|
||||
|
||||
SnowballGame(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.file = new File(mgr.plugin().getDataFolder(), "snowball.yml");
|
||||
}
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public boolean hasArena() { return arenaWorld != null && radius > 0; }
|
||||
|
||||
void load() {
|
||||
startArenaTask();
|
||||
if (!file.exists()) return;
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||
arenaWorld = y.getString("arena.world");
|
||||
ax = y.getDouble("arena.x");
|
||||
ay = y.getDouble("arena.y");
|
||||
az = y.getDouble("arena.z");
|
||||
radius = y.getInt("arena.radius", 0);
|
||||
}
|
||||
|
||||
public void setArena(Location center, int r) {
|
||||
arenaWorld = center.getWorld().getName();
|
||||
ax = center.getX();
|
||||
ay = center.getY();
|
||||
az = center.getZ();
|
||||
radius = Math.max(3, r);
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
y.set("arena.world", arenaWorld);
|
||||
y.set("arena.x", ax);
|
||||
y.set("arena.y", ay);
|
||||
y.set("arena.z", az);
|
||||
y.set("arena.radius", radius);
|
||||
try {
|
||||
y.save(file);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("snowball.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Laeuft immer, fuellt aber nur waehrend einer laufenden Runde die Schneebaelle der Spieler in der Arena auf. */
|
||||
private void startArenaTask() {
|
||||
if (arenaTask != null) arenaTask.cancel();
|
||||
arenaTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
|
||||
if (!hasArena()) return;
|
||||
if (!active) return; // nur waehrend einer laufenden Runde
|
||||
int refill = Math.max(1, mgr.cfg().getInt("snowball.refill-amount", 16));
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (inArena(p)) refill(p, refill);
|
||||
}
|
||||
}, 20L, 20L);
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
stop(false);
|
||||
if (arenaTask != null) arenaTask.cancel();
|
||||
arenaTask = null;
|
||||
}
|
||||
|
||||
public boolean inArena(Player p) {
|
||||
if (!hasArena()) return false;
|
||||
World w = p.getWorld();
|
||||
if (!w.getName().equals(arenaWorld)) return false;
|
||||
double dx = p.getLocation().getX() - ax, dz = p.getLocation().getZ() - az;
|
||||
return dx * dx + dz * dz <= (double) radius * radius && Math.abs(p.getLocation().getY() - ay) <= 40;
|
||||
}
|
||||
|
||||
// ---------- Ablauf ----------
|
||||
|
||||
public boolean start(int seconds) {
|
||||
if (active || !hasArena()) return false;
|
||||
points.clear();
|
||||
names.clear();
|
||||
active = true;
|
||||
left = Math.max(10, seconds);
|
||||
Bukkit.broadcastMessage(mgr.msg("sb-started", "%time%", SantaEvent.formatDuration(left),
|
||||
"%world%", arenaWorld, "%x%", String.valueOf((int) ax), "%z%", String.valueOf((int) az)));
|
||||
int refill = Math.max(1, mgr.cfg().getInt("snowball.refill-amount", 16));
|
||||
for (Player p : Bukkit.getOnlinePlayers()) if (inArena(p)) refill(p, refill);
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
|
||||
left--;
|
||||
if (left <= 0) { stop(true); return; }
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (!inArena(p)) continue;
|
||||
String text = mgr.raw("sb-actionbar").replace("%points%", String.valueOf(points(p.getUniqueId())))
|
||||
.replace("%rank%", String.valueOf(Math.max(1, rank(p.getUniqueId()))))
|
||||
.replace("%time%", String.valueOf(left));
|
||||
p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(text));
|
||||
}
|
||||
}, 20L, 20L);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void refill(Player p, int amount) {
|
||||
int have = 0;
|
||||
for (ItemStack it : p.getInventory().getContents()) {
|
||||
if (it != null && it.getType() == Material.SNOWBALL) have += it.getAmount();
|
||||
}
|
||||
if (have < amount) p.getInventory().addItem(new ItemStack(Material.SNOWBALL, amount - have));
|
||||
}
|
||||
|
||||
public void stop(boolean announce) {
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
if (!active) return;
|
||||
active = false;
|
||||
if (!announce) return;
|
||||
|
||||
List<Map.Entry<UUID, Integer>> top = ranking();
|
||||
Bukkit.broadcastMessage(mgr.msg("sb-ended"));
|
||||
for (int i = 0; i < Math.min(3, top.size()); i++) {
|
||||
var e = top.get(i);
|
||||
Bukkit.broadcastMessage(mgr.prefixed(mgr.raw("sb-top-line").replace("%rank%", String.valueOf(i + 1))
|
||||
.replace("%player%", names.getOrDefault(e.getKey(), "?"))
|
||||
.replace("%points%", String.valueOf(e.getValue()))));
|
||||
Player winner = Bukkit.getPlayer(e.getKey());
|
||||
if (winner != null) {
|
||||
for (Map<?, ?> reward : mgr.cfg().getMapList("snowball.rewards." + (i + 1))) {
|
||||
ChristmasExtras.applyMap(winner, reward);
|
||||
}
|
||||
winner.sendMessage(mgr.msg("sb-reward", "%rank%", String.valueOf(i + 1)));
|
||||
}
|
||||
}
|
||||
// Spieler bekommen ihre Schneebaelle nicht zurueck: einfach liegen lassen
|
||||
}
|
||||
|
||||
// ---------- Punkte ----------
|
||||
|
||||
public void onHit(Player shooter, Player victim) {
|
||||
if (!active || shooter.equals(victim) || !inArena(shooter) || !inArena(victim)) return;
|
||||
points.merge(shooter.getUniqueId(), Math.max(1, mgr.cfg().getInt("snowball.points-per-hit", 1)), Integer::sum);
|
||||
names.put(shooter.getUniqueId(), shooter.getName());
|
||||
}
|
||||
|
||||
public int points(UUID id) { return points.getOrDefault(id, 0); }
|
||||
|
||||
private List<Map.Entry<UUID, Integer>> ranking() {
|
||||
List<Map.Entry<UUID, Integer>> list = new ArrayList<>(points.entrySet());
|
||||
list.sort((a, b) -> Integer.compare(b.getValue(), a.getValue()));
|
||||
return list;
|
||||
}
|
||||
|
||||
public int rank(UUID id) {
|
||||
List<Map.Entry<UUID, Integer>> r = ranking();
|
||||
for (int i = 0; i < r.size(); i++) if (r.get(i).getKey().equals(id)) return i + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public List<String> topLines(int n) {
|
||||
List<String> out = new ArrayList<>();
|
||||
List<Map.Entry<UUID, Integer>> r = ranking();
|
||||
for (int i = 0; i < Math.min(n, r.size()); i++) {
|
||||
out.add(mgr.prefixed(mgr.raw("sb-top-line").replace("%rank%", String.valueOf(i + 1))
|
||||
.replace("%player%", names.getOrDefault(r.get(i).getKey(), "?"))
|
||||
.replace("%points%", String.valueOf(r.get(i).getValue()))));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.RewardUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Snowman;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.event.block.EntityBlockFormEvent;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Schneemaenner: Deko-Schneemaenner (per Befehl aufgestellt) und Spieler-Schneemaenner
|
||||
* (normal gebaut), die gelegentlich ein Geschenk abwerfen.
|
||||
*/
|
||||
public final class SnowmanManager {
|
||||
private static final String DECO = "deco";
|
||||
private static final String PLAYER = "player";
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private final NamespacedKey typeKey;
|
||||
private final NamespacedKey ownerKey;
|
||||
private final NamespacedKey homeKey;
|
||||
private final NamespacedKey walkKey;
|
||||
private final Map<UUID, Long> nextGift = new HashMap<>();
|
||||
private BukkitTask task;
|
||||
private final Set<Block> trail = new HashSet<>();
|
||||
|
||||
SnowmanManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.typeKey = new NamespacedKey(mgr.plugin(), "snowman_type");
|
||||
this.ownerKey = new NamespacedKey(mgr.plugin(), "snowman_owner");
|
||||
this.walkKey = new NamespacedKey(mgr.plugin(), "snowman_walk");
|
||||
this.homeKey = new NamespacedKey(mgr.plugin(), "snowman_home");
|
||||
}
|
||||
|
||||
void start() {
|
||||
stop();
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 100L, 40L);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
for (Block b : trail) if (b.getType() == Material.SNOW) b.setType(Material.AIR, false);
|
||||
trail.clear();
|
||||
}
|
||||
|
||||
/** Schnee-Spur der Schneemaenner: standardmaessig verschwindet sie nach einigen Sekunden wieder. */
|
||||
public void onSnowForm(EntityBlockFormEvent e) {
|
||||
if (!(e.getEntity() instanceof Snowman)) return;
|
||||
if (e.getNewState().getType() != Material.SNOW) return;
|
||||
String mode = mgr.cfg().getString("snowman.trail", "fade").toLowerCase();
|
||||
if (mode.equals("keep")) return;
|
||||
if (mode.equals("none")) { e.setCancelled(true); return; }
|
||||
Block b = e.getBlock();
|
||||
trail.add(b);
|
||||
long ticks = Math.max(1, mgr.cfg().getLong("snowman.trail-fade-seconds", 8)) * 20L;
|
||||
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
|
||||
if (trail.remove(b) && b.getType() == Material.SNOW) b.setType(Material.AIR, false);
|
||||
}, ticks);
|
||||
}
|
||||
|
||||
// ---------- Abfragen ----------
|
||||
|
||||
public boolean isDeco(Entity e) { return DECO.equals(type(e)); }
|
||||
|
||||
private String type(Entity e) {
|
||||
return e.getPersistentDataContainer().get(typeKey, PersistentDataType.STRING);
|
||||
}
|
||||
|
||||
private UUID owner(Entity e) {
|
||||
String s = e.getPersistentDataContainer().get(ownerKey, PersistentDataType.STRING);
|
||||
try { return s == null ? null : UUID.fromString(s); } catch (IllegalArgumentException ex) { return null; }
|
||||
}
|
||||
|
||||
// ---------- Deko ----------
|
||||
|
||||
public void spawnDeco(Location loc, String name) { spawnDeco(loc, name, null); }
|
||||
|
||||
/** walk: null = Standard aus der Config, true = laufend, false = stehend. */
|
||||
public void spawnDeco(Location loc, String name, Boolean walkMode) {
|
||||
boolean hat = mgr.cfg().getBoolean("snowman.deco.hat", true);
|
||||
boolean walk = walkMode != null ? walkMode : mgr.cfg().getBoolean("snowman.deco.walk", false);
|
||||
loc.getWorld().spawn(loc, Snowman.class, s -> {
|
||||
s.setAI(walk);
|
||||
setHome(s, loc);
|
||||
s.getPersistentDataContainer().set(walkKey, PersistentDataType.BYTE, (byte) (walk ? 1 : 0));
|
||||
s.setInvulnerable(true);
|
||||
s.setPersistent(true);
|
||||
s.setRemoveWhenFarAway(false);
|
||||
s.setSilent(true);
|
||||
s.setDerp(!hat);
|
||||
if (name != null && !name.isBlank()) {
|
||||
s.setCustomName(ChristmasManager.color(name));
|
||||
s.setCustomNameVisible(true);
|
||||
}
|
||||
s.getPersistentDataContainer().set(typeKey, PersistentDataType.STRING, DECO);
|
||||
});
|
||||
loc.getWorld().spawnParticle(Particle.SNOWFLAKE, loc.clone().add(0, 1, 0), 30, 0.4, 0.6, 0.4, 0.02);
|
||||
}
|
||||
|
||||
private void setHome(Entity e, Location l) {
|
||||
e.getPersistentDataContainer().set(homeKey, PersistentDataType.STRING,
|
||||
l.getX() + ";" + l.getY() + ";" + l.getZ());
|
||||
}
|
||||
|
||||
private Location home(Entity e) {
|
||||
String h = e.getPersistentDataContainer().get(homeKey, PersistentDataType.STRING);
|
||||
if (h == null) return null;
|
||||
try {
|
||||
String[] p = h.split(";");
|
||||
return new Location(e.getWorld(), Double.parseDouble(p[0]), Double.parseDouble(p[1]), Double.parseDouble(p[2]));
|
||||
} catch (Exception ex) { return null; }
|
||||
}
|
||||
|
||||
/** Laufende Deko-Schneemaenner: frei herumlaufen, aber im Umkreis um den Startpunkt bleiben. */
|
||||
private void walkDeco(Snowman s, boolean defaultWalk, double radius) {
|
||||
Byte flag = s.getPersistentDataContainer().get(walkKey, PersistentDataType.BYTE);
|
||||
boolean walk = flag != null ? flag == 1 : defaultWalk;
|
||||
if (s.hasAI() != walk) s.setAI(walk);
|
||||
Location home = home(s);
|
||||
if (home == null) { setHome(s, s.getLocation()); return; }
|
||||
if (!walk) {
|
||||
if (s.getLocation().distanceSquared(home) > 1) s.teleport(home);
|
||||
return;
|
||||
}
|
||||
double d = s.getLocation().distance(home);
|
||||
if (d <= radius) return;
|
||||
if (d > radius * 2.5 || !pathTo(s, home)) s.teleport(home);
|
||||
}
|
||||
|
||||
/** Laeuft per Paper-Pathfinder zurueck (per Reflection, damit Spigot weiter geht). */
|
||||
private boolean pathTo(Snowman s, Location target) {
|
||||
try {
|
||||
Object pf = s.getClass().getMethod("getPathfinder").invoke(s);
|
||||
Object r = pf.getClass().getMethod("moveTo", Location.class, double.class)
|
||||
.invoke(pf, target, 1.0D);
|
||||
return r instanceof Boolean b && b;
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Entfernt den naechsten Deko-Schneemann im Umkreis. */
|
||||
public boolean removeNearestDeco(Player p, double radius) {
|
||||
Snowman best = null;
|
||||
double bestDist = Double.MAX_VALUE;
|
||||
for (Entity e : p.getNearbyEntities(radius, radius, radius)) {
|
||||
if (e instanceof Snowman s && isDeco(s)) {
|
||||
double d = s.getLocation().distanceSquared(p.getLocation());
|
||||
if (d < bestDist) { bestDist = d; best = s; }
|
||||
}
|
||||
}
|
||||
if (best == null) return false;
|
||||
best.remove();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- Spieler-Schneemaenner ----------
|
||||
|
||||
/** Wird aufgerufen, wenn ein Schneemann normal gebaut wurde. */
|
||||
public void onBuilt(Snowman s) {
|
||||
if (!mgr.cfg().getBoolean("snowman.player.enable", true)) return;
|
||||
Player builder = null;
|
||||
double best = 12 * 12;
|
||||
for (Player p : s.getWorld().getPlayers()) {
|
||||
double d = p.getLocation().distanceSquared(s.getLocation());
|
||||
if (d < best) { best = d; builder = p; }
|
||||
}
|
||||
if (builder == null) return;
|
||||
|
||||
PersistentDataContainer pdc = s.getPersistentDataContainer();
|
||||
pdc.set(typeKey, PersistentDataType.STRING, PLAYER);
|
||||
pdc.set(ownerKey, PersistentDataType.STRING, builder.getUniqueId().toString());
|
||||
String name = mgr.cfg().getString("snowman.player.name", "&f%player%s Schneemann");
|
||||
s.setCustomName(ChristmasManager.color(name.replace("%player%", builder.getName())));
|
||||
s.setCustomNameVisible(true);
|
||||
s.setPersistent(true);
|
||||
s.setRemoveWhenFarAway(false);
|
||||
nextGift.put(s.getUniqueId(), System.currentTimeMillis() + interval());
|
||||
|
||||
int max = mgr.cfg().getInt("snowman.player.max-per-player", 3);
|
||||
long count = playerSnowmen(builder.getUniqueId()).size();
|
||||
builder.sendMessage(mgr.msg(count > max ? "snowman-limit" : "snowman-built", "%max%", String.valueOf(max)));
|
||||
}
|
||||
|
||||
private long interval() {
|
||||
return Math.max(10, mgr.cfg().getLong("snowman.player.gift-interval", 300)) * 1000L;
|
||||
}
|
||||
|
||||
private List<Snowman> playerSnowmen(UUID owner) {
|
||||
List<Snowman> list = new ArrayList<>();
|
||||
for (World w : Bukkit.getWorlds()) {
|
||||
for (Snowman s : w.getEntitiesByClass(Snowman.class)) {
|
||||
if (PLAYER.equals(type(s)) && owner.equals(owner(s))) list.add(s);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ---------- Tick ----------
|
||||
|
||||
private void tick() {
|
||||
long now = System.currentTimeMillis();
|
||||
boolean particles = mgr.cfg().getBoolean("snowman.deco.particles", true);
|
||||
boolean walk = mgr.cfg().getBoolean("snowman.deco.walk", false);
|
||||
double radius = Math.max(2, mgr.cfg().getDouble("snowman.deco.walk-radius", 8));
|
||||
int max = mgr.cfg().getInt("snowman.player.max-per-player", 3);
|
||||
List<String> gifts = mgr.cfg().getStringList("snowman.player.gifts");
|
||||
|
||||
Map<UUID, List<Snowman>> byOwner = new HashMap<>();
|
||||
for (World w : Bukkit.getWorlds()) {
|
||||
for (Snowman s : w.getEntitiesByClass(Snowman.class)) {
|
||||
String t = type(s);
|
||||
if (DECO.equals(t)) {
|
||||
walkDeco(s, walk, radius);
|
||||
if (particles) w.spawnParticle(Particle.SNOWFLAKE, s.getLocation().add(0, 1.2, 0), 4, 0.5, 0.7, 0.5, 0.01);
|
||||
} else if (PLAYER.equals(t)) {
|
||||
UUID o = owner(s);
|
||||
if (o != null) byOwner.computeIfAbsent(o, k -> new ArrayList<>()).add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (List<Snowman> list : byOwner.values()) {
|
||||
// Aelteste zuerst: nur die ersten "max" Schneemaenner geben Geschenke
|
||||
list.sort(Comparator.comparingInt(Snowman::getTicksLived).reversed());
|
||||
for (int i = 0; i < list.size() && i < max; i++) {
|
||||
Snowman s = list.get(i);
|
||||
long due = nextGift.computeIfAbsent(s.getUniqueId(), k -> now + interval());
|
||||
if (now < due || gifts.isEmpty()) continue;
|
||||
nextGift.put(s.getUniqueId(), now + interval());
|
||||
ItemStack it = ChristmasManager.parseItem(gifts.get(ChristmasManager.RANDOM.nextInt(gifts.size())));
|
||||
if (it == null) continue;
|
||||
Location at = s.getLocation().add(0, 1.2, 0);
|
||||
s.getWorld().dropItem(at, it);
|
||||
s.getWorld().spawnParticle(Particle.SNOWFLAKE, at, 20, 0.4, 0.4, 0.4, 0.03);
|
||||
s.getWorld().playSound(at, Sound.BLOCK_SNOW_PLACE, 1f, 1.2f);
|
||||
}
|
||||
}
|
||||
nextGift.keySet().removeIf(id -> Bukkit.getEntity(id) == null && nextGift.size() > 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.SoundCategory;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Weihnachtsflug: Am Weihnachtstag (und einen Tag danach) fliegt Santa von aufgestellter Socke zu
|
||||
* aufgestellter Socke und legt ein Geschenk darunter ab.
|
||||
*/
|
||||
final class StockingFlight {
|
||||
private final ChristmasManager mgr;
|
||||
private final StockingManager stockings;
|
||||
private BukkitTask scheduler;
|
||||
private BukkitTask flightTask;
|
||||
private LivingEntity santa;
|
||||
private long lastFlight;
|
||||
|
||||
// Ablauf eines Fluges
|
||||
private List<StockingManager.Placed> queue = new ArrayList<>();
|
||||
private int index;
|
||||
private int hover;
|
||||
private boolean leaving;
|
||||
private int leaveTicks;
|
||||
private Location leaveTarget;
|
||||
private float yaw;
|
||||
private int tickCount;
|
||||
|
||||
StockingFlight(ChristmasManager mgr, StockingManager stockings) {
|
||||
this.mgr = mgr;
|
||||
this.stockings = stockings;
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (scheduler != null) scheduler.cancel();
|
||||
scheduler = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::check, 600L, 20L * 30);
|
||||
}
|
||||
|
||||
void stopAll() {
|
||||
if (scheduler != null) scheduler.cancel();
|
||||
scheduler = null;
|
||||
finish();
|
||||
}
|
||||
|
||||
boolean running() { return flightTask != null; }
|
||||
|
||||
// ---------- Zeitplan ----------
|
||||
|
||||
private LocalDate date(String key, String def) {
|
||||
LocalDate today = LocalDate.now(mgr.plugin().getZone());
|
||||
String[] p = mgr.cfg().getString(key, def).split("-");
|
||||
try {
|
||||
return LocalDate.of(today.getYear(), Integer.parseInt(p[0].trim()), Integer.parseInt(p[1].trim()));
|
||||
} catch (Exception e) {
|
||||
return LocalDate.of(today.getYear(), 12, 24);
|
||||
}
|
||||
}
|
||||
|
||||
/** Liegt heute im Flug-Zeitraum (Standard 24.12. bis 25.12.)? */
|
||||
boolean inWindow() {
|
||||
LocalDate today = LocalDate.now(mgr.plugin().getZone());
|
||||
return !today.isBefore(date("stocking-flight.from", "12-24")) && !today.isAfter(date("stocking-flight.until", "12-25"));
|
||||
}
|
||||
|
||||
private void check() {
|
||||
stockings.cleanup();
|
||||
if (flightTask != null) return;
|
||||
if (!mgr.cfg().getBoolean("stocking-flight.enable", true) || !mgr.cfg().getBoolean("stocking.enable", true)) return;
|
||||
if (!inWindow() || Bukkit.getOnlinePlayers().isEmpty()) return;
|
||||
long every = Math.max(1, mgr.cfg().getLong("stocking-flight.interval-minutes", 20)) * 60_000L;
|
||||
if (System.currentTimeMillis() - lastFlight < every) return;
|
||||
launch(false);
|
||||
}
|
||||
|
||||
// ---------- Flug ----------
|
||||
|
||||
/** Startet einen Flug; force = auch ausserhalb des Zeitraums und trotz heutiger Lieferung (Admin-Test). */
|
||||
boolean launch(boolean force) {
|
||||
if (flightTask != null) return false;
|
||||
String today = LocalDate.now(mgr.plugin().getZone()).toString();
|
||||
|
||||
// Socken in geladenen Chunks, nach Welt gruppiert; die Welt mit den meisten gewinnt
|
||||
java.util.Map<String, List<StockingManager.Placed>> byWorld = new java.util.HashMap<>();
|
||||
for (StockingManager.Placed pl : new ArrayList<>(stockings.placed().values())) {
|
||||
World w = Bukkit.getWorld(pl.world);
|
||||
if (w == null || !w.isChunkLoaded(pl.x >> 4, pl.z >> 4)) continue;
|
||||
ItemFrame f = stockings.frameOf(pl);
|
||||
if (f == null) continue;
|
||||
if (!force && (today.equals(pl.lastDay) || pl.presentKey != null)) continue;
|
||||
byWorld.computeIfAbsent(pl.world, k -> new ArrayList<>()).add(pl);
|
||||
}
|
||||
List<StockingManager.Placed> best = new ArrayList<>();
|
||||
for (var l : byWorld.values()) if (l.size() > best.size()) best = l;
|
||||
if (best.isEmpty()) return false;
|
||||
|
||||
// Reihenfolge: immer zur naechsten Socke
|
||||
int max = Math.max(1, mgr.cfg().getInt("stocking-flight.max-stockings-per-flight", 12));
|
||||
List<StockingManager.Placed> pool = new ArrayList<>(best);
|
||||
queue = new ArrayList<>();
|
||||
StockingManager.Placed cur = pool.remove(ChristmasManager.RANDOM.nextInt(pool.size()));
|
||||
queue.add(cur);
|
||||
while (!pool.isEmpty() && queue.size() < max) {
|
||||
final StockingManager.Placed from = cur;
|
||||
StockingManager.Placed next = pool.stream().min(java.util.Comparator.comparingDouble(
|
||||
x -> dist2(from, x))).get();
|
||||
pool.remove(next);
|
||||
queue.add(next);
|
||||
cur = next;
|
||||
}
|
||||
|
||||
World w = Bukkit.getWorld(queue.get(0).world);
|
||||
Location first = hoverPoint(queue.get(0));
|
||||
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
|
||||
Location spawn = first.clone().add(Math.cos(ang) * 45, 22, Math.sin(ang) * 45);
|
||||
try {
|
||||
santa = mgr.santa().createModel(spawn);
|
||||
santa.setGravity(false);
|
||||
santa.setInvulnerable(true);
|
||||
santa.setSilent(true);
|
||||
santa.setPersistent(false);
|
||||
} catch (Throwable t) {
|
||||
mgr.plugin().getLogger().warning("Santa-Flug konnte nicht gestartet werden: " + t);
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
lastFlight = System.currentTimeMillis();
|
||||
index = 0;
|
||||
hover = 0;
|
||||
leaving = false;
|
||||
tickCount = 0;
|
||||
yaw = spawn.getYaw();
|
||||
for (Player p : w.getPlayers()) p.sendMessage(mgr.msg("stocking-flight-started"));
|
||||
w.playSound(spawn, Sound.BLOCK_AMETHYST_BLOCK_CHIME, SoundCategory.RECORDS, 4f, 1.4f);
|
||||
flightTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 1L, 1L);
|
||||
return true;
|
||||
}
|
||||
|
||||
private double dist2(StockingManager.Placed a, StockingManager.Placed b) {
|
||||
double dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private Location hoverPoint(StockingManager.Placed pl) {
|
||||
return new Location(Bukkit.getWorld(pl.world), pl.x + 0.5, pl.y + 2.2, pl.z + 0.5);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (santa == null || !santa.isValid()) { finish(); return; }
|
||||
World w = santa.getWorld();
|
||||
Location pos = santa.getLocation();
|
||||
tickCount++;
|
||||
|
||||
// Schweif und Glocken
|
||||
w.spawnParticle(Particle.END_ROD, pos.clone().add(0, 1, 0), 2, 0.3, 0.3, 0.3, 0.01);
|
||||
w.spawnParticle(Particle.SNOWFLAKE, pos.clone().add(0, 0.8, 0), 4, 0.4, 0.4, 0.4, 0.02);
|
||||
if (tickCount % 16 == 0) {
|
||||
float pitch = (float) Math.pow(2.0, (ChristmasManager.RANDOM.nextInt(8) - 4) / 12.0);
|
||||
w.playSound(pos, Sound.BLOCK_NOTE_BLOCK_BELL, SoundCategory.RECORDS, 2.5f, pitch);
|
||||
}
|
||||
|
||||
if (leaving) {
|
||||
leaveTicks--;
|
||||
moveToward(pos, leaveTarget, 0.9);
|
||||
if (leaveTicks <= 0) { finish(); }
|
||||
return;
|
||||
}
|
||||
|
||||
// Naechste noch vorhandene Socke suchen
|
||||
while (index < queue.size() && stockings.frameOf(queue.get(index)) == null) index++;
|
||||
if (index >= queue.size()) { startLeaving(pos); return; }
|
||||
|
||||
StockingManager.Placed pl = queue.get(index);
|
||||
Location target = hoverPoint(pl);
|
||||
if (hover == 0) {
|
||||
double d = pos.distance(target);
|
||||
double speed = Math.max(0.2, mgr.cfg().getDouble("stocking-flight.speed", 0.7));
|
||||
moveToward(pos, target, Math.min(speed, Math.max(0.08, d * 0.12)));
|
||||
if (d < 0.5) hover = 1;
|
||||
return;
|
||||
}
|
||||
// Schweben und Geschenk ablegen
|
||||
hover++;
|
||||
if (hover == 14) placePresent(pl);
|
||||
if (hover > 36) { hover = 0; index++; }
|
||||
}
|
||||
|
||||
private void moveToward(Location pos, Location target, double step) {
|
||||
double dx = target.getX() - pos.getX(), dy = target.getY() - pos.getY(), dz = target.getZ() - pos.getZ();
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len < 1e-6) return;
|
||||
double f = Math.min(1.0, step / len);
|
||||
double nx = pos.getX() + dx * f, ny = pos.getY() + dy * f, nz = pos.getZ() + dz * f;
|
||||
double hx = target.getX() - pos.getX(), hz = target.getZ() - pos.getZ();
|
||||
if (hx * hx + hz * hz > 0.25) {
|
||||
float want = (float) Math.toDegrees(Math.atan2(-hx, hz));
|
||||
float diff = ((want - yaw + 540f) % 360f) - 180f;
|
||||
yaw += Math.max(-14f, Math.min(14f, diff));
|
||||
}
|
||||
santa.teleport(new Location(pos.getWorld(), nx, ny, nz, yaw, 0f));
|
||||
}
|
||||
|
||||
private void startLeaving(Location pos) {
|
||||
leaving = true;
|
||||
leaveTicks = 70;
|
||||
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
|
||||
leaveTarget = pos.clone().add(Math.cos(ang) * 60, 35, Math.sin(ang) * 60);
|
||||
}
|
||||
|
||||
private void placePresent(StockingManager.Placed pl) {
|
||||
World w = Bukkit.getWorld(pl.world);
|
||||
if (w == null) return;
|
||||
String today = LocalDate.now(mgr.plugin().getZone()).toString();
|
||||
|
||||
// Geschenk unter die Socke: freier Block darunter (bis zu 2 Bloecke tiefer)
|
||||
Block frame = w.getBlockAt(pl.x, pl.y, pl.z);
|
||||
Block spot = null;
|
||||
for (int i = 1; i <= 2 && spot == null; i++) {
|
||||
Block b = frame.getRelative(0, -i, 0);
|
||||
if (b.getType().isAir()) spot = b;
|
||||
else if (b.getType().isSolid()) break;
|
||||
}
|
||||
if (spot == null) return;
|
||||
|
||||
List<String> skins = mgr.cfg().getStringList("magic-tree.presents");
|
||||
if (skins.isEmpty()) {
|
||||
spot.setType(Material.CHEST, false);
|
||||
} else {
|
||||
mgr.trees().setSkin(spot, skins.get(ChristmasManager.RANDOM.nextInt(skins.size())));
|
||||
}
|
||||
stockings.registerPresent(pl, spot);
|
||||
pl.lastDay = today;
|
||||
stockings.save();
|
||||
|
||||
Location at = spot.getLocation().add(0.5, 0.6, 0.5);
|
||||
w.spawnParticle(Particle.END_ROD, at, 25, 0.3, 0.3, 0.3, 0.05);
|
||||
w.spawnParticle(Particle.HAPPY_VILLAGER, at, 12, 0.3, 0.3, 0.3, 0);
|
||||
w.playSound(at, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.3f);
|
||||
w.playSound(at, Sound.BLOCK_NOTE_BLOCK_CHIME, SoundCategory.RECORDS, 1.5f, 1.5f);
|
||||
|
||||
Player owner = Bukkit.getPlayer(pl.owner);
|
||||
if (owner != null) {
|
||||
owner.sendMessage(mgr.msg("stocking-present-placed",
|
||||
"%x%", String.valueOf(spot.getX()), "%y%", String.valueOf(spot.getY()), "%z%", String.valueOf(spot.getZ())));
|
||||
}
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
if (flightTask != null) flightTask.cancel();
|
||||
flightTask = null;
|
||||
if (santa != null) {
|
||||
try { santa.remove(); } catch (Throwable ignored) { }
|
||||
santa = null;
|
||||
}
|
||||
queue = new ArrayList<>();
|
||||
leaving = false;
|
||||
}
|
||||
|
||||
/** Fuer Aufraeumen: ID der Socke zum Flug (nur zur Info). */
|
||||
UUID currentSock() { return index < queue.size() ? queue.get(index).id : null; }
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.LeatherArmorMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.util.RayTraceResult;
|
||||
import java.util.List;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Kamin-Geschenke: Socke aufhaengen, am 25.12. liegt etwas darin. */
|
||||
public final class StockingManager {
|
||||
private static final class Data {
|
||||
int hungYear;
|
||||
int claimedYear;
|
||||
}
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private final File file;
|
||||
private final Map<UUID, Data> data = new HashMap<>();
|
||||
private BukkitTask task;
|
||||
|
||||
/** Aufgestellte Socke (Item-Rahmen an der Wand). */
|
||||
static final class Placed {
|
||||
UUID id;
|
||||
UUID owner;
|
||||
String world;
|
||||
int x, y, z;
|
||||
String presentKey; // noch nicht abgeholtes Geschenk unter der Socke
|
||||
String lastDay = ""; // Datum der letzten Lieferung
|
||||
}
|
||||
|
||||
private final Map<UUID, Placed> placed = new HashMap<>();
|
||||
private final Map<String, UUID> presents = new HashMap<>(); // Blockschluessel -> Besitzer
|
||||
private final NamespacedKey sockKey;
|
||||
private final StockingFlight flight;
|
||||
|
||||
StockingManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.file = new File(mgr.plugin().getDataFolder(), "stockings.yml");
|
||||
this.sockKey = new NamespacedKey(mgr.plugin(), "stocking_frame");
|
||||
this.flight = new StockingFlight(mgr, this);
|
||||
}
|
||||
|
||||
StockingFlight flight() { return flight; }
|
||||
|
||||
void load() {
|
||||
data.clear();
|
||||
if (file.exists()) {
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||
ConfigurationSection s = y.getConfigurationSection("players");
|
||||
if (s != null) {
|
||||
for (String k : s.getKeys(false)) {
|
||||
try {
|
||||
Data d = new Data();
|
||||
d.hungYear = s.getInt(k + ".hung");
|
||||
d.claimedYear = s.getInt(k + ".claimed");
|
||||
data.put(UUID.fromString(k), d);
|
||||
} catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
placed.clear();
|
||||
presents.clear();
|
||||
if (file.exists()) {
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||
ConfigurationSection ps = y.getConfigurationSection("placed");
|
||||
if (ps != null) {
|
||||
for (String k : ps.getKeys(false)) {
|
||||
try {
|
||||
Placed pl = new Placed();
|
||||
pl.id = UUID.fromString(k);
|
||||
pl.owner = UUID.fromString(ps.getString(k + ".owner", ""));
|
||||
pl.world = ps.getString(k + ".world", "");
|
||||
pl.x = ps.getInt(k + ".x");
|
||||
pl.y = ps.getInt(k + ".y");
|
||||
pl.z = ps.getInt(k + ".z");
|
||||
pl.presentKey = ps.getString(k + ".present");
|
||||
pl.lastDay = ps.getString(k + ".last-day", "");
|
||||
placed.put(pl.id, pl);
|
||||
} catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
}
|
||||
ConfigurationSection gs = y.getConfigurationSection("presents");
|
||||
if (gs != null) {
|
||||
for (String k : gs.getKeys(false)) {
|
||||
try { presents.put(k.replace('|', ';'), UUID.fromString(gs.getString(k, ""))); }
|
||||
catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
stop();
|
||||
flight.start();
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
|
||||
for (Player p : Bukkit.getOnlinePlayers()) tryDeliver(p, false);
|
||||
}, 400L, 20L * 60);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
flight.stopAll();
|
||||
}
|
||||
|
||||
void save() {
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
for (var e : data.entrySet()) {
|
||||
y.set("players." + e.getKey() + ".hung", e.getValue().hungYear);
|
||||
y.set("players." + e.getKey() + ".claimed", e.getValue().claimedYear);
|
||||
}
|
||||
for (Placed pl : placed.values()) {
|
||||
String b = "placed." + pl.id;
|
||||
y.set(b + ".owner", pl.owner.toString());
|
||||
y.set(b + ".world", pl.world);
|
||||
y.set(b + ".x", pl.x);
|
||||
y.set(b + ".y", pl.y);
|
||||
y.set(b + ".z", pl.z);
|
||||
y.set(b + ".present", pl.presentKey);
|
||||
y.set(b + ".last-day", pl.lastDay);
|
||||
}
|
||||
for (var e : presents.entrySet()) y.set("presents." + e.getKey().replace(';', '|'), e.getValue().toString());
|
||||
try {
|
||||
y.save(file);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("stockings.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Datumslogik ----------
|
||||
|
||||
private LocalDate date(String key, String def) {
|
||||
LocalDate today = LocalDate.now(mgr.plugin().getZone());
|
||||
String[] p = mgr.cfg().getString(key, def).split("-");
|
||||
try {
|
||||
return LocalDate.of(today.getYear(), Integer.parseInt(p[0].trim()), Integer.parseInt(p[1].trim()));
|
||||
} catch (Exception e) {
|
||||
return LocalDate.of(today.getYear(), 12, 25);
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate today() { return LocalDate.now(mgr.plugin().getZone()); }
|
||||
|
||||
// ---------- Aktionen ----------
|
||||
|
||||
/** /xmas stocking: aufhaengen oder (ab dem Tag) abholen. */
|
||||
public void use(Player p) {
|
||||
if (!mgr.cfg().getBoolean("stocking.enable", true)) return;
|
||||
LocalDate today = today();
|
||||
LocalDate from = date("stocking.hang-from", "12-01");
|
||||
LocalDate open = date("stocking.open-on", "12-25");
|
||||
int year = today.getYear();
|
||||
Data d = data.computeIfAbsent(p.getUniqueId(), k -> new Data());
|
||||
|
||||
if (today.isBefore(open)) {
|
||||
if (today.isBefore(from)) {
|
||||
p.sendMessage(mgr.msg("stocking-too-early", "%from%", from.getDayOfMonth() + "." + from.getMonthValue() + "."));
|
||||
return;
|
||||
}
|
||||
if (d.hungYear == year) { p.sendMessage(mgr.msg("stocking-already", "%date%", open.getDayOfMonth() + "." + open.getMonthValue() + ".")); return; }
|
||||
d.hungYear = year;
|
||||
save();
|
||||
p.sendMessage(mgr.msg("stocking-hung", "%date%", open.getDayOfMonth() + "." + open.getMonthValue() + "."));
|
||||
return;
|
||||
}
|
||||
if (d.hungYear != year) { p.sendMessage(mgr.msg("stocking-nothing")); return; }
|
||||
if (d.claimedYear == year) { p.sendMessage(mgr.msg("stocking-claimed")); return; }
|
||||
deliver(p, d);
|
||||
}
|
||||
|
||||
/** Automatisch: nach dem Oeffnungstag bekommt jeder mit aufgehaengter Socke sein Geschenk. */
|
||||
public void tryDeliver(Player p, boolean announceIfNone) {
|
||||
if (!mgr.cfg().getBoolean("stocking.enable", true)) return;
|
||||
LocalDate today = today();
|
||||
if (today.isBefore(date("stocking.open-on", "12-25")) || today.isAfter(date("stocking.until", "12-31"))) return;
|
||||
Data d = data.get(p.getUniqueId());
|
||||
if (d == null || d.hungYear != today.getYear() || d.claimedYear == today.getYear()) return;
|
||||
deliver(p, d);
|
||||
}
|
||||
|
||||
private void deliver(Player p, Data d) {
|
||||
d.claimedYear = today().getYear();
|
||||
save();
|
||||
for (var reward : mgr.cfg().getMapList("stocking.rewards")) ChristmasExtras.applyMap(p, reward);
|
||||
p.sendMessage(mgr.msg("stocking-received"));
|
||||
p.getWorld().spawnParticle(org.bukkit.Particle.SNOWFLAKE, p.getLocation().add(0, 1.5, 0), 30, 0.5, 0.5, 0.5, 0.03);
|
||||
}
|
||||
|
||||
// ---------- Aufgestellte Socken ----------
|
||||
|
||||
Map<UUID, Placed> placed() { return placed; }
|
||||
|
||||
private static String key(Block b) {
|
||||
return b.getWorld().getName() + ";" + b.getX() + ";" + b.getY() + ";" + b.getZ();
|
||||
}
|
||||
|
||||
private ItemStack sockItem() {
|
||||
ItemStack it = new ItemStack(Material.LEATHER_BOOTS);
|
||||
if (it.getItemMeta() instanceof LeatherArmorMeta m) {
|
||||
m.setColor(Color.RED);
|
||||
m.setDisplayName(ChristmasManager.color("&cWeihnachtssocke"));
|
||||
it.setItemMeta(m);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
/** /xmas stocking place: haengt eine Socke an die Wand, auf die der Spieler schaut. */
|
||||
public void place(Player p) {
|
||||
if (!mgr.cfg().getBoolean("stocking.enable", true)) return;
|
||||
int max = mgr.cfg().getInt("stocking.max-placed-per-player", 3);
|
||||
long mine = placed.values().stream().filter(x -> x.owner.equals(p.getUniqueId())).count();
|
||||
if (mine >= max && !p.hasPermission("xmas.admin")) {
|
||||
p.sendMessage(mgr.msg("stocking-place-limit", "%max%", String.valueOf(max)));
|
||||
return;
|
||||
}
|
||||
RayTraceResult r = p.rayTraceBlocks(5);
|
||||
if (r == null || r.getHitBlock() == null || r.getHitBlockFace() == null) {
|
||||
p.sendMessage(mgr.msg("stocking-place-usage"));
|
||||
return;
|
||||
}
|
||||
BlockFace face = r.getHitBlockFace();
|
||||
if (face == BlockFace.UP || face == BlockFace.DOWN) { p.sendMessage(mgr.msg("stocking-place-usage")); return; }
|
||||
Block at = r.getHitBlock().getRelative(face);
|
||||
if (!at.getType().isAir()) { p.sendMessage(mgr.msg("stocking-place-usage")); return; }
|
||||
for (Placed o : placed.values()) {
|
||||
if (o.world.equals(at.getWorld().getName()) && o.x == at.getX() && o.y == at.getY() && o.z == at.getZ()) {
|
||||
p.sendMessage(mgr.msg("stocking-place-usage"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
ItemFrame f = at.getWorld().spawn(at.getLocation(), ItemFrame.class, fr -> {
|
||||
fr.setFacingDirection(face, true);
|
||||
fr.setVisible(false);
|
||||
fr.setItem(sockItem(), false);
|
||||
fr.setFixed(true);
|
||||
fr.setInvulnerable(true);
|
||||
fr.setPersistent(true);
|
||||
fr.getPersistentDataContainer().set(sockKey, PersistentDataType.STRING, p.getUniqueId().toString());
|
||||
});
|
||||
Placed pl = new Placed();
|
||||
pl.id = f.getUniqueId();
|
||||
pl.owner = p.getUniqueId();
|
||||
pl.world = at.getWorld().getName();
|
||||
pl.x = at.getX();
|
||||
pl.y = at.getY();
|
||||
pl.z = at.getZ();
|
||||
placed.put(pl.id, pl);
|
||||
save();
|
||||
at.getWorld().spawnParticle(Particle.SNOWFLAKE, at.getLocation().add(0.5, 0.5, 0.5), 20, 0.3, 0.3, 0.3, 0.02);
|
||||
p.sendMessage(mgr.msg("stocking-placed"));
|
||||
}
|
||||
|
||||
/** /xmas stocking remove: entfernt die naechste eigene Socke (Admins: jede). */
|
||||
public void removeNearest(Player p) {
|
||||
ItemFrame best = null;
|
||||
double bd = Double.MAX_VALUE;
|
||||
for (Entity e : p.getNearbyEntities(6, 6, 6)) {
|
||||
if (!(e instanceof ItemFrame f) || !placed.containsKey(f.getUniqueId())) continue;
|
||||
Placed pl = placed.get(f.getUniqueId());
|
||||
if (!pl.owner.equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) continue;
|
||||
double d = f.getLocation().distanceSquared(p.getLocation());
|
||||
if (d < bd) { bd = d; best = f; }
|
||||
}
|
||||
if (best == null) { p.sendMessage(mgr.msg("stocking-none-near")); return; }
|
||||
Placed pl = placed.remove(best.getUniqueId());
|
||||
best.remove();
|
||||
if (pl != null && pl.presentKey != null) presents.remove(pl.presentKey);
|
||||
save();
|
||||
p.sendMessage(mgr.msg("stocking-removed"));
|
||||
}
|
||||
|
||||
/** Entfernt Eintraege, deren Rahmen es nicht mehr gibt (z.B. im Kreativmodus zerstoert). */
|
||||
void cleanup() {
|
||||
boolean changed = false;
|
||||
for (var it = placed.values().iterator(); it.hasNext(); ) {
|
||||
Placed pl = it.next();
|
||||
World w = Bukkit.getWorld(pl.world);
|
||||
if (w == null || !w.isChunkLoaded(pl.x >> 4, pl.z >> 4)) continue;
|
||||
if (frameOf(pl) == null) { it.remove(); changed = true; }
|
||||
}
|
||||
if (changed) save();
|
||||
}
|
||||
|
||||
/** Rahmen der Socke (null, wenn nicht geladen/vorhanden). */
|
||||
ItemFrame frameOf(Placed pl) {
|
||||
Entity e = Bukkit.getEntity(pl.id);
|
||||
return e instanceof ItemFrame f && f.isValid() ? f : null;
|
||||
}
|
||||
|
||||
// ---------- Geschenke unter der Socke ----------
|
||||
|
||||
public boolean isPresentBlock(Block b) { return presents.containsKey(key(b)); }
|
||||
|
||||
void registerPresent(Placed pl, Block b) {
|
||||
String k = key(b);
|
||||
presents.put(k, pl.owner);
|
||||
pl.presentKey = k;
|
||||
}
|
||||
|
||||
/** Spieler oeffnet ein Socken-Geschenk (Rechtsklick oder Abbauen). */
|
||||
public void openPresent(Player p, Block b) {
|
||||
String k = key(b);
|
||||
UUID owner = presents.get(k);
|
||||
if (owner == null) return;
|
||||
if (mgr.cfg().getBoolean("stocking-flight.owner-only", true)
|
||||
&& !owner.equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) {
|
||||
p.sendMessage(mgr.msg("stocking-present-notyours"));
|
||||
return;
|
||||
}
|
||||
presents.remove(k);
|
||||
for (Placed pl : placed.values()) if (k.equals(pl.presentKey)) pl.presentKey = null;
|
||||
b.setType(Material.AIR, false);
|
||||
World w = b.getWorld();
|
||||
w.spawnParticle(Particle.END_ROD, b.getLocation().add(0.5, 0.5, 0.5), 20, 0.3, 0.3, 0.3, 0.05);
|
||||
w.playSound(b.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.4f);
|
||||
List<Map<?, ?>> rewards = mgr.cfg().getMapList("stocking-flight.rewards");
|
||||
if (rewards.isEmpty()) rewards = mgr.cfg().getMapList("stocking.rewards");
|
||||
for (Map<?, ?> reward : rewards) ChristmasExtras.applyMap(p, reward);
|
||||
p.sendMessage(mgr.msg("stocking-present-opened"));
|
||||
save();
|
||||
}
|
||||
|
||||
/** Geschenke unter Socken sind vor Zerstoerung geschuetzt. */
|
||||
public boolean isProtected(Block b) { return isPresentBlock(b); }
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.RewardUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Wichteln: Anmelden, zufaellig ziehen, Geschenk ablegen, am Ende verteilen. */
|
||||
public final class WichtelnManager {
|
||||
public enum State { CLOSED, OPEN, DRAWN, DELIVERED }
|
||||
|
||||
private record Pending(String from, ItemStack item) {}
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private final File file;
|
||||
private State state = State.CLOSED;
|
||||
private final Map<UUID, String> participants = new LinkedHashMap<>();
|
||||
private final Map<UUID, UUID> assign = new LinkedHashMap<>();
|
||||
private final Map<UUID, ItemStack> gifts = new LinkedHashMap<>();
|
||||
private final Map<UUID, List<Pending>> pending = new LinkedHashMap<>();
|
||||
|
||||
WichtelnManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.file = new File(mgr.plugin().getDataFolder(), "wichteln.yml");
|
||||
}
|
||||
|
||||
public State state() { return state; }
|
||||
public int participantCount() { return participants.size(); }
|
||||
public boolean joined(UUID id) { return participants.containsKey(id); }
|
||||
|
||||
public String targetName(UUID giver) {
|
||||
UUID t = assign.get(giver);
|
||||
return t == null ? "" : participants.getOrDefault(t, "");
|
||||
}
|
||||
|
||||
// ---------- Phasen ----------
|
||||
|
||||
public boolean open() {
|
||||
if (state != State.CLOSED && state != State.DELIVERED) return false;
|
||||
reset();
|
||||
state = State.OPEN;
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean join(Player p) {
|
||||
if (state != State.OPEN) return false;
|
||||
participants.put(p.getUniqueId(), p.getName());
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean leave(Player p) {
|
||||
if (state != State.OPEN) return false;
|
||||
boolean had = participants.remove(p.getUniqueId()) != null;
|
||||
save();
|
||||
return had;
|
||||
}
|
||||
|
||||
/** Zieht zufaellig, sodass niemand sich selbst beschenkt. false bei zu wenigen Teilnehmern. */
|
||||
public boolean draw() {
|
||||
if (state != State.OPEN || participants.size() < 3) return false;
|
||||
List<UUID> givers = new ArrayList<>(participants.keySet());
|
||||
List<UUID> receivers = new ArrayList<>(givers);
|
||||
boolean ok = false;
|
||||
for (int attempt = 0; attempt < 2000 && !ok; attempt++) {
|
||||
Collections.shuffle(receivers, ChristmasManager.RANDOM);
|
||||
ok = true;
|
||||
for (int i = 0; i < givers.size(); i++) {
|
||||
if (givers.get(i).equals(receivers.get(i))) { ok = false; break; }
|
||||
}
|
||||
}
|
||||
if (!ok) return false;
|
||||
assign.clear();
|
||||
for (int i = 0; i < givers.size(); i++) assign.put(givers.get(i), receivers.get(i));
|
||||
state = State.DRAWN;
|
||||
save();
|
||||
for (UUID id : givers) {
|
||||
Player p = Bukkit.getPlayer(id);
|
||||
if (p != null) p.sendMessage(mgr.msg("wi-who", "%name%", targetName(id)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Legt das Item in der Hand als Geschenk ab (ersetzt ein vorheriges, das zurueckgegeben wird). */
|
||||
public String setGift(Player p) {
|
||||
if (state != State.DRAWN) return "wi-wrong-state";
|
||||
if (!assign.containsKey(p.getUniqueId())) return "wi-not-joined";
|
||||
ItemStack hand = p.getInventory().getItemInMainHand();
|
||||
if (hand == null || hand.getType().isAir()) return "wi-gift-empty";
|
||||
ItemStack old = gifts.put(p.getUniqueId(), hand.clone());
|
||||
p.getInventory().setItemInMainHand(null);
|
||||
if (old != null) RewardUtil.give(p, old);
|
||||
save();
|
||||
return "wi-gift-saved";
|
||||
}
|
||||
|
||||
public int giftCount() { return gifts.size(); }
|
||||
|
||||
/** Verteilt alle Geschenke; Offline-Spieler bekommen ihres beim naechsten Join. */
|
||||
public int deliver() {
|
||||
if (state != State.DRAWN) return -1;
|
||||
boolean reveal = mgr.cfg().getBoolean("wichteln.reveal-giver", false);
|
||||
int n = 0;
|
||||
for (var e : gifts.entrySet()) {
|
||||
UUID receiver = assign.get(e.getKey());
|
||||
if (receiver == null) continue;
|
||||
String from = reveal ? participants.getOrDefault(e.getKey(), "?") : "";
|
||||
pending.computeIfAbsent(receiver, k -> new ArrayList<>()).add(new Pending(from, e.getValue()));
|
||||
n++;
|
||||
}
|
||||
gifts.clear();
|
||||
state = State.DELIVERED;
|
||||
save();
|
||||
for (Player p : Bukkit.getOnlinePlayers()) onJoin(p);
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Uebergibt wartende Geschenke. */
|
||||
public void onJoin(Player p) {
|
||||
List<Pending> list = pending.remove(p.getUniqueId());
|
||||
if (list == null) return;
|
||||
for (Pending pe : list) {
|
||||
RewardUtil.give(p, pe.item());
|
||||
p.sendMessage(mgr.msg(pe.from().isEmpty() ? "wi-received" : "wi-received-from", "%from%", pe.from()));
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
participants.clear();
|
||||
assign.clear();
|
||||
gifts.clear();
|
||||
pending.clear();
|
||||
state = State.CLOSED;
|
||||
save();
|
||||
}
|
||||
|
||||
// ---------- Persistenz ----------
|
||||
|
||||
void load() {
|
||||
if (!file.exists()) return;
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||
try { state = State.valueOf(y.getString("state", "CLOSED")); } catch (IllegalArgumentException e) { state = State.CLOSED; }
|
||||
participants.clear(); assign.clear(); gifts.clear(); pending.clear();
|
||||
ConfigurationSection ps = y.getConfigurationSection("participants");
|
||||
if (ps != null) for (String k : ps.getKeys(false)) participants.put(UUID.fromString(k), ps.getString(k, "?"));
|
||||
ConfigurationSection as = y.getConfigurationSection("assign");
|
||||
if (as != null) for (String k : as.getKeys(false)) assign.put(UUID.fromString(k), UUID.fromString(as.getString(k, "")));
|
||||
ConfigurationSection gs = y.getConfigurationSection("gifts");
|
||||
if (gs != null) for (String k : gs.getKeys(false)) { ItemStack it = gs.getItemStack(k); if (it != null) gifts.put(UUID.fromString(k), it); }
|
||||
ConfigurationSection pe = y.getConfigurationSection("pending");
|
||||
if (pe != null) {
|
||||
for (String k : pe.getKeys(false)) {
|
||||
List<Pending> list = new ArrayList<>();
|
||||
for (Map<?, ?> m : pe.getMapList(k)) {
|
||||
Object item = m.get("item");
|
||||
if (item instanceof ItemStack is) list.add(new Pending(String.valueOf(m.get("from")), is));
|
||||
}
|
||||
pending.put(UUID.fromString(k), list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void save() {
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
y.set("state", state.name());
|
||||
participants.forEach((k, v) -> y.set("participants." + k, v));
|
||||
assign.forEach((k, v) -> y.set("assign." + k, v.toString()));
|
||||
gifts.forEach((k, v) -> y.set("gifts." + k, v));
|
||||
pending.forEach((k, list) -> {
|
||||
List<Map<String, Object>> maps = new ArrayList<>();
|
||||
for (Pending p : list) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("from", p.from());
|
||||
m.put("item", p.item());
|
||||
maps.add(m);
|
||||
}
|
||||
y.set("pending." + k, maps);
|
||||
});
|
||||
try {
|
||||
y.save(file);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("wichteln.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import de.mviper.adventskalender.RewardUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Wunschzettel: Spieler schreiben Wuensche, Admins sehen sie in einem Menue und erfuellen sie. */
|
||||
public final class WishManager {
|
||||
|
||||
private static final class Wish {
|
||||
int id;
|
||||
UUID owner;
|
||||
String name;
|
||||
String text;
|
||||
long created;
|
||||
boolean done;
|
||||
boolean notified;
|
||||
ItemStack gift;
|
||||
}
|
||||
|
||||
public static final class Menu implements InventoryHolder {
|
||||
Inventory inv;
|
||||
int page;
|
||||
final Map<Integer, Integer> slotToId = new LinkedHashMap<>();
|
||||
@Override public Inventory getInventory() { return inv; }
|
||||
}
|
||||
|
||||
private static final int PAGE_SIZE = 45;
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private final File file;
|
||||
private final Map<Integer, Wish> wishes = new LinkedHashMap<>();
|
||||
private int nextId = 1;
|
||||
|
||||
WishManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.file = new File(mgr.plugin().getDataFolder(), "wishes.yml");
|
||||
}
|
||||
|
||||
// ---------- Persistenz ----------
|
||||
|
||||
void load() {
|
||||
wishes.clear();
|
||||
if (!file.exists()) return;
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||
nextId = y.getInt("next-id", 1);
|
||||
ConfigurationSection s = y.getConfigurationSection("wishes");
|
||||
if (s == null) return;
|
||||
for (String k : s.getKeys(false)) {
|
||||
try {
|
||||
Wish w = new Wish();
|
||||
w.id = Integer.parseInt(k);
|
||||
w.owner = UUID.fromString(s.getString(k + ".owner", ""));
|
||||
w.name = s.getString(k + ".name", "?");
|
||||
w.text = s.getString(k + ".text", "");
|
||||
w.created = s.getLong(k + ".created");
|
||||
w.done = s.getBoolean(k + ".done");
|
||||
w.notified = s.getBoolean(k + ".notified");
|
||||
w.gift = s.getItemStack(k + ".gift");
|
||||
wishes.put(w.id, w);
|
||||
} catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
void save() {
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
y.set("next-id", nextId);
|
||||
for (Wish w : wishes.values()) {
|
||||
String p = "wishes." + w.id;
|
||||
y.set(p + ".owner", w.owner.toString());
|
||||
y.set(p + ".name", w.name);
|
||||
y.set(p + ".text", w.text);
|
||||
y.set(p + ".created", w.created);
|
||||
y.set(p + ".done", w.done);
|
||||
y.set(p + ".notified", w.notified);
|
||||
if (w.gift != null) y.set(p + ".gift", w.gift);
|
||||
}
|
||||
try {
|
||||
y.save(file);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("wishes.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Spieler ----------
|
||||
|
||||
private List<Wish> openOf(UUID owner) {
|
||||
List<Wish> list = new ArrayList<>();
|
||||
for (Wish w : wishes.values()) if (!w.done && w.owner.equals(owner)) list.add(w);
|
||||
return list;
|
||||
}
|
||||
|
||||
/** @return Nachrichten-Schluessel */
|
||||
public String add(Player p, String text) {
|
||||
text = text.trim();
|
||||
int max = Math.max(10, mgr.cfg().getInt("wishes.max-length", 120));
|
||||
if (text.isEmpty()) return "wish-usage";
|
||||
if (text.length() > max) return "wish-too-long";
|
||||
if (openOf(p.getUniqueId()).size() >= Math.max(1, mgr.cfg().getInt("wishes.max-open-per-player", 3))) return "wish-limit";
|
||||
Wish w = new Wish();
|
||||
w.id = nextId++;
|
||||
w.owner = p.getUniqueId();
|
||||
w.name = p.getName();
|
||||
w.text = text.replace('&', '+'); // keine Farbcodes von Spielern
|
||||
w.created = System.currentTimeMillis();
|
||||
wishes.put(w.id, w);
|
||||
save();
|
||||
for (Player admin : Bukkit.getOnlinePlayers()) {
|
||||
if (admin.hasPermission("xmas.admin")) admin.sendMessage(mgr.msg("wish-admin-new", "%player%", p.getName()));
|
||||
}
|
||||
return "wish-saved";
|
||||
}
|
||||
|
||||
public void list(Player p) {
|
||||
List<Wish> open = openOf(p.getUniqueId());
|
||||
if (open.isEmpty()) { p.sendMessage(mgr.msg("wish-empty")); return; }
|
||||
p.sendMessage(mgr.msg("wish-list-header"));
|
||||
for (Wish w : open) p.sendMessage(mgr.raw("wish-list-line").replace("%text%", w.text));
|
||||
}
|
||||
|
||||
public int clearOwn(Player p) {
|
||||
List<Wish> open = openOf(p.getUniqueId());
|
||||
for (Wish w : open) wishes.remove(w.id);
|
||||
if (!open.isEmpty()) save();
|
||||
return open.size();
|
||||
}
|
||||
|
||||
/** Erfuellte Wuensche zustellen (beim Join oder sofort). */
|
||||
public void deliver(Player p) {
|
||||
boolean changed = false;
|
||||
List<Wish> done = new ArrayList<>();
|
||||
for (Wish w : wishes.values()) if (w.done && !w.notified && w.owner.equals(p.getUniqueId())) done.add(w);
|
||||
for (Wish w : done) {
|
||||
if (w.gift != null) RewardUtil.give(p, w.gift);
|
||||
p.sendMessage(mgr.msg(w.gift != null ? "wish-fulfilled-gift" : "wish-fulfilled", "%wish%", w.text));
|
||||
wishes.remove(w.id);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) save();
|
||||
}
|
||||
|
||||
// ---------- Admin-Menue ----------
|
||||
|
||||
private List<Wish> allOpen() {
|
||||
List<Wish> list = new ArrayList<>();
|
||||
for (Wish w : wishes.values()) if (!w.done) list.add(w);
|
||||
return list;
|
||||
}
|
||||
|
||||
public void openMenu(Player admin, int page) {
|
||||
List<Wish> open = allOpen();
|
||||
int pages = Math.max(1, (open.size() + PAGE_SIZE - 1) / PAGE_SIZE);
|
||||
page = Math.max(0, Math.min(page, pages - 1));
|
||||
|
||||
Menu holder = new Menu();
|
||||
holder.page = page;
|
||||
holder.inv = Bukkit.createInventory(holder, 54, mgr.raw("wish-menu-title") + " (" + (page + 1) + "/" + pages + ")");
|
||||
SimpleDateFormat fmt = new SimpleDateFormat("dd.MM. HH:mm");
|
||||
for (int i = 0; i < PAGE_SIZE; i++) {
|
||||
int idx = page * PAGE_SIZE + i;
|
||||
if (idx >= open.size()) break;
|
||||
Wish w = open.get(idx);
|
||||
ItemStack it = new ItemStack(Material.PAPER);
|
||||
ItemMeta meta = it.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(mgr.raw("wish-menu-item-name").replace("%player%", w.name));
|
||||
List<String> lore = new ArrayList<>();
|
||||
for (String line : wrap(w.text, 34)) lore.add("§f" + line);
|
||||
lore.add("§8" + fmt.format(new Date(w.created)));
|
||||
lore.add(" ");
|
||||
lore.add(mgr.raw("wish-menu-hint-left"));
|
||||
lore.add(mgr.raw("wish-menu-hint-shift"));
|
||||
lore.add(mgr.raw("wish-menu-hint-right"));
|
||||
meta.setLore(lore);
|
||||
it.setItemMeta(meta);
|
||||
}
|
||||
holder.inv.setItem(i, it);
|
||||
holder.slotToId.put(i, w.id);
|
||||
}
|
||||
if (page > 0) holder.inv.setItem(45, named(Material.ARROW, mgr.raw("wish-page-prev")));
|
||||
if (page < pages - 1) holder.inv.setItem(53, named(Material.ARROW, mgr.raw("wish-page-next")));
|
||||
if (open.isEmpty()) holder.inv.setItem(22, named(Material.BARRIER, mgr.raw("wish-menu-empty")));
|
||||
admin.openInventory(holder.inv);
|
||||
}
|
||||
|
||||
private static ItemStack named(Material m, String name) {
|
||||
ItemStack it = new ItemStack(m);
|
||||
ItemMeta meta = it.getItemMeta();
|
||||
if (meta != null) { meta.setDisplayName(name); it.setItemMeta(meta); }
|
||||
return it;
|
||||
}
|
||||
|
||||
private static List<String> wrap(String text, int width) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
StringBuilder cur = new StringBuilder();
|
||||
for (String word : text.split(" ")) {
|
||||
if (cur.length() + word.length() + 1 > width && cur.length() > 0) {
|
||||
lines.add(cur.toString());
|
||||
cur = new StringBuilder();
|
||||
}
|
||||
if (cur.length() > 0) cur.append(' ');
|
||||
cur.append(word);
|
||||
}
|
||||
if (cur.length() > 0) lines.add(cur.toString());
|
||||
return lines;
|
||||
}
|
||||
|
||||
public void click(Player admin, Menu menu, int slot, boolean right, boolean shift) {
|
||||
if (slot == 45 && menu.page > 0) { openMenu(admin, menu.page - 1); return; }
|
||||
if (slot == 53) { openMenu(admin, menu.page + 1); return; }
|
||||
Integer id = menu.slotToId.get(slot);
|
||||
if (id == null) return;
|
||||
Wish w = wishes.get(id);
|
||||
if (w == null || w.done) { openMenu(admin, menu.page); return; }
|
||||
|
||||
if (right) {
|
||||
wishes.remove(id);
|
||||
save();
|
||||
admin.sendMessage(mgr.msg("wish-admin-deleted", "%player%", w.name));
|
||||
openMenu(admin, menu.page);
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack hand = admin.getInventory().getItemInMainHand();
|
||||
boolean hasItem = hand != null && !hand.getType().isAir();
|
||||
if (!hasItem && !shift) {
|
||||
// Kein Item in der Hand: nicht stillschweigend ohne Geschenk erfuellen
|
||||
admin.sendMessage(mgr.msg("wish-admin-hold"));
|
||||
return;
|
||||
}
|
||||
|
||||
String itemText = "";
|
||||
if (hasItem && !shift) {
|
||||
w.gift = hand.clone();
|
||||
itemText = hand.getAmount() + "x " + hand.getType().name();
|
||||
admin.getInventory().setItemInMainHand(null);
|
||||
}
|
||||
w.done = true;
|
||||
w.notified = false;
|
||||
save();
|
||||
|
||||
Player owner = Bukkit.getPlayer(w.owner);
|
||||
if (owner != null) {
|
||||
deliver(owner);
|
||||
admin.sendMessage(w.gift != null
|
||||
? mgr.msg("wish-admin-done-gift", "%player%", w.name, "%item%", itemText)
|
||||
: mgr.msg("wish-admin-done", "%player%", w.name));
|
||||
} else {
|
||||
admin.sendMessage(mgr.msg("wish-admin-done-offline", "%player%", w.name));
|
||||
}
|
||||
openMenu(admin, menu.page);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import com.sk89q.worldedit.bukkit.BukkitAdapter;
|
||||
import com.sk89q.worldguard.WorldGuard;
|
||||
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Nur laden, wenn WorldGuard installiert ist (sonst NoClassDefFoundError). */
|
||||
final class WorldGuardHook {
|
||||
private WorldGuardHook() {}
|
||||
|
||||
static boolean inAnyRegion(Location loc, List<String> names) {
|
||||
var set = WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery()
|
||||
.getApplicableRegions(BukkitAdapter.adapt(loc));
|
||||
for (ProtectedRegion r : set.getRegions()) {
|
||||
for (String n : names) if (r.getId().equalsIgnoreCase(n)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.type.Candle;
|
||||
import org.bukkit.block.data.type.Leaves;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** Adventskranz: Vier Kerzen, die an den vier Adventssonntagen nacheinander angehen. */
|
||||
public final class WreathManager {
|
||||
private record Wreath(String world, int x, int y, int z) {}
|
||||
|
||||
// Kerzen: 1., 2., 4. Advent violett, 3. Advent rosa
|
||||
private static final int[][] CANDLE_OFFSETS = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};
|
||||
private static final Material[] CANDLES = {Material.PURPLE_CANDLE, Material.PURPLE_CANDLE,
|
||||
Material.PINK_CANDLE, Material.PURPLE_CANDLE};
|
||||
|
||||
private final ChristmasManager mgr;
|
||||
private final File file;
|
||||
private final List<Wreath> wreaths = new ArrayList<>();
|
||||
private final Set<String> blocks = new HashSet<>();
|
||||
private String lastAnnounced = "";
|
||||
private BukkitTask task;
|
||||
|
||||
WreathManager(ChristmasManager mgr) {
|
||||
this.mgr = mgr;
|
||||
this.file = new File(mgr.plugin().getDataFolder(), "wreaths.yml");
|
||||
}
|
||||
|
||||
// ---------- Adventsberechnung ----------
|
||||
|
||||
/** Aktueller Advent 0-4 (0 = vor dem 1. Advent oder nach dem Brenn-Ende). */
|
||||
public static int adventNumber(LocalDate today, ChristmasManager mgr) {
|
||||
LocalDate fourth = LocalDate.of(today.getYear(), 12, 24);
|
||||
while (fourth.getDayOfWeek() != DayOfWeek.SUNDAY) fourth = fourth.minusDays(1);
|
||||
LocalDate burnUntil = LocalDate.of(today.getYear(), 12, 26);
|
||||
String until = mgr.cfg().getString("wreath.burn-until", "12-26");
|
||||
try {
|
||||
String[] p = until.split("-");
|
||||
burnUntil = LocalDate.of(today.getYear(), Integer.parseInt(p[0].trim()), Integer.parseInt(p[1].trim()));
|
||||
} catch (Exception ignored) { }
|
||||
if (today.isAfter(burnUntil)) return 0;
|
||||
int n = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (!today.isBefore(fourth.minusWeeks(3 - i))) n = i + 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private int currentAdvent() {
|
||||
return adventNumber(LocalDate.now(mgr.plugin().getZone()), mgr);
|
||||
}
|
||||
|
||||
// ---------- Verwaltung ----------
|
||||
|
||||
private static String key(Block b) {
|
||||
return b.getWorld().getName() + ";" + b.getX() + ";" + b.getY() + ";" + b.getZ();
|
||||
}
|
||||
|
||||
public boolean isProtected(Block b) { return blocks.contains(key(b)); }
|
||||
|
||||
void load() {
|
||||
wreaths.clear();
|
||||
blocks.clear();
|
||||
if (file.exists()) {
|
||||
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||
lastAnnounced = y.getString("announced", "");
|
||||
ConfigurationSection s = y.getConfigurationSection("wreaths");
|
||||
if (s != null) {
|
||||
for (String k : s.getKeys(false)) {
|
||||
ConfigurationSection w = s.getConfigurationSection(k);
|
||||
if (w == null) continue;
|
||||
Wreath wr = new Wreath(w.getString("world", "world"), w.getInt("x"), w.getInt("y"), w.getInt("z"));
|
||||
wreaths.add(wr);
|
||||
index(wr);
|
||||
}
|
||||
}
|
||||
}
|
||||
stop();
|
||||
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 100L, 20L * 30);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
}
|
||||
|
||||
private void save() {
|
||||
YamlConfiguration y = new YamlConfiguration();
|
||||
y.set("announced", lastAnnounced);
|
||||
int i = 0;
|
||||
for (Wreath w : wreaths) {
|
||||
y.set("wreaths." + i + ".world", w.world());
|
||||
y.set("wreaths." + i + ".x", w.x());
|
||||
y.set("wreaths." + i + ".y", w.y());
|
||||
y.set("wreaths." + i + ".z", w.z());
|
||||
i++;
|
||||
}
|
||||
try {
|
||||
y.save(file);
|
||||
} catch (IOException e) {
|
||||
mgr.plugin().getLogger().warning("wreaths.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void index(Wreath w) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
blocks.add(w.world() + ";" + (w.x() + dx) + ";" + w.y() + ";" + (w.z() + dz));
|
||||
blocks.add(w.world() + ";" + (w.x() + dx) + ";" + (w.y() + 1) + ";" + (w.z() + dz));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Baut einen Kranz auf dem Block unter dem Ziel; Mitte des Kranzes = ground.getRelative(UP). */
|
||||
public boolean create(Block ground) {
|
||||
Block center = ground.getRelative(0, 1, 0);
|
||||
World w = center.getWorld();
|
||||
Leaves leaves = (Leaves) Material.SPRUCE_LEAVES.createBlockData();
|
||||
leaves.setPersistent(true);
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
if (dx == 0 && dz == 0) continue;
|
||||
Block b = w.getBlockAt(center.getX() + dx, center.getY(), center.getZ() + dz);
|
||||
b.setBlockData(leaves, false);
|
||||
}
|
||||
}
|
||||
// Mitte: Moosteppich-aehnlich als Dekoration
|
||||
w.getBlockAt(center.getX(), center.getY(), center.getZ()).setType(Material.RED_CARPET, false);
|
||||
Wreath wr = new Wreath(w.getName(), center.getX(), center.getY(), center.getZ());
|
||||
wreaths.add(wr);
|
||||
index(wr);
|
||||
save();
|
||||
applyCandles(wr, currentAdvent());
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeNear(Block b) {
|
||||
for (Wreath w : new ArrayList<>(wreaths)) {
|
||||
if (!w.world().equals(b.getWorld().getName())) continue;
|
||||
if (Math.abs(w.x() - b.getX()) <= 2 && Math.abs(w.z() - b.getZ()) <= 2 && Math.abs(w.y() - b.getY()) <= 3) {
|
||||
World world = b.getWorld();
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
for (int dy = 0; dy <= 1; dy++) {
|
||||
blocks.remove(w.world() + ";" + (w.x() + dx) + ";" + (w.y() + dy) + ";" + (w.z() + dz));
|
||||
world.getBlockAt(w.x() + dx, w.y() + dy, w.z() + dz).setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
wreaths.remove(w);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- Kerzen ----------
|
||||
|
||||
private void applyCandles(Wreath w, int advent) {
|
||||
World world = Bukkit.getWorld(w.world());
|
||||
if (world == null || !world.isChunkLoaded(w.x() >> 4, w.z() >> 4)) return;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Block b = world.getBlockAt(w.x() + CANDLE_OFFSETS[i][0], w.y() + 1, w.z() + CANDLE_OFFSETS[i][1]);
|
||||
Candle data = (Candle) CANDLES[i].createBlockData();
|
||||
data.setLit(i < advent);
|
||||
data.setCandles(1);
|
||||
b.setBlockData(data, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
int advent = currentAdvent();
|
||||
for (Wreath w : wreaths) applyCandles(w, advent);
|
||||
|
||||
if (advent > 0 && !wreaths.isEmpty() && mgr.cfg().getBoolean("wreath.announce", true)) {
|
||||
String id = LocalDate.now(mgr.plugin().getZone()).getYear() + "-" + advent;
|
||||
if (!id.equals(lastAnnounced)) {
|
||||
lastAnnounced = id;
|
||||
save();
|
||||
Bukkit.broadcastMessage(mgr.msg("wreath-advent", "%n%", String.valueOf(advent)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.mviper.adventskalender.christmas;
|
||||
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
|
||||
/**
|
||||
* PlaceholderAPI-Platzhalter mit dem Praefix xmas. Nur laden, wenn PlaceholderAPI installiert ist.
|
||||
* <pre>
|
||||
* %xmas_days_left% Tage bis Heiligabend
|
||||
* %xmas_countdown% Text des Countdowns
|
||||
* %xmas_advent% aktueller Advent (0-4)
|
||||
* %xmas_trees% Magic Trees des Spielers
|
||||
* %xmas_snowball_points% Punkte im Schneeballspiel
|
||||
* %xmas_snowball_rank% Platz im Schneeballspiel
|
||||
* %xmas_streak% aktuelle Login-Serie
|
||||
* %xmas_streak_best% beste Login-Serie
|
||||
* %xmas_wichteln% Name des Wichtel-Partners (falls gezogen)
|
||||
* </pre>
|
||||
*/
|
||||
final class XmasExpansion extends PlaceholderExpansion {
|
||||
private final ChristmasManager mgr;
|
||||
|
||||
XmasExpansion(ChristmasManager mgr) { this.mgr = mgr; }
|
||||
|
||||
@Override public String getIdentifier() { return "xmas"; }
|
||||
@Override public String getAuthor() { return "M_Viper"; }
|
||||
@Override public String getVersion() { return mgr.plugin().getDescription().getVersion(); }
|
||||
@Override public boolean persist() { return true; }
|
||||
|
||||
@Override
|
||||
public String onRequest(OfflinePlayer p, String params) {
|
||||
switch (params.toLowerCase()) {
|
||||
case "days_left": return String.valueOf(mgr.countdown().daysLeft());
|
||||
case "countdown": return org.bukkit.ChatColor.stripColor(mgr.countdown().text());
|
||||
case "advent": return String.valueOf(WreathManager.adventNumber(
|
||||
java.time.LocalDate.now(mgr.plugin().getZone()), mgr));
|
||||
default: break;
|
||||
}
|
||||
if (p == null) return "";
|
||||
switch (params.toLowerCase()) {
|
||||
case "trees": return String.valueOf(mgr.trees().countOf(p.getUniqueId()));
|
||||
case "snowball_points": return String.valueOf(mgr.snowball().points(p.getUniqueId()));
|
||||
case "snowball_rank": return String.valueOf(mgr.snowball().rank(p.getUniqueId()));
|
||||
case "streak": return String.valueOf(mgr.streaks().streak(p.getUniqueId()));
|
||||
case "streak_best": return String.valueOf(mgr.streaks().best(p.getUniqueId()));
|
||||
case "wichteln": return mgr.wichteln().targetName(p.getUniqueId());
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
# =============================================================================
|
||||
# Texte stehen in christmas_messages_de.yml / christmas_messages_en.yml
|
||||
# (Sprache folgt general.language aus der config.yml).
|
||||
#
|
||||
# Weihnachts-Module (ersetzen Christmas+ und das X-Mas "Magic Tree" Plugin)
|
||||
# Befehle: /christmas (alias /xmas) und /cracker
|
||||
# Items: "MATERIAL:Anzahl", z.B. "DIAMOND:2"
|
||||
# =============================================================================
|
||||
|
||||
crystal:
|
||||
material: NETHER_STAR
|
||||
name: "&bWeihnachts-Kristall"
|
||||
lore:
|
||||
- "&7Konzentrierter Weihnachtsgeist"
|
||||
- "&7Rechtsklick auf einen Setzling,"
|
||||
- "&7um ihn mit Magie zu füllen!"
|
||||
|
||||
# Befehl, mit dem Permissions als Belohnung vergeben werden (Kalender, Cracker).
|
||||
# Beispiel LuckPerms: "lp user %player% permission set %permission% true"
|
||||
permission-command: ""
|
||||
|
||||
# --- Schnee ---------------------------------------------------------------
|
||||
snow:
|
||||
enable: true
|
||||
# Stärke 1-5
|
||||
level: 3
|
||||
# Ticks zwischen zwei Partikel-Wellen (höher = weniger Last)
|
||||
display-task-interval: 2
|
||||
default-on: true
|
||||
disable-in-worlds: []
|
||||
# Nur in diesen WorldGuard-Regionen (nur wenn WorldGuard installiert ist). Leer = überall.
|
||||
enable-only-in-regions: false
|
||||
regions: []
|
||||
|
||||
# --- Schneemänner --------------------------------------------------------
|
||||
snowman:
|
||||
# Deko-Schneemänner (/xmas snowman spawn [stand|walk] [Name]): stehend oder herumlaufend, unverwundbar
|
||||
deco:
|
||||
particles: true
|
||||
# Kürbis-Kopf behalten
|
||||
hat: true
|
||||
# Standard beim Aufstellen ohne Angabe: true = laufend, false = stehend
|
||||
# (/xmas snowman spawn stand|walk [Name] überschreibt das pro Schneemann)
|
||||
walk: false
|
||||
# Umkreis in Blöcken
|
||||
walk-radius: 8
|
||||
# Schnee-Spur der laufenden Schneemänner: fade = verschwindet wieder, none = keine Spur, keep = bleibt liegen
|
||||
trail: fade
|
||||
trail-fade-seconds: 8
|
||||
# Normal gebaute Schneemänner (Schneeblöcke + Kürbis) geben ab und zu ein Geschenk
|
||||
player:
|
||||
enable: true
|
||||
max-per-player: 3
|
||||
name: "&f%player%s Schneemann"
|
||||
# Sekunden zwischen zwei Geschenken
|
||||
gift-interval: 300
|
||||
gifts:
|
||||
- "SNOWBALL:8"
|
||||
- "COOKIE:3"
|
||||
- "COAL:1"
|
||||
- "GOLD_NUGGET:2"
|
||||
|
||||
# --- Santa-Event ----------------------------------------------------------
|
||||
santa:
|
||||
# Weihnachtsmelodie mit Glocken, wenn Santa kommt
|
||||
melody:
|
||||
enable: true
|
||||
volume: 1.0
|
||||
# Ticks pro Achtelnote (kleiner = schneller)
|
||||
tempo: 3
|
||||
# false = nur Spieler in Santas Welt, true = alle Spieler
|
||||
everyone: false
|
||||
duration: 300
|
||||
drop-interval: 3
|
||||
# Chat-Nachricht "Santa hat ein Geschenk fallen lassen" höchstens alle X Sekunden (0 = nie)
|
||||
drop-message-interval: 60
|
||||
# Chance (in %) pro Drop-Intervall, dass wirklich ein Geschenk fällt
|
||||
drop-chance: 100
|
||||
radius: 6
|
||||
name: "&c&lSanta Claus"
|
||||
# AUTO = Spielermodell (Mannequin, ab Minecraft 1.21.9) wenn möglich, sonst Rüstungsständer.
|
||||
# ARMOR_STAND erzwingt den Rüstungsständer.
|
||||
model: AUTO
|
||||
# Skin: Textur-URL (textures.minecraft.net) ODER Spielername. Leer = Standard-Skin mit roter Lederrüstung.
|
||||
skin-url: "https://textures.minecraft.net/texture/b2aad465030fedcd6e5a255ac228c357f731343bdf159ea78a61f4146b27289d"
|
||||
skin-player: ""
|
||||
# Santa läuft in diesem Radius herum (0 = steht still)
|
||||
walk-radius: 8
|
||||
# Lauftempo in Blöcken pro Tick (0.07 = gemütlicher Spaziergang)
|
||||
walk-speed: 0.07
|
||||
# Anflug mit Schlitten und zwei Rentieren, bevor Santa landet
|
||||
sleigh:
|
||||
enable: true
|
||||
flight-seconds: 15
|
||||
distance: 70
|
||||
height: 30
|
||||
reindeer-names: ["Rudolph", "Blitzen"]
|
||||
snow-trail:
|
||||
enabled: true
|
||||
fade-seconds: 5
|
||||
gifts:
|
||||
- "DIAMOND:1"
|
||||
- "EMERALD:2"
|
||||
- "COOKIE:8"
|
||||
- "CAKE:1"
|
||||
- "GOLD_INGOT:3"
|
||||
- "COAL:5"
|
||||
# Cracker als Santa-Geschenk: "cracker:<id>:<Anzahl>" als Eintrag in gifts, z.B. "cracker:cracker:1"
|
||||
locations: []
|
||||
# - world: world
|
||||
# x: 0.5
|
||||
# y: 80
|
||||
# z: 0.5
|
||||
|
||||
# --- Cracker --------------------------------------------------------------
|
||||
crackers:
|
||||
cracker:
|
||||
material: FIREWORK_STAR
|
||||
name: "&c&lWeihnachts-Cracker"
|
||||
lore:
|
||||
- "&7Rechtsklick zum Öffnen!"
|
||||
rewards:
|
||||
- { chance: 40, item: "COOKIE:8" }
|
||||
- { chance: 30, item: "GOLD_INGOT:3" }
|
||||
- { chance: 20, item: "DIAMOND:1" }
|
||||
- { chance: 10, item: "EMERALD:5" }
|
||||
# Beispiel Befehl: - { chance: 5, command: "give %player% diamond 3" }
|
||||
|
||||
# --- Magic Tree -----------------------------------------------------------
|
||||
magic-tree:
|
||||
enable: true
|
||||
# Max. Bäume pro Spieler
|
||||
tree-limit: 3
|
||||
# Das Event endet automatisch an diesem Datum (Format dd-MM-yyyy HH-mm-ss)
|
||||
holiday-ends:
|
||||
enabled: true
|
||||
date: "10-01-2027 00-00-00"
|
||||
# Beim Abbau nach Eventende alle ausgegebenen Ressourcen zurückgeben
|
||||
resource-back: true
|
||||
# Chance, dass ein Geschenk Inhalt hat
|
||||
luck:
|
||||
enabled: false
|
||||
chance: 75
|
||||
# Weihnachts-Kristall in der Werkbank herstellbar:
|
||||
# Diamanten oben/links/rechts/unten, Smaragd in der Mitte
|
||||
crystal-recipe: true
|
||||
# Partikel-Show am Baum (Spirale, Lichter, Stern, Glitzer über Geschenken)
|
||||
particles:
|
||||
enable: true
|
||||
# 0.2 (dezent) bis 5.0 (sehr viel). Standard 2.0
|
||||
intensity: 2.0
|
||||
max-presents-per-tree: 5
|
||||
# Geschenk-Köpfe (Texturen-URLs von textures.minecraft.net)
|
||||
presents:
|
||||
- "http://textures.minecraft.net/texture/21bc9d42b0041e8f95cb9b26628fdaf50cd0e36f7bb9d6b3a4d2af3949da97d6"
|
||||
- "http://textures.minecraft.net/texture/2b1ec7dc753061ca174424ea45cf9490b39cd5dcca477d138a603e6be755ec72"
|
||||
# Weitere Geschenke per /xmas addhand (gespeichert in gifts.yml)
|
||||
gifts:
|
||||
- DIAMOND
|
||||
- EMERALD
|
||||
- IRON_SWORD
|
||||
- IRON_HELMET
|
||||
- IRON_CHESTPLATE
|
||||
- IRON_BOOTS
|
||||
- CAKE
|
||||
- COOKIE
|
||||
- BLAZE_ROD
|
||||
- DIAMOND_SWORD
|
||||
- IRON_INGOT
|
||||
- GOLD_INGOT
|
||||
- SPONGE
|
||||
- DIAMOND_HOE
|
||||
- NAME_TAG
|
||||
# gift-cooldown in Sekunden (-1 = keine Geschenke), lvlup = Items, die zum nächsten Level nötig sind
|
||||
levels:
|
||||
sapling:
|
||||
gift-cooldown: -1
|
||||
lvlup:
|
||||
DIAMOND: 1
|
||||
REDSTONE: 10
|
||||
ENDER_PEARL: 1
|
||||
small_tree:
|
||||
gift-cooldown: 300
|
||||
lvlup:
|
||||
DIAMOND: 3
|
||||
GOLD_INGOT: 5
|
||||
BLAZE_POWDER: 10
|
||||
SNOWBALL: 30
|
||||
tree:
|
||||
gift-cooldown: 180
|
||||
lvlup:
|
||||
DIAMOND: 5
|
||||
EMERALD: 3
|
||||
GOLD_NUGGET: 8
|
||||
GLOWSTONE_DUST: 16
|
||||
magic_tree:
|
||||
gift-cooldown: 120
|
||||
lvlup: {}
|
||||
|
||||
# --- Kalender-Zusatz (Christmas+ Funktionen) -------------------------------
|
||||
# Wirken auf den Adventskalender (config.yml -> rewards.<Tag>)
|
||||
calendar-extras:
|
||||
# Beim Abholen eines Tages allen Spielern eine Meldung zeigen
|
||||
broadcast-claim: false
|
||||
# true: Nur der heutige Tag ist öffnbar; vergangene Tage nur mit Recht christmasplus.adventcalendar.claimpast
|
||||
restrict-past-days: false
|
||||
|
||||
# --- Countdown bis Heiligabend ------------------------------------------------
|
||||
countdown:
|
||||
enable: true
|
||||
# Zieldatum Monat-Tag
|
||||
target: "12-24"
|
||||
# Ab wie vielen Tagen vorher wird der Countdown angezeigt
|
||||
show-days-before: 60
|
||||
bossbar: true
|
||||
join-message: true
|
||||
|
||||
# --- Plätzchen und Lebkuchen --------------------------------------------------
|
||||
# Verteilen mit /xmas cookie give <Spieler> <id> oder als Item "cookie:<id>:<Anzahl>"
|
||||
# in Belohnungen, Crackern, Santa-Geschenken usw.
|
||||
# effects: "EFFEKT:Sekunden:Stufe" (Stufe 0 = I)
|
||||
cookies:
|
||||
zimtstern:
|
||||
material: COOKIE
|
||||
name: "&6Zimtstern"
|
||||
lore:
|
||||
- "&7Knusprig und zimtig."
|
||||
- "&7Gibt kurz Tempo."
|
||||
effects: ["speed:30:1"]
|
||||
lebkuchen:
|
||||
material: BREAD
|
||||
name: "&cLebkuchen"
|
||||
lore:
|
||||
- "&7Frisch aus dem Ofen."
|
||||
- "&7Regeneriert Leben."
|
||||
effects: ["regeneration:10:1"]
|
||||
bratapfel:
|
||||
material: APPLE
|
||||
name: "&eBratapfel"
|
||||
lore:
|
||||
- "&7Warm und süß."
|
||||
- "&7Gibt Absorption."
|
||||
effects: ["absorption:60:1"]
|
||||
|
||||
# --- Wichteln ----------------------------------------------------------------
|
||||
wichteln:
|
||||
# Beim Verteilen den Schenkenden verraten?
|
||||
reveal-giver: false
|
||||
|
||||
# --- Adventskranz ------------------------------------------------------------
|
||||
wreath:
|
||||
# Meldung an alle am Adventssonntag
|
||||
announce: true
|
||||
# Bis zu diesem Tag (Monat-Tag) brennen die Kerzen
|
||||
burn-until: "12-26"
|
||||
|
||||
# --- Schneeball-Minispiel ----------------------------------------------------
|
||||
snowball:
|
||||
duration: 180
|
||||
points-per-hit: 1
|
||||
# So viele Schneebälle haben Spieler in der Arena immer mindestens
|
||||
refill-amount: 16
|
||||
# Belohnungen für Platz 1 bis 3 (item / command / permission, %player% wird ersetzt)
|
||||
rewards:
|
||||
1:
|
||||
- { item: "DIAMOND:3" }
|
||||
2:
|
||||
- { item: "EMERALD:5" }
|
||||
3:
|
||||
- { item: "GOLD_INGOT:5" }
|
||||
|
||||
# --- Login-Serie -------------------------------------------------------------
|
||||
login-streak:
|
||||
enable: true
|
||||
# Zeitraum (Monat-Tag), in dem die Serie zählt
|
||||
from: "12-01"
|
||||
until: "12-31"
|
||||
# Nach so vielen Tagen beginnen die Belohnungen von vorn
|
||||
cycle: 7
|
||||
rewards:
|
||||
1:
|
||||
- { item: "COOKIE:5" }
|
||||
2:
|
||||
- { item: "cookie:zimtstern:2" }
|
||||
3:
|
||||
- { item: "GOLD_INGOT:3" }
|
||||
4:
|
||||
- { item: "cookie:lebkuchen:2" }
|
||||
5:
|
||||
- { item: "EMERALD:2" }
|
||||
6:
|
||||
- { item: "cracker:cracker:1" }
|
||||
7:
|
||||
- { item: "DIAMOND:2" }
|
||||
|
||||
# --- Weihnachts- und Neujahrsgruß ---------------------------------------------
|
||||
# Einmal pro Tag Chat-Gruß an alle (und beim Join), Schlüssel verweisen auf die Sprachdatei.
|
||||
greetings:
|
||||
enable: true
|
||||
join-fireworks: true
|
||||
fireworks-on-broadcast: true
|
||||
days:
|
||||
"12-24": greeting-christmas-eve
|
||||
"12-25": plugin-enabled
|
||||
"12-31": greeting-new-years-eve
|
||||
"01-01": final-wish
|
||||
|
||||
# --- Kamin-Geschenke (Socke aufhängen) -----------------------------------------
|
||||
stocking:
|
||||
enable: true
|
||||
# Socke aufhängen ab / Geschenk ab / Abholen möglich bis (Monat-Tag)
|
||||
hang-from: "12-01"
|
||||
open-on: "12-25"
|
||||
until: "12-31"
|
||||
# Wie viele Socken ein Spieler per /xmas stocking place aufstellen darf
|
||||
max-placed-per-player: 3
|
||||
rewards:
|
||||
- { item: "COOKIE:6" }
|
||||
- { item: "cookie:zimtstern:2" }
|
||||
- { item: "GOLD_INGOT:2" }
|
||||
- { item: "present:1" }
|
||||
|
||||
# --- Santas Socken-Flug ---------------------------------------------------------
|
||||
# Am Weihnachtstag (und dem Tag danach) fliegt Santa von aufgestellter Socke zu aufgestellter
|
||||
# Socke (/xmas stocking place) und legt unter jeder ein Geschenk ab.
|
||||
stocking-flight:
|
||||
enable: true
|
||||
# Zeitraum (Monat-Tag, einschließlich): Weihnachtstag und ein Tag danach
|
||||
from: "12-24"
|
||||
until: "12-25"
|
||||
# Mindestabstand zwischen zwei Flügen in Minuten
|
||||
interval-minutes: 20
|
||||
# Wie viele Socken pro Flug beliefert werden
|
||||
max-stockings-per-flight: 12
|
||||
# Flugtempo in Blöcken pro Tick
|
||||
speed: 0.7
|
||||
# Nur der Besitzer der Socke darf das Geschenk öffnen
|
||||
owner-only: true
|
||||
# Inhalt des Geschenks (leer = die Belohnungen von stocking.rewards)
|
||||
rewards: []
|
||||
|
||||
# --- Nikolausmütze ------------------------------------------------------------
|
||||
santa-hat:
|
||||
enable: true
|
||||
from: "12-01"
|
||||
until: "12-31"
|
||||
# Beim Join anklickbares Angebot im Chat
|
||||
offer-on-join: true
|
||||
# Statt Angebot direkt aufsetzen
|
||||
auto-equip: false
|
||||
|
||||
# --- Weihnachtsquiz ------------------------------------------------------------
|
||||
quiz:
|
||||
answer-seconds: 30
|
||||
# Automatisch alle X Minuten eine Frage (nur wenn genug Spieler online sind)
|
||||
auto:
|
||||
enable: false
|
||||
interval-minutes: 20
|
||||
min-players: 2
|
||||
default-reward:
|
||||
- { item: "EMERALD:2" }
|
||||
# a = erlaubte Antworten (Groß-/Kleinschreibung und Sonderzeichen egal), reward optional
|
||||
questions:
|
||||
- { q: "Wie viele Kerzen hat ein Adventskranz?", a: ["4", "vier"] }
|
||||
- { q: "Welches Tier zieht den Schlitten des Weihnachtsmanns?", a: ["Rentier", "Rentiere"] }
|
||||
- { q: "Wie heißt das Rentier mit der roten Nase?", a: ["Rudolph", "Rudolf"] }
|
||||
- { q: "Aus wie vielen Türchen besteht ein klassischer Adventskalender?", a: ["24", "vierundzwanzig"] }
|
||||
- { q: "In welchem Monat ist Weihnachten?", a: ["Dezember"] }
|
||||
- { q: "Welcher Baum wird traditionell zu Weihnachten geschmückt?", a: ["Tannenbaum", "Tanne", "Weihnachtsbaum", "Fichte"] }
|
||||
- { q: "Wie heißt der Weihnachtsmann auf Englisch?", a: ["Santa Claus", "Santa"] }
|
||||
- { q: "Wie viele Adventssonntage gibt es?", a: ["4", "vier"] }
|
||||
- { q: "Wie viele Schneeblöcke braucht man für einen Schneegolem?", a: ["2", "zwei"] }
|
||||
- { q: "Welcher Mob wirft Schneebälle auf Monster?", a: ["Schneegolem", "Schneemann", "Snow Golem"] }
|
||||
|
||||
# --- Wunschzettel --------------------------------------------------------------
|
||||
wishes:
|
||||
max-open-per-player: 3
|
||||
max-length: 120
|
||||
|
||||
# --- Weihnachtsmarkt-Stände ---------------------------------------------------
|
||||
# Aufstellen mit /xmas market spawn <id>. give: wie bei allen Items, auch cookie:/cracker:/crystal:/present:.
|
||||
# price: bis zu 2 Items. Nach /xmas reload werden die Angebote aktualisiert.
|
||||
market:
|
||||
stalls:
|
||||
plaetzchen:
|
||||
name: "&6Plätzchen-Stand"
|
||||
profession: BUTCHER
|
||||
trades:
|
||||
- { give: "cookie:zimtstern:2", price: ["EMERALD:1"] }
|
||||
- { give: "cookie:lebkuchen:2", price: ["EMERALD:1"] }
|
||||
- { give: "cookie:bratapfel:1", price: ["GOLD_INGOT:2"] }
|
||||
- { give: "COOKIE:8", price: ["WHEAT:8"] }
|
||||
deko:
|
||||
name: "&2Deko-Stand"
|
||||
profession: LIBRARIAN
|
||||
trades:
|
||||
- { give: "present:1", price: ["EMERALD:3"] }
|
||||
- { give: "SPRUCE_SAPLING:4", price: ["GOLD_INGOT:1"] }
|
||||
- { give: "RED_CANDLE:4", price: ["IRON_INGOT:2"] }
|
||||
- { give: "LANTERN:2", price: ["IRON_INGOT:3"] }
|
||||
- { give: "SNOWBALL:16", price: ["COAL:1"] }
|
||||
- { give: "crystal:1", price: ["DIAMOND:5", "EMERALD:5"] }
|
||||
|
||||
# --- Portale zum Weihnachtsmarkt -----------------------------------------------
|
||||
# Portale funktionieren NUR, wenn ein Admin/OP den Marktplatz mit /xmas market set festgelegt hat
|
||||
# (aufheben mit /xmas market unset). Spieler bauen sich mit /xmas portal create (auf den Boden
|
||||
# schauen) ein Portal aus Schneebloecken, das dorthin fuehrt.
|
||||
portal:
|
||||
enable: true
|
||||
# Kosten fuer ein Portal: Das Portal besteht aus 10 Schneebloecken (Admins bauen kostenlos)
|
||||
cost: ["SNOW_BLOCK:10"]
|
||||
# Beim Entfernen durch den Besitzer die Kosten zurueckgeben
|
||||
refund: true
|
||||
limit-per-player: 1
|
||||
# Wie lange man im Portal stehen muss
|
||||
teleport-delay-seconds: 3
|
||||
cooldown-seconds: 10
|
||||
# Leer = ueberall erlaubt, sonst nur in diesen Welten
|
||||
allowed-worlds: []
|
||||
@@ -0,0 +1,275 @@
|
||||
messages:
|
||||
prefix: "&c[Christmas] &f"
|
||||
no-permission: "&cDazu hast du keine Berechtigung."
|
||||
player-only: "&cDieser Befehl geht nur im Spiel."
|
||||
player-offline: "&cSpieler nicht gefunden."
|
||||
usage: "&cNutzung: /christmas <help|snow|santa|give|gifts|addhand|snowman|countdown|cookie|wichteln|wreath|snowball|deco|stocking|hat|quiz|wish|wishes|market|portal|end|reload>"
|
||||
help:
|
||||
- "&2/xmas &7- Version und Status"
|
||||
- "&2/xmas snow &7- Schnee für dich an/aus"
|
||||
- "&2/xmas countdown &7- Countdown-Leiste an/aus"
|
||||
- "&2/xmas santa start [Zeit] [Welt x y z | random] | stop | teleport"
|
||||
- "&2/xmas give <Name> [Anzahl] &7- Weihnachts-Kristall geben"
|
||||
- "&2/xmas gifts &7- Geschenke unter alle Bäume"
|
||||
- "&2/xmas addhand &7- Item in der Hand als Geschenk hinzufügen"
|
||||
- "&2/xmas snowman spawn [stand|walk] [Name] | remove &7- Deko-Schneemann (stehend/laufend)"
|
||||
- "&2/xmas cookie list | give <Spieler> <id> [Anzahl]"
|
||||
- "&2/xmas wichteln join|leave|who|gift|status &7- Wichteln"
|
||||
- "&2/xmas wichteln open|draw|deliver|reset &7- (Admin)"
|
||||
- "&2/xmas wreath create|remove &7- Adventskranz"
|
||||
- "&2/xmas snowball arena [Radius] | start [Zeit] | stop | top"
|
||||
- "&2/xmas deco [undo] &7- Deko-Menü"
|
||||
- "&2/xmas stocking [place | remove | flight] &7- Socke aufhängen / abholen / an die Wand hängen"
|
||||
- "&2/xmas hat &7- Nikolausmütze"
|
||||
- "&2/xmas wish <Text> | list | clear &7- Wunschzettel"
|
||||
- "&2/xmas wishes &7- Wunschzettel ansehen (Admin)"
|
||||
- "&2/xmas quiz start [Nr] | skip &7- Weihnachtsquiz (Admin)"
|
||||
- "&2/xmas market spawn <id> | remove | list &7- Marktstände (Admin)"
|
||||
- "&2/xmas portal create|remove|info &7- Portal zum Weihnachtsmarkt"
|
||||
- "&2/xmas market set|unset|tp|back &7- Marktpunkt (Admin) / Rückweg"
|
||||
- "&2/xmas end &7- Event beenden"
|
||||
- "&2/xmas reload &7- Konfiguration neu laden"
|
||||
- "&2/cracker list | get [id] [Anzahl] | give <Spieler> [id] [Anzahl]"
|
||||
info: "&aX-Mas Modul aktiv. Status: &e%status% &7- Bäume: &e%trees%"
|
||||
status-active: "AKTIV"
|
||||
status-ended: "BEENDET"
|
||||
reloaded: "&aChristmas-Konfiguration neu geladen."
|
||||
calendar-test-on: "&aKalender-Testmodus &leingeschaltet&a: Du kannst jeden Tag öffnen."
|
||||
calendar-test-off: "&7Kalender-Testmodus ausgeschaltet."
|
||||
past-day-restricted: "&cVergangene Tage kannst du nicht mehr öffnen."
|
||||
plugin-enabled: "&aFrohe Weihnachten und ein gutes neues Jahr!"
|
||||
timeout: "&cDie Feiertage sind vorbei! Bis nächstes Jahr!"
|
||||
final-wish: "&6Alles Gute zum neuen Jahr, meine Freunde!"
|
||||
end-set: "&7Event beendet. Spieler können ihre Bäume abbauen und bekommen Ressourcen zurück."
|
||||
gifts-spawned: "&cHo! &2Ho! &cHo! &3Zeit, die Geschenke zu prüfen!"
|
||||
addhand-ok: "&aItem als Geschenk hinzugefügt."
|
||||
addhand-empty: "&cDu hältst kein Item in der Hand."
|
||||
crystal-given: "&aDu hast &e%amount%x Weihnachts-Kristall &aerhalten."
|
||||
crystal-gave: "&a%player% hat %amount% Kristall(e) bekommen."
|
||||
|
||||
# Schnee
|
||||
snow-on: "&aSchnee ist für dich &leingeschaltet&a."
|
||||
snow-off: "&7Schnee ist für dich &causgeschaltet&7."
|
||||
snow-disabled-global: "&cSchnee ist auf dem Server deaktiviert."
|
||||
|
||||
# Schneemänner
|
||||
snowman-usage: "&cNutzung: /christmas snowman spawn [stand|walk] [Name] | remove"
|
||||
snowman-spawned: "&aDeko-Schneemann aufgestellt."
|
||||
snowman-removed: "&7Deko-Schneemann entfernt."
|
||||
snowman-none: "&cKein Deko-Schneemann in 6 Blöcken Umkreis."
|
||||
snowman-built: "&aDein Schneemann gibt dir ab und zu ein Geschenk! (maximal %max% Schneemänner)"
|
||||
snowman-limit: "&cDu hast schon %max% Schneemänner, dieser gibt keine Geschenke."
|
||||
|
||||
# Santa
|
||||
santa-started: "&aDas Santa-Claus-Event hat begonnen! Santa ist in &e%world% &abei &ex=%x%, y=%y%, z=%z%&a und wartet &e%duration%&a. (/christmas santa teleport)"
|
||||
santa-started-sender: "&aDu hast das Santa-Claus-Event für &e%duration% &agestartet."
|
||||
santa-dropped: "&aSanta hat gerade ein Geschenk fallen lassen!"
|
||||
calendar-claimed-broadcast: "&e%player% &ahat das Geschenk von Kalendertag &e%day% &aabgeholt."
|
||||
calendar-claimed-self: "&aDu hast das Geschenk von Tag &e%day% &aabgeholt."
|
||||
santa-stopped: "&7Der Weihnachtsmann ist wieder weitergezogen."
|
||||
santa-already: "&cDer Weihnachtsmann ist bereits aktiv."
|
||||
santa-not-active: "&cDer Weihnachtsmann ist gerade nicht aktiv."
|
||||
santa-no-locations: "&cEs sind keine Santa-Orte in der christmas.yml hinterlegt."
|
||||
santa-usage: "&cNutzung: /christmas santa start [Sekunden] [Welt x y z | random] | stop | teleport"
|
||||
santa-bossbar: "&cSanta Claus &f- noch &e%time%s"
|
||||
santa-teleported: "&aDu wurdest zu Santa teleportiert."
|
||||
|
||||
# Cracker
|
||||
cracker-given: "&aDu hast &e%amount%x %id% &aerhalten."
|
||||
cracker-gave: "&a%player% hat %amount%x %id% bekommen."
|
||||
cracker-unknown: "&cUnbekannter Cracker: %id%"
|
||||
cracker-opened: "&6Knall! &fDu hast einen Cracker geöffnet."
|
||||
cracker-usage: "&cNutzung: /cracker list | get [id] [Anzahl] | give <Spieler> [id] [Anzahl]"
|
||||
|
||||
# Magic Tree
|
||||
tree:
|
||||
grow-lvl-ready: "&aWusch! Rechtsklick auf den Baum, damit er wächst!"
|
||||
grow-req-list-title: "&eBenötigt (Items neben den Baum werfen):"
|
||||
grow-not-enough-place: "&cHier ist nicht genug Platz zum Wachsen."
|
||||
grow-lvl-max: "&6Dieser Baum hat sein maximales Level erreicht!"
|
||||
grow-fed: "&7Der Baum hat &e%amount%x %item% &7aufgenommen."
|
||||
tree-limit: "&cSei nicht gierig! (Baum-Limit erreicht)"
|
||||
tree-planted: "&aDein Magic Tree wurde gepflanzt! Wirf die benötigten Items neben den Baum."
|
||||
tree-disabled: "&cMagic Trees sind beendet/deaktiviert."
|
||||
destroy-sapling: "&cDieses Wunder kommt nicht wieder. Wirklich abbauen? Nochmal abbauen zum Bestätigen."
|
||||
destroy-leaves-santa: "&cDer Weihnachtsmann sieht alles."
|
||||
destroy-leaves-tut: "&7Um den Baum zu zerstören, fälle den Stamm."
|
||||
destroy-warning: "&cBist du sicher? Dein Fortschritt geht verloren, keine Ressourcen zurück. Nochmal abbauen zum Bestätigen."
|
||||
destroy-fail-owner: "&cDas ist nicht dein Baum!"
|
||||
destroy-complete: "&4Du bist ein MONSTER!"
|
||||
destroy-refund: "&aDer Baum wurde abgebaut, deine Ressourcen kommen zurück."
|
||||
not-owner: "&cDas ist nicht dein Baum."
|
||||
info: "&aMagic Tree von &e%owner%&a, Level: &e%level%"
|
||||
gift:
|
||||
unluck-message: "&7Vielleicht hat der Weihnachtsmann dich auf die schwarze Liste gesetzt. Nächstes Mal klappt es bestimmt!"
|
||||
opened: "&aDu hast ein Geschenk geöffnet!"
|
||||
|
||||
# Countdown
|
||||
countdown-bossbar: "&cNoch &e%days% &cTage bis Heiligabend"
|
||||
countdown-bossbar-one: "&cNoch &e1 &cTag bis Heiligabend"
|
||||
countdown-not-yet: "&7Noch &e%days% &7Tage bis Heiligabend. Die Anzeige startet in &e%in% &7Tag(en)."
|
||||
countdown-today: "&aHeute ist Heiligabend! Frohe Weihnachten!"
|
||||
countdown-on: "&aCountdown-Leiste eingeschaltet."
|
||||
countdown-off: "&7Countdown-Leiste ausgeschaltet."
|
||||
|
||||
# Plätzchen
|
||||
cookie-usage: "&cNutzung: /xmas cookie list | give <Spieler> <id> [Anzahl]"
|
||||
cookie-unknown: "&cUnbekanntes Plätzchen: %id%"
|
||||
cookie-given: "&aDu hast &e%amount%x %id% &aerhalten."
|
||||
|
||||
# Wichteln
|
||||
wi-open: "&aDas Wichteln ist eröffnet! Mit &e/xmas wichteln join &amachst du mit."
|
||||
wi-joined: "&aDu bist beim Wichteln dabei!"
|
||||
wi-already: "&7Du bist schon angemeldet."
|
||||
wi-left: "&7Du bist aus dem Wichteln ausgetreten."
|
||||
wi-not-joined: "&cDu nimmst nicht am Wichteln teil."
|
||||
wi-wrong-state: "&cDas geht gerade nicht (falsche Phase)."
|
||||
wi-too-few: "&cEs braucht mindestens 3 Teilnehmer."
|
||||
wi-drawn: "&aGezogen! Jeder hat einen Wichtel-Partner. Schau mit &e/xmas wichteln who &anach, wen du beschenkst."
|
||||
wi-who: "&aDu beschenkst: &e%name%"
|
||||
wi-gift-empty: "&cNimm das Geschenk in die Hand."
|
||||
wi-gift-saved: "&aDein Geschenk für &e%name% &aist gespeichert. (Mit einem anderen Item in der Hand kannst du es ersetzen.)"
|
||||
wi-delivered: "&6Die Wichtel-Geschenke wurden verteilt! (&e%count% &6Geschenke)"
|
||||
wi-received: "&aDu hast ein Wichtel-Geschenk bekommen!"
|
||||
wi-received-from: "&aDu hast ein Wichtel-Geschenk von &e%from% &abekommen!"
|
||||
wi-reset: "&7Wichteln zurückgesetzt."
|
||||
wi-status: "&aWichteln: &e%state% &7- Teilnehmer: &e%count% &7- Geschenke: &e%gifts%"
|
||||
|
||||
# Adventskranz
|
||||
wreath-created: "&aAdventskranz gebaut."
|
||||
wreath-removed: "&7Adventskranz entfernt."
|
||||
wreath-none: "&cKein Adventskranz in der Nähe."
|
||||
wreath-usage: "&cNutzung: /xmas wreath create | remove (schau auf den Boden)"
|
||||
wreath-advent: "&6Heute ist der &e%n%. Advent&6! Am Adventskranz brennen jetzt &e%n% &6Kerzen."
|
||||
|
||||
# Schneeball-Spiel
|
||||
sb-usage: "&cNutzung: /xmas snowball arena [Radius] | start [Zeit] | stop | top"
|
||||
sb-arena-set: "&aSchneeball-Arena gesetzt (Radius &e%radius%&a)."
|
||||
sb-no-arena: "&cEs ist noch keine Arena gesetzt (/xmas snowball arena)."
|
||||
sb-already: "&cDas Schneeball-Spiel läuft schon."
|
||||
sb-not-active: "&cDas Schneeball-Spiel läuft nicht."
|
||||
sb-started: "&b&lSchneeballschlacht! &7Für &e%time% &7in der Arena (&e%world% %x% %z%&7). Wirf Schneebälle auf andere Spieler!"
|
||||
sb-ended: "&b&lSchneeballschlacht beendet! &7Die Besten:"
|
||||
sb-top-line: "&e%rank%. &f%player% &7- &a%points% Punkte"
|
||||
sb-no-scores: "&7Noch keine Punkte."
|
||||
sb-actionbar: "&fPunkte: &a%points% &7| Platz &e%rank% &7| noch &c%time%s"
|
||||
sb-reward: "&aGlückwunsch zu Platz %rank%! Deine Belohnung wurde ausgegeben."
|
||||
|
||||
# Deko
|
||||
deco-menu-title: "Weihnachts-Deko"
|
||||
deco-click-hint: "&7Klicken, dann wird sie dort aufgestellt, wo du hinschaust."
|
||||
deco-present: "&aGeschenk"
|
||||
deco-candy_cane: "&cZuckerstange"
|
||||
deco-lantern_post: "&eLaternenpfahl"
|
||||
deco-tree: "&2Weihnachtsbaum"
|
||||
deco-wreath: "&4Kranz"
|
||||
deco-snowman: "&fSchneemann"
|
||||
deco-lights: "&6Lichterkette"
|
||||
deco-snow_pile: "&bSchneehaufen"
|
||||
deco-placed: "&aDeko aufgestellt. &7/xmas deco undo macht es rückgängig."
|
||||
deco-undone: "&7Letzte Deko entfernt."
|
||||
deco-nothing-undo: "&cNichts zum Rückgängigmachen."
|
||||
deco-look: "&cSchau auf einen Block."
|
||||
|
||||
# Login-Serie
|
||||
streak-reward: "&6Login-Serie: Tag &e%day% &6in Folge! Belohnung (Stufe &e%cycle%&6) erhalten."
|
||||
|
||||
# Grüße
|
||||
greeting-christmas-eve: "&6Heute ist Heiligabend! &fWir wünschen allen einen schönen Abend."
|
||||
greeting-new-years-eve: "&6Heute ist Silvester! &fRutscht gut ins neue Jahr."
|
||||
present-name: "&aGeschenk"
|
||||
|
||||
# Socken
|
||||
stocking-hung: "&aDeine Socke hängt am Kamin! Am Morgen des &e%date% &aliegt etwas darin."
|
||||
stocking-already: "&7Deine Socke hängt schon. Am &e%date% &7liegt etwas darin."
|
||||
stocking-too-early: "&cDu kannst deine Socke erst ab dem &e%from% &caufhängen."
|
||||
stocking-nothing: "&cDu hast dieses Jahr keine Socke aufgehängt."
|
||||
stocking-claimed: "&7Du hast deine Socke schon geleert."
|
||||
stocking-received: "&aIn deiner Socke lag etwas für dich! Frohe Weihnachten!"
|
||||
stocking-placed: "&aDeine Socke hängt jetzt an der Wand! Am Weihnachtstag fliegt Santa vorbei und legt ein Geschenk darunter."
|
||||
stocking-place-usage: "&cSchau auf eine freie Wand (Seitenfläche eines Blocks), um die Socke aufzuhängen."
|
||||
stocking-place-limit: "&cDu kannst höchstens &e%max% &cSocken aufstellen."
|
||||
stocking-removed: "&7Socke entfernt."
|
||||
stocking-none-near: "&cIn der Nähe hängt keine Socke von dir."
|
||||
stocking-present-placed: "&aSanta hat ein Geschenk unter deine Socke gelegt! (x=%x%, y=%y%, z=%z%)"
|
||||
stocking-present-notyours: "&cDieses Geschenk gehört jemand anderem."
|
||||
stocking-present-opened: "&aDu hast dein Socken-Geschenk geöffnet. Frohe Weihnachten!"
|
||||
stocking-flight-started: "&aHo ho ho! Santa fliegt von Socke zu Socke und bringt Geschenke!"
|
||||
stocking-flight-ok: "&aSantas Socken-Flug wurde gestartet."
|
||||
stocking-flight-none: "&cKein Flug möglich: Keine aufgestellten Socken in geladenen Chunks (oder ein Flug läuft schon)."
|
||||
|
||||
# Nikolausmütze
|
||||
hat-name: "&cNikolausmütze"
|
||||
hat-lore:
|
||||
- "&7Ho ho ho!"
|
||||
hat-offer: "&aMöchtest du eine Nikolausmütze?"
|
||||
hat-offer-click: "&e[Hier klicken]"
|
||||
hat-offer-hover: "&7Gibt dir eine Nikolausmütze"
|
||||
hat-given: "&aHo ho ho! Du hast eine Nikolausmütze bekommen."
|
||||
hat-have: "&7Du hast schon eine Nikolausmütze."
|
||||
hat-season: "&cDie Nikolausmütze gibt es nur in der Weihnachtszeit."
|
||||
|
||||
# Quiz
|
||||
quiz-question: "&6&lWeihnachtsquiz! &f%question% &7(%time%s)"
|
||||
quiz-win: "&a%player% &fhat es zuerst gewusst: &e%answer%"
|
||||
quiz-timeout: "&7Niemand wusste es. Die Antwort war: &e%answer%"
|
||||
quiz-reward: "&aDeine Belohnung wurde ausgegeben."
|
||||
quiz-usage: "&cNutzung: /xmas quiz start [Nr] | skip | count"
|
||||
quiz-already: "&cEs läuft schon eine Frage."
|
||||
quiz-none: "&cEs sind keine Quizfragen in der christmas.yml."
|
||||
quiz-stopped: "&7Frage beendet."
|
||||
quiz-not-active: "&cGerade läuft keine Frage."
|
||||
|
||||
# Wunschzettel
|
||||
wish-usage: "&cNutzung: /xmas wish <Text> | list | clear"
|
||||
wish-saved: "&aDein Wunsch steht auf dem Wunschzettel! Vielleicht erfüllt ihn der Weihnachtsmann."
|
||||
wish-limit: "&cDu hast schon genug offene Wünsche. (/xmas wish list, /xmas wish clear)"
|
||||
wish-too-long: "&cDer Wunsch ist zu lang (maximal %max% Zeichen)."
|
||||
wish-empty: "&7Du hast keine offenen Wünsche."
|
||||
wish-list-header: "&aDeine offenen Wünsche:"
|
||||
wish-list-line: "&7- &f%text%"
|
||||
wish-cleared: "&7%count% Wunsch/Wünsche gelöscht."
|
||||
wish-fulfilled: "&aDein Wunsch wurde erfüllt: &e%wish%"
|
||||
wish-fulfilled-gift: "&aDein Wunsch wurde erfüllt, ein Geschenk liegt im Inventar: &e%wish%"
|
||||
wish-admin-new: "&6Neuer Wunsch von &e%player%&6. Ansehen mit /xmas wishes"
|
||||
wish-admin-done: "&aWunsch von %player% erfüllt (ohne Geschenk)."
|
||||
wish-admin-done-gift: "&aWunsch von %player% erfüllt. Geschenk: &e%item%"
|
||||
wish-admin-done-offline: "&7%player% ist offline. Der Wunsch ist erfüllt, das Geschenk wird beim nächsten Join übergeben."
|
||||
wish-admin-hold: "&cHalte das Geschenk in der Hand und klicke dann links. (Shift+Linksklick erfüllt den Wunsch ohne Geschenk.)"
|
||||
wish-admin-deleted: "&7Wunsch von %player% gelöscht."
|
||||
wish-menu-title: "Wunschzettel"
|
||||
wish-menu-item-name: "&e%player%"
|
||||
wish-menu-hint-left: "&aLinksklick: erfüllen &7(Item in der Hand wird verschenkt)"
|
||||
wish-menu-hint-shift: "&eShift+Linksklick: ohne Geschenk erfüllen"
|
||||
wish-menu-hint-right: "&cRechtsklick: löschen"
|
||||
wish-page-prev: "&7Vorherige Seite"
|
||||
wish-page-next: "&7Nächste Seite"
|
||||
wish-menu-empty: "&7Keine offenen Wünsche"
|
||||
|
||||
# Marktstände
|
||||
market-usage: "&cNutzung: /xmas market spawn <id> | remove | list"
|
||||
market-unknown: "&cUnbekannter Stand: %id%"
|
||||
market-spawned: "&aMarktstand aufgestellt."
|
||||
market-removed: "&7Marktstand entfernt."
|
||||
market-none: "&cKein Marktstand in 6 Blöcken Umkreis."
|
||||
|
||||
# Portale und Weihnachtsmarkt
|
||||
market-unset: "&7Marktpunkt aufgehoben. Portale funktionieren erst wieder, wenn ein Admin ihn neu setzt."
|
||||
market-set: "&aWeihnachtsmarkt-Punkt gesetzt. Portale führen jetzt hierher."
|
||||
portal-usage: "&cNutzung: /xmas portal create | remove | info (schau auf den Boden bzw. aufs Portal)"
|
||||
portal-created: "&aDein Portal zum Weihnachtsmarkt steht! Geh hinein und warte kurz."
|
||||
portal-removed: "&7Portal entfernt."
|
||||
portal-none: "&cDu schaust kein Portal an."
|
||||
portal-not-owner: "&cDas ist nicht dein Portal."
|
||||
portal-limit: "&cDu hast schon %limit% Portal(e)."
|
||||
portal-need: "&cFür das Portal brauchst du Schneeblöcke: &f%cost%"
|
||||
portal-no-space: "&cHier ist nicht genug Platz (5 Blöcke breit, 5 hoch, fester Boden)."
|
||||
portal-no-market: "&cDer Weihnachtsmarkt wurde noch nicht von einem Admin festgelegt. Portale funktionieren erst danach."
|
||||
portal-world: "&cIn dieser Welt kannst du kein Portal bauen."
|
||||
portal-disabled: "&cPortale sind deaktiviert."
|
||||
portal-countdown: "&bPortal: &f%s% s"
|
||||
portal-teleported: "&aWillkommen auf dem Weihnachtsmarkt!"
|
||||
portal-back-click: "&e[Zurück zum Portal]"
|
||||
portal-back-ok: "&7Du bist zurück am Portal."
|
||||
portal-back-none: "&cEs ist kein Rückweg gespeichert."
|
||||
portal-info: "&aPortal-Kosten: &f%cost% &7- Deine Portale: &e%count%/%limit%"
|
||||
@@ -0,0 +1,276 @@
|
||||
messages:
|
||||
prefix: "&c[Christmas] &f"
|
||||
no-permission: "&cYou don't have permission."
|
||||
player-only: "&cThis command is for players only."
|
||||
player-offline: "&cPlayer not found."
|
||||
player-not-found: "&cPlayer not found."
|
||||
usage: "&cUsage: /christmas <help|snow|santa|give|gifts|addhand|snowman|countdown|cookie|wichteln|wreath|snowball|deco|stocking|hat|quiz|wish|wishes|market|portal|end|reload>"
|
||||
help:
|
||||
- "&2/xmas &7- version and status"
|
||||
- "&2/xmas snow &7- toggle snow for yourself"
|
||||
- "&2/xmas countdown &7- toggle the countdown bar"
|
||||
- "&2/xmas santa start [time] [world x y z | random] | stop | teleport"
|
||||
- "&2/xmas give <name> [amount] &7- give a Christmas crystal"
|
||||
- "&2/xmas gifts &7- spawn presents under every tree"
|
||||
- "&2/xmas addhand &7- add the held item as a gift"
|
||||
- "&2/xmas snowman spawn [stand|walk] [name] | remove &7- decorative snowman (standing/walking)"
|
||||
- "&2/xmas cookie list | give <player> <id> [amount]"
|
||||
- "&2/xmas wichteln join|leave|who|gift|status &7- secret santa"
|
||||
- "&2/xmas wichteln open|draw|deliver|reset &7- (admin)"
|
||||
- "&2/xmas wreath create|remove &7- advent wreath"
|
||||
- "&2/xmas snowball arena [radius] | start [time] | stop | top"
|
||||
- "&2/xmas deco [undo] &7- decoration menu"
|
||||
- "&2/xmas stocking [place | remove | flight] &7- hang up / open your stocking / place it on a wall"
|
||||
- "&2/xmas hat &7- Santa hat"
|
||||
- "&2/xmas wish <text> | list | clear &7- wish list"
|
||||
- "&2/xmas wishes &7- view wishes (admin)"
|
||||
- "&2/xmas quiz start [no.] | skip &7- Christmas quiz (admin)"
|
||||
- "&2/xmas market spawn <id> | remove | list &7- market stalls (admin)"
|
||||
- "&2/xmas portal create|remove|info &7- portal to the Christmas market"
|
||||
- "&2/xmas market set|unset|tp|back &7- market point (admin) / way back"
|
||||
- "&2/xmas end &7- end the event"
|
||||
- "&2/xmas reload &7- reload configuration"
|
||||
- "&2/cracker list | get [id] [amount] | give <player> [id] [amount]"
|
||||
info: "&aX-Mas module active. Status: &e%status% &7- Trees: &e%trees%"
|
||||
status-active: "ACTIVE"
|
||||
status-ended: "ENDED"
|
||||
reloaded: "&aChristmas configuration reloaded."
|
||||
past-day-restricted: "&cYou can't open past days any more."
|
||||
calendar-test-on: "&aCalendar test mode &eenabled&a: you can open any day."
|
||||
calendar-test-off: "&7Calendar test mode disabled."
|
||||
calendar-claimed-broadcast: "&e%player% &aclaimed the gift of calendar day &e%day%&a."
|
||||
calendar-claimed-self: "&aYou claimed the gift of day &e%day%&a."
|
||||
plugin-enabled: "&aMerry Christmas and a happy new year!"
|
||||
timeout: "&cThe holidays are over! See you next year!"
|
||||
final-wish: "&6Happy New Year, my friends!"
|
||||
end-set: "&7Event ended. Players can take down their trees and get their resources back."
|
||||
gifts-spawned: "&cHo! &2Ho! &cHo! &3Time to check the presents!"
|
||||
addhand-ok: "&aItem added as a gift."
|
||||
addhand-empty: "&cYou aren't holding an item."
|
||||
crystal-given: "&aYou received &e%amount%x Christmas crystal&a."
|
||||
crystal-gave: "&a%player% received %amount% crystal(s)."
|
||||
|
||||
# Snow
|
||||
snow-on: "&aSnow is now &eenabled &afor you."
|
||||
snow-off: "&7Snow is now &cdisabled &7for you."
|
||||
snow-disabled-global: "&cSnow is disabled on this server."
|
||||
|
||||
# Snowmen
|
||||
snowman-usage: "&cUsage: /christmas snowman spawn [stand|walk] [name] | remove"
|
||||
snowman-spawned: "&aDecorative snowman placed."
|
||||
snowman-removed: "&7Decorative snowman removed."
|
||||
snowman-none: "&cNo decorative snowman within 6 blocks."
|
||||
snowman-built: "&aYour snowman will give you a gift now and then! (max. %max% snowmen)"
|
||||
snowman-limit: "&cYou already have %max% snowmen, this one won't give gifts."
|
||||
|
||||
# Santa
|
||||
santa-started: "&aThe Santa Claus event has started! Santa is in &e%world% &aat &ex=%x%, y=%y%, z=%z% &aand waits &e%duration%&a. (/christmas santa teleport)"
|
||||
santa-started-sender: "&aYou started the Santa Claus event for &e%duration%&a."
|
||||
santa-dropped: "&aSanta has just dropped a gift!"
|
||||
santa-stopped: "&7Santa has moved on."
|
||||
santa-already: "&cSanta is already active."
|
||||
santa-not-active: "&cSanta is not active right now."
|
||||
santa-no-locations: "&cNo Santa locations are set in christmas.yml."
|
||||
santa-usage: "&cUsage: /christmas santa start [time] [world x y z | random] | stop | teleport"
|
||||
santa-bossbar: "&cSanta Claus &f- &e%time%s left"
|
||||
santa-teleported: "&aYou were teleported to Santa."
|
||||
|
||||
# Crackers
|
||||
cracker-given: "&aYou received &e%amount%x %id%&a."
|
||||
cracker-gave: "&a%player% received %amount%x %id%."
|
||||
cracker-unknown: "&cUnknown cracker: %id%"
|
||||
cracker-opened: "&6Bang! &fYou opened a cracker."
|
||||
cracker-usage: "&cUsage: /cracker list | get [id] [amount] | give <player> [id] [amount]"
|
||||
|
||||
# Magic tree
|
||||
tree:
|
||||
grow-lvl-ready: "&aWoosh! Right-click the tree to let it grow!"
|
||||
grow-req-list-title: "&eRequired (throw the items next to the tree):"
|
||||
grow-not-enough-place: "&cNot enough space to grow here."
|
||||
grow-lvl-max: "&6This tree has reached its maximum level!"
|
||||
grow-fed: "&7The tree absorbed &e%amount%x %item%&7."
|
||||
tree-limit: "&cDon't be greedy! (tree limit reached)"
|
||||
tree-planted: "&aYour Magic Tree was planted! Throw the required items next to it."
|
||||
tree-disabled: "&cMagic Trees are ended/disabled."
|
||||
destroy-sapling: "&cThis miracle won't come back. Really destroy it? Break it again to confirm."
|
||||
destroy-leaves-santa: "&cSanta sees everything."
|
||||
destroy-leaves-tut: "&7To destroy the tree, cut down the trunk."
|
||||
destroy-warning: "&cAre you sure? Your progress will be lost, no resources back. Break it again to confirm."
|
||||
destroy-fail-owner: "&cThis is not your tree!"
|
||||
destroy-complete: "&4You monster!"
|
||||
destroy-refund: "&aThe tree was taken down, your resources were returned."
|
||||
not-owner: "&cThis is not your tree."
|
||||
info: "&aMagic Tree of &e%owner%&a, level: &e%level%"
|
||||
gift:
|
||||
unluck-message: "&7Maybe Santa put you on his naughty list. Better luck next time!"
|
||||
opened: "&aYou opened a gift!"
|
||||
|
||||
# Countdown
|
||||
countdown-bossbar: "&cOnly &e%days% &cdays until Christmas Eve"
|
||||
countdown-bossbar-one: "&cOnly &e1 &cday until Christmas Eve"
|
||||
countdown-not-yet: "&7%days% days until Christmas Eve. The display starts in &e%in% &7day(s)."
|
||||
countdown-today: "&aIt's Christmas Eve! Merry Christmas!"
|
||||
countdown-on: "&aCountdown bar enabled."
|
||||
countdown-off: "&7Countdown bar disabled."
|
||||
|
||||
# Cookies
|
||||
cookie-usage: "&cUsage: /xmas cookie list | give <player> <id> [amount]"
|
||||
cookie-unknown: "&cUnknown cookie: %id%"
|
||||
cookie-given: "&aYou received &e%amount%x %id%&a."
|
||||
|
||||
# Secret Santa
|
||||
wi-open: "&aSecret Santa is open! Join with &e/xmas wichteln join&a."
|
||||
wi-joined: "&aYou joined the Secret Santa!"
|
||||
wi-already: "&7You are already signed up."
|
||||
wi-left: "&7You left the Secret Santa."
|
||||
wi-not-joined: "&cYou are not part of the Secret Santa."
|
||||
wi-wrong-state: "&cThat is not possible right now (wrong phase)."
|
||||
wi-too-few: "&cAt least 3 participants are needed."
|
||||
wi-drawn: "&aDrawn! Everyone has a partner. Check with &e/xmas wichteln who &ayour gift receiver."
|
||||
wi-who: "&aYou give a gift to: &e%name%"
|
||||
wi-gift-empty: "&cHold the gift in your hand."
|
||||
wi-gift-saved: "&aYour gift for &e%name% &ais saved. (Hold another item and repeat to replace it.)"
|
||||
wi-delivered: "&6The Secret Santa gifts were handed out! (&e%count% &6gifts)"
|
||||
wi-received: "&aYou received a Secret Santa gift!"
|
||||
wi-received-from: "&aYou received a Secret Santa gift from &e%from%&a!"
|
||||
wi-reset: "&7Secret Santa reset."
|
||||
wi-status: "&aSecret Santa: &e%state% &7- participants: &e%count% &7- gifts: &e%gifts%"
|
||||
|
||||
# Advent wreath
|
||||
wreath-created: "&aAdvent wreath built."
|
||||
wreath-removed: "&7Advent wreath removed."
|
||||
wreath-none: "&cNo advent wreath nearby."
|
||||
wreath-usage: "&cUsage: /xmas wreath create | remove (look at the ground)"
|
||||
wreath-advent: "&6Today is the &e%n%. Advent&6! &e%n% &6candles are burning on the advent wreath."
|
||||
|
||||
# Snowball game
|
||||
sb-usage: "&cUsage: /xmas snowball arena [radius] | start [time] | stop | top"
|
||||
sb-arena-set: "&aSnowball arena set (radius &e%radius%&a)."
|
||||
sb-no-arena: "&cNo arena set yet (/xmas snowball arena)."
|
||||
sb-already: "&cThe snowball game is already running."
|
||||
sb-not-active: "&cThe snowball game is not running."
|
||||
sb-started: "&b&lSnowball fight! &7For &e%time% &7in the arena (&e%world% %x% %z%&7). Throw snowballs at other players!"
|
||||
sb-ended: "&b&lSnowball fight over! &7The best:"
|
||||
sb-top-line: "&e%rank%. &f%player% &7- &a%points% points"
|
||||
sb-no-scores: "&7No points yet."
|
||||
sb-actionbar: "&fPoints: &a%points% &7| Rank &e%rank% &7| &c%time%s &7left"
|
||||
sb-reward: "&aCongratulations on place %rank%! Your reward was handed out."
|
||||
|
||||
# Decoration
|
||||
deco-menu-title: "Christmas decoration"
|
||||
deco-click-hint: "&7Click to place it where you are looking."
|
||||
deco-present: "&aGift"
|
||||
deco-candy_cane: "&cCandy cane"
|
||||
deco-lantern_post: "&eLantern post"
|
||||
deco-tree: "&2Christmas tree"
|
||||
deco-wreath: "&4Wreath"
|
||||
deco-snowman: "&fSnowman"
|
||||
deco-lights: "&6String lights"
|
||||
deco-snow_pile: "&bSnow pile"
|
||||
deco-placed: "&aDecoration placed. &7/xmas deco undo reverts it."
|
||||
deco-undone: "&7Last decoration removed."
|
||||
deco-nothing-undo: "&cNothing to undo."
|
||||
deco-look: "&cLook at a block."
|
||||
|
||||
# Login streak
|
||||
streak-reward: "&6Login streak: day &e%day% &6in a row! Reward (step &e%cycle%&6) received."
|
||||
|
||||
# Greetings
|
||||
greeting-christmas-eve: "&6Today is Christmas Eve! &fWe wish everyone a lovely evening."
|
||||
greeting-new-years-eve: "&6Today is New Year's Eve! &fHave a great start into the new year."
|
||||
present-name: "&aGift"
|
||||
|
||||
# Stockings
|
||||
stocking-hung: "&aYour stocking is hanging by the fireplace! On &e%date% &asomething will be inside."
|
||||
stocking-already: "&7Your stocking is already hanging. On &e%date% &7there will be something inside."
|
||||
stocking-too-early: "&cYou can hang your stocking from &e%from% &con."
|
||||
stocking-nothing: "&cYou didn't hang up a stocking this year."
|
||||
stocking-claimed: "&7You already emptied your stocking."
|
||||
stocking-received: "&aThere was something in your stocking! Merry Christmas!"
|
||||
stocking-placed: "&aYour stocking is now hanging on the wall! On Christmas Santa will fly by and leave a gift below it."
|
||||
stocking-place-usage: "&cLook at a free wall (the side of a block) to hang your stocking."
|
||||
stocking-place-limit: "&cYou can place at most &e%max% &cstockings."
|
||||
stocking-removed: "&7Stocking removed."
|
||||
stocking-none-near: "&cThere is none of your stockings nearby."
|
||||
stocking-present-placed: "&aSanta left a gift below your stocking! (x=%x%, y=%y%, z=%z%)"
|
||||
stocking-present-notyours: "&cThis gift belongs to someone else."
|
||||
stocking-present-opened: "&aYou opened your stocking gift. Merry Christmas!"
|
||||
stocking-flight-started: "&aHo ho ho! Santa is flying from stocking to stocking, bringing gifts!"
|
||||
stocking-flight-ok: "&aSanta's stocking flight has started."
|
||||
stocking-flight-none: "&cNo flight possible: no placed stockings in loaded chunks (or a flight is already running)."
|
||||
|
||||
# Santa hat
|
||||
hat-name: "&cSanta hat"
|
||||
hat-lore:
|
||||
- "&7Ho ho ho!"
|
||||
hat-offer: "&aWould you like a Santa hat?"
|
||||
hat-offer-click: "&e[Click here]"
|
||||
hat-offer-hover: "&7Gives you a Santa hat"
|
||||
hat-given: "&aHo ho ho! You got a Santa hat."
|
||||
hat-have: "&7You already have a Santa hat."
|
||||
hat-season: "&cThe Santa hat is only available during the holiday season."
|
||||
|
||||
# Quiz
|
||||
quiz-question: "&6&lChristmas quiz! &f%question% &7(%time%s)"
|
||||
quiz-win: "&a%player% &fknew it first: &e%answer%"
|
||||
quiz-timeout: "&7Nobody knew it. The answer was: &e%answer%"
|
||||
quiz-reward: "&aYour reward was handed out."
|
||||
quiz-usage: "&cUsage: /xmas quiz start [no.] | skip | count"
|
||||
quiz-already: "&cA question is already running."
|
||||
quiz-none: "&cThere are no quiz questions in christmas.yml."
|
||||
quiz-stopped: "&7Question ended."
|
||||
quiz-not-active: "&cNo question is running."
|
||||
|
||||
# Wish list
|
||||
wish-usage: "&cUsage: /xmas wish <text> | list | clear"
|
||||
wish-saved: "&aYour wish is on the wish list! Maybe Santa will fulfil it."
|
||||
wish-limit: "&cYou already have enough open wishes. (/xmas wish list, /xmas wish clear)"
|
||||
wish-too-long: "&cThe wish is too long (max. %max% characters)."
|
||||
wish-empty: "&7You have no open wishes."
|
||||
wish-list-header: "&aYour open wishes:"
|
||||
wish-list-line: "&7- &f%text%"
|
||||
wish-cleared: "&7%count% wish(es) deleted."
|
||||
wish-fulfilled: "&aYour wish was fulfilled: &e%wish%"
|
||||
wish-fulfilled-gift: "&aYour wish was fulfilled, a gift is in your inventory: &e%wish%"
|
||||
wish-admin-new: "&6New wish from &e%player%&6. View with /xmas wishes"
|
||||
wish-admin-done: "&aWish of %player% fulfilled (without a gift)."
|
||||
wish-admin-done-gift: "&aWish of %player% fulfilled. Gift: &e%item%"
|
||||
wish-admin-done-offline: "&7%player% is offline. The wish is fulfilled, the gift will be handed over on their next join."
|
||||
wish-admin-hold: "&cHold the gift in your hand and then left-click. (Shift+left-click fulfils the wish without a gift.)"
|
||||
wish-admin-deleted: "&7Wish of %player% deleted."
|
||||
wish-menu-title: "Wish list"
|
||||
wish-menu-item-name: "&e%player%"
|
||||
wish-menu-hint-left: "&aLeft-click: fulfil &7(the item in your hand is given away)"
|
||||
wish-menu-hint-shift: "&eShift+left-click: fulfil without a gift"
|
||||
wish-menu-hint-right: "&cRight-click: delete"
|
||||
wish-page-prev: "&7Previous page"
|
||||
wish-page-next: "&7Next page"
|
||||
wish-menu-empty: "&7No open wishes"
|
||||
|
||||
# Market stalls
|
||||
market-usage: "&cUsage: /xmas market spawn <id> | remove | list"
|
||||
market-unknown: "&cUnknown stall: %id%"
|
||||
market-spawned: "&aMarket stall placed."
|
||||
market-removed: "&7Market stall removed."
|
||||
market-none: "&cNo market stall within 6 blocks."
|
||||
|
||||
# Portals and Christmas market
|
||||
market-unset: "&7Market point removed. Portals work again once an admin sets it anew."
|
||||
market-set: "&aChristmas market point set. Portals now lead here."
|
||||
portal-usage: "&cUsage: /xmas portal create | remove | info (look at the ground / at the portal)"
|
||||
portal-created: "&aYour portal to the Christmas market is ready! Step in and wait a moment."
|
||||
portal-removed: "&7Portal removed."
|
||||
portal-none: "&cYou are not looking at a portal."
|
||||
portal-not-owner: "&cThis is not your portal."
|
||||
portal-limit: "&cYou already have %limit% portal(s)."
|
||||
portal-need: "&cYou need snow blocks for the portal: &f%cost%"
|
||||
portal-no-space: "&cNot enough space here (5 blocks wide, 5 high, solid ground)."
|
||||
portal-no-market: "&cThe Christmas market has not been set by an admin yet. Portals only work afterwards."
|
||||
portal-world: "&cYou can't build a portal in this world."
|
||||
portal-disabled: "&cPortals are disabled."
|
||||
portal-countdown: "&bPortal: &f%s% s"
|
||||
portal-teleported: "&aWelcome to the Christmas market!"
|
||||
portal-back-click: "&e[Back to the portal]"
|
||||
portal-back-ok: "&7You are back at the portal."
|
||||
portal-back-none: "&cNo way back is saved."
|
||||
portal-info: "&aPortal cost: &f%cost% &7- Your portals: &e%count%/%limit%"
|
||||
@@ -8,6 +8,13 @@ general:
|
||||
# Verfügbare Optionen: 'de', 'en'
|
||||
language: "de"
|
||||
|
||||
# Zeitzone für das Datum, z.B. "Europe/Berlin". Leer = Zeitzone des Servers.
|
||||
timezone: ""
|
||||
|
||||
# Nur für Testzwecke: Simuliert einen Tag beim Start (0 = echtes Datum verwenden)
|
||||
# Mit /ak admin test <Tag> kann der Tag zur Laufzeit geändert werden (wird nicht gespeichert).
|
||||
test_day: 0
|
||||
|
||||
# --- Kalender-Einstellungen ---
|
||||
calendar:
|
||||
# Soll jeder Spieler seinen eigenen, individuellen Kalender haben?
|
||||
@@ -19,6 +26,15 @@ calendar:
|
||||
# Dieser Text ist ein Schlüssel, der in der messages.yml übersetzt wird.
|
||||
gui_title_key: "adventskalender.gui_title"
|
||||
|
||||
# Datenbank für Bungeecord Nutzung
|
||||
mysql:
|
||||
enable: false
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
database: "adventskalender"
|
||||
user: "root"
|
||||
password: ""
|
||||
|
||||
# --- Belohnungen für die Tage 1 bis 24 ---
|
||||
# Hier definierst du die Items für jeden Tag.
|
||||
# 'material': Der Materialname des Items (z.B. DIAMOND, ELYTRA).
|
||||
@@ -196,4 +212,275 @@ rewards:
|
||||
material: FIREWORK_ROCKET
|
||||
amount: 64
|
||||
key: "rewards.day_24"
|
||||
enchantments: []
|
||||
enchantments: []
|
||||
|
||||
# =============================================================================
|
||||
# Events (Kalender)
|
||||
# =============================================================================
|
||||
# Jedes Event hat einen eigenen Kalender, eigene Belohnungen und eigene Daten.
|
||||
# Öffnen mit /ak (laufendes Event) oder /ak <event>.
|
||||
#
|
||||
# enabled: Event an/aus
|
||||
# days: Anzahl der Türchen (1-36)
|
||||
# start: Startdatum im Format MM-dd (z.B. "10-01") ODER relativ zu Ostersonntag
|
||||
# (z.B. "easter-13" = 13 Tage vor Ostersonntag, "easter+1" = Ostermontag).
|
||||
# Das Datum wird jedes Jahr automatisch berechnet.
|
||||
# grace_days: Wie viele Tage nach dem letzten Türchen der Kalender noch geöffnet bleibt.
|
||||
# global: (optional) true/false überschreibt calendar.use_global_calendar nur für dieses Event.
|
||||
# closed_material / opened_material: Items für geschlossene bzw. geöffnete Türchen.
|
||||
# Die Stackgröße zeigt den Tag an. (Das Advent-Event nutzt eigene Köpfe.)
|
||||
# closed_texture / opened_texture: (optional) Textur-URL eines Kopfes statt der Materialien.
|
||||
#
|
||||
# Die Texte (Titel, Namen, Lore) stehen in der Sprachdatei unter events.<id>.
|
||||
# Die Belohnungen des Advent-Events stehen weiterhin oben unter "rewards".
|
||||
events:
|
||||
advent:
|
||||
enabled: true
|
||||
days: 24
|
||||
start: "12-01"
|
||||
grace_days: 7
|
||||
|
||||
halloween:
|
||||
enabled: true
|
||||
days: 31
|
||||
start: "10-01"
|
||||
grace_days: 0
|
||||
closed_material: PUMPKIN
|
||||
opened_material: JACK_O_LANTERN
|
||||
rewards:
|
||||
1:
|
||||
material: PUMPKIN_PIE
|
||||
amount: 8
|
||||
enchantments: []
|
||||
2:
|
||||
material: JACK_O_LANTERN
|
||||
amount: 4
|
||||
enchantments: []
|
||||
3:
|
||||
material: BONE
|
||||
amount: 16
|
||||
enchantments: []
|
||||
4:
|
||||
material: SPIDER_EYE
|
||||
amount: 8
|
||||
enchantments: []
|
||||
5:
|
||||
material: COBWEB
|
||||
amount: 16
|
||||
enchantments: []
|
||||
6:
|
||||
material: BLACK_CANDLE
|
||||
amount: 4
|
||||
enchantments: []
|
||||
7:
|
||||
material: SOUL_TORCH
|
||||
amount: 16
|
||||
enchantments: []
|
||||
8:
|
||||
material: FERMENTED_SPIDER_EYE
|
||||
amount: 4
|
||||
enchantments: []
|
||||
9:
|
||||
material: GHAST_TEAR
|
||||
amount: 2
|
||||
enchantments: []
|
||||
10:
|
||||
material: SOUL_LANTERN
|
||||
amount: 4
|
||||
enchantments: []
|
||||
11:
|
||||
material: PHANTOM_MEMBRANE
|
||||
amount: 4
|
||||
enchantments: []
|
||||
12:
|
||||
material: REDSTONE
|
||||
amount: 32
|
||||
enchantments: []
|
||||
13:
|
||||
material: GLOWSTONE_DUST
|
||||
amount: 16
|
||||
enchantments: []
|
||||
14:
|
||||
material: BLAZE_ROD
|
||||
amount: 4
|
||||
enchantments: []
|
||||
15:
|
||||
material: ENDER_PEARL
|
||||
amount: 4
|
||||
enchantments: []
|
||||
16:
|
||||
material: SKELETON_SKULL
|
||||
amount: 1
|
||||
enchantments: []
|
||||
17:
|
||||
material: ZOMBIE_HEAD
|
||||
amount: 1
|
||||
enchantments: []
|
||||
18:
|
||||
material: CREEPER_HEAD
|
||||
amount: 1
|
||||
enchantments: []
|
||||
19:
|
||||
material: GOLDEN_APPLE
|
||||
amount: 3
|
||||
enchantments: []
|
||||
20:
|
||||
material: DIAMOND_SWORD
|
||||
amount: 1
|
||||
enchantments:
|
||||
- type: SMITE
|
||||
level: 5
|
||||
- type: UNBREAKING
|
||||
level: 3
|
||||
21:
|
||||
material: BOW
|
||||
amount: 1
|
||||
enchantments:
|
||||
- type: POWER
|
||||
level: 4
|
||||
- type: INFINITY
|
||||
level: 1
|
||||
22:
|
||||
material: NETHERITE_SCRAP
|
||||
amount: 4
|
||||
enchantments: []
|
||||
23:
|
||||
material: WITHER_ROSE
|
||||
amount: 2
|
||||
enchantments: []
|
||||
24:
|
||||
material: EXPERIENCE_BOTTLE
|
||||
amount: 16
|
||||
enchantments: []
|
||||
25:
|
||||
material: TOTEM_OF_UNDYING
|
||||
amount: 1
|
||||
enchantments: []
|
||||
26:
|
||||
material: NAME_TAG
|
||||
amount: 2
|
||||
enchantments: []
|
||||
27:
|
||||
material: MUSIC_DISC_13
|
||||
amount: 1
|
||||
enchantments: []
|
||||
28:
|
||||
material: DIAMOND
|
||||
amount: 8
|
||||
enchantments: []
|
||||
29:
|
||||
material: WITHER_SKELETON_SKULL
|
||||
amount: 1
|
||||
enchantments: []
|
||||
30:
|
||||
material: NETHER_STAR
|
||||
amount: 1
|
||||
enchantments: []
|
||||
31:
|
||||
material: NETHERITE_INGOT
|
||||
amount: 3
|
||||
enchantments: []
|
||||
|
||||
easter:
|
||||
enabled: true
|
||||
days: 14
|
||||
start: "easter-13"
|
||||
grace_days: 7
|
||||
closed_material: EGG
|
||||
opened_material: TURTLE_EGG
|
||||
rewards:
|
||||
1:
|
||||
material: EGG
|
||||
amount: 16
|
||||
enchantments: []
|
||||
2:
|
||||
material: CARROT
|
||||
amount: 16
|
||||
enchantments: []
|
||||
3:
|
||||
material: GOLDEN_CARROT
|
||||
amount: 8
|
||||
enchantments: []
|
||||
4:
|
||||
material: RABBIT_FOOT
|
||||
amount: 2
|
||||
enchantments: []
|
||||
5:
|
||||
material: COOKIE
|
||||
amount: 16
|
||||
enchantments: []
|
||||
6:
|
||||
material: HONEY_BOTTLE
|
||||
amount: 4
|
||||
enchantments: []
|
||||
7:
|
||||
material: CAKE
|
||||
amount: 1
|
||||
enchantments: []
|
||||
8:
|
||||
material: PINK_TULIP
|
||||
amount: 8
|
||||
enchantments: []
|
||||
9:
|
||||
material: SADDLE
|
||||
amount: 1
|
||||
enchantments: []
|
||||
10:
|
||||
material: GOLDEN_APPLE
|
||||
amount: 2
|
||||
enchantments: []
|
||||
11:
|
||||
material: FIREWORK_ROCKET
|
||||
amount: 16
|
||||
enchantments: []
|
||||
12:
|
||||
material: DIAMOND
|
||||
amount: 5
|
||||
enchantments: []
|
||||
13:
|
||||
material: ENCHANTED_GOLDEN_APPLE
|
||||
amount: 1
|
||||
enchantments: []
|
||||
14:
|
||||
material: GOLD_BLOCK
|
||||
amount: 4
|
||||
enchantments: []
|
||||
|
||||
# =============================================================================
|
||||
# Geburtstage
|
||||
# =============================================================================
|
||||
# Spieler tragen ihren Geburtstag (ohne Jahr) mit /ak birthday set <TT.MM> ein und holen
|
||||
# ihr Geschenk mit /ak birthday claim ab. Einmal pro Jahr, am Geburtstag und bis zu
|
||||
# grace_days Tage danach.
|
||||
birthday:
|
||||
enabled: true
|
||||
# Wie viele Tage nach dem Geburtstag das Geschenk noch abholbar ist.
|
||||
grace_days: 2
|
||||
# Dürfen Spieler ihren Geburtstag nach dem Eintragen selbst ändern/löschen?
|
||||
# false = nur Admins (/ak admin birthday set|remove <Spieler>), verhindert Mehrfach-Abholung.
|
||||
allow_change: false
|
||||
# Nachricht an alle Spieler, wenn jemand sein Geschenk abholt.
|
||||
broadcast: true
|
||||
# Konsolenbefehle, die zusätzlich ausgeführt werden (%player% = Spielername), z.B. "eco give %player% 500"
|
||||
commands: []
|
||||
rewards:
|
||||
1:
|
||||
material: CAKE
|
||||
amount: 1
|
||||
enchantments: []
|
||||
2:
|
||||
material: COOKIE
|
||||
amount: 16
|
||||
enchantments: []
|
||||
3:
|
||||
material: GOLDEN_APPLE
|
||||
amount: 3
|
||||
enchantments: []
|
||||
4:
|
||||
material: DIAMOND
|
||||
amount: 5
|
||||
enchantments: []
|
||||
5:
|
||||
material: FIREWORK_ROCKET
|
||||
amount: 16
|
||||
enchantments: []
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
# =============================================================================
|
||||
# Adventskalender Plugin - Deutsche Sprachdatei
|
||||
# Adventskalender Plugin - Deutsche Sprachdatei (v2.0 - 2026)
|
||||
# =============================================================================
|
||||
|
||||
# --- Prefix ---
|
||||
# Dieser Prefix wird vor fast jeder Nachricht im Chat angezeigt.
|
||||
prefix: "&6[Adventskalender] &r"
|
||||
|
||||
# --- GUI-Texte ---
|
||||
gui:
|
||||
# Der Titel des Adventskalender-Inventars.
|
||||
title: "&6✦ Adventskalender 2024 ✦"
|
||||
title: "&6✦ Adventskalender 2026 ✦"
|
||||
default_name: "&6Türchen #%day%"
|
||||
click_open: "&8» &eKlicke zum Öffnen!"
|
||||
locked_name: "&c&lTag #%day%"
|
||||
locked_status: "&8» &7Noch verschlossen"
|
||||
locked_hint: "&7Komme am %date% wieder!"
|
||||
claimed_status: "&8» &6Bereits abgeholt"
|
||||
claimed_hint: "&aDu hast dieses Geschenk bereits geöffnet! ✔"
|
||||
|
||||
# --- Nachrichten an Spieler ---
|
||||
# Verfügbare Platzhalter:
|
||||
@@ -18,90 +24,284 @@ gui:
|
||||
messages:
|
||||
no_permission: "&cDafür hast du keine Berechtigung."
|
||||
only_player: "&cDieser Befehl kann nur von einem Spieler ausgeführt werden."
|
||||
inventory_full: "&cDein Inventar war voll! Das Item wurde auf den Boden geworfen."
|
||||
reward_received: "&aDu hast dein Geschenk für Tag %day% erhalten!"
|
||||
day_not_available: "&cDieses Geschenk kannst du (noch) nicht öffnen."
|
||||
day_already_claimed: "&7Du hast dein Geschenk für diesen Tag bereits geholt."
|
||||
inventory_full: "&cDein Inventar ist voll! Dein Geschenk wurde vor deine Füße geworfen."
|
||||
reward_received: "&aGlückwunsch! Du hast dein Geschenk für Tag &e%day% &aerhalten!"
|
||||
day_not_available: "&cDieses Türchen ist noch fest verschlossen. Gedulde dich noch etwas!"
|
||||
day_already_claimed: "&7Du hast dieses Geschenk bereits abgeholt."
|
||||
global_already_claimed: "&cDieses Geschenk wurde heute bereits von jemand anderem abgeholt!"
|
||||
no_events: "&cEs ist derzeit kein Kalender aktiv."
|
||||
unknown_event: "&cUnbekanntes Event '%event%'. Verfügbar: &e%events%"
|
||||
db_error: "&cDie Datenbank ist gerade nicht erreichbar. Bitte versuche es später erneut."
|
||||
reward_error: "&cDie Belohnung für Tag %day% ist falsch konfiguriert. Bitte melde das einem Admin."
|
||||
|
||||
# --- Admin-Nachrichten ---
|
||||
admin:
|
||||
reload_success: "&aKonfiguration und Sprachdateien wurden neu geladen."
|
||||
all_events: "alle Events"
|
||||
reload_success: "&aKonfiguration und Sprachdateien wurden erfolgreich neu geladen."
|
||||
test_mode_on: "&e[Admin] &7Test-Modus (%event%): Tag &6%day% &7gesetzt."
|
||||
test_mode_off: "&e[Admin] &7Test-Modus (%event%) deaktiviert. Es wird wieder das reale Datum genutzt."
|
||||
player_not_found: "&cDer Spieler '%player%' wurde nicht gefunden."
|
||||
invalid_day: "&cUngültiger Tag. Bitte wähle eine Zahl zwischen 1 und 24."
|
||||
open_success: "&aDu hast für %player% das Türchen für Tag %day% geöffnet."
|
||||
reset_day_global: "&a[Admin] &7Tag &6%day% &7global zurückgesetzt (%event%)."
|
||||
reset_day_player: "&a[Admin] &7Tag &6%day% &7für &e%player% &7zurückgesetzt (%event%)."
|
||||
reset_all: "&a[Admin] &7Kompletter Reset durchgeführt (%event%)."
|
||||
reset_player: "&a[Admin] &7Daten für &e%player% &7gelöscht (%event%)."
|
||||
reset_offline: "&a[Admin] &7Offline-Daten für &e%player% &7gelöscht (%event%)."
|
||||
help:
|
||||
- "&8&m---&r &6Advent Admin &8&m---"
|
||||
- "&e/ak admin reload &7- Config neu laden"
|
||||
- "&e/ak admin test [event] <Tag/reset> &7- Datum simulieren"
|
||||
- "&e/ak admin resetday [event] <global/Spieler> <Tag> &7- Tag löschen"
|
||||
- "&e/ak admin reset [event] <global/Spieler> &7- Alles löschen"
|
||||
- "&7Ohne [event] gilt der Befehl für alle Events."
|
||||
- "&e/ak admin birthday <set|remove|reset> <Spieler> [TT.MM] &7- Geburtstage verwalten"
|
||||
|
||||
# --- Namen und Beschreibungen für die Belohnungs-Items ---
|
||||
# Die Schlüssel hier (z.B. 'day_1') müssen mit den 'keys' in der config.yml übereinstimmen.
|
||||
# --- Belohnungs-Texte im GUI (Advent) ---
|
||||
# Diese Schlüssel werden vom AdventInventory genutzt, um die Items im Menü zu benennen.
|
||||
rewards:
|
||||
day_1:
|
||||
name: "&bErste Diamanten!"
|
||||
name: "&bTag 1: Erste Diamanten!"
|
||||
lore: ["&7Ein kleiner Vorgeschmack auf den Reichtum...", "&7Viel Spaß damit!"]
|
||||
day_2:
|
||||
name: "&6Goldene Äpfel"
|
||||
name: "&6Tag 2: Goldene Äpfel"
|
||||
lore: ["&7Für den Fall, dass es mal eng wird..."]
|
||||
day_3:
|
||||
name: "&cSchwert des Helden"
|
||||
name: "&cTag 3: Schwert des Helden"
|
||||
lore: ["&7Ein mächtiges Schwert, um Monster zu jagen."]
|
||||
day_4:
|
||||
name: "&aFlügel der Freiheit"
|
||||
name: "&aTag 4: Flügel der Freiheit"
|
||||
lore: ["&7Sei frei wie ein Adler!"]
|
||||
day_5:
|
||||
name: "&8Ein Hauch von Netherit"
|
||||
name: "&8Tag 5: Ein Hauch von Netherit"
|
||||
lore: ["&7Sehr wertvoll und selten."]
|
||||
day_6:
|
||||
name: "&6Göttlicher Apfel"
|
||||
name: "&6Tag 6: Göttlicher Apfel"
|
||||
lore: ["&7Ein Geschenk der Götter... oder des Admins."]
|
||||
day_7:
|
||||
name: "&eBergbau-Meister"
|
||||
name: "&eTag 7: Bergbau-Meister"
|
||||
lore: ["&7Für die größten Schätze der Welt."]
|
||||
day_8:
|
||||
name: "&dTotem des Unvergänglichen"
|
||||
name: "&dTag 8: Totem des Unvergänglichen"
|
||||
lore: ["&7Ein kleines Extra-Leben."]
|
||||
day_9:
|
||||
name: "&5Stern des Nethers"
|
||||
name: "&5Tag 9: Stern des Nethers"
|
||||
lore: ["&7Der Kern eines mächtigen Wesens."]
|
||||
day_10:
|
||||
name: "&9Magische Truhe"
|
||||
name: "&9Tag 10: Magische Truhen"
|
||||
lore: ["&7Hält all deine Schätze sicher."]
|
||||
day_11:
|
||||
name: "&3Stachel des Ozeans"
|
||||
name: "&3Tag 11: Stachel des Ozeans"
|
||||
lore: ["&7Die Waffe der Wächter."]
|
||||
day_12:
|
||||
name: "&7Helm der Weisheit"
|
||||
name: "&7Tag 12: Helm der Weisheit"
|
||||
lore: ["&7Schützt deinen Kopf."]
|
||||
day_13:
|
||||
name: "&fSattel für ein treues Tier"
|
||||
name: "&fTag 13: Sattel"
|
||||
lore: ["&7Für dein nächstes Abenteuer zu Pferd."]
|
||||
day_14:
|
||||
name: "&eNamensschilder"
|
||||
name: "&eTag 14: Namensschilder"
|
||||
lore: ["&7Gib deinem Lieblings-Wolf einen Namen!"]
|
||||
day_15:
|
||||
name: "&aSchallplatte - cat"
|
||||
name: "&aTag 15: Schallplatte - cat"
|
||||
lore: ["&7Relaxende Musik für eine gemütliche Zeit."]
|
||||
day_16:
|
||||
name: "&5Endkristall"
|
||||
name: "&5Tag 16: Endkristalle"
|
||||
lore: ["&7Mächtig und gefährlich. Mit Vorsicht genießen!"]
|
||||
day_17:
|
||||
name: "&7Brustpanzer der Unzerstörbarkeit"
|
||||
name: "&7Tag 17: Brustpanzer der Unzerstörbarkeit"
|
||||
lore: ["&7Bietet maximalen Schutz."]
|
||||
day_18:
|
||||
name: "&bHerz des Ozeans"
|
||||
name: "&bTag 18: Herz des Ozeans"
|
||||
lore: ["&7Das Zentrum einer Conduit-Struktur."]
|
||||
day_19:
|
||||
name: "&fNautilusmuschel"
|
||||
name: "&fTag 19: Nautilusmuscheln"
|
||||
lore: ["&7Ein seltenes Gut aus den Tiefen."]
|
||||
day_20:
|
||||
name: "&8Kopf des Enderdrachen"
|
||||
name: "&8Tag 20: Kopf des Enderdrachen"
|
||||
lore: ["&7Eine beeindruckende Trophäe."]
|
||||
day_21:
|
||||
name: "&8Stiefel der Tiefe"
|
||||
name: "&8Tag 21: Stiefel der Tiefe"
|
||||
lore: ["&7Lass dich nicht im Feuer verbrennen."]
|
||||
day_22:
|
||||
name: "&dVerzauberungstisch"
|
||||
name: "&dTag 22: Verzauberungstisch"
|
||||
lore: ["&7Kreiere deine eigenen magischen Items."]
|
||||
day_23:
|
||||
name: "&bDie ultimative Hacke"
|
||||
name: "&bTag 23: Die ultimative Hacke"
|
||||
lore: ["&7Nicht nur für den Ackerbau..."]
|
||||
day_24:
|
||||
name: "&cDas große Finale! - Feuerwerk"
|
||||
lore: ["&7Frohe Weihnachten!", "&7Feiere mit einem riesigen Feuerwerk!"]
|
||||
name: "&cTag 24: Das große Finale!"
|
||||
lore: ["&7Frohe Weihnachten!", "&7Feiere mit einem riesigen Feuerwerk!"]
|
||||
|
||||
# --- Weitere Events: Titel und Belohnungs-Texte ---
|
||||
events:
|
||||
halloween:
|
||||
prefix: "&6[Halloween] &r"
|
||||
title: "&6✦ Halloween-Kalender ✦"
|
||||
rewards:
|
||||
day_1:
|
||||
name: "&6Kürbiskuchen"
|
||||
lore: ["&7Süßes oder Saures?"]
|
||||
day_2:
|
||||
name: "&6Leuchtende Kürbisse"
|
||||
lore: ["&7Gegen die Dunkelheit."]
|
||||
day_3:
|
||||
name: "&fKnochenvorrat"
|
||||
lore: ["&7Für den treuen Wolf."]
|
||||
day_4:
|
||||
name: "&cSpinnenaugen"
|
||||
lore: ["&7Zutat für düstere Tränke."]
|
||||
day_5:
|
||||
name: "&fSpinnennetze"
|
||||
lore: ["&7Gruselige Deko für dein Haus."]
|
||||
day_6:
|
||||
name: "&8Schwarze Kerzen"
|
||||
lore: ["&7Für das perfekte Ritual."]
|
||||
day_7:
|
||||
name: "&bSeelenfackeln"
|
||||
lore: ["&7Blaues Licht in der Nacht."]
|
||||
day_8:
|
||||
name: "&5Gegorene Spinnenaugen"
|
||||
lore: ["&7Nur für mutige Brauer."]
|
||||
day_9:
|
||||
name: "&fGeisterträne"
|
||||
lore: ["&7Ein Hauch vom Jenseits."]
|
||||
day_10:
|
||||
name: "&bSeelenlaternen"
|
||||
lore: ["&7Sie leuchten für verlorene Seelen."]
|
||||
day_11:
|
||||
name: "&7Phantom-Membranen"
|
||||
lore: ["&7Beute der nächtlichen Jagd."]
|
||||
day_12:
|
||||
name: "&cHexenstaub"
|
||||
lore: ["&7Funkelt unheimlich rot."]
|
||||
day_13:
|
||||
name: "&eZauberstaub"
|
||||
lore: ["&7Ein Hauch von Magie."]
|
||||
day_14:
|
||||
name: "&6Lohenruten"
|
||||
lore: ["&7Heiß und gefährlich."]
|
||||
day_15:
|
||||
name: "&5Enderperlen"
|
||||
lore: ["&7Verschwinde wie ein Geist."]
|
||||
day_16:
|
||||
name: "&fSkelettschädel"
|
||||
lore: ["&7Ein Totenkopf als Deko."]
|
||||
day_17:
|
||||
name: "&2Zombiekopf"
|
||||
lore: ["&7Er starrt dich an..."]
|
||||
day_18:
|
||||
name: "&aCreeperkopf"
|
||||
lore: ["&7Zzzzisch... nur Deko."]
|
||||
day_19:
|
||||
name: "&6Goldäpfel"
|
||||
lore: ["&7Stärkung nach der Nacht."]
|
||||
day_20:
|
||||
name: "&bGeisterjäger-Schwert"
|
||||
lore: ["&7Schlag zu gegen Untote."]
|
||||
day_21:
|
||||
name: "&fKnochenbogen"
|
||||
lore: ["&7Schieß die Geister weg."]
|
||||
day_22:
|
||||
name: "&8Netherit-Schrott"
|
||||
lore: ["&7Aus den Tiefen des Nethers."]
|
||||
day_23:
|
||||
name: "&8Wither-Rosen"
|
||||
lore: ["&7Schön und tödlich."]
|
||||
day_24:
|
||||
name: "&aErfahrungsfläschchen"
|
||||
lore: ["&7Schnell Level sammeln."]
|
||||
day_25:
|
||||
name: "&dTotem des Unvergänglichen"
|
||||
lore: ["&7Gib dem Tod keine Chance."]
|
||||
day_26:
|
||||
name: "&eGrusel-Namensschilder"
|
||||
lore: ["&7Benenne deine Kreaturen."]
|
||||
day_27:
|
||||
name: "&fSchallplatte - 13"
|
||||
lore: ["&7Unheimliche Klänge."]
|
||||
day_28:
|
||||
name: "&bDiamanten"
|
||||
lore: ["&7Ein glänzender Schatz."]
|
||||
day_29:
|
||||
name: "&8Wither-Skelettschädel"
|
||||
lore: ["&7Beschwört nichts Gutes."]
|
||||
day_30:
|
||||
name: "&eNetherstern"
|
||||
lore: ["&7Das Herz der Nacht."]
|
||||
day_31:
|
||||
name: "&c&lHalloween-Finale!"
|
||||
lore: ["&7Netherit zum Gruseln - frohes Halloween!"]
|
||||
easter:
|
||||
prefix: "&e[Ostern] &r"
|
||||
title: "&e✦ Oster-Kalender ✦"
|
||||
rewards:
|
||||
day_1:
|
||||
name: "&fOstereier"
|
||||
lore: ["&7Frisch vom Hasen."]
|
||||
day_2:
|
||||
name: "&6Möhren"
|
||||
lore: ["&7Des Hasen Lieblingsessen."]
|
||||
day_3:
|
||||
name: "&eGoldene Karotten"
|
||||
lore: ["&7Ein edler Snack."]
|
||||
day_4:
|
||||
name: "&fHasenpfote"
|
||||
lore: ["&7Bringt Glück!"]
|
||||
day_5:
|
||||
name: "&6Schoko-Kekse"
|
||||
lore: ["&7Süße Osterleckerei."]
|
||||
day_6:
|
||||
name: "&eHonigflaschen"
|
||||
lore: ["&7Süß wie der Frühling."]
|
||||
day_7:
|
||||
name: "&dOsterkuchen"
|
||||
lore: ["&7Mit viel Liebe gebacken."]
|
||||
day_8:
|
||||
name: "&dFrühlingsblumen"
|
||||
lore: ["&7Ein bunter Strauß."]
|
||||
day_9:
|
||||
name: "&6Osterhasen-Sattel"
|
||||
lore: ["&7Hoppel los!"]
|
||||
day_10:
|
||||
name: "&6Goldäpfel"
|
||||
lore: ["&7Eine Belohnung für Eiersucher."]
|
||||
day_11:
|
||||
name: "&cFrühlingsfeuerwerk"
|
||||
lore: ["&7Lass es knallen!"]
|
||||
day_12:
|
||||
name: "&bDiamanten"
|
||||
lore: ["&7Im Osternest versteckt."]
|
||||
day_13:
|
||||
name: "&6Verzauberter Goldapfel"
|
||||
lore: ["&7Der Preis fürs Eiersuchen."]
|
||||
day_14:
|
||||
name: "&e&lDas goldene Osterei!"
|
||||
lore: ["&7Frohe Ostern!"]
|
||||
|
||||
# --- Geburtstage ---
|
||||
birthday:
|
||||
prefix: "&6[Geburtstag] &r"
|
||||
disabled: "&cGeburtstags-Geschenke sind deaktiviert."
|
||||
help:
|
||||
- "&8&m---&r &6Geburtstag &8&m---"
|
||||
- "&e/ak birthday set <TT.MM> &7- Geburtstag eintragen (z.B. 15.03)"
|
||||
- "&e/ak birthday claim &7- Geschenk abholen (am Geburtstag und %days% Tage danach)"
|
||||
invalid_date: "&cUngültiges Datum. Bitte nutze das Format &eTT.MM &c(z.B. 15.03)."
|
||||
set_success: "&aDein Geburtstag wurde auf den &e%date% &agesetzt. Am Tag selbst und %days% Tage danach kannst du dir mit &e/ak birthday claim &adein Geschenk abholen!"
|
||||
already_set: "&cDu hast deinen Geburtstag bereits eingetragen (&e%date%&c). Bei Fehlern wende dich an einen Admin."
|
||||
info: "&7Dein Geburtstag: &e%date%"
|
||||
info_none: "&7Du hast noch keinen Geburtstag eingetragen: &e/ak birthday set <TT.MM>"
|
||||
removed: "&7Dein Geburtstag wurde gelöscht."
|
||||
change_not_allowed: "&cDu kannst deinen Geburtstag nicht selbst ändern. Bitte wende dich an einen Admin."
|
||||
not_set: "&cDu hast noch keinen Geburtstag eingetragen. Nutze &e/ak birthday set <TT.MM>&c."
|
||||
not_today: "&cDein Geburtstag (&e%date%&c) ist gerade nicht. Das Geschenk gibt es am Tag selbst und %days% Tage danach."
|
||||
already_claimed: "&7Du hast dein Geburtstagsgeschenk dieses Jahr schon abgeholt."
|
||||
reward_received: "&6&lAlles Gute zum Geburtstag! &eViel Spaß mit deinem Geschenk!"
|
||||
broadcast: "&6%player% &efeiert Geburtstag - alles Gute!"
|
||||
join_notice: "&6&lAlles Gute zum Geburtstag! &eKlicke hier, um dein Geschenk abzuholen!"
|
||||
join_hover: "&7Führt /ak birthday claim aus"
|
||||
reward_error: "&cDas Geburtstagsgeschenk ist falsch konfiguriert. Bitte melde das einem Admin."
|
||||
admin_set: "&a[Admin] &7Geburtstag von &e%player% &7auf &6%date% &7gesetzt."
|
||||
admin_removed: "&a[Admin] &7Geburtstag von &e%player% &7gelöscht."
|
||||
admin_reset: "&a[Admin] &7Geschenk-Abholung von &e%player% &7zurückgesetzt."
|
||||
admin_none: "&cFür &e%player% &cist kein Geburtstag eingetragen."
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
# =============================================================================
|
||||
# Adventskalender Plugin - English Language File
|
||||
# Adventskalender Plugin - English Language File (v2.0 - 2026)
|
||||
# =============================================================================
|
||||
|
||||
# --- Prefix ---
|
||||
# This prefix will be shown before most chat messages.
|
||||
prefix: "&6[Adventskalender] &r"
|
||||
prefix: "&6[Advent] &r"
|
||||
|
||||
# --- GUI Texts ---
|
||||
gui:
|
||||
# The title of the Advent Calendar inventory.
|
||||
title: "&6✦ Advent Calendar 2024 ✦"
|
||||
title: "&6✦ Advent Calendar 2026 ✦"
|
||||
default_name: "&6Door #%day%"
|
||||
click_open: "&8» &eClick to open!"
|
||||
locked_name: "&c&lDay #%day%"
|
||||
locked_status: "&8» &7Still locked"
|
||||
locked_hint: "&7Come back on %date%!"
|
||||
claimed_status: "&8» &6Already claimed"
|
||||
claimed_hint: "&aYou have already opened this gift! ✔"
|
||||
|
||||
# --- Messages to Players ---
|
||||
# Available placeholders:
|
||||
@@ -18,90 +24,283 @@ gui:
|
||||
messages:
|
||||
no_permission: "&cYou don't have permission for that."
|
||||
only_player: "&cThis command can only be executed by a player."
|
||||
inventory_full: "&cYour inventory was full! The item was dropped on the ground."
|
||||
reward_received: "&aYou have received your gift for day %day%!"
|
||||
day_not_available: "&cYou cannot open this gift (yet)."
|
||||
day_already_claimed: "&7You have already claimed your gift for this day."
|
||||
inventory_full: "&cYour inventory is full! Your gift was dropped at your feet."
|
||||
reward_received: "&aCongratulations! You have received your gift for day &e%day%&a!"
|
||||
day_not_available: "&cThis door is still locked. You'll have to wait a bit longer!"
|
||||
day_already_claimed: "&7You have already claimed this gift."
|
||||
global_already_claimed: "&cThis gift has already been claimed by someone else today!"
|
||||
no_events: "&cNo calendar is active right now."
|
||||
unknown_event: "&cUnknown event '%event%'. Available: &e%events%"
|
||||
db_error: "&cThe database is currently unavailable. Please try again later."
|
||||
reward_error: "&cThe reward for day %day% is misconfigured. Please report this to an admin."
|
||||
|
||||
# --- Admin Messages ---
|
||||
admin:
|
||||
all_events: "all events"
|
||||
reload_success: "&aConfiguration and language files have been reloaded."
|
||||
test_mode_on: "&e[Admin] &7Test mode (%event%) set to day &6%day%&7."
|
||||
test_mode_off: "&e[Admin] &7Test mode (%event%) disabled. Using real-time date again."
|
||||
player_not_found: "&cThe player '%player%' was not found."
|
||||
invalid_day: "&cInvalid day. Please choose a number between 1 and 24."
|
||||
open_success: "&aYou have opened day %day% for %player%."
|
||||
reset_day_global: "&a[Admin] &7Day &6%day% &7reset globally (%event%)."
|
||||
reset_day_player: "&a[Admin] &7Day &6%day% &7reset for &e%player%&7 (%event%)."
|
||||
reset_all: "&a[Admin] &7Full reset completed (%event%)."
|
||||
reset_player: "&a[Admin] &7Data for &e%player% &7deleted (%event%)."
|
||||
reset_offline: "&a[Admin] &7Offline data for &e%player% &7deleted (%event%)."
|
||||
help:
|
||||
- "&8&m---&r &6Advent Admin &8&m---"
|
||||
- "&e/ak admin reload &7- Reload config"
|
||||
- "&e/ak admin test [event] <day/reset> &7- Simulate date"
|
||||
- "&e/ak admin resetday [event] <global/player> <day> &7- Reset a day"
|
||||
- "&e/ak admin reset [event] <global/player> &7- Reset everything"
|
||||
- "&7Without [event] the command applies to all events."
|
||||
- "&e/ak admin birthday <set|remove|reset> <player> [DD.MM] &7- Manage birthdays"
|
||||
|
||||
# --- Names and Descriptions for Reward Items ---
|
||||
# The keys here (e.g., 'day_1') must match the 'keys' in the config.yml.
|
||||
# --- Reward Texts in GUI ---
|
||||
rewards:
|
||||
day_1:
|
||||
name: "&bFirst Diamonds!"
|
||||
name: "&bDay 1: First Diamonds!"
|
||||
lore: ["&7A little taste of wealth...", "&7Have fun with it!"]
|
||||
day_2:
|
||||
name: "&6Golden Apples"
|
||||
name: "&6Day 2: Golden Apples"
|
||||
lore: ["&7In case things get tight..."]
|
||||
day_3:
|
||||
name: "&cSword of the Hero"
|
||||
name: "&cDay 3: Sword of the Hero"
|
||||
lore: ["&7A mighty sword to hunt monsters."]
|
||||
day_4:
|
||||
name: "&aWings of Freedom"
|
||||
name: "&aDay 4: Wings of Freedom"
|
||||
lore: ["&7Be free as an eagle!"]
|
||||
day_5:
|
||||
name: "&8A Touch of Netherite"
|
||||
name: "&8Day 5: A Touch of Netherite"
|
||||
lore: ["&7Very valuable and rare."]
|
||||
day_6:
|
||||
name: "&6Godly Apple"
|
||||
name: "&6Day 6: Godly Apple"
|
||||
lore: ["&7A gift from the gods... or the admin."]
|
||||
day_7:
|
||||
name: "&eMaster Pickaxe"
|
||||
name: "&eDay 7: Master Pickaxe"
|
||||
lore: ["&7For the greatest treasures in the world."]
|
||||
day_8:
|
||||
name: "&dTotem of Undying"
|
||||
name: "&dDay 8: Totem of Undying"
|
||||
lore: ["&7An extra life."]
|
||||
day_9:
|
||||
name: "&5Nether Star"
|
||||
name: "&5Day 9: Nether Star"
|
||||
lore: ["&7The core of a powerful being."]
|
||||
day_10:
|
||||
name: "&9Magic Box"
|
||||
name: "&9Day 10: Magic Chests"
|
||||
lore: ["&7Keeps all your treasures safe."]
|
||||
day_11:
|
||||
name: "&3Spear of the Ocean"
|
||||
name: "&3Day 11: Spear of the Ocean"
|
||||
lore: ["&7The weapon of the guardians."]
|
||||
day_12:
|
||||
name: "&7Helmet of Wisdom"
|
||||
name: "&7Day 12: Helmet of Wisdom"
|
||||
lore: ["&7Protects your head."]
|
||||
day_13:
|
||||
name: "&fSaddle for a Loyal Steed"
|
||||
name: "&fDay 13: Saddle"
|
||||
lore: ["&7For your next equestrian adventure."]
|
||||
day_14:
|
||||
name: "&eName Tags"
|
||||
name: "&eDay 14: Name Tags"
|
||||
lore: ["&7Give your favorite wolf a name!"]
|
||||
day_15:
|
||||
name: "&aMusic Disc - cat"
|
||||
name: "&aDay 15: Music Disc - cat"
|
||||
lore: ["&7Relaxing music for a cozy time."]
|
||||
day_16:
|
||||
name: "&5End Crystal"
|
||||
name: "&5Day 16: End Crystals"
|
||||
lore: ["&7Powerful and dangerous. Handle with care!"]
|
||||
day_17:
|
||||
name: "&7Chestplate of Durability"
|
||||
name: "&7Day 17: Chestplate of Durability"
|
||||
lore: ["&7Offers maximum protection."]
|
||||
day_18:
|
||||
name: "&bHeart of the Sea"
|
||||
name: "&bDay 18: Heart of the Sea"
|
||||
lore: ["&7The center of a Conduit structure."]
|
||||
day_19:
|
||||
name: "&fNautilus Shell"
|
||||
name: "&fDay 19: Nautilus Shells"
|
||||
lore: ["&7A rare goodie from the depths."]
|
||||
day_20:
|
||||
name: "&8Head of the Ender Dragon"
|
||||
name: "&8Day 20: Head of the Ender Dragon"
|
||||
lore: ["&7An impressive trophy."]
|
||||
day_21:
|
||||
name: "&8Boots of the Deep"
|
||||
name: "&8Day 21: Boots of the Deep"
|
||||
lore: ["&7Don't let yourself get burned in fire."]
|
||||
day_22:
|
||||
name: "&dEnchanting Table"
|
||||
name: "&dDay 22: Enchanting Table"
|
||||
lore: ["&7Create your own magical items."]
|
||||
day_23:
|
||||
name: "&bThe Ultimate Hoe"
|
||||
name: "&bDay 23: The Ultimate Hoe"
|
||||
lore: ["&7Not just for farming..."]
|
||||
day_24:
|
||||
name: "&cThe Grand Finale! - Fireworks"
|
||||
lore: ["&7Merry Christmas!", "&7Celebrate with a huge firework!"]
|
||||
name: "&cDay 24: The Grand Finale!"
|
||||
lore: ["&7Merry Christmas!", "&7Celebrate with a huge firework!"]
|
||||
|
||||
# --- More events: titles and reward texts ---
|
||||
events:
|
||||
halloween:
|
||||
prefix: "&6[Halloween] &r"
|
||||
title: "&6✦ Halloween Calendar ✦"
|
||||
rewards:
|
||||
day_1:
|
||||
name: "&6Pumpkin Pie"
|
||||
lore: ["&7Trick or treat?"]
|
||||
day_2:
|
||||
name: "&6Jack o'Lanterns"
|
||||
lore: ["&7Against the darkness."]
|
||||
day_3:
|
||||
name: "&fBone Stash"
|
||||
lore: ["&7For your loyal wolf."]
|
||||
day_4:
|
||||
name: "&cSpider Eyes"
|
||||
lore: ["&7An ingredient for dark potions."]
|
||||
day_5:
|
||||
name: "&fCobwebs"
|
||||
lore: ["&7Spooky decoration for your home."]
|
||||
day_6:
|
||||
name: "&8Black Candles"
|
||||
lore: ["&7For the perfect ritual."]
|
||||
day_7:
|
||||
name: "&bSoul Torches"
|
||||
lore: ["&7Blue light in the night."]
|
||||
day_8:
|
||||
name: "&5Fermented Spider Eyes"
|
||||
lore: ["&7Only for brave brewers."]
|
||||
day_9:
|
||||
name: "&fGhost Tears"
|
||||
lore: ["&7A breath from the beyond."]
|
||||
day_10:
|
||||
name: "&bSoul Lanterns"
|
||||
lore: ["&7They shine for lost souls."]
|
||||
day_11:
|
||||
name: "&7Phantom Membranes"
|
||||
lore: ["&7Loot from the night hunt."]
|
||||
day_12:
|
||||
name: "&cWitch's Dust"
|
||||
lore: ["&7Glows an eerie red."]
|
||||
day_13:
|
||||
name: "&eMagic Dust"
|
||||
lore: ["&7A touch of magic."]
|
||||
day_14:
|
||||
name: "&6Blaze Rods"
|
||||
lore: ["&7Hot and dangerous."]
|
||||
day_15:
|
||||
name: "&5Ender Pearls"
|
||||
lore: ["&7Vanish like a ghost."]
|
||||
day_16:
|
||||
name: "&fSkeleton Skull"
|
||||
lore: ["&7A skull as decoration."]
|
||||
day_17:
|
||||
name: "&2Zombie Head"
|
||||
lore: ["&7It stares at you..."]
|
||||
day_18:
|
||||
name: "&aCreeper Head"
|
||||
lore: ["&7Ssssss... just decoration."]
|
||||
day_19:
|
||||
name: "&6Golden Apples"
|
||||
lore: ["&7A boost after the night."]
|
||||
day_20:
|
||||
name: "&bGhostbuster Sword"
|
||||
lore: ["&7Strike down the undead."]
|
||||
day_21:
|
||||
name: "&fBone Bow"
|
||||
lore: ["&7Shoot the ghosts away."]
|
||||
day_22:
|
||||
name: "&8Netherite Scrap"
|
||||
lore: ["&7From the depths of the Nether."]
|
||||
day_23:
|
||||
name: "&8Wither Roses"
|
||||
lore: ["&7Beautiful and deadly."]
|
||||
day_24:
|
||||
name: "&aBottles o' Enchanting"
|
||||
lore: ["&7Gain levels fast."]
|
||||
day_25:
|
||||
name: "&dTotem of Undying"
|
||||
lore: ["&7Give death no chance."]
|
||||
day_26:
|
||||
name: "&eSpooky Name Tags"
|
||||
lore: ["&7Name your creatures."]
|
||||
day_27:
|
||||
name: "&fMusic Disc - 13"
|
||||
lore: ["&7Eerie sounds."]
|
||||
day_28:
|
||||
name: "&bDiamonds"
|
||||
lore: ["&7A shiny treasure."]
|
||||
day_29:
|
||||
name: "&8Wither Skeleton Skull"
|
||||
lore: ["&7Summons nothing good."]
|
||||
day_30:
|
||||
name: "&eNether Star"
|
||||
lore: ["&7The heart of the night."]
|
||||
day_31:
|
||||
name: "&c&lHalloween Finale!"
|
||||
lore: ["&7Spooky netherite - Happy Halloween!"]
|
||||
easter:
|
||||
prefix: "&e[Easter] &r"
|
||||
title: "&e✦ Easter Calendar ✦"
|
||||
rewards:
|
||||
day_1:
|
||||
name: "&fEaster Eggs"
|
||||
lore: ["&7Fresh from the bunny."]
|
||||
day_2:
|
||||
name: "&6Carrots"
|
||||
lore: ["&7The bunny's favorite food."]
|
||||
day_3:
|
||||
name: "&eGolden Carrots"
|
||||
lore: ["&7A noble snack."]
|
||||
day_4:
|
||||
name: "&fRabbit's Foot"
|
||||
lore: ["&7Brings luck!"]
|
||||
day_5:
|
||||
name: "&6Chocolate Cookies"
|
||||
lore: ["&7A sweet Easter treat."]
|
||||
day_6:
|
||||
name: "&eHoney Bottles"
|
||||
lore: ["&7Sweet as spring."]
|
||||
day_7:
|
||||
name: "&dEaster Cake"
|
||||
lore: ["&7Baked with lots of love."]
|
||||
day_8:
|
||||
name: "&dSpring Flowers"
|
||||
lore: ["&7A colorful bouquet."]
|
||||
day_9:
|
||||
name: "&6Easter Bunny Saddle"
|
||||
lore: ["&7Hop to it!"]
|
||||
day_10:
|
||||
name: "&6Golden Apples"
|
||||
lore: ["&7A reward for egg hunters."]
|
||||
day_11:
|
||||
name: "&cSpring Fireworks"
|
||||
lore: ["&7Let it bang!"]
|
||||
day_12:
|
||||
name: "&bDiamonds"
|
||||
lore: ["&7Hidden in the Easter nest."]
|
||||
day_13:
|
||||
name: "&6Enchanted Golden Apple"
|
||||
lore: ["&7The prize for egg hunting."]
|
||||
day_14:
|
||||
name: "&e&lThe Golden Easter Egg!"
|
||||
lore: ["&7Happy Easter!"]
|
||||
|
||||
# --- Birthdays ---
|
||||
birthday:
|
||||
prefix: "&6[Birthday] &r"
|
||||
disabled: "&cBirthday gifts are disabled."
|
||||
help:
|
||||
- "&8&m---&r &6Birthday &8&m---"
|
||||
- "&e/ak birthday set <DD.MM> &7- Set your birthday (e.g. 15.03)"
|
||||
- "&e/ak birthday claim &7- Claim your gift (on your birthday and %days% days after)"
|
||||
invalid_date: "&cInvalid date. Please use the format &eDD.MM &c(e.g. 15.03)."
|
||||
set_success: "&aYour birthday has been set to &e%date%&a. On the day and for %days% days after you can claim your gift with &e/ak birthday claim&a!"
|
||||
already_set: "&cYou have already set your birthday (&e%date%&c). Contact an admin if it is wrong."
|
||||
info: "&7Your birthday: &e%date%"
|
||||
info_none: "&7You have not set a birthday yet: &e/ak birthday set <DD.MM>"
|
||||
removed: "&7Your birthday has been deleted."
|
||||
change_not_allowed: "&cYou cannot change your birthday yourself. Please contact an admin."
|
||||
not_set: "&cYou have not set a birthday yet. Use &e/ak birthday set <DD.MM>&c."
|
||||
not_today: "&cYour birthday (&e%date%&c) is not right now. The gift is available on the day and for %days% days after."
|
||||
already_claimed: "&7You have already claimed your birthday gift this year."
|
||||
reward_received: "&6&lHappy Birthday! &eEnjoy your gift!"
|
||||
broadcast: "&6%player% &eis celebrating a birthday - happy birthday!"
|
||||
join_notice: "&6&lHappy Birthday! &eClick here to claim your gift!"
|
||||
join_hover: "&7Runs /ak birthday claim"
|
||||
reward_error: "&cThe birthday gift is misconfigured. Please report this to an admin."
|
||||
admin_set: "&a[Admin] &7Birthday of &e%player% &7set to &6%date%&7."
|
||||
admin_removed: "&a[Admin] &7Birthday of &e%player% &7deleted."
|
||||
admin_reset: "&a[Admin] &7Gift claim of &e%player% &7reset."
|
||||
admin_none: "&cNo birthday is set for &e%player%&c."
|
||||
|
||||
@@ -1,20 +1,59 @@
|
||||
name: Adventskalender
|
||||
version: '1.0.0'
|
||||
version: '1.0.3'
|
||||
main: de.mviper.adventskalender.Adventskalender
|
||||
api-version: 1.20
|
||||
author: M_Viper
|
||||
description: Ein konfigurierbarer Adventskalender für Minecraft.
|
||||
description: Ein moderner, performanter Adventskalender (2026 Edition).
|
||||
|
||||
softdepend:
|
||||
- PlaceholderAPI
|
||||
- WorldGuard
|
||||
|
||||
commands:
|
||||
adventskalender:
|
||||
description: "Öffnet den Adventskalender oder führt Admin-Befehle aus."
|
||||
aliases: [advent, kalender, ak]
|
||||
aliases: [advent, kalender, ak, adventcalendar, ac, calendar, acalendar]
|
||||
usage: "/adventskalender"
|
||||
christmas:
|
||||
description: "Weihnachts-Module: Schnee, Santa, Magic Tree, Wichteln, Quiz, Portale und mehr."
|
||||
aliases: [ch, chr, christmasplus, xmas]
|
||||
usage: "/christmas help"
|
||||
cracker:
|
||||
description: "Weihnachts-Cracker."
|
||||
aliases: [crackers]
|
||||
usage: "/cracker <list|get|give>"
|
||||
|
||||
permissions:
|
||||
adventskalender.use:
|
||||
description: "Erlaubt dem Spieler, den Adventskalender zu benutzen."
|
||||
default: true
|
||||
adventskalender.birthday:
|
||||
description: "Erlaubt dem Spieler, seinen Geburtstag einzutragen und das Geburtstagsgeschenk abzuholen."
|
||||
default: true
|
||||
adventskalender.admin:
|
||||
description: "Erlaubt den Zugriff auf alle Admin-Befehle."
|
||||
default: op
|
||||
default: op
|
||||
christmasplus.christmas.snow:
|
||||
default: true
|
||||
christmasplus.christmas.santa:
|
||||
default: op
|
||||
christmasplus.christmas.reload:
|
||||
default: op
|
||||
christmasplus.cracker.list:
|
||||
default: op
|
||||
christmasplus.cracker.get:
|
||||
default: op
|
||||
christmasplus.cracker.get.multiple:
|
||||
default: op
|
||||
christmasplus.cracker.give:
|
||||
default: op
|
||||
christmasplus.adventcalendar.test:
|
||||
default: op
|
||||
christmasplus.adventcalendar.reset:
|
||||
default: op
|
||||
christmasplus.adventcalendar.claimpast:
|
||||
default: true
|
||||
xmas.use:
|
||||
default: true
|
||||
xmas.admin:
|
||||
default: op
|
||||
|
||||
Reference in New Issue
Block a user