Compare commits

...

169 Commits

Author SHA1 Message Date
Andreas Troelsen 29c5d7f56d Release 0.108. 2024-01-01 20:15:55 +01:00
Andreas Troelsen e40fc6ef84 Add `publish-hangar` GitHub Actions workflow.
Introduces a new workflow that runs when a new build has been published
on GitHub Releases. It converts the release notes to Hangar Markdown and
sends it to Hangar along with the jar-file.

Note: The workflow currently relies on the version string being appended
to the filename of the jar-file. Without it, the file reference in the
`curl` request that uploads the build would need to change.

The workflow references a new secret, `HANGAR_TOKEN`, which is just an
API key for the Hangar API. The token was created in the Hangar profile
settings (API Keys), and its only permission is `create_version`.

In order to properly upload a new build to Hangar, we need to construct
a somewhat complex JSON object. This is because the Hangar API allows
for publishing releases on multiple platforms and for multiple versions,
which makes the simple use case for MobArena's single file upload look a
bit overcomplicated. Unlike the CurseForge API, the Hangar API supports
"normal" platform version strings, so we don't need to map anything. It
also supports patch version wildcards, so we can get away with `1.18.x`,
`1.19.x`, etc. for each version supported. The API only uses the API key
for authentication, which means we need to grab a JWT and use that for
the actual upload request. Note that the `pluginDependencies` property
is currently required, but it can be left empty.

The workflow can be invoked directly via the `workflow_dispatch` event,
which might come in handy if something in the pipeline breaks.

The Hangar base URL and project slug are both hardcoded, and things
would probably be cleaner if they were made into variables, but we don't
need this workflow anywhere else, so it's fine for now.
2024-01-01 17:05:41 +01:00
Andreas Troelsen be6fd85a6d Add `publish-curseforge` GitHub Actions workflow.
Introduces a new workflow that runs when a new build has been published
on GitHub Releases. It converts the release notes to CurseForge HTML and
sends it to CurseForge along with the jar-file.

Note: The workflow currently relies on the version string being appended
to the filename of the jar-file. Without it, the file reference in the
`curl` request that uploads the build would need to change.

The workflow references a new secret, `CURSEFORGE_TOKEN`, which is just
an API key for the CurseForge API. The token was created on CurseForge
under profile settings (My API Tokens).

In order to properly upload a new build to CurseForge, we need a list of
"game version IDs", which isn't completely trivial. The API gives us a
means of looking up _all_ Minecraft game version IDs, but we then have
to manually filter out the ones that don't apply to Bukkit plugins, as
there are duplicate entries for each Minecraft version, and only some of
them work for Bukkit plugins (which turns out to be the ones with game
version type ID 1). The structure of the `metadata` field combined with
how incredibly difficult bash can be to work with has resulted in some
gnarly text processing trying to filter the JSON response and turning it
into a list for use in the `jq` template, but it gets the job done.

The CurseForge base URL and project ID are both hardcoded, and things
would probably be cleaner if they were made into variables, but we don't
need this workflow anywhere else, so it's fine for now.

The workflow can be invoked directly via the `workflow_dispatch` event,
which might come in handy if something in the pipeline breaks.

Lots of inspiration was found in the probably really great GitHub Action
`curseforge-upload` [1]. We could have probably just used that, but it's
nice to have full control of the process. At any rate, thanks to itsmeow
and NotMyFault for publishing their work.

---

[1] https://github.com/itsmeow/curseforge-upload
2024-01-01 16:46:38 +01:00
Andreas Troelsen b881943656 Add `hangar` format to release note script.
Hangar uses Markdown, so this is a very easy release note "conversion"
just like GitHub Releases.
2023-12-31 15:14:25 +01:00
Andreas Troelsen 8e5d2f0d23 Make InventoryThingParser package-private.
We don't need the parser exposed outside of the `things` package, and
none of the other thing parsers are public anyway. Coincidentally, this
fixes a warning about exposing InventoryThing outside of its visibility
scope, so yay.
2024-01-01 18:34:20 +01:00
Andreas Troelsen 3b7b638b00 Make LexeMatcher package-private.
We don't need it outside of the `formula` package.

This fixes warnings about exposing Lexeme outside of its visibility
scope, so yay.
2024-01-01 18:26:44 +01:00
Andreas Troelsen eb51a31720 Remove unused ThingManager constructor.
This fixes a warning about exposing ItemStackThingParser outside of its
visibility scope, but really it's just a good little cleanup step, since
the constructor in question is never used for anything. We might want to
eventually expose the ItemStackThingParser and use it in more places in
the code base, but in that case, and in that case it would probably make
sense to re-introduce the constructor, but I'm calling YAGNI on this in
order to nuke a warning.
2024-01-01 18:23:54 +01:00
Andreas Troelsen 82f00c5535 Fix "unary operator" warnings in FormulaManagerIT.
Okay, the reason the code included the unary plus was to more directly
represent the resulting expression, but I'm guessing the compiler isn't
going to respect that intent even if it could somehow understand it, so
it will probably remove the symbols and just parse it all the same.

Unlike with the unary plus, the unary minus can be "fixed" by wrapping
it in parentheses. The end result is of course the exact same, but the
intent is perhaps a bit clearer this way. We want to try to coerce the
compiler into creating an expression with "add a negative value", just
for the sake of "correctness" at the runtime evaluation level, but even
if that isn't what will actually happen, the explicit code is still a
bit easier to read. While unary plus is easy to disregard, "fixing" an
unnecessary unary minus would mean having to change the binary operator
before it, which muddies the intent of the expression.
2024-01-01 17:49:21 +01:00
Andreas Troelsen d8fdbb80c0 Simplify formula operation interfaces.
This commit releases the BinaryOperation and UnaryOperation interfaces
of the `formula` package from their `java.util.function` supertypes and
redeclares the previously inherited functions directly in the operation
interfaces, but also reifies them by explicitly using primitive doubles
instead of generics and wrapper classes. Doing so does not change the
functionality or any other code at all, but it makes the interfaces much
"stronger", since they no longer need to consider `null` values, which
they didn't actually take into account anyway. This fixes a warning in
Visual Studio Code (not sure how to get the same warning in IntelliJ)
about the operator registrations in the default formula environment
factory method being unsafe.
2024-01-01 19:39:59 +01:00
Andreas Troelsen e5ffe169a1 Create release drafts from "Release ..." commits.
This commit adds a second job to the build workflow that runs after the
build job has completed. The job creates a GitHub Releases _draft_ that
needs to be manually published in order to be publicly available.

The job runs if, and only if, the following conditions are met:

- The build job has completed _successfully_, i.e. if the Gradle `build`
  task fails, the draft job doesn't run.

- The `push` event that triggered the workflow happened on the `master`
  branch, i.e. releases will never be created from temporary branches.

- The commit message of the most recent commit in the `push` event that
  triggered the workflow starts with `"Release "`, i.e. there must be a
  commit that explicitly tries to "release" a build.

- The version string does _not_ end with `-SNAPSHOT`, i.e. development
  builds will not be released.

All of these conditions act as safeguards, so we don't end up releasing
something we don't want to release, but they also prevent bloating the
Releases page with a bunch of useless drafts.

The job uses the `version` output variable from the build job that was
introduced in a recent commit to extract release notes using the script
that was also introduced recently, as well as for the name of the _tag_
to create when the release is published.

Note that the `GITHUB_TOKEN` environment variable is required to be set
when we want to use the GitHub CLI in a workflow [1]. The job also has
an explicit `contents: write` permission, which is required for creating
releases from GitHub Actions.
2023-12-31 04:43:53 +01:00
Andreas Troelsen 798ae0f578 Output version string in build workflow.
This commit makes the build workflow output the version string as found
in the `version` property in `build.gradle.kts`. The version string will
be necessary further down the pipeline when we need to extract release
notes and create tags.

There are many ways to extract the version string:

- Use `grep` to grab it from `build.gradle.kts` directly. This is pretty
  brittle, since we don't really know for sure if the structure of the
  file will change in the future.

- Create a Gradle task in `build.gradle.kts` that prints the version
  string. This is probably the most direct approach we could take, but
  such a hyper-specific task feels like a code smell. It also requires
  running Gradle again, which is a bit slow.

- Use the built-in `properties` task in Gradle to print the `version`
  property and `grep` it. We avoid changing `build.gradle.kts`, which is
  a plus, but we still have to actually run Gradle.

- Parse the filename of the resulting jar-file in `build/libs/`, since
  it now contains the version string. This is also brittle, because we
  don't know if we're gonna continue to append the version string to the
  jar-file, and depending so much on it being there is a little scary.

- Extract `plugin.yml` from the resulting jar-file and `grep` it. This
  is perhaps a little crude, but it is much faster than running Gradle,
  and as a bonus, we get a bit closer to "what's inside the jar-file",
  which should give us a bit more confidence that any given release is
  actually the version it claims to be.

It may seem like a small thing to invest so much text on, but from an
automation standpoint, it is much easier to be confident in automations
with predictable and robust mechanisms for deriving input variables.
2023-12-31 04:16:08 +01:00
Andreas Troelsen 84776990b9 Add release note extraction script.
Introduces a crude Python script that can extract release notes from the
changelog for a given version and convert it to one of three different
output formats:

- `github` for GitHub Releases. This is just the Markdown itself, but
  with the very first line (the version) removed, because the version is
also the title of the release itself.

- `spigot` for Spigot Resources. This is the BBCode format used on the
  forums and in the resource descriptions.

- `curse` for CurseForge File Uploads. Curse uses a so-called "WYSIWYG"
  format that's really just HTML underneath.

The formats for Spigot and CurseForge are straightforward to convert to
as long as we only use simple text formatting, bullet lists, and links,
but that is really all the changelog should consist of anyway.

While this script already makes the release process quite a bit easier
on its own, the end goal is to _automate_ releases as much as possible,
and to do that, we need to be able to extract release notes, and we need
to be able to do it from GitHub Actions, which is quite a bit simpler if
we don't use third-party libraries.

Publishing releases on GitHub is almost trivial, while CurseForge is
pretty easy, and Hangar should be very doable as well. Spigot, on the
other hand, is stuck in the dark ages, so we must continue to upload
files manually there.
2023-12-31 03:25:14 +01:00
Andreas Troelsen 590f877756 Build on push to branches, not tags.
According to the documentation, by not specifying anything in the `push`
event, GitHub Actions will run the workflow for both branches and tags,
but by specifying `branches` alone, it will _only_ run the workflow for
branches and _not_ tags [1].

We want to build on pushes to _all_ branches so we can give folks a jar
to try out for feature requests and bug fixes. We don't want to build on
tags, however, because they don't provide any value in the current build
pipeline. If the version was derived from Git tags, it could make sense
to build on tags, but that's not where we are.

As for the `**` glob pattern instead of just `*`, the documentation says
that the latter does not match forward slashes [2], and while our naming
convention for branches doesn't use slashes right now, there's no reason
that they shouldn't be viable later down the road.

---

[1] https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushbranchestagsbranches-ignoretags-ignore
[2] https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
2023-12-31 02:25:41 +01:00
Andreas Troelsen 932b9de8f2 Minor tweaks to build workflow.
Removes trailing whitespace and simplifies some step names.
2023-12-31 02:24:53 +01:00
Andreas Troelsen 614d95683e Update `upload-artifact` to v4.
Apparently there are numerous performance and behavioral improvements.
2023-12-30 23:49:03 +01:00
Andreas Troelsen 4c855e6705 Include version in artifact filename.
Removes the "archive version" override for the `shadowJar` task in the
build file, resulting in a jar-file that contains the current version
number in the filename. It's mostly a convenience tradeoff; either we
can see the version directly in the filename and avoid assumptions, or
we can easily overwrite an existing jar-file with a new one.

Also updates the upload step of the build workflow to a glob pattern so
we grab the file regardless of the version.
2023-12-30 23:36:07 +01:00
Andreas Troelsen 7942c33e67 Add `monster-teleporting` to resource file.
This should have been part of commit 511d16f, but apparently I forgot to
stage the file.

Whoops.
2023-12-17 21:20:25 +01:00
Andreas Troelsen f57f10ecd5 Convert to Gradle.
This commit converts the project from Maven to Gradle by removing the
old Maven-related files such as `pom.xml` and the Maven Wrapper, and
replacing them with their Gradle counterparts (more or less).

Comparing the artifacts from Maven and Gradle indicates no significant
differences in the resulting jar-files, and a quick test of the plugin
shows that things are still working as expected.

Bits of `build.gradle.kts` may need a bit of a tune-up later down the
road, e.g. the test sources "hack" put in place. It may be cleaner to
omit this hack and just suck up having to repeat dependencies, but in
that case, it might be better to embrace the "libs" file instead of
having to repeat dependencies in full.

Note that this commit changes the caching mechanism used in the GitHub
Actions build workflow, opting for the one built into `setup-java`.
2023-12-09 21:08:09 +01:00
Andreas Troelsen 511d16f45a Add per-arena setting `monster-teleporting`.
This setting allows server owners to allow arena monsters to teleport
around _while inside the region_. They still can't teleport out of the
region.

Taken at face value, this should just be the default behavior. However,
due to arena regions being boxes, any non-box shaped arena will need a
region that covers more than the physical arena structure, which means
mobs like Endermen will be able to teleport into possibly unreachable
areas of the physical structure. So we have to make do with a setting.

Closes #762
2023-11-13 16:29:51 +01:00
Andreas Troelsen 457bf2ffff Clean up PvP activation logic.
The idea behind the previous implementation worked, but it was a tad bit
confusing. This commit refactors the activation logic by simply removing
it entirely. The "activation" part of the logic is now derived from the
wave number (if 0, it means we haven't _really_ started yet), instead of
relying on the spawn thread to toggle the flag on and off. This kind of
dependency inversion (spawn thread -> listener, listener -> "phase") is
a pretty decent (albeit super tiny) step towards cleaning up the whole
session system, so I call that a victory in and of itself!
2023-11-13 16:19:01 +01:00
Andreas Troelsen a662157cbf Reformat ArenaListener fields.
What a mess, sheesh...
2023-11-13 15:37:51 +01:00
Andreas Troelsen 9e083a91de Report Spigot health error to admins.
Using the new `mobarena.admin.errors` permission, this commit provides
server owners with a way to make the infamous Spigot health error much
more visible by sending the error message to any "admins" online when
the error occurs.

Closes #764
2023-11-13 14:39:05 +01:00
Andreas Troelsen 12314f476c Add `mobarena.admin.errors` permission.
Introduces a new permission for "admins" that can be used to increase
visibility of errors caught by the plugin. Server owners may not want
_all_ online players to see these types of messages, so the permission
gives error handlers a way to filter the list of online players before
sending the error message.
2023-11-13 14:37:34 +01:00
Andreas Troelsen feb257213c Add support for saved items.
Introduces the concept of a _saved item_; an in-game item that has been
captured in a YAML file via Bukkit's item serialization mechanism. These
items can be referenced in the config-file in all places that any other
normal item can be used, assuming the ThingManager is in charge of the
parsing. This should help bridge the gap between class chests and the
config-file by allowing any Bukkit serializable item stack to be stored
and referenced as if MobArena's item syntax directly supported it.

Three new setup commands are introduced to help manage the items, such
that they can be created, deleted, and loaded (for "editing" purposes).
The commands are somewhat rough around the edges and may need a little
bit of polish going forward.

Together with the new inventory referencing Things, this functionality
should help provide most of the flexibility people have been missing
from the item syntax for about a decade... Hell, it's about time.

Closes #212
2023-11-13 14:33:31 +01:00
Andreas Troelsen d7336526e1 Add InventoryThing collection.
Adds three new Thing types that can be used to reference items in chests
(or any block-based InventoryHolder):

- InventoryIndexThing looks up an item by index/slot in an inventory.
- InventoryGroupThing groups all non-null/non-air items in an inventory
  into a ThingGroup.
- InventoryRangeThing groups all non-null/non-air items in a given range
  of an inventory into a ThingGroup.

The new Thing types aim to bridge a gap between the class chests and the
rest of the Thing-based parts of the config-file. The goal is two-fold:
allow for more in-game configuration so access to the config-file isn't
_quite_ as crucial, and propagate the item-wise feature completeness of
class chests to other parts of the plugin.

While class chests are low configuration and a bit "all or nothing", the
inventory Thing types require manually punching in the coords for chests
and possibly indices/ranges for items. This means that the initial setup
could be a bit unwieldy, and highly volatile wave setups are definitely
not a good fit. If the wave setup is mostly pre-defined, it is fairly
easy to tweak upgrade waves and rewards in the same way class chests are
tweaked.

As for item-wise feature completeness, the inventory Thing types share
the same "if Bukkit can copy it, it will work" rule of thumb as class
chests do, which means items with metadata such as custom names, lore,
or even NBTs, should just work. This could remove the need to employ
other plugins.

By no means can this solution be considered "optimal", but it it _does_
enable some long-requested features.

Closes #456
2023-11-13 14:32:35 +01:00
Andreas Troelsen 1a7109a1d4 Add Hangar link to README.
We'll get around to the Gradle plugin and auto-publishing at some point,
but for now, let's at least just link to the project page.
2023-11-13 12:21:40 +01:00
Andreas Troelsen af513b03b0 Prevent unauthorized sign edits.
Since Minecraft 1.20, players can edit signs by right-clicking on them,
and that poses a problem for the sign-centric portions of the plugin,
such as class selection signs and the various types of arena signs.

This commit refactors the PlayerInteractEvent handler in ArenaListener
in order to break open the possibility of handling non-lobby players as
well. We're a little more strict with lobby players, and we still want
to handle class sign clicks and iron block clicks here. For players who
aren't in the lobby, we're really just blocking the event according to
the regular protection rules (block is inside region, protect is on, and
arena is not in edit mode).

It also blanket cancels events in the HandlesSignClicks event handler,
because there is no meaningful way to edit an arena sign, since their
contents come from the template file and not from what is written on
them by the sign renderer.

Ideally, we'd refactor some of this event handler logic, because it's a
lot easier to take care of the individual responsibilities in separate
event handlers.

Fixes #765
2023-11-04 00:20:39 +01:00
Andreas Troelsen 6579c4bf0e Call `update()` when reparing signs.
It boggles the mind that this tiny little class has worked as intended
since 2011 (!!), and all of a sudden, signs no longer retain their text
in the repair procedure...

By calling the somewhat arbitrary `update()` method on the sign after
setting the contents, the sign appears to correctly update again.

Fixes #772
2023-10-23 18:55:08 +02:00
Andreas Troelsen a79678430d Add `-Xlint:all` to compiler args.
Nice little flag to get all those juicy warnings out into the build
output for better visibility.
2023-10-23 18:35:54 +02:00
Andreas Troelsen 61550161a7 Remove unused class.
I had no idea this class existed, but it seems like it's actually never
been used for anything, since the commit that introduced it didn't even
use it either.
2023-10-23 00:30:55 +02:00
Andreas Troelsen bbe7ed491d Bulk up the test suite for FormulaManager.
Introduces a couple of tests for the FormulaManager test suite in order
to cover all the methods the class exposes. This means it is no longer
necessary to suppress the "unused" warnings.
2023-10-23 00:30:55 +02:00
Tad Hunt 58f1423a6e
Upgrade versions in GitHub Actions
Fixes a bunch of deprecation warnings.
2023-10-22 01:27:53 +02:00
Tad Hunt a8b2cf90e9
Fix scanner resource leak.
Wraps the Scanner in try-with-resources to ensure resource cleanup after
reading the config-file from the plugin jar.
2023-10-22 01:24:53 +02:00
Andreas Troelsen 15a79bdd7e Remove discrepancy in auto-ready behavior.
It's not clear why this variation in the auto-ready logic exists, and
the commit history doesn't seem to have any clues either. Perhaps the
actual readying up logic was incompatible with auto-ready at some point,
but at this point in time it doesn't seem like this is necessary at all,
and it appears to be causing a bug with the MobArenaStats extension.

By simply calling the player ready procedure regardless of the status of
the auto start timer, MobArena fires the arena player ready event that
MobArenaStats depends on for some of its pre-session bookkeeping. It
could be argued that MobArenaStats should be more robust, but we would
much rather fix the root problem than slack on the otherwise fairly
sound strictness of the MobArenaStats data model.

Fixes #746
2023-05-07 11:52:31 +02:00
Andreas Troelsen 4f78936716 Use boss entity as source for obsidian bomb.
Makes the boss entity the source of the obsidian bomb explosion, which
then makes the damage event listener handle the explosion damage as if
the boss is the damager, which means the `monster-infight` flag should
be respected.

Fixes #759
2023-05-07 11:29:36 +02:00
Andreas Troelsen 2eb1761e76 Use exploding sheep as source for explosion.
Makes the sheep entity the source of the explosion that's created when
it triggers close to a player. This, in turn, makes the damage event
listener handle the explosion damage as if the sheep is the damager,
which means it will respect the `monster-infight` flag.

Fixes #758
2023-05-07 11:28:13 +02:00
Andreas Troelsen 9871bb85d8 Bump version to 0.107.1. 2023-05-07 11:21:21 +02:00
Andreas Troelsen 00605b54f9 Release 0.107. 2022-07-30 20:24:03 +02:00
Andreas Troelsen ec644df05b Adapt to the new `SPELL` spawn reason.
This new spawn reason was introduced somewhere between 1.18 and 1.18.1,
and unfortunately it is a breaking change, so we have to employ it for
MobArena to properly allow and register vexes (again...), but we also
have to maintain a variation of the old logic so we don't break support
for older server versions.

Fixes #719
2022-07-26 13:20:54 +02:00
Andreas Troelsen 2c0d3e292c Update SpigotAPI to 1.19.
This time around we're trying to keep the API version bumped for the
sake of not leaving the people who actually keep their servers updated
behind. It does present some potential backwards compatibility issues
when we do employ new API, but we'll just have to be mindful of _how_ we
go about doing so...
2022-07-26 13:10:06 +02:00
Andreas Troelsen d963d90cb9 Update VaultAPI to 1.7.1. 2022-07-26 12:23:53 +02:00
Andreas Troelsen b76b0e6719 Add `workflow_dispatch` trigger to build workflow.
This should allow us to just initiate a build if the artifact of the
latest build expires.
2022-06-25 17:21:16 +02:00
Andreas Troelsen 6cec72ebfb Add some whitespace to build workflow.
Let's clean this up a little bit.
2022-06-25 17:19:49 +02:00
Andreas Troelsen 8c5ae13bef Rename build workflow.
The previous name was really just a way to try to "prettify" the whole
thing, but the badge in the README is a little wonky compared to other
projects, so we're just gonna go with "build" for now for consistency.
2022-06-25 17:18:02 +02:00
Andreas Troelsen 36908cbe85 Remove class pets on player death.
When a player with pets dies in the arena, we want their pets to be
removed. One could probably argue that the pets _should_ be able to
stick around, but the original intent was for them to be removed
alongside their owner.

Fixes #721
2022-06-25 17:16:09 +02:00
Nesseley 3e2c614c18
Remove normal/undyed shulker boxes from My Items inventories.
Removes the first `_` in the `_SHULKER_BOX` matching to ensure that normal shulker boxes with ID `SHULKER_BOX` are removed from My Items as well.


Co-authored-by: Andreas Troelsen <garbagemule@gmail.com>
2022-03-07 17:45:16 +01:00
Andreas Troelsen c88f20c46f Add per-arena setting `auto-ignite-fuse`.
This new setting allows changing the fuse time for auto-ignited TNT,
which is normally a hardcoded 80 ticks in Minecraft. Note the somewhat
weak safeguarding without any sort of error message - with great power
comes great responsibility...

Closes #715
2022-02-13 00:33:18 +01:00
Andreas Troelsen b9b4d0d204 Replace metadata "planter" logic with TNT primed source.
Now that MobArena sets the TNTPrimed source on auto-ignited TNT, the
"planter" logic becomes _somewhat_ obsolete. This is due to the fact
that manually ignited TNT blocks produce a TNTPrimed entity with the
source property set to the player that ignited the block.

Because the "planter" logic in the BlockIgniteEvent handler didn't
actually work, this change shouldn't actually do anything in that
regard. That is, a TNT block ignited by a player would still have that
player as its source regardless of who the planter was, because the
procedure that would have otherwise set the planter of the TNTPrimed
entity never ran.
2022-02-12 23:53:04 +01:00
Andreas Troelsen 4470b60aaf Remove unused code block.
Turns out igniting TNT _doesn't_ fire a BlockIgniteEvent but rather an
ExplosionPrimeEvent, which means this code has never actually been of
any use. This code block was likely dead on arrival and has been dead
for almost 9 years. Wonderful...

The proper handling here would be to listen for the PlayerInteractEvent
and detect flint and steel interactions with TNT blocks. This is super
cumbersome, however, because the event handler would have to listen on
the MONITOR priority to ensure that nothing changes down the line. What
we are _actually_ need to do is remove the TNT block from the arena's
block set when it is ignited, but since this hasn't worked for almost a
decade, we're not really in a hurry to fix it now. It just makes for a
slightly slower (but negligible) cleanup procedure most of the time.
2022-02-12 23:30:21 +01:00
Andreas Troelsen d3e5d44cb2 Set source property on TNTPrimed entities.
This property makes TNT explosions look more "real" to other plugins who
may be consuming events from MobArena's sessions. It also gives way to a
potential rework of the "planter" logic that currently makes use of the
Bukkit Metadata API.

Closes #718
2022-02-12 22:42:39 +01:00
Andreas Troelsen d4dcc8dc90 Don't nag players with `mobarena.admin.teleport`.
Changes the behavior of the ArenaListener's teleport event handler such
that it _ignores_ teleport attempts instead of _rejecting_ them when the
player in question has the `mobarena.admin.teleport` permission. Because
the global listener's event handler only ever checks the permission if
at least one per-arena listener has _rejected_ the teleport attempt, and
none of them have explicitly _allowed_ it, the change means that it will
never check the permission, because its internal `allow` flag will never
change to `false`. Thus, the check can be safely removed from the global
listener's logic.

When the response is to ignore instead of reject, the message that would
have otherwise been sent to the player is skipped. This fixes #702.

It is perhaps tempting to move the permission check up into the section
of sanity checks in the global listener, but this is very specifically
avoided to allow MobArena to _uncancel_ teleport events that have been
cancelled by other plugins, but that MobArena might need to go through.
Please see afcc526a71 for more info.
2022-02-12 19:06:40 +01:00
Andreas Troelsen 77b2525707 Cancel damage to players by pets.
This is technically not necessary when pets are simple entities like
wolves and zombies, because these types of pets will never target and
thus attack other players in the arena. However, projectile entities
such as Blazes and Ghasts may hit players in the line of fire, and so
any such damage should be cancelled.

Fixes #712
2022-02-12 17:55:58 +01:00
Ghmmy d5ea15fa08
Add per-arena setting `clear-wave-leeway`.
Introduces a new arena setting that changes the "emptiness check" for 
`clear-wave-before-next`, `clear-wave-before-boss`, and the final wave 
check, allowing for a customizable _leeway_.

By default, the leeway is 0, which means the monster set has to be
completely empty for the checks to pass. With a value of 2, the set
may contain up to two monsters for the checks to pass, and so on.

The leeway should help alleviate some of the issues some people have
with their arena sessions when monsters "disappear" behind terrain or
end up in hard-to-reach areas. This is by no means a real "solution"
to the underlying problem, since the build-up of monsters in longer
sessions will just result in the issue being pushed to later waves.

We'll see if the setting leaves any additional customization to be
desired and perhaps defer any such requests to the Sessions Rework.

Closes #706
2022-02-12 16:57:47 +01:00
sepulzera be6be4bb98 Reset player velocity on select teleports.
Sets the player fall-distance to 0 before performing a player teleport
during "move player" steps. This should cancel out the any fall damage
that may have built up before the teleport was initiated.

Fixes #698

Co-authored-by: Andreas Troelsen <garbagemule@gmail.com>
2022-02-12 16:24:44 +01:00
Andreas Troelsen 9164b125bd Allow flaming arrows to ignite TNT.
It turns out that, according to the Spigot API, flaming arrows actually
_change_ TNT blocks to air before/instead of igniting them. While this
is a little counter-intuitive, the fix seems to revolve around allowing
the change to happen if the changed block is TNT and the "changer" is an
arrow.

Allowing the change event to happen means "foreign" primed TNT entities
will spawn, which makes it necessary to clean them up during the session
cleanup phase in ArenaImpl.

Fixes #696
2021-12-05 17:30:24 +01:00
Andreas Troelsen c80f0375ee Copy cancel state to fake EntityExplodeEvent.
It's not clear if this change actually solves the underlying issue,
because it has not been tested in a controlled environment. However,
considering the EntityExplodeEvent constructor, which doesn't take a
cancel state value, and the `HIGHEST` priority setting of MobArena's
event handler, the issue is realistic, and this change very likely
solves the issue.

Fixes #704
2021-12-05 16:18:54 +01:00
Andreas Troelsen 2c12112fd0 Update JUnit to 4.13.2. 2021-10-13 23:25:13 +02:00
Andreas Troelsen fe9d160edd Update Mockito to 3.12.4. 2021-10-13 23:25:13 +02:00
Andreas Troelsen 4348a0497e Use arena slugs in arena sign update handler.
The arena signs predate the use of arena slugs everywhere, so something
slipped through the cracks in this regard. Incidentally, the handler for
arena updates is one of the few classes in the signs package that has no
unit tests, probably due to it being "obvious implementation". Not so
obvious after all, it seems, so now we have a basic test for it.

Fixes #705
2021-10-13 23:25:13 +02:00
Maroon28 3693c039a6
Force adult entities; additional baby entities.
This commit (unfortunately) makes two somewhat unrelated changes:

- Forces certain ageable entities into adulthood to prevent the occasional baby spawns (fixes #687).
- Introduces support for baby versions of certain ageable entities (fixes #689).


Co-authored-by: Andreas Troelsen <garbagemule@gmail.com>
2021-10-13 23:24:56 +02:00
Griffin caee8be6ca
Add boss abilities with `all` modifiers.
Introduces four "new" boss abilities that work like their pre-existing counterparts, but affect all players in the arena. This makes the four _ability sets_ a bit more complete, as it were, in that it is now possible to choose between single targets, nearby, distant, or _all_ players for the given abilities.

Closes #434
2021-08-30 19:02:30 +02:00
Nesseley c9766dee97 Use spaces for "empty" titles.
Due to a breaking change in Spigot 1.17, sending a title with an empty string no longer works, even if a non-empty subtitle is provided. The TitleAnnouncer and TitleHealthBar components only use the subtitle portion of the title API to avoid obtrusive behavior.

With this commit, the two components send a single space character as the title. Effectively, there is no difference between showing the empty string or a single space on the client-side. It's a dirty hack to fix something that should be fixed in Spigot instead, but odds are that it won't be.

Fixes #683
2021-08-09 21:32:55 +02:00
Andreas Troelsen c143cc81c9 Add per-arena setting `auto-leave-on-end`.
Introduces a new arena setting to force spectators to leave the arena
when the current session ends. If set to `true`, it "overrides" the
behavior of `spectate-on-death: true` when players respawn, such that
they only become spectators if there is an active arena session.

The respawn behavior technically means that it is possible for a player
to begin spectating "the next session" if a new session begins between
them dying and clicking Respawn on the death screen. Ideally, we would
want the player to auto-leave, because the session _they_ participated
in ended, but we don't have a concept of "previous sessions", so this
quirky behavior will have to do.

Closes #682
2021-08-08 00:56:41 +02:00
Andreas Troelsen 52226fa1c9 Make Thing extend ThingPicker.
By turning things into their own thing pickers, we can avoid creating a
bunch of SingleThingPicker wrappers, which are now redundant. Because
the semantics of using things and thing pickers are quite different, it
does perhaps make it necessary to be a bit more careful about picking
the right type.
2021-08-07 16:08:23 +02:00
Maroon28 1c225d93f9
Make piglins and hoglins immune to zombification
When piglins, piglin brutes, and hoglins go through zombification, the current entity is removed, and a new one spawns. MobArena prevents the zombified entity from spawning, so it feels like the mobs just "vanish".

We don't actually want the mobs to zombify, however, so this commit makes them immune to the zombification process. Note that this shouldn't cause any problems on server versions prior to 1.16, because the piglin and hoglin keys never get registered on those versions, so there is no risk of hitting those branches in the `switch` statement.

Fixes #684

Co-authored-by: Andreas Troelsen <garbagemule@gmail.com>
2021-08-07 16:05:18 +02:00
Bobcat00 6be130daf1
Make pet names per-class configurable.
Adds a new optional `pet-name` property to arena class configurations that lets server owners set a custom pet name template instead of the hardcoded "<player>'s pet". This allows for translation and color coding. To accommodate setups where the player's "display name" isn't a good fit, e.g. because it is too long, the more generic "player name" is available for use instead.

Closes #595 

Co-authored-by: Bobcat00 <Bobcat00@users.noreply.github.com>
Co-authored-by: Andreas Troelsen <garbagemule@gmail.com>
2021-08-07 15:13:44 +02:00
Maroon28 a21f47e193 Don't remove monster weapons.
Changes the way monster equipment is handled from clearing _all_ items to clearing just the armor contents. This means that monsters that naturally spawn with weapons won't need their weapons added back in.

It also means that monsters that _occasionally_ spawn naturally with weapons now may do that in arenas. This is deemed acceptable, because:

- occasional weapons don't have as much of an impact as occasional armor,
- the forwards compatibility aspect is too valuable to pass up on, and
- should occasional weapons become too much of an issue, they can be conditionally removed, i.e. we can implement the _inverse_ behavior of what we had previously.

Fixes #641
Fixes #686

Co-authored-by: Andreas Troelsen <garbagemule@gmail.com>
2021-08-07 14:59:07 +02:00
Andreas Troelsen 7b9a9505b9 Reformat code in `things` package.
This is mostly just line breaks at the top and bottom of classes and
interfaces, but also marks some fields `final`.
2021-08-07 12:29:44 +02:00
Andreas Troelsen cad2eef8ba Optimize imports in `things` package.
We're good with on-demand imports for the static Hamcrest and Mockito
functions in test code, because the arrange and assert steps of unit
tests all make use of them, so it's known where they come from.
2021-08-07 12:20:45 +02:00
Andreas Troelsen 9fec49cc58 Make ThingGroupPicker aware of result set size.
This commit makes the ThingGroupPicker return different values depending
on the size of the picked result set. If the set is empty, the picker
returns null, emulating a NothingPicker. If the set is a singleton, the
element itself is returned, emulating the SingleThingPicker. Finally, if
the set contains more than one element, a ThingGroup containing those
elements is returned.
2021-08-07 01:36:37 +02:00
Andreas Troelsen 823be96b4e Guard against `nothing` in ThingGroupPicker.
This commit filters the result list of a ThingGroupPicker by a non-null
predicate to avoid null values in the resulting ThingGroup instance.

Since null values represent `nothing`, and we don't usually announce it
when players earn a `nothing` reward, it makes sense that they wouldn't
bubble up and somehow "manifest" in groups of things either.

Fixes #691
2021-08-07 01:36:35 +02:00
Andreas Troelsen 286071871f Add support for angry bees.
This commit introduces a new type variant, `angry-bee`, which is a bee
whose anger level is maximized upon spawning, much in the same vein as
its angry wolf cousin.

Note that bees are not available prior to Minecraft 1.15, so a sentinel
`null` name is used in the registration to prevent warnings from being
logged on server versions that don't have a concept of bees.

Closes #584
2021-07-30 18:21:41 +02:00
Andreas Troelsen c1716709f3 Add changelog bullet about targeting 1.17.
Because this bullet covers both the commit with the version bump and the
following compatibility commits, it didn't really fit properly into any
one of those commits, so now it gets its own!
2021-07-30 18:21:41 +02:00
Andreas Troelsen 15698d3eee Use block data to check if blocks are signs.
Instead of relying on the evolving Material enum values, we can use the
new BlockData API as advised by some of the folks "in the know". It is
unclear how much of a performance impact the the "block data gathering"
and `instanceof` checks incur, but this is a pretty secluded place in
the code base, so probably nothing to worry about.

An alternative solution could have been to check if the _name_ of the
Material equals "SIGN" or "WALL_SIGN", or ends with "_SIGN". That should
cover all cases in a sorta kinda safe manner, but it isn't as resilient
as the BlockData/BlockState hierarchies.

We could also employ the new Materials utility class and enumerate all
sign types by name and just check for membership of the resulting set,
but this creates another brittle crash point.
2021-07-30 18:21:41 +02:00
Andreas Troelsen b4cd509eff Use "legacy-aware" utility class for Material types.
Introduces the Materials utility class, which works much like the static
MACreature registration process, but for certain material types. Instead
of storing everything in a stringly typed map, certain Material values
are stored as constants.

Right now it's just the `OAK_SIGN`/`SIGN` pair for the autogeneration in
MAUtils, and chances are we can throw it out at some point in the near
future, but at least now there's room for more materials, should the
need arise.
2021-07-30 18:21:41 +02:00
Andreas Troelsen b49920fc38 Make fallback default wave consistent with default config.
Changes the fallback default wave in the wave parser to be consistent
with the default wave of the default config-file. Not a huge deal, but
it does feel a bit more neat.
2021-07-30 18:21:41 +02:00
Andreas Troelsen 36237ebe2d Replace pig zombies in the special wave of the default config.
Replaces the `zombie-pigmen` entry in the `spec1` wave of the default
configuration with `slimes-big`. This is due to the pig zombie rework
into piglins, which makes the custom name a little flaky. If we ever
decide to ditch the custom names somehow, this will probably make such a
transition a little easier.

While `slimes-big` is also a custom name, so are `powered-creepers` and
`angry-wolves`, but at least they are "API stable" in comparison, which
is really what this change is about.
2021-07-30 18:21:41 +02:00
Andreas Troelsen 4de0ce258b Tidy up code style in MACreature.
It's K&R style braces in all modern portions of the code base, and
IntelliJ keeps complaining about final values, so let's just make it
happy.

The `plural` field should go away at some point, but for now it's fine
to just null it out, since the legacy `register()` method is only ever
used by the deprecated-for-removal constructors, both of which actually
set it to something non-null.
2021-07-30 18:21:41 +02:00
Andreas Troelsen 72f4d16e6f Deprecate old MACreature constructors for removal.
The new constructor and registration method pair is what other plugins
should be using. They provide a slightly cleaner interface, and we don't
need so many different ways to do the same thing.
2021-07-30 18:21:41 +02:00
Andreas Troelsen 490df61375 Refactor MACreature registrations.
This commit changes the way MACreature initializes its internal map of
available monster types. Specifically, it replaces all static references
to EntityType enum values with a series of registration helper methods
that try to resolve EntityType enum values from a given set of strings,
stopping on the first match. If nothing matches, no registration, but a
warning is logged unless a specific sentinel `null` value is passed as
an argument, in which case the key is just silently skipped (not in use
as of this commit, but it will be necessary for modern types like bees
from 1.15+).

While this is arguably a huge step back in terms of type safety, it does
make the code base more resilient to API version bumps, and it allows
compiling against modern API versions and still run on older (1.13-1.16)
servers. It _does_ mean that changes to the EntityType enum (such as
PIG_ZOMBIE to ZOMBIFIED_PIGLIN) must be discovered manually, which makes
maintenance a little more difficult. The warning in the server log does
make it fairly easy to fire up an early build of a new server version
and check if everything initializes correctly.
2021-07-30 18:21:41 +02:00
Andreas Troelsen 95b65371f1 Switch to Spigot API and bump version to 1.17.
Spigot has decided to discontinue distribution of the Bukkit artifacts,
which means Bukkit "doesn't exist" after Minecraft 1.15. This commit
takes the plunge on that change and moves to the Spigot API rather than
the Bukkit API. Goodbye, Bukkit. Hello, Spigot.

The version is also bumped to 1.17 to give way to new features and bug
fixes otherwise blocked by staying on 1.13. Unfortunately, this brings
with it a swath of potential issues with version compatibility going
forward, because backwards compatibility in the API sometimes takes a
back seat, such as with the Material and EntityType enums. This commit
specifically changes some references that break compatibility with older
server versions, but compatibility with those is reinstated later.

Finally, the Spigot repository is changed to match the consensus about
which path on the Spigot Nexus instance to use. This seems to be what
everyone else is doing, so `bandwagon.jumpOn()`.
2021-07-30 18:21:39 +02:00
Andreas Troelsen f1cfd4136e Update Mockito version.
This makes the test setup compatible with Java 16.
2021-07-07 00:39:59 +02:00
Andreas Troelsen b99713f1ec Add reload events.
This commit introduces two reload events, MobArenaPreReloadEvent and
MobArenaReloadEvent. Both events are fired when MobArena reloads, the
former right before the reload, and the latter right after.

The reason for two events is to allow plugins to reload either entirely
or partially along with MobArena, if it makes sense for them to do so,
_when_ it makes sense for them to do so. Some plugins need to reload and
re-register themselves with MobArena before the config-file itself is
loaded (e.g. ThingParsers), while others either require MobArena to be
fully loaded or are more decoupled and thus don't depend on anything in
MobArena's innards to function. The "pre" event is for the former, while
the other event is for the latter.

As for naming, the choice of "Pre" and no prefix was made for the sake
of consistency with the event names in the Bukkit API, which has just
one example of such a pair (PlayerPreLoginEvent and PlayerLoginEvent).
Some event naming conventions (e.g. in the .NET world) seem to favor
present and past tense (reloading, reloaded), but this would be wildly
inconsistent with the rest of the event names, so it might be better to
just stay consistent. Names may change before an actual release, but for
now, this is what we're rolling with.

Closes #677
2021-07-07 00:14:29 +02:00
Andreas Troelsen 903752d23a Add support for registering subcommands by instance.
This commit introduces a new `register()` method on the CommandHandler
class to allow registering pre-instantiated subcommands. This means that
subcommands are no longer restricted in terms of instantiation, so they
can have their dependencies injected at creation, rather than having to
resort to Singleton anti-pattern means.

Also refactors the existing internal MobArena command registration to
use the new method to "drink our own champagne" and to reuse the code.

Fixes #675
2021-07-01 22:59:05 +02:00
Andreas Troelsen 614da20df8 Make `/ma playerlist` pattern less greedy.
This commit changes the pattern of the player list command to one that
isn't quite as greedy. This change is enough to fix issue #676 and thus
allow MobArenaStats to take control of its own command.

The command framework could definitely do with a bit of a rework away
from pattern matching towards aliases, which would prevent similar
issues from cropping up down the line. For now, this is good enough.

Fixes #676
2021-07-01 22:57:21 +02:00
Andreas Troelsen 2c2c36b880 Bump version to 0.106.1. 2021-05-10 09:08:19 +02:00
Andreas Troelsen 498cef9ec7 Release 0.106. 2021-05-10 09:07:50 +02:00
Andreas Troelsen 30c059f51b Include bStats, Github Actions in changelog.
These should have been included in their respective commits, but they
didn't seem particularly interesting for end users at the time. Might as
well include them, though.
2021-05-09 13:43:55 +02:00
Andreas Troelsen b08c74afbd Replace Travis CI with Github Actions.
This commit introduces a Github Actions workflow in the form of the file
`.github/workflows/build.yml`, replacing Travis CI and its `.travis.yml`
file.

The new workflow does a little bit more than Travis CI: it uploads an
artifact after a successful build. Due to a known issue with Github's
package UI, the MobArena.jar file is dynamically zipped on download, so
we get MobArena.jar.zip containing just MobArena.jar. It's redundant,
but there doesn't seem to be a simple way around it at the time of this
commit, so we'll leave it be.

For now, the workflow runs on every push to every branch. I'm probably
going to regret that, but we'll leave that as it is for now as well.
2021-04-15 17:24:56 +02:00
Andreas Troelsen c7e740040c Update bStats Metrics to 2.2.1.
The previously nested classes are now standalone, so imports and super
class definitions of the custom various charts are updated as well.
2021-04-11 13:11:47 +02:00
Andreas Troelsen 9081ec8055 Add support for custom formulas.
This commit re-frames the formula concept used by the wave growth, swarm
amount, and boss health wave configuration properties. It fundamentally
changes how these values are calculated, from a static, compile-time set
of enum values and hardcoded expressions, to a powerful math expression
feature that supports constants, variables, operators, and functions.

In part to remain backwards compatible with existing MobArena setups,
and in part for a better user experience, the old enum-based expressions
are relocated into a new file, `formulas.yml`, as _macros_. The file is
written to the plugin folder if missing, and it contains a formula for
each of the legacy values for each of the enums. Additionally, it has a
global section with some predefined macros for inspiration's sake. The
goal of this file is to allow people to define new formulas and reuse
them in their wave configurations instead of having to duplicate the
same formulas again and again.

Parts of the system are extensible. It is possible for other plugins to
register additional constants, variables, operators, and functions.

Closes #460
Closes #461
2021-04-07 22:13:03 +02:00
Andreas Troelsen 0da90f3963 Rework arena sign data store.
This commit constitutes a major rewrite of how arena signs are stored
and loaded. It fixes an issue where an unloaded or missing world would
cause MobArena to throw errors if there were any arena signs recorded in
said world.

The solution is to load signs of a given world when it is available,
rather than loading all signs at once indiscriminately. At startup,
signs for all _currently available_ worlds are loaded. This fixes the
errors. When a world loads and a WorldLoadEvent is fired, signs in that
world are loaded. This ensures that all valid signs in existing worlds
will _eventually_ load. To keep things tidy, a WorldUnloadEvent will
unload all signs for the unloaded world.

Bukkit's own YAML deserialization implementation doesn't re-throw all
deserialization errors, which means we can't actually catch the problem
of missing worlds without doing an awkward "scan" of the deserialized
objects. This prompted a rewrite of the serialization and data storage
into a custom CSV-like format which is both simpler and also provides a
lot more control over the process. Instead of relying on world _names_,
the new format uses world _UUIDs_. While the rest of the plugin won't
necessarily adapt well to a world being renamed, the signs data store
should be resilient enough to handle it.

Most of the actual sign rendering code and almost all of the template
code is intact, but quite a lot of other classes have been rewritten or
replaced. Some of the rewrites weren't strictly necessary, but because
the components were already fairly small, rewrites were much faster and
a lot less awkward than attempting to adapt existing code.

Fixes #645
2021-03-25 10:20:49 +01:00
Andreas Troelsen cf296704b0 Clean up .gitignore.
This file has an interesting history with content that goes all the way
back to MobArena's dark days of both Ant and Eclipse. This commit cleans
up the file a bit, removing a lot of unnecessary bits and pieces, and
making it a little more tightly bound to the current project setup.
2020-11-08 19:27:22 +01:00
Andreas Troelsen 9bc23c61a0 Bump version to 0.105.1. 2020-11-08 18:34:37 +01:00
Andreas Troelsen 3bef4948a6 Fix date of 0.105 release in changelog.md.
Time flies, oh my!
2020-11-08 18:33:18 +01:00
Andreas Troelsen 5f62b8013b Release 0.105. 2020-11-08 17:35:31 +01:00
Andreas Troelsen 532a2ce0da Add Maven Wrapper.
This should make it a lot easier to get started with the project and to
build a custom artifact from source.
2020-11-04 01:21:50 +01:00
Andreas Troelsen 07f0ce5d08 Add changelog entry for water bottle fix on 1.8.
This is a bit awkward to deal with, because the actual fix exists only
on the 1.8 branch, but we release from the main branch, so we want the
changelog entry to come from there.
2020-11-04 01:12:27 +01:00
Andreas Troelsen d960aebbb0 Reformat changelog for upcoming changes.
Rearranges and reformats the changelog entry for the upcoming version to
better fit with the Keep a Changelog guidelines. We're already grouping
stuff decently, but we can do better with section headers. In an entry
like this one with lots of changes, it just makes the reading experience
much better, and we do want people to read the changelog :)
2020-11-04 01:09:28 +01:00
Andreas Troelsen 4ceb5b7bec Allow use of "config name" in `default-class`.
This is a very simple change that could potentially save a lot of
headaches when people look elsewhere in their config-file and see a
class name like "Big Gary" and wonder why putting that value in the
`default-class` property won't work.
2020-11-04 01:05:53 +01:00
Andreas Troelsen 6c693294bd Remove unused methods in MAUtils.
Slowly but surely, we're getting rid of this abomination...
2020-11-03 20:08:00 +01:00
Andreas Troelsen 70e51d24fb Return config name in Arena `arenaName()` method.
This is really just an internal change, since the `arenaName()` method
is likely not used anywhere. If it is, however, it shouldn't be depended
on as an identifier anyway, so I'm confident that removing it won't mess
up anything that doesn't need a rework anyway.
2020-11-03 20:04:16 +01:00
Andreas Troelsen 7288fc566b Return class name slugs in MobArenaHandler. 2020-11-03 19:58:30 +01:00
Andreas Troelsen 31282014b3 Use slugs internally in Upgrade Waves.
This just removes the internal use of "lowercase name" in Upgrade Waves
and _should_ have no effect on the actual config-file and the parsing
done there.
2020-11-03 19:58:30 +01:00
Andreas Troelsen 2a87aef9f3 Use class "config name" on leaderboard signs.
Same as with arena names, we can just use the class "config name" on
leaderboards for a prettier experience.
2020-11-03 19:58:30 +01:00
Andreas Troelsen 06cedde031 Allow multi-word names in `/ma addarena` and `/ma autogenerate`.
Introduces support for multi-word arena names in the two commands. The
approach is to simply join the arguments by spaces. Because of the new
slug-based lookups, multi-word names are fairly straightforward.
2020-11-03 19:58:30 +01:00
Andreas Troelsen 0f93d8ac05 Resolve arenas by name using slugs.
This commit is a minor refactor of how arenas are resolved by name,
similar to the previous arena class resolution refactoring.

The difference this time around is that there is an ArenaMaster method
that does most of the work for a lot of different areas of the plugin,
`getArenaWithName(String)`. This method is called from well over 30
different places in the code base, making it a cornerstone of all arena
resolution. Luckily, it is called primarily from places that shouldn't
care _how_ an arena is resolved.

Affected by this commit are all commands that resolve arenas by name,
including all those not listed in the diff, because of the change to
`getArenaWithName(String)` on ArenaMaster.

Commands that can tab complete arena name arguments now complete slugs
instead of config names.
2020-11-03 19:58:30 +01:00
Andreas Troelsen fdb84dfaf4 Use Arena "config name" on leaderboard headers.
Going forward, it should be possible to show "pretty" arena names by
simply using a pretty name in the config-file instead of slug-like names
now that slugs are on the horizon.
2020-11-03 19:58:30 +01:00
Andreas Troelsen f10c7e464c Deprecate Arena `arenaName()` method.
Conversely to how the "lowercase name" of ArenaClass was deprecated in
favor of the slug, the "arena name" method here is deprecated in favor
of the "config name" of arenas. Instead of trying to pretty up an arena
name (which is doomed to fail), we just use the "config name" for pretty
printing instead, and start using slugs elsewhere.

This should give way to a much better experience with multi-word arena
names as well.
2020-11-03 19:58:30 +01:00
Andreas Troelsen 037c2ffa43 Use arena class "config names" in `/ma autogenerate`.
Swaps the weird "camel casing" approach in the autogenerate command out
with simply using the "config name" of the arena classes when creating
class selection signs.

This fixes the breaking change to how classes are resolved in a previous
commit, but only in future arena generation procedures. Arena generated
before this change may still contain broken class signs.
2020-11-03 19:58:30 +01:00
Andreas Troelsen 5bcab8fa46 Use arena class slugs for class selection.
This commit is a minor refactoring of the class selection functionality
plugin-wide. Instead of selecting classes based on the "lowercase name"
of a class, commands and listeners are now "slug aware", as it were.

The ArenaClass class now uses its slug instead of its "lowercase name"
for equality and hash codes.

The `/ma class` command now tab completes slugs, but it still supports
class names as they appear in the config-file when executing the command
itself. The same applies to the `/ma classchest` command.

The sign handling in ArenaListener slugifies sign text instead of just
lowercasing and stripping spaces.
2020-11-03 19:58:30 +01:00
Andreas Troelsen 519886cf3e Use slugs in arena and class permission checks.
This commit changes the permission checks for arenas and classes to a
slug-based approach instead of the "config names", which are somewhat
arbitrary and may contain spaces, which are generally not supported by
permissions plugins.

This is a breaking change, which means it will be necessary for users
to change their permission setups. Backwards compatibility could have
been implemented, but it just leaves more room for ambiguity and will
make a necessary transition later down the road less obvious. Instead,
we burn the ships!

As a result of this change, access to the "My Items" class can now be
revoked as intended with the key `mobarena.classes.my-items`.

Fixes #647
2020-11-03 19:50:29 +01:00
Andreas Troelsen 1b46e17e38 Deprecate ArenaClass `getLowercaseName()` method.
This commit deprecates the "lowercase name" method on ArenaClass to push
the new slug concept throughout the code base in a "healthy" way.
2020-11-01 14:28:40 +01:00
Andreas Troelsen aed57f5cb6 Introduce slugs in arenas and classes.
Whitespace and punctuation in identifiers is a fairly big source of
issues in areas like permissions and commands where whitespace isn't
directly supported (or at least makes things needlessly difficult).

This commit introduces the concept of a "slug" in arenas and classes,
giving them a _consistent_ `kebab-case` name for use in such places, but
it does not implement their use anywhere. Slugs are expected to be the
solution to problems like the one posed in issue #647.

At the time of writing, we're only concerned with simple stuff like
removing periods, commas, parentheses and replacing underscores and
spaces with dashes. If it turns out that people have unanticipatedly
problematic arena and class names, we may have to expand the slug
definition rules or allow for custom slugs.
2020-11-01 14:28:33 +01:00
dependabot[bot] 5ebdf45f6a
Bump junit from 4.12 to 4.13.1
Bumps [junit](https://github.com/junit-team/junit4) from 4.12 to 4.13.1.
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.12.md)
- [Commits](https://github.com/junit-team/junit4/compare/r4.12...r4.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-13 04:37:10 +00:00
Andreas Troelsen efc66820c6 Add specific Msg value for `/ma addreward`.
This commit introduces a new announcement value for the `addreward`
command. This allows server owners to completely remove the message by
setting it to the empty string while retaining wave reward messages.

In general, we shouldn't reuse announcements unless the actions that
send them are identical, because it makes everything a lot more tangled
than it has to be.
2020-09-19 10:57:17 +02:00
Andreas Troelsen dafae0cf07 Add `/ma addreward` command.
This command takes an arena player and a Thing as input. The Thing is
parsed in the same way as all other rewards are parsed, so it supports
the new `all()` and `random()` functions and the `nothing` keyword.

The "target audience" of the command is mainly scripts. It opens up the
possibility of hooking into MobArena's rewards from command blocks or
other plugins, but with very low coupling.

Closes #643
2020-09-19 10:32:03 +02:00
Andreas Troelsen 3b132d28dd Use ThingPicker for boss rewards.
By using a ThingPicker instead of a Thing, boss rewards can now, just
like regular wave rewards, make use of the new `random()` and `all()`
functions as well as the `nothing` keyword.

Closes #628
2020-08-24 22:53:00 +02:00
Andreas Troelsen d30bd96a2a Add support for picking `nothing`.
This commit introduces the strange concept of a singleton ThingPicker
that only ever picks `null`. The purpose of this picker is to allow for
a type of "loot table" experience similar to that found in other games.
An example would be a piece of equipment that only has a 50 % chance of
dropping. With the current state of MobArena, it would be necessary to
something conjure up a CommandThing or something to emulate nothingness,
but now there is native support for it with the `nothing` keyword.

The nullability of rewards also has the side effect that we got to clean
up the MASpawnThread `addReward` method a bit.

Closes #638
2020-08-22 17:36:30 +02:00
Andreas Troelsen 994ebaff81 Allow for reward grouping.
This commit makes the MAUtils class responsible for parsing reward maps
use the new ThingPickerManager to parse the reward lists. As a result,
the new `all()` and `random()` pickers are available for use in the
rewards section.

This should allow for granting item sets and similar types of "bundles"
as rewards in the arena, e.g. a diamond sword and permission to join a
more difficult arena.

Closes #386
2020-08-22 17:12:29 +02:00
Andreas Troelsen bff1ab5694 Convert rewards to the new ThingPicker framework.
This commit makes breaking changes to the Arena interface to switch to
the new ThingPicker framework for rewards. It is unfortuante that the
Arena interface has so many disparate responsibilities that changes like
these are necessary on such a central component when the change itself
is actually very isolated to just rewards handling.

With this change, rewards are now wrapped in pickers, which should give
way to some grouping and "well-defined entropy".
2020-08-22 17:12:29 +02:00
Andreas Troelsen dd54f70682 Add ThingPickerManager to main plugin class.
Introduces the ThingPicker framework to the plugin by bootstrapping a
manager for it in the main plugin class. By default, we're just running
the group and random picker parsers, but other plugins should be able to
hook in with their own parsers.

Nothing actually _uses_ the pickers yet, but that's next on the menu.
2020-08-22 17:12:29 +02:00
Andreas Troelsen 2d0aad19d6 Add ThingPickerManager.
This little nugget is what we will wire up in the main plugin class once
we're ready to introduce it to the code base for real. Its purpose is
similar to that of the ThingManager, in that it _is_ a parser and can be
passed around wherever a parser is needed, but it also is the main entry
point for anything that is ThingPickerParser instances.
2020-08-22 17:12:29 +02:00
Andreas Troelsen 5566d8fd86 Introduce ThingPickerParser.
To ensure that we are extensible from the get-go, this commit introduces
the parser aspect of the ThingPicker framework with parsers for the two
non-trivial picker implementations.

Nothing is wired up yet.
2020-08-22 17:12:26 +02:00
Andreas Troelsen e8bb8a9e4d Introduce ThingPicker.
Things work best as hardwired, atomic pieces of "stuff", so fitting RNG
into the framework is a little difficult. We could have a RandomThing,
but the issue with that is that the entropy has to live inside of it for
it to be random every time it is given, taken, or checked. This makes it
impossible to make a well-defined, atomic Thing with entropy involved.

Enter the pickers.

The ThingPicker is an early stage extension to the Things framework that
encapsulates the possibilities of entropy and grouping _around_ Things,
a type of Factory pattern.

This introductory stage consists of a "leaf picker" that can only ever
pick a single Thing, a Composite Pattern "group picker" that wraps a
list of other pickers and shoves their resulting Thing instances into a
ThingGroup (introduced in the previous commit).

These simple constructs should now make it possible to introduce entropy
without placing the responsibility on the Thing framework itself. Using
this extension requires adapting code to using the ThingPicker interface
instead of using Thing directly.
2020-08-22 16:10:07 +02:00
Andreas Troelsen e1784552de Add ThingGroup.
Introduces a group of Things as a construct for bundling Things that
should constitute a single unit for whatever reason.

One reason could be to make an _item set_ an atomic reward, e.g. a set
of diamond tools for beating the `workshop` arena. Instead of having to
spread the rewards out into multiple waves or use a different plugin to
create item groups, this component allows MobArena to support that kind
of intent natively.

Note that nothing is wired up in this commit, so really this commit is
just introducing unused code.
2020-08-22 15:00:11 +02:00
Andreas Troelsen 4f11889549 Add per-arena setting `announcer-type`.
Introduces the concept of an Announcer, which is invoked whenever the
`announce` methods on the Arena interface are invoked. Previously, all
announcements would simply be sent to the arena's Messenger, but this
new feature allows us to move them up into titles to help declutter the
somewhat overloaded chat box.

By default, to help people discover the new feature, the announcer type
is `title`, but it is possible to switch back to `chat` for the original
behavior.

Closes #272
2020-08-22 11:32:45 +02:00
Andreas Troelsen 31010c1576 Initiate player leave when `spectate-on-death: false`.
Errata to commit 84a7a2ed8a.

The protocol after the join/leave process rework was:

- `spectate-on-death: true`: leave, then spec
- `spectate-on-death: false`: leave

The protocol now is:

- `spectate-on-death: true`: spec
- `spectate-on-death: false`: spec, then "leave"

I put "leave" in quotes, because _discarding_ the player here does not
invoke the part of the leave protocol that tries to ensure teleports go
through according to plan. By capitalizing on the "leave" part, making
it explicit that that's what we're doing in the next tick after warping
the player to the spectator area, we return to the form of the original
rework where we use coarser-grained state control to make things easier
to reason about.

A side-effect of this change is that if players somehow manage to kill
themselves in the lobby, they will be refunded the entry fee.
2020-08-22 09:42:45 +02:00
Andreas Troelsen 514c03dad0 Strip trailing whitespace; add missing newlines.
This promiscuous commit goes around touching almost every file in the
repository with the intention of removing trailing whitespace and adding
newlines to files missing them.

Trailing whitespace has been a pain for along time, especially in terms
of contributing to the project without accidentally littering commits
with whitespace stripping, so this commit is long overdue.

As for the newlines, well, the script I found on StackOverflow to strip
trailing whitespace also happened to add missing newlines, which is
something I wanted to tackle anyway, but in a different commit. It's all
good though.
2020-08-21 00:23:14 +02:00
Andreas Troelsen 043d970593 Remove obsolete TODOs.
Some of them have been mangled somehow, and others just straight up
aren't relevant anymore. Tidy up!
2020-08-20 23:45:07 +02:00
Andreas Troelsen 64d55673a4 Add per-arena setting `arena-warp-offset`.
This commit introduces a quality-of-life feature for spreading players
out during the warp to the arena floor. The feature is configured via
the new per-arena setting `arena-warp-offset`. When the value is greater
than 0.01, MobArena will add a random number of units between 0 and the
offset value to the X and Z axes in either direction. For example, if
the value is set to 3, players will be teleported to a random location
in a 3x3 square centered on the arena warp, rather than directly to the
arena warp itself.

Closes #371
2020-08-20 23:30:57 +02:00
Andreas Troelsen b343976dc8 Clone class chest contents before modifying them.
Certain class chest items are cloned before they are made unbreakable,
but not all. This commit changes that, so all items in the input array
are cloned prior to setting the unbreakable flag on them. The items that
were previously cloned specifically no longer need to be, since they are
included in the initial cloning.

Fixes #637
2020-08-19 23:03:08 +02:00
Andreas Troelsen 7f7fb1631c Bump to `HIGHEST` priority for PlayerRespawnEvent.
Plugins like Multiverse and EssentialsX have features that override the
world spawn location for players respawning both with and without bed
locations. If MobArena catches the PlayerRespawnEvent too early, these
plugins will likely overwrite the respawn location set for players that
respawn after dying in an arena session.

Because MobArena is a minigame and its respawn manipulation logic only
runs for respawning arena players, it is perfectly reasonable for it to
get the final say in where these players should respawn. It would have
been nice to just use `HIGH`, but that is what EssentialsX runs at, so
we gotta go higher than that.
2020-08-19 22:55:23 +02:00
Andreas Troelsen 84a7a2ed8a Don't kick players when `spectate-on-death: true`.
This commit forgoes some of the work done in the join/leave refactoring
of commit b1c6b61827.

The intent behind the original commit was to make the join/leave process
a very strictly defined set of steps that would work atomically to try
to ensure a stable, predictable process. As a result, the idea of making
spectators out of respawning players meant first kicking them out of the
arena and then re-joining as spectators, as if they had manually typed
`/ma leave` followed by `/ma spec`.

However, on some multi-world setups, this process causes people to get
thrown around a bit, which makes for a poor user experience. This commit
changes the behavior when `spectate-on-death: true` such that the player
isn't kicked, but is instead added to the spectator player pool when
they respawn. If the flag is false, players will still get sent to the
spectator area for one tick and then immediately kicked out as if they
had manually typed `/ma leave`. This does create a little bit of jumping
around, but because there is only one world change (as there would be
anyway), it is deemed acceptable at this point in time.

Closes #508
2020-08-19 22:51:27 +02:00
Andreas Troelsen 7fc0473a72 Guess Equippables from Material name suffix.
This commit changes the way Equippable wrappers are created, such that
they more closely match the way class chest armor pieces are "guessed".
That is, instead of looking directly at the Material value of the given
item, we instead look at the name _suffix_, i.e. the part after the last
underscore in the name, e.g. `BOOTS` in `IRON_BOOTS`.

The neat thing about this approach is that it is compatible with future
items that follow the same naming convention, such as Netherite armor
pieces.

The downside is that it is stringly typed and not particularly "pretty",
and if the naming convention breaks or new items are introduced (such as
the elytra), we will have to make modifications anyway.

Fixes #636
2020-08-09 12:42:44 +02:00
Andreas Troelsen b0969c655c Prevent guardians from instantly retargeting.
The vanilla behavior for most mobs is that they just stop minding their
targets' business if aggro is lost. The same applies to guardians and
elder guardians.

To prevent the pace of gameplay from stagnating, MobArena compensates
for target loss by helping the mobs find a new target. The issue with
this in terms of guardians is that breaking line of sight is the only
way - besides killing it very quickly - to avoid taking damage from a
guardian due to its lock-on laser attack. This means that guardians and
elder guardians need to be excluded from the retargeting logic to make
sense in an arena setting.

This commit introduces a new EnumSet of EntityTypes to exclude from the
retargeting logic. It is not a particularly pretty solution, especially
not since ArenaListener is such a huge class already, but it does make
it easier to add more mobs later down the road, and it does a slightly
better job at giving way to a config setting at some point.

For the Mobs Rework, a per-monster flag like `auto-retarget` or similar
might be a much better solution, so it's possible to have encounters
like a small batch of guardians that _don't_ lose their targets but have
very little health, so the "race against time"-aspect can exist, but in
a much more configurable way.

Fixes #601
2020-07-28 09:45:18 +02:00
Andreas Troelsen 6de0a2fa83 Fix `soft-restore` for blocks broken by players.
This commit fixes the per-arena config setting `soft-restore`. Due to
commit 92c4ce1a8b, the soft restore logic
won't run on BlockBreakEvents, because to reach that specific part of
the event handler, the `protect` flag has to be set to `true`. However,
due to an early return in the soft restore logic if the `protect` flag
_is_ set to `true`, it is effectively impossible for it to run.

The fact that this functionality has been broken for over 6 years (!)
with almost no reports of it is perhaps a testament to how little it is
being used in the wild.
2020-07-21 16:32:59 +02:00
Andreas Troelsen c682e45714 Remove MagicSpells integration.
The integration only works reliably with the `clear-wave-` settings set
to `true`, because it relies on there being only one "current" wave.
This is a fundamental issue with the way sessions run right now, and is
another good example of why the Sessions Rework is necessary.

It is possible to achieve the exact same functionality by just making a
couple of Upgrade Waves before and after the waves that need to have
limited skill sets (thanks Lucy). With just a couple of quality-of-life
features like wave "triggers", it's possible to make things just the way
they were with the integration, but by using some more general building
blocks.

Closes #609
2020-07-21 09:14:58 +02:00
Andreas Troelsen 962eb7aaba Improve config-file errors from `/ma setting`.
Catches ConfigError and prints its message rather than letting it bubble
up to the command handler as an uncaught exception. This means that
instead of seeing "An internal error occurred...", command senders now
see the underlying error message, as well as a short message telling
them to fix the error in the config-file and reload.

Closes #599
2020-05-31 21:12:47 +02:00
Andreas Troelsen 684116918a Remove `nexus.hc.to` repository from pom.xml.
Jitpack is the recommended Vault repository now, and we've actually
already updated the comment for Jitpack to include Vault previously, so
let's just get rid of the HeroCraft repo now.
2020-05-31 21:09:13 +02:00
Andreas Troelsen 62496127d7 Use `toUpperCase()` in SetPlayerTime step.
This commit fixes the `player-time-in-arena` per-arena setting by switching to `toUpperCase()` on the string value, which means that the values can actually result in something meaningful, rather than always throwing an exception.

The feature was broken in commit b1c6b61827, and it appears to be the only such instance to sneak through.

Fixes #621

Co-authored-by: Bobcat00 <Bobcat00@users.noreply.github.com>
Co-authored-by: Chew <chew@chew.pw>
2020-05-17 08:22:56 +02:00
Andreas Troelsen 58cb29ae97 Remove obsolete class comment in Root Target.
For some reason, this is the only ability that has such a comment, and
it doesn't really provide any value, so yeet.
2020-05-04 07:59:57 +02:00
Nesseley 7e37d93a0e Rework Root Target.
This commit completely reworks the Root Target ability implementation by
replacing the repeated teleportation code with potion effects.

The old implementation relied on teleporting the target every tick for a
given duration, but because teleporting also involves pitch and yaw, the
result was a constant "snapping back in place" experience.

The new implementation works by applying the following potion effects
with a very large amplification:

- Slowness, to prevent the player from moving out of place.
- Slow falling, to negate fall damage.
- Negative jump boost, to prevent the player from jumping around.

Note that the jump boost uses a _negative_ amplification to make it have
the inverse effect, i.e. a much worse jump ability. It is still possible
to jump an arguably negligible amount away from the root location.

Fixes #490
2020-05-04 07:57:43 +02:00
Nesseley e8cce8e620 Add support for color codes in boss names.
Translates color codes for the boss name string right before setting it
in the BossWave object.

Closes #610
2020-04-27 11:21:05 +02:00
Andreas Troelsen ce9f07e6b5 Remove `docs` folder.
This commit nukes the readthedocs documentation from the repository, but
it is kept as a local backup.

The RTD effort was a valiant one, but it fell by the way side, mostly
due to a lack of attention from my part. At this point in time, the wiki
and RTD have diverged too much, and I would rather have just one place
for documentation than two that are out of sync.

If we ever pick up RTD again, it's probably going to be with a complete
documentation rework in mind.
2020-04-25 21:59:23 +02:00
Andreas Troelsen 787f1120d1 Remove readthedocs status from README. 2020-04-25 21:57:05 +02:00
Nesseley 5e7485682a Add support for Elytra as chest pieces in class chests.
Makes the class chest armor guessing logic pair `"ELYTRA"` up with the
chest piece slot. This specific matching works, because there are no
underscores in the `"ELYTRA"` item name.

Fixes #616
2020-04-25 21:20:07 +02:00
Andreas Troelsen 8426be46fb Tidy up SheepBouncer.
This was getting on my nerves.
2020-04-09 11:39:23 +02:00
Andreas Troelsen 9245e53509 Mark constants in abilities `static`.
No need to have instance-local constants.
2020-04-09 11:37:30 +02:00
Andreas Troelsen c9b0f85bee Remove unused fields, imports, warning suppressions.
Just a bit of spring cleaning!
2020-04-09 11:31:46 +02:00
Andreas Troelsen 299a16ca8c Remove old, unused Updater class.
This was superseded by the PluginVersionCheck class and hasn't been in
use since the beginning of 2020.
2020-04-09 11:28:59 +02:00
Chew c9b36f4993 Allow non-existent `pet-items` nodes.
If someone spins up MobArena with a really old config-file or simply
removes the `pet-items` section, the plugin throws an NPE. This commit
fixes that by allowing the section to not exist.

It might be a good idea to log some helpful information, but let's wait
and see if this isn't good enough.

Fixes #606, closes #608.

Thanks Chew!
2020-03-29 05:32:08 +02:00
Andreas Troelsen 5369b1fbfa Update Mockito version. 2020-03-28 16:52:50 +01:00
Andreas Troelsen 0237dcdbd7 Update maven-jar-plugin version. 2020-03-28 16:52:50 +01:00
Andreas Troelsen c2cd5ae219 Update maven-compiler-plugin version. 2020-03-28 16:52:50 +01:00
Andreas Troelsen a9d39fe345 Update bStats metrics version.
Apparently in 1.7 we now have to specify the plugin ID in the
constructor. So mote it be.
2020-03-28 16:52:50 +01:00
Andreas Troelsen 9fcfad3748 Update MagicSpells version.
The old artifact ID doesn't appear to be working anymore, but if we can
depend on a real version number, all the better...
2020-03-28 16:52:50 +01:00
Andreas Troelsen 7547e74278 Update Vault repo and version.
Apparently we're on JitPack now and v1.7.
2020-03-28 16:52:48 +01:00
Andreas Troelsen 15736fcaa0 Add missing bStats comments in `pom.xml`.
Not that it's necessary, it's just nice to be consistent :)
2020-03-28 16:51:51 +01:00
Andreas Troelsen 86be8bdfbd Add changelog entry for 90d1f211fa.
Adds a changelog entry for the changes made in 90d1f211fa.
2020-01-13 11:07:33 +01:00
Bobcat00 90d1f211fa Add `ready` state to sign templates.
Adds support for a new `ready` state in the template engine for arena signs.

A sign is in the `ready` state when there are players in the lobby and all of them have readied up. This is only relevant in arenas with start delay timers, as the arena session automatically starts when all players are ready otherwise.

The new state is completely optional, even when no base state is provided, and it inherits from the `joining` state when not defined.

Closes #593
2020-01-13 11:03:19 +01:00
Andreas Troelsen 5f7be69f0f Add changelog entry for 6e8a3e1626.
Adds a changelog entry for the changes made in 6e8a3e1626.
2020-01-11 01:37:08 +01:00
Andreas Troelsen 01b253d304 Add list entry variable test case to RendersTemplate.
Adds test cases that cover a some of what was changed in 6e8a3e1626.
2020-01-11 01:31:49 +01:00
Bobcat00 6e8a3e1626 Add support for list entries in sign templates.
This commit adds support for listing arena players on arena signs by
introducing the following _dynamic variables_:

- `<arena-n>` the _nth_ live arena player
- `<lobby-n>` the _nth_ player in the lobby
- `<ready-n>` the _nth_ ready player in the lobby
- `<notready-n>` the _nth_ player in the lobby who hasn't readied up

Each variable points to a list of players (sorted by name), and the `n`
is the index into that list (1-indexed). This means that putting the
variable `<notready-1>` in a template will result in the name of the
first player in the list of lobby players who haven't readied up, sorted
by player name.

Implements #592
2020-01-11 01:05:06 +01:00
Andreas Troelsen 8f9664670f Bump version to 0.104.3. 2020-01-03 22:00:33 +01:00
Andreas Troelsen 5c30164ba7 Release 0.104.2. 2020-01-03 21:28:44 +01:00
Andreas Troelsen 14ad15b15d Cross check arena/lobby regions in `intersects()`.
This commit changes how the ArenaRegion `intersects()` method works:

- The implicit null checks in the `setup` and `lobbySetup` flags have been replaced with actual null checks inside the auxiliary `intersects()` function. Not only does this make the auxiliary method more robust for potential future use, it also helps tidy up the code a bit. So neat!
- The semantics have changed, since `setup` depends on more than just the `p1` and `p2` points. This fixes an (unreported) bug where the check would report a false negative in case an overlapping arena region was defined, but e.g. the arena warp was missing.
- Instead of only checking arena vs. arena and lobby vs. lobby, we now also check arena vs. lobby and lobby vs. arena. That is, if the arena region is defined, we check it against both the arena region and lobby region of the other ArenaRegion (if they are defined). Same deal with the lobby region. This should ensure that no combination of overlaps pass through the check.
2020-01-03 02:05:24 +01:00
Andreas Troelsen 3a017b179d Fix NPE in region overlap check.
This commit fixes an issue with the new `intersects()` method on ArenaRegion. Instead of blindly assuming that the region points `p1` and `p2` are set when the method is called, we first make sure both regions are properly set.

Fixes #590
2020-01-03 01:28:33 +01:00
Andreas Troelsen b9e1e07a15 Bump version to 0.104.2. 2020-01-01 00:03:36 +01:00
322 changed files with 11370 additions and 6432 deletions

79
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,79 @@
name: build
on:
workflow_dispatch:
push:
branches:
- '**'
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: 'Checkout'
uses: actions/checkout@v4
- name: 'Set up JDK'
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt'
cache: 'gradle'
- name: 'Build'
run: ./gradlew build --no-daemon
- name: 'Upload artifact'
uses: actions/upload-artifact@v4
with:
name: MobArena.jar
path: build/libs/MobArena-*.jar
- name: 'Output version'
id: version
run: |
version=$(
unzip -p build/libs/MobArena-*.jar plugin.yml \
| grep '^version: ' \
| awk '{printf $2}' \
| tr -d "'" \
)
echo "version=${version}" >> "${GITHUB_OUTPUT}"
draft:
needs: build
if: |
needs.build.result == 'success' &&
github.ref_name == 'master' &&
startsWith(github.event.head_commit.message, 'Release ') &&
!endsWith(needs.build.outputs.version, '-SNAPSHOT')
runs-on: ubuntu-latest
permissions:
contents: write
env:
VERSION: ${{ needs.build.outputs.version }}
steps:
- name: 'Checkout'
uses: actions/checkout@v4
- name: 'Download artifact'
uses: actions/download-artifact@v4
with:
name: MobArena.jar
- name: 'Extract release notes'
run: scripts/extract-release-notes -f github "${VERSION}" > release-notes.md
- name: 'Create release draft'
run: gh release create "${VERSION}" --draft --notes-file release-notes.md MobArena-*.jar
env:
GITHUB_TOKEN: ${{ github.token }}

View File

@ -0,0 +1,77 @@
name: publish-curseforge
on:
release:
types:
- 'released'
workflow_dispatch:
inputs:
tag_name:
description: 'The tag name of the release to publish'
required: true
type: string
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
env:
TAG_NAME: ${{ github.event.release.tag_name || inputs.tag_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download release assets
run: gh release download "${TAG_NAME}"
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Publish to CurseForge
run: |
echo 'Extract release notes'
changelog=$(scripts/extract-release-notes -f curse "${TAG_NAME}")
echo 'Look up game version IDs'
game_version_type_id=1
game_version_names='"1.20","1.19","1.18","1.17","1.16","1.15","1.14","1.13"'
type_condition="(.gameVersionTypeID == ${game_version_type_id})"
name_condition="(.name | startswith(${game_version_names}))"
game_version_ids=$(
curl -s -X GET 'https://minecraft.curseforge.com/api/game/versions' \
-H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
| jq -c ".[] | select(${type_condition} and ${name_condition}) | .id" \
| paste -sd, - \
)
echo 'Create metadata file'
cat << EOF > metadata.jq
{
changelog: \$changelog,
changelogType: "html",
displayName: \$displayName,
gameVersions: \$gameVersions,
releaseType: "beta"
}
EOF
jq -c -n \
--arg changelog "${changelog}" \
--arg displayName "MobArena v${TAG_NAME}" \
--argjson gameVersions "[${game_version_ids}]" \
-f metadata.jq \
> metadata.json
echo 'Publish build to CurseForge'
base_url='https://minecraft.curseforge.com'
project_id=31265
curl -s -X POST "${base_url}/api/projects/${project_id}/upload-file" \
-H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
-F 'metadata=<metadata.json' \
-F "file=@MobArena-${TAG_NAME}.jar"

79
.github/workflows/publish-hangar.yml vendored Normal file
View File

@ -0,0 +1,79 @@
name: publish-hangar
on:
release:
types:
- 'released'
workflow_dispatch:
inputs:
tag_name:
description: 'The tag name of the release to publish'
required: true
type: string
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
env:
TAG_NAME: ${{ github.event.release.tag_name || inputs.tag_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download release assets
run: gh release download "${TAG_NAME}"
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Publish to Hangar
run: |
echo 'Extract release notes'
changelog=$(scripts/extract-release-notes -f hangar "${TAG_NAME}")
echo 'Create version upload file'
cat << EOF > version-upload.jq
{
version: \$version,
channel: "Release",
description: \$changelog,
platformDependencies: {
"PAPER": [
"1.13.x",
"1.14.x",
"1.15.x",
"1.16.x",
"1.17.x",
"1.18.x",
"1.19.x",
"1.20.x"
]
},
pluginDependencies: {},
files: [
{ platforms: ["PAPER"] }
]
}
EOF
jq -c -n \
--arg version "${TAG_NAME}" \
--arg changelog "${changelog}" \
-f version-upload.jq \
> version-upload.json
echo 'Authenticate with Hangar'
base_url='https://hangar.papermc.io/api/v1'
key=${{ secrets.HANGAR_TOKEN }}
jwt=$(curl -s -X POST "${base_url}/authenticate?apiKey=${key}" | jq -r '.token')
echo 'Publish build to Hangar'
project_slug='MobArena'
curl -s -X POST "${base_url}/projects/${project_slug}/upload" \
-H "Authorization: ${jwt}" \
-F 'versionUpload=<version-upload.json;type=application/json' \
-F "files=@MobArena-${TAG_NAME}.jar"

43
.gitignore vendored
View File

@ -1,40 +1,11 @@
# IDE files and folders
.project
.classpath
*.iml
.idea/
.settings/
# Compiled files
# Java
*.class
# OS files.
.DS_Store*
Thumbs.db
# Misc folders
bin/
# Gradle
.gradle/
build/
misc/
target/
# Misc files
MobArena.jar
build.properties
# Virtualenv (for docs)
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib/python*/
[Ll]ib64
[Ll]ocal
[Mm]an
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json
# ReadTheDocs generated documentation
docs/_build
# IntelliJ
.idea/
*.iml
out/

View File

@ -1,3 +0,0 @@
language: java
jdk:
- openjdk8

View File

@ -1,4 +1,4 @@
MobArena [![Build Status](https://travis-ci.org/garbagemule/MobArena.svg?branch=master)](https://travis-ci.org/garbagemule/MobArena) [![Documentation Status](https://readthedocs.org/projects/mobarena/badge/?version=latest)](http://mobarena.readthedocs.io/en/latest/?badge=latest)
MobArena [![Build Status](https://github.com/garbagemule/MobArena/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/garbagemule/MobArena/actions/workflows/build.yml)
========
MobArena is an arena-style minigame for Spigot-based Minecraft servers
@ -11,6 +11,7 @@ one of these two sites:
- [Bukkit](https://dev.bukkit.org/projects/mobarena)
- [Spigot](https://www.spigotmc.org/resources/34110/)
- [Hangar](https://hangar.papermc.io/garbagemule/MobArena)
The wiki here on Github should have all the information you need to get
started using the plugin.

68
build.gradle.kts Normal file
View File

@ -0,0 +1,68 @@
plugins {
id("java-library")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
group = "com.garbagemule"
version = "0.108"
repositories {
mavenLocal()
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
maven("https://jitpack.io")
maven("https://repo.maven.apache.org/maven2/")
}
dependencies {
compileOnly("org.spigotmc:spigot-api:1.19-R0.1-SNAPSHOT")
compileOnly("com.github.MilkBowl:VaultAPI:1.7.1")
api("org.bstats:bstats-bukkit:2.2.1")
testImplementation("junit:junit:4.13.2")
testImplementation("org.hamcrest:hamcrest-all:1.3")
testImplementation("org.mockito:mockito-core:3.12.4")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(8))
}
}
sourceSets {
test {
// This is a bit of a hack. It ensures that the dependencies for the
// "main" source set are available to the test suite. Without it, the
// dependencies would have to be listed twice, and even that doesn't
// seem to work with the currently used version of Mockito.
configurations.testImplementation.configure {
extendsFrom(configurations.compileOnly.get())
}
}
}
tasks {
processResources {
filesMatching("plugin.yml") {
expand("project" to mapOf(
"name" to "MobArena",
"version" to version,
))
}
}
shadowJar {
minimize()
relocate("org.bstats", "com.garbagemule.MobArena.metrics")
archiveBaseName = "MobArena"
archiveClassifier = ""
}
// We're using shadowJar, so we can skip the regular jar task.
jar { enabled = false }
// Let the build task produce the final artifact.
build { dependsOn(shadowJar) }
}

View File

@ -12,6 +12,120 @@ These changes will (most likely) be included in the next version.
## [Unreleased]
## [0.108] - 2024-01-01
### Added
- Support for chest references in item syntax. The new `inv` syntax allows for referencing container indices in the config-file. This should help bridge the gap between class chests and various other parts of the config-file, such as rewards and upgrade waves.
- Support for saved items. The new `/ma save-item` command can be used to save the currently held item to disk, which allows it to be used in various places in the config-file. This should help bridge the gap between the config-file and class chests for config-file centric setups.
- New per-arena setting `monster-teleporting` allows monsters to teleport _inside_ the arena region. This should allow for stuff like `/tp` commands and for Endermen to "do their thing".
- New permission `mobarena.admin.errors` for better error visibility. Players with this permission will get a message if an arena encounters an error. Currently, the only such error is the one resulting from Spigot's "max max health" setting throwing an exception when monster health is set "too high".
### Fixed
- Explosion damage caused by Exploding Sheep now correctly counts as monster damage. This means that the explosions only affect other mobs if the per-arena setting `monster-infight` is set to `true`.
- Explosion damage caused by the boss ability `obsidian-bomb` now correctly counts as monster damage. This means that the explosions only affect other mobs if the per-arena setting `monster-infight` is set to `true`.
- An old discrepancy with auto start timers in the auto-ready logic has been removed. This fixes an issue in MobArenaStats where the extension would throw errors in arenas with `auto-ready: true` and a non-zero auto start timer. Note that the combination of `auto-ready: true` and a `default-class` now _requires_ the use of a `start-delay-timer` to prevent the arena from starting immediately when the first player joins.
- Signs in arena regions, as well as Arena Signs anywhere, can no longer be edited by right-clicking.
- Signs now correctly restore themselves again in arenas with `soft-restore: true`.
## [0.107] - 2022-07-30
### Added
- New monster variant `angry-bees` can be used to spawn angry bees.
- Husks, drowned, piglins, hoglins, and zoglins can now be spawned in their baby versions using the `baby` prefix seen on other monster types (e.g. `baby-zombie`).
- Pet names are now per-class configurable via the optional `pet-name` property, which defaults to `<display-name>'s pet` (the `<player-name>` variable is also supported).
- New per-arena setting `auto-leave-on-end` can be used to automatically "kick" spectators when the current session ends.
- New per-arena setting `clear-wave-leeway` allows for tweaking the number of mobs allowed to be alive before the next wave spawns. The setting affects `clear-wave-before-next`, `clear-wave-before-boss`, and the final wave check, and it defaults to 0.
- New per-arena setting `auto-ignite-fuse` makes the fuse time for auto-ignited TNT configurable. The unit is ticks and defaults to 80.
- Added boss abilities `disorient-all`, `fetch-all`, `pull-all`, and `throw-all`. These abilities work like their target-specific and distance-based counterparts, but affect all players in the arena.
- (API) MobArena's internal command handler now supports registering pre-instantiated subcommand instances. This should make it easier for extensions to avoid the Singleton anti-pattern for command dependencies.
- (API) MobArena now fires MobArenaPreReloadEvent and MobArenaReloadEvent before and after, respectively, reloading its config-file. This should allow extensions and other plugins to better respond to configuration changes.
### Changed
- MobArena now targets the Minecraft 1.19 version of the Spigot API (but still works on 1.13-1.18). This should make it easier to tackle feature requests and bug reports related to modern Minecraft.
- Monsters are no longer stripped of the _weapons_ they spawn with naturally, only their _armor_. This should improve forwards compatibility with new weapon-reliant monsters.
- The regex pattern for the player list command is now less greedy, so it will only match on `/ma players`, `/ma playerlist`, and `/ma player-list`. The previous pattern matched on anything that starts with `player`, which rendered the `/ma player-stats` command in MobArenaStats impossible to invoke.
### Fixed
- Pillagers and vindicators no longer spawn without their much-needed weapons.
- Piglins, piglin brutes, and hoglins no longer zombify. This fixes a bug where the mobs would despawn due to the zombification process.
- Zombies, husks, drowned, zombie villagers, piglins, hoglins, and zoglins without the `baby` prefix are now forced into adulthood to prevent them from occasionally spawning as babies.
- Evokers are once again capable of spawning vexes on 1.18.1+.
- Reward groups with `nothing` in them no longer cause errors when earned/granted.
- The title-based announcer and the title-based boss health bar have been fixed to work with the breaking change to the Title API in Spigot 1.17.
- Arena Signs now correctly update for arenas that don't have `kebab-case` names in the config-file.
- Block explosion events cancelled by other plugins now remain cancelled unless MobArena specifically uncancels them for an arena.
- Flaming arrows now ignite TNT blocks in the arena.
- Players no longer take fall damage when they leave (or get removed from) an arena while falling.
- Players no longer take damage from projectiles shot by pets of other players.
- Normal shulker boxes are now properly removed from inventories of players using the My Items class.
- Class pets are now correctly removed from the arena when their owner dies, rather than when they leave.
- MobArena no longer nags players with the `mobarena.admin.teleport` permission when they engage in a teleport that would have otherwise been blocked.
- MobArena now correctly sets the source property on auto-ignited TNT.
## [0.106] - 2021-05-09
### Added
- It is now possible to write custom formulas for wave growth in Default Wave, swarm amounts in Swarm Waves, and boss health in Boss Waves, allowing for much more control and fine-tuning. The formulas support various session-related variables as well as various mathematical operators and functions. Formulas can be predefined as macros in the new `formulas.yml` file. Check the wiki for details.
### Changed
- bStats Metrics client updated to 2.2.1.
- MobArena now uses Github Actions instead of Travis CI. This should make it easier to get development builds directly from Github for those interested.
### Fixed
- Arena signs in unloaded or missing worlds no longer break the startup procedure. Sign data is stored in a new format that MobArena will automatically migrate to on a per-world basis during startup.
## [0.105] - 2020-11-08
### Minor breaking changes
MobArena 0.105 includes a rework of how arenas and classes are referenced, both internally in the plugin, but also in permissions, commands, etc.
Instead of the ambiguous and arbitrary "config names", MobArena now uses "slugs", which just means a `kebab-case` version of a name.
For example, the slug for an arena named "Castle of Doom" will be `castle-of-doom`, and the slug for the implicit "My Items" class is `my-items`.
All commands that take arena or class names as arguments will tab complete slugs, so they should be fairly easy to figure out.
The goal of this change is to make the plugin more consistent about arena and class references in commands, permissions, etc., as well as to allow for multi-word names.
Backwards compatibility has taken a backseat for the sake of maintainability and clarity, so certain adjustments may be necessary in some setups:
- **Permissions:** All permissions for arenas and classes must be changed to the new slug-based permission keys. For example, the permission for the "My Items" class is now `mobarena.classes.my-items`.
- **Class signs:** It may be necessary to recreate the signs for classes with multi-word names. The name on the sign should match the name in the config-file (or the slug). For example, `My Items` (or `my-items`) instead of `Myitems`.
- **Default classes:** The per-arena setting `default-class` may need to be adjusted. The class name should match the name in the config-file (or the slug). For example, if the default class is "My Items", the value should be `My Items` (or `my-items`).
- **Custom integrations:** Custom commands or integrations may need to be adjusted accordingly. If you're running a setup like that, you probably already know what you're doing and what you need to do. If not, hop on Discord.
### Added
- A new `ready` state is now available for arena sign templates. Signs are in this state when all players in the lobby have readied up, but the arena has not yet started due to a start delay timer. Check the wiki for details.
- Arena signs now support dynamic list entry variables for 4 different player lists. As an example, `<notready-1>` results in the name of a player in the lobby who hasn't readied up yet. This is useful for visualizing who is holding up the lobby. Check the wiki for details.
- Elytra are now supported chest pieces in class chests.
- Boss names now support color codes.
- New per-arena setting `arena-warp-offset` can be used to spread out players randomly by an offset from the arena warp. This should help prevent players taking suffocation damage.
- New per-arena setting `announcer-type` determines where to display per-arena announcements such as wave spawns, auto start timers, boss abilities, and death messages. Options are `title` (default) or `chat`.
- It is now possible to group rewards. For example, `all(stick, bone)` results a stick and a bone, while `random(all(stick, bone), all(dirt, stone))` results in getting _either_ a stick and a bone _or_ a dirt block and a stone block.
- The new `nothing` keyword can be used to _not_ grant a reward. This can be used in a crude way to create "loot table"-style reward systems where there is a _chance_ that something is reward, but it might also just be nothing.
- Boss rewards also support the `all()` and `random()` functions as well as the `nothing` keyword.
- New command `/ma addreward <player> <thing>` can be used to add a reward to an arena player's reward list. This can be useful for hooking into the rewards system from scripts or other plugins.
- The `/ma addarena` and `/ma autogenerate` commands now supports multi-word arena names.
### Changed
- The Root Target ability now uses potion effects (slowness, slow falling, and negative jump boost) instead of repeated teleports. This should make for a smoother root experience.
- Permissions for arenas and classes are now based on "slugs". It is now possible to configure permissions for arenas and classes with multi-word names (including "My Items"). Check the Permissions page on the wiki for details.
- Commands that resolve arena and/or class names now consistently resolve and tab complete "slugs" instead of arbitrarily "squashed" names. This greatly improves support for multi-word names.
- The class signs generated by the `/ma autogenerate` command now use class names from the config-file instead of arbitrarily "squashed" names.
- Leaderboards now use arena and class names from the config-file instead of arbitrarily "prettified" names.
- Using `spectate-on-death: true` no longer forces players out to their join location/exit warp before moving them to the spectator area. This should prevent "jumpy" behavior in multi-world setups.
- Config-file errors imposed by incorrect usage of `/ma setting` no longer cause "internal errors". Instead, the errors are properly communicated in the command output similar to how the `/ma reload` command works.
- Guardians and elder guardians no longer instantly retarget players when they break line of sight. This should make their behavior work a bit closer to vanilla.
- (API) MobArenaHandler now returns class name slugs in the `getPlayerClass()` methods.
### Fixed
- Elytra and Netherite armor pieces now correctly auto-equip if specified in the generic `armor` node in classes in the config-file.
- Players should now properly respawn at the spectator area rather than at world spawn on servers with plugins that override respawn locations.
- Config-files with missing `pet-items` nodes no longer errors. A missing `pet-items` node in `global-settings` is treated as empty, i.e. no pet items will be registered.
- The `player-time-in-arena` setting has been fixed.
- The `soft-restore` setting has been fixed for blocks broken by players. Note that the functionality is still unreliable for non-trivial blocks.
- Items in class chests are now cloned before they are made unbreakable and given to players. This fixes an issue where setting `unbreakable-weapons: false` had no effect on the items. Note that any affected items in existing class chests will need to be replaced.
- (1.8) Potions no longer turn into water bottles.
### Removed
- The MagicSpells integration has been removed. This means that the extra `magicspells.yml` config-file (if it exists) no longer does anything and can be removed.
## [0.104.2] - 2020-01-03
- The region overlap check now works across both arena and lobby regions, i.e. all four combinations of intersections between two regions (arena-arena, arena-lobby, lobby-arena, and lobby-lobby) are evaluated.
- Arenas with missing regions no longer cause errors in the region overlap check.
## [0.104.1] - 2019-12-31
- It is no longer necessary to have recurrent waves for an arena to work. MobArena automatically creates a "catch all" recurrent wave in case the arena session reaches a wave number that isn't covered by any other wave definitions.
- Entities outside of the arena can no longer target players, pets, or monsters inside of the arena.
@ -147,7 +261,12 @@ Thanks to:
- Swatacular for help with testing bug fixes
- Haileykins for contributions to the code base
[Unreleased]: https://github.com/garbagemule/MobArena/compare/0.104.1...HEAD
[Unreleased]: https://github.com/garbagemule/MobArena/compare/0.108...HEAD
[0.108]: https://github.com/garbagemule/MobArena/compare/0.107...0.108
[0.107]: https://github.com/garbagemule/MobArena/compare/0.106...0.107
[0.106]: https://github.com/garbagemule/MobArena/compare/0.105...0.106
[0.105]: https://github.com/garbagemule/MobArena/compare/0.104.2...0.105
[0.104.2]: https://github.com/garbagemule/MobArena/compare/0.104.1...0.104.2
[0.104.1]: https://github.com/garbagemule/MobArena/compare/0.104...0.104.1
[0.104]: https://github.com/garbagemule/MobArena/compare/0.103.2...0.104
[0.103.2]: https://github.com/garbagemule/MobArena/compare/0.103.1...0.103.2

View File

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = MobArena
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -1,180 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# MobArena documentation build configuration file, created by
# sphinx-quickstart on Wed Nov 29 13:24:13 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import os
import sys
sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.todo']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'MobArena'
copyright = '2017, garbagemule, Justin W. Flory (jflory7), MobArena contributors'
author = 'garbagemule, Justin W. Flory (jflory7), MobArena contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.99'
# The full version, including alpha/beta/rc tags.
release = '0.99.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
# html_sidebars = {
# '**': [
# 'relations.html', # needs 'show_related': True theme option to display
# 'searchbox.html',
# ]
# }
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'MobArenadoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'MobArena.tex', 'MobArena Documentation',
'garbagemule, Justin W. Flory (jflory7), MobArena contributors', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'mobarena', 'MobArena Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'MobArena', 'MobArena Documentation',
author, 'MobArena', 'One line description of project.',
'Miscellaneous'),
]
# -- Use the RTD Sphinx Theme -------------------------------------------
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# RTD theme options (see theme.conf for more information)
html_theme_options = {
'canonical_url': "https://mobarena.readthedocs.io/en/latest/",
'collapse_navigation': False,
'navigation_depth': 2,
}

View File

@ -1,32 +0,0 @@
.. MobArena documentation master file, created by
sphinx-quickstart on Wed Nov 29 13:24:13 2017.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to MobArena's documentation!
====================================
.. toctree::
:maxdepth: 1
:name: dev
:caption: Developer resources
:glob:
dev/*
.. toctree::
:maxdepth: 1
:name: user
:caption: User documentation
:glob:
user/*
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -1,36 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=MobArena
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

View File

@ -1,3 +0,0 @@
## Theme for Sphinx documentation
Sphinx
sphinx_rtd_theme

View File

@ -1,11 +0,0 @@
#!/usr/bin/env sh
#
# Script to automatically build and test the Sphinx documentation currently in
# the repo. This script should always be run before submitting a new pull
# request.
#
# If you're on Windows, please use the `make.bat` script in `docs/` directory.
#
make clean html

View File

@ -1,64 +0,0 @@
####################
Announcement strings
####################
MobArena supports custom strings for announcement and messages. This lets you
create custom messages unique to your server based on certain actions, like a
game ending. The ``announcements.yml`` file stores MobArena announcement and
message strings. [#]_
.. [#] Default strings are found in the `Msg.java`_ class
*************
Color support
*************
Color codes are supported. **To add color to a message**, use an ampersand
(``&``) followed by a valid color code. `Color codes`_ are available in the
Gamepedia Minecraft wiki. Examples are in the default file.
*********
Variables
*********
Some announcements use a variable. In the string, variables are represented by a
percent sign (``%``). Remove it if you do not want a variable in the message.
There are some limitations to variables:
- Not possible to add variables to announcements that don't take them by default
- Announcements that take one variable cannot take more than that variable
***********************
Disable an announcement
***********************
If you want to **disable a specific announcement**, set its value to two single
quotes (``''``). MobArena ignores announcements set to an empty value. To
disable one, you must override it. Deleting the option does not disable an
announcement since MobArena adds them in by default.
********
Examples
********
Three examples below show all of the features explained above.
.. code-block:: yaml
:emphasize-lines: 2, 5, 8
# Use a red message for the start of a new game
arena-start: 'Let the games begin! &cMay the odds be ever in your favor!'
# Use a variable for the number of seconds until the game begins
arena-auto-start: 'Arena will auto-start in &c%&r seconds.'
# Turn off the golem-died message
golem-died: ''
.. _`Msg.java`: https://github.com/garbagemule/MobArena/blob/master/src/main/java/com/garbagemule/MobArena/Msg.java
.. _`color codes`: https://minecraft.gamepedia.com/Formatting_codes#Color_codes

View File

@ -1,277 +0,0 @@
###########
Arena setup
###########
This page explains how to set up an arena, from defining it to configuring it.
*****************
Building an arena
*****************
There are four key parts to an arena:
#. `Lobby`_
#. `Arena floor`_
#. `Spectator area`_
#. `Exit point`_
Lobby
=====
A lobby is where players choose a class before joining an arena. It's also the
"waiting area" before a new match begins.
Lobbies have two requirements:
- **Class selection signs**: Clicked to select a class
- **Iron block**: Clicked to mark self as "ready"
Class selection signs must have the name of the class on the first line
(case-sensitive). The last three lines are not checked and they can have any
text.
You will not receive a confirmation message after making a new sign since
MobArena does not register the creation of class selection signs. If you get a
confirmation message, another plugin is interferring.
Arena floor
===========
An arena floor is where the action happens. Players fight through mob waves on
the arena floor. How the arena floor looks is up to you, but we recommend these
minimum requirements:
- Closed in by walls on all sides
- Have a ceiling / roof
This prevents players and mobs from escaping and also blocks players from
wandering out of the arena with class items.
Spectator area
==============
A spectator area lets non-players watch an on-going match. The ``/ma spec``
command teleports a player into the spectator area. If configured, [#]_ players
warp into the spectator arena when they die.
Design the area so spectators cannot escape the spectator area, since they are
invincible. Spectators should *not* enter the arena floor or exit the spectator
area on foot.
.. [#] Set ``spectate-after-death`` to ``true`` in the config file to force
players to the spectator area after dying
Exit point
==========
When players leave the arena or when the last player standing dies, arena
players and spectators teleport to the location they joined from. Optionally, an
arena can have an exit warp. This controls where players go after leaving a
match.
**************
Defining areas
**************
By now, you have a full arena map and you are ready to configure it in MobArena.
When an area is built, it must be **defined** by MobArena. This tells MobArena
where the arena boundaries are, such as whether an area is a lobby or the arena
floor.
MobArena has four types of areas:
#. `Regions`_
#. `Warps`_
#. `Spawnpoints`_
#. `Containers`_
Regions
=======
Arenas **must** have an arena region and optionally a lobby region. Regions are
set using the Regions tools. MobArena uses the arena region to…
- Stop cheating by kicking players if they leave the arena region
- Only spawn MobArena mobs inside arena region
Warps
=====
Players teleport to different warps for different events. There are four warps
used in MobArena:
- **Lobby warp**: Warp location for players joining a new match; leave when all
players "ready up" or match countdown timer ends
- **Arena warp**: Warp location for players to spawn in the arena floor
- **Spectator warp**: Warp location for spectators to watch an on-going match
- **Exit warp**: Optional warp location for players to teleport to after a match
finishs or they leave the arena
Spawnpoints
===========
Mobs spawn at the spawnpoint(s) of an area. MobArena only uses spawnpoints in a
*15-block radius* from any player. An arena can have multiple spawnpoints.
Spawnpoints are added using the Spawnpoints tool.
The number of mobs spawned is not determined by the number of spawnpoints, but
actual formulas. See :doc:`wave-formulas` for more information.
Containers
==========
Containers are locations of chests, dispensers, or other containers with
renewable contents. Any containers added to an arena must be registered using
the Chests tool.
**********
Setup Mode
**********
Configure a new arena with *Setup Mode*. Setup Mode is a special mode that
temporarily stores inventory and gives an administrator a set of golden tools.
The golden tools are called the `Toolbox`_.
Flying is enabled to simplify arena setup. Talking in server chat is also
disabled because Setup Mode starts an isolated conversation with the
administrator (explained below).
- **Create a new arena**: ``/ma addarena <arena name>``
- **Enter Setup Mode**: ``/ma setup <arena name>`` [#]_
- **Leave Setup Mode**: ``done`` (no slash)
- **Delete an arena**: ``/ma delarena <arena name>`` [#]_
.. [#] If you only have one arena, you don't have to specify the arena name
.. [#] An arena named ``default`` is created on first use. You can remove this
arena if you want to use an arena with a different name.
Setup Mode commands
===================
Setup Mode is an *isolated conversation*, which means Setup Mode intercepts
everything an administrator types. This makes commands in Setup Mode shorter and
prevents accidental use of other plugins.
Below is a list of all commands in Setup Mode:
+-------------------+-------------------------------------+------------+
| Command | Description | Aliases |
+===================+=====================================+============+
| done | Leave Setup Mode | end, stop, |
| | | done, quit |
+-------------------+-------------------------------------+------------+
| help | Display help screen | ?, h |
+-------------------+-------------------------------------+------------+
| missing | Display list of missing (mandatory) | miss |
| | regions, warps, spawnpoints. Useful | |
| | to check what is left to set up. | |
+-------------------+-------------------------------------+------------+
| expand | Expand region by some amount in a | exp |
| ``<region>`` | given direction. *Example*: | |
| ``<amount>`` | ``expand ar 5 up`` | |
| ``<direction>`` | | |
+-------------------+-------------------------------------+------------+
| show | Show a region, warp, spawnpoint(s), | N/A |
| ``[<region>|`` | or container as red wool blocks. | |
| ``<warp>|`` | *Example*: ``show sp`` | |
| ``<spawnpoint>|`` | | |
| ``<container>]`` | | |
+-------------------+-------------------------------------+------------+
- **Valid regions**: ``ar`` (arena region), ``lr`` (lobby region)
- **Valid amounts**: Any positive integer (i.e. whole number)
- **Valid directions**: ``up``, ``down``, ``out``
- **Valid warps**: ``arena``, ``lobby``, ``spec``, ``exit``
- **Valid spawnpoints**: ``spawns`` (or ``sp``)
- **Valid containers**: ``chests`` (or ``c``)
Toolbox
=======
The Toolbox is a set of golden tools. Each tool has a specific function. We use
them to set up regions, warps, spawnpoints, and containers. Toolbox tools are
used with either a left- or right-click.
Tool functions are also described in the *item tooltip* in your inventory.
Region tools
------------
|r-icon|
Arena and lobby regions are defined with Region tools (golden axes). There are
two golden axes in the Toolbox. One is for *arena setup* and the other is for
*lobby setup*. The tools are named accordingly.
Region tools behave similarly to the WorldEdit wand (wooden axe). If you are
familiar with regions in WorldEdit, Region tools should feel familiar.
- **Left-click**: Sets first point on clicked block
- **Right-click**: Sets second point on clicked block
When both points are set, the region is defined. ``show ar`` (or ``show lr``)
lets you check the region spans the desired area. If the region is too small,
use the ``expand`` command (see above) to make it bigger.
The region must be three-dimensional (like a box) and not two-dimensional (flat
rectangle). Make sure your arena floor is contained in the region selection
(expanding a block or two below the floor is recommended).
Warp tool
---------
|w-icon|
All warps are defined using the Warp tool (golden hoe). The tool defines any of
the four types of warps depending which one is selected.
- **Left-click:** Set selected warp type on top of clicked block
- **Right-click:** Cycle between warp types
A selected warp is placed on top of the clicked block. The direction you are
looking is also taken into account.
Arena, lobby, and spectator warps are required. An exit warp is optional.
Spawnpoint tool
---------------
|s-icon|
Spawnpoints are set up with the Spawnpoint tool (golden sword). The tool allows
an administrator to set or remove spawnpoints for mobs.
- **Left-click:** Add spawnpoint on top of clicked block
- **Right-click:** Remove spawnpoint on top of clicked block (if one exists)
A **high number of spawnpoints** is recommended. Mobs only spawn at spawnpoints
within 15 blocks of a player. Every area in the arena should have one or more
spawnpoints in a 15 block radius from each other.
If a player is not within 15 blocks of a spawnpoint, MobArena prints a warning
to the console with coordinates. If no players are within 15 blocks of a
spawnpoint, MobArena uses a random spawnpoint. This means mobs may spawn far
away from players.
Container tool
--------------
|c-icon|
Containers are set up with the Container tool (golden shovel). It works like the
Spawnpoint tool, but checks that the clicked block is a valid container.
- **Left-click:** Register clicked container (if not registered)
- **Right-click:** Unregister clicked container (if registered)
At the end of a match, a container is restored to its contents from the
beginning of the match.
.. |r-icon| image:: http://puu.sh/4wwCH.png
.. |w-icon| image:: http://puu.sh/4wwIB.png
.. |s-icon| image:: http://puu.sh/4wwCJ.png
.. |c-icon| image:: http://puu.sh/4wwIF.png

View File

@ -1,106 +0,0 @@
############
Class chests
############
**On this page:** \* `About Class Chests <#about-class-chests>`__ \*
`Linked Class Chests <#linked-class-chests>`__
About Class Chests
------------------
If some of your favorite items aren't supported by MobArena's internal
[[item parser\|Item and Reward Syntax]], or if you just want to be able
to configure your class items from in-game, the **class chests** may be
what you're looking for!
--------------
**Note: The Class Chests will only work for arenas with
``use-class-chests: true``, and the classes *MUST* exist in the
config-file for MobArena to recognize them, however the items and armor
lists can be empty.** \* \* \*
The idea behind the class chests is to simply place some **chests below
the corresponding class signs** in the lobby, and fill them with
whatever items you want the given class to have. When the players
activate the class signs, the **contents of the chests are copied to the
player inventory**. This suggests a type of "control room" setup, where
an admin-only access room below the lobby contains the chests, allowing
admins to warp down there and change the contents of the chests.
.. figure:: img/1.png
:alt: Lobby and Control Room
Lobby and Control Room
For easier access and modification of the class chests, omitting the
control room from the arena or lobby region may prove useful. Otherwise,
arenas may have to be temporarily disabled or put into edit mode to
allow warping to and changing the contents of the chests.
The class chests can be located **up to 6 blocks below the sign** itself
or below the block right behind the sign (for wall signs, this would be
the block the sign is attached to). The chest may also be in the block
directly behind the sign itself - this is safe, because MobArena
prevents players in the lobby from opening inventories, so if your lobby
is in a tight spot, this might be the better option.
**Multiple sign rows:** It is possible to have two rows of class signs
in the lobby and still use this feature. Simply place the class chest
for the sign of the bottom row exactly at the 6-block limit, and the
class chest for the sign of the top row one block up and behind the
other chest (in a stair-like fashion). The blocks are searched in a
vertical/pillar-like fashion, which is the reason this works.
.. figure:: img/2.png
:alt: Chests Below
Chests Below
To get **auto-equipped armor** from the class chests, place the armor
pieces in the **last four slots of the third row** in the chest.
MobArena will check these four slots, and if any of them are armor
pieces, they will be equipped. Note that the item placed in the very
last slot (bottom right), will always be equipped as a helmet (this
allows wool blocks, pumpkins, etc. to be used as helmets). The order of
the other three slots doesn't matter.
The **fifth last slot**, right next to the armor slots, will be equipped
as an **off-hand** item.
.. figure:: img/3.png
:alt: Armor Slots
Armor Slots
The class chests are the best way to add items that are not currently
supported by the MobArena [[item parser\|Item Syntax]]. This is because
the class chests **simply copy the contents of the chests** to the
player inventories, thus making any items supported by Bukkit supported
by MobArena.
.. figure:: img/4.png
:alt: Dyed Armor
Dyed Armor
Linked Class Chests
-------------------
If per-arena class chest setups is too troublesome (e.g. if you have
many arenas), if you don't need per-arena setups, or if you simply want
a single, global class chest for each class, *linked class chests* are
what you're looking for.
When you link a chest to a class, MobArena will always copy the contents
of that chest to the player's inventory, when they pick the given class,
regardless of any local class chests (note that the arena must still
have ``use-class-chests: true``).
To link a chest to a class, simply look at the chest and type
``/ma classchest <class>``, and you're done! The linked class chests may
exist in any world, but remember that there can only be one class chest
per class, and that local class chests will be ignored!
To unlink a class chest, you will have to open the config-file and
remove the ``classchest`` node from the given class.

View File

@ -1,119 +0,0 @@
########
Commands
########
This page documents the commands available in MobArena, both for players and
administrators.
*Note*: Parentheses (``()``) around an parameter means it's optional (i.e. not
required to work).
***************
Player commands
***************
Any player on the server can use these commands by default.
+---------------------------+----------------------------+---------------------+
| Command | Description | Alias |
+===========================+============================+=====================+
| ``/ma join (<arena>)`` | Join arena with given name | ``/ma j (<arena>)`` |
+---------------------------+----------------------------+---------------------+
| ``/ma leave`` | Leave current arena or | ``/ma l`` |
| | spectator area | |
+---------------------------+----------------------------+---------------------+
| ``/ma notready`` | List players who are not | |
| | ready in arena lobby | |
+---------------------------+----------------------------+---------------------+
| ``/ma spec (<arena>)`` | Enter an arena's spectator | ``/ma s (<arena>)`` |
| | arena | |
+---------------------------+----------------------------+---------------------+
| ``/ma arenas`` | List all arenas. Green | |
| | names are enabled, gray | |
| | names are disabled | |
+---------------------------+----------------------------+---------------------+
| ``/ma players (<arena>)`` | List all players in an | |
| | area | |
+---------------------------+----------------------------+---------------------+
| ``/ma class <class>`` | Manually choose a class in | |
| | an arena lobby (instead of | |
| | punching sign) | |
+---------------------------+----------------------------+---------------------+
**************
Admin commands
**************
Players with OP privileges or assigned permissions can use these commands.
+-----------------------------+------------------------------------------------+
| Command | Description |
+=============================+================================================+
| ``/ma enable (<arena>)`` | Enable MobArena (optionally a specific arena) |
+-----------------------------+------------------------------------------------+
| ``/ma disable`` | Disable MobArena (optionally a specific arena) |
+-----------------------------+------------------------------------------------+
| ``/ma force end (<arena>)`` | Forcefully end all arenas or a specific arena |
+-----------------------------+------------------------------------------------+
| ``/ma force start <arena>`` | Forcefully start an arena (players that aren't |
| | ready are removed from arena) |
+-----------------------------+------------------------------------------------+
| ``/ma notready <arena>`` | List all players in an arena that aren't ready |
+-----------------------------+------------------------------------------------+
| ``/ma restore <player>`` | Restore a player's inventory (if possible) |
+-----------------------------+------------------------------------------------+
| ``/ma config reload`` | Reload config file into memory |
+-----------------------------+------------------------------------------------+
**************
Setup commands
**************
Players with OP privileges or assigned permissions can use these commands.
+-----------------------------+------------------------------------------------+
| Command | Description |
+=============================+================================================+
| ``/ma setup <arena>`` | Enter Setup Mode for an arena (see |
| | :doc:`arena-setup` for more info) |
+-----------------------------+------------------------------------------------+
| ``/ma addarena <arena>`` | Create new arena node in current world |
+-----------------------------+------------------------------------------------+
| ``/ma delarena <arena>`` | Delete arena with given name |
+-----------------------------+------------------------------------------------+
| ``/ma editarena <arena>`` | Toggle Edit Mode for an arena |
+-----------------------------+------------------------------------------------+
| ``/ma editarena <arena>`` | Turn Edit Mode on or off for an arena |
| ``[true|false]`` | |
+-----------------------------+------------------------------------------------+
| ``/ma setting <arena>`` | List per-arena settings for an arena |
+-----------------------------+------------------------------------------------+
| ``/ma setting <arena>`` | Check current value of a setting for an arena |
| ``<setting>`` | |
+-----------------------------+------------------------------------------------+
| ``/ma setting <arena>`` | Change a setting for an arena to given value |
| ``<setting> <value>`` | |
+-----------------------------+------------------------------------------------+
| ``/ma checkspawns`` | Show all spawnpoints in arena you are standing |
| | in as red wool blocks (helpful to check |
| | spawnpoint coverage) |
+-----------------------------+------------------------------------------------+
| ``/ma classchest <class>`` | Create a linked class chest for a class (see |
| | :doc:`class-chests` for more info) |
+-----------------------------+------------------------------------------------+
| ``/ma auto-generate`` | Auto-generate new arena with given name |
| ``<arena>`` | (generated directly below player) |
+-----------------------------+------------------------------------------------+
| ``/ma auto-degenerate`` | Degenerate an auto-generated arena with given |
| ``<arena>`` | name |
+-----------------------------+------------------------------------------------+
***********
Permissions
***********
See :doc:`permissions`.

View File

@ -1,60 +0,0 @@
###############
Getting started
###############
To get MobArena up and running on your server, you'll first have to
download it and stick it in your server's plugins-folder.
When the plugin has been loaded up, you'll have to build an arena if you
haven't already. Then it's time to set it up. Follow the instructions on
the [[Arena Setup]] page, which has all the information you'll need to
get your arena up and running in no time at all. After setup, you're
ready to play! Check out the [[Using MobArena]] page for a brief
description of the usage commands.
The default arena setup is only meant as a starting point, so you may
want to configure the waves, the rewards, the classes, or perhaps some
of the arena-specific settings. The various pages on this wiki will help
you configure MobArena to your liking. Here are a couple of links to get
you started:
- [[Setting up the config-file]]
- [[Setting up the waves]]
- [[MobArena Commands]]
Also make sure to have a look at the [[Item and Reward Syntax]] page, if
you're planning to change the classes or the rewards. MobArena uses its
own semi-compact item syntax in the config-file, so you'll need to get
familiar with it to get things like enchantments and sub-types to work.
****************
More information
****************
Always make sure to check the pages on the wiki when you're in doubt -
99% of the information you'll ever need is right here. Check out the
[[list of wiki
pages\|https://github.com/garbagemule/MobArena/wiki/\_pages]] to see if
maybe you'd be able to find your answer there. You can also check the
project pages on
[[Spigot\|https://www.spigotmc.org/resources/mobarena.34110/]] and
[[Bukkit\|http://dev.bukkit.org/bukkit-plugins/mobarena/]].
***********************
Suggestions, bugs, etc.
***********************
If you think you have found a bug, or if you have a suggestion for the
plugin, you can create a ticket here on Github. Make sure to be as
descriptive as possible, because the more information you can provide,
the easier it is to do something about your bug report or feature
request.
If you'd rather have a little chat about the plugin, you can visit the
[[IRC channel\|http://webchat.esper.net/?channels=#mobarena]] (#mobarena
@ EsperNet) - please note that IRC is *idle chat*, which means people
will appear to be in the channel, but they might not be at their
computers when you join. Hop on, ask your question, and then hang around
until you get an answer; it may be seconds, minutes or hours, but there
is no harm in hanging around the channel indefinitely (as many people
do).

View File

@ -1,151 +0,0 @@
######################
Item and reward syntax
######################
This page explains the syntax for items and rewards for completing MobArena
waves.
**********
Item types
**********
MobArena items must use a specific syntax to work as expected. The plugin allows
three different ways of defining items:
- **Single**: ``[<id>|<name>]``
- **Multiple**: ``[<id>|<name>]:<amount>``
- **Sub-types**: ``[<id>|<name>]:<data>:<amount>``
Single items
============
``[<id>|<name>]``
*Either* an item ID number [#]_ (``<id>``) or item name (``<name>``) can be used.
Item names are defined in the `Material enum`_ of the Bukkit API. Item names are
case-insensitive.
The Material enum changes when items are added, changed, or reworked. The
Minecraft server version determines what items are available. Check the
documentation for what item names are available.
This example gives the ``barbarian`` class a bow and leather leggings.
.. code-block:: yaml
classes:
barbarian:
items: bow
armor: leather_leggings
.. [#] Each item ID number must be wrapped in apostrophes (``''``) to work
correctly.
Multiple items
==============
``[<id>|<name>]:<amount>``
This method lets you add an amount to an item. This is useful for items like
arrows or potions.
Now, our example also gives 64 arrows and four pieces of cooked porkchops.
.. code-block:: yaml
classes:
barbarian:
items: bow, arrow:64, grilled_pork:4
armor: leather_leggings
Item sub-types
==============
``[<id>|<name>]:<data>:<amount>``
To add an item sub-type, both the sub-type name and an amount must be specified.
This is helpful for items like potions, wool, and/or dyes.
- `Potion effect types`_
- `Color names`_
Now, our example gives one Potion of Strength I and one piece of purple wool.
.. code-block:: yaml
classes:
barbarian:
items: bow, arrow:64, grilled_pork:4, potion:increase_damage:1,
wool:purple:1
armor: leather_leggings
************
Enchantments
************
``<item> <eid>:<level>;<eid>:<level>;...``
Add enchantments to an item by adding a space with a semi-colon separated list
with the enchantment name and the strength. Find valid enchantment names in the
`Enchantment class`_ in the Spigot API.
Now, our example adds Power II and Flame I to the bow and a diamond sword with
Sharpness I and Knockback II.
.. code-block:: yaml
classes:
barbarian:
items: bow arrow_damage:2;arrow_fire:1, arrow:64, grilled_pork:4,
potion:increase_damage:1, wool:purple:1,
diamond_sword damage_all:1;knockback:2
armor: leather_leggings
*************
Economy Money
*************
``$<amount>``
MobArena supports entry fees and rewards with economy plugins. You must use
`Vault`_ for this to work.
Since v0.96, floating point numbers are also supported.
Examples:
- ``$1``
- ``$5``
- ``$3.14``
- ``$0.99``
***************
Command Rewards
***************
.. code-block:: yaml
cmd:/give <player> dirt
cmd(description of reward):/give <player> dirt
You can run a command to give a special reward to a player. Command rewards are
supported since v0.99. Useful examples might be to give a permission (maybe
unlocking a new arena) or integrating with other plugins.
It is possible to customize the message a player receives when they receive a
command reward. By default, the plugin prints the command. You can specify a
more user-friendly message in the second format listed above.
For example, ``cmd(New arena to play!)/perm add <player> <permission>``
appears to the player as ``You just earned a reward: New arena to play!``.
.. _`Material enum`: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html
.. _`Potion effect types`: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/potion/PotionEffectType.html
.. _`Color names`: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/DyeColor.html
.. _`Enchantment class`: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/enchantments/Enchantment.html
.. _`Vault`: https://dev.bukkit.org/projects/vault

View File

@ -1,68 +0,0 @@
############
Leaderboards
############
MobArena supports so-called *leaderboards* (although technically they
are more like scoreboards or session signs). By arranging signs in a
two-dimensional grid on a wall, spectators can see which classes each
player has chosen, which wave they died on, how many kills and how much
damage they have done, etc.
.. figure:: img/leaderboards/1.png
:alt: Sign Grid
Sign Grid
The requirements for leaderboards are at the very least two rows of
signs on a wall as seen in the screenshot above. The top row should not
be empty, however, as it should contain the leaderboard *headers*, which
denote what kind of information the signs below them display. How many
headers (and which) you want is entirely up to you.
.. figure:: img/leaderboards/2.png
:alt: Top Left Sign
Top Left Sign
To get started, replace the top left empty sign (or place it if you
haven't already), and write ``[MA]<name>``, where ``<name>`` is the name
of your arena, on the first line. In the screenshot above, I have set up
the top left sign for the arena named Jail by writing ``[MA]jail`` on
it. MobArena automatically fills in the rest of the text and the colors
for you.
.. figure:: img/leaderboards/3.png
:alt: Sign Text Screen
Sign Text Screen
MobArena will then tell you that the sign has been created, and that you
should set up the rest of the signs. The rest of the headers follow the
same kind of format as the top left sign, so you simply write
``[MA]<stat>``, where ``<stat>`` is one of the following:
- ``class`` - The class name of the player
- ``lastWave`` - The last wave the player was part of (current wave if
still alive)
- ``kills`` - The number of monsters the player has killed
- ``dmgDone`` - The amount of damage the player has dealt
- ``dmgTaken`` - The amount of damage the player has taken
- ``swings`` - The number of times the player has swung their weapon
- ``hits`` - The number of times the player has swung their weapon and
successfully hit a monster
In the screenshot above, I have already set up a couple of signs, and
I'm about to set up the sign for damage done. As with the top left sign,
your only job is to tell MobArena which stat you want - it'll take care
of colors and formatting automatically. Note that MobArena's sign
handling is case sensitive, so make sure you get it right.
.. figure:: img/leaderboards/4.png
:alt: Final Setup
Final Setup
When you're done setting up the leaderboards, they should look something
like the screenshot above, and you should be good to go! Leaderboards
can be set up anywhere (even outside of the world the arena is in), but
you can only have a single leaderboard per arena.

View File

@ -1,38 +0,0 @@
#############
Monster types
#############
MobArena supports any monster available in the `EntityType
enum <https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html>`__.
You don't have to write the names in all caps, and you can omit or
include underscores (``_``), hyphens (``-``), and periods (``.``) as you
please.
Some monsters are a little special. Creepers, for instance, can be
charged or powered, meaning their explosions become much more powerful,
and their appearance changes. Another example is slimes and magma cubes,
which have different sizes.
MobArena supports some of the variations of these different monster
types, and they are listed here:
- ``explodingsheep`` is a MobArena-specific type of sheep that bounces
around and explodes when in the proximity of a player
- ``poweredcreeper`` is a charged/powered creeper
- ``angrywolf`` is an aggressive wolf with red eyes
- ``babyzombie`` is a baby-version of a zombie
- ``babypigman`` is a baby-version of a pigman
- ``babyzombievillager`` is a baby-version of a zombie villager
- ``killerbunny`` is a killer bunny version of a rabbit
As for slimes and magma cubes, both monster types are assigned a random
size when they spawn. However, they also both support size suffixes that
force them to be a specific size. They are:
- ``tiny`` size 1
- ``small`` size 2
- ``big`` size 3
- ``huge`` size 4
As an example, ``slimehuge`` will spawn a size 4 slime, while
``magmacubetiny`` will spawn a size 1 magma cube.

View File

@ -1,179 +0,0 @@
###########
Permissions
###########
A permissions plugin **is NOT required** for MobArena to work, but if
you want that extra bit of control, here's a rundown of the different
types of permission nodes you can use with MobArena.
**NOTE: MobArena uses sane defaults. This means that by default, all
players can use all arenas and all classes, and ops can use all admin
and setup commands. Unless you want to prevent some groups from
accessing certain arenas or classes, or you want to give non-ops admin
and setup permissions, there is no need to mess with any permissions at
all, so go away from this page and remove all occurrences of
``mobarena`` in your permissions-file!**
Arenas
~~~~~~
Did you read the note at the top? If not, read it before you continue.
So, you want to remove permissions for certain arenas from certain
users? Alright, that means you will have to *negate* or *revoke* the
permissions in your permissions plugin. In bPermissions, the negation
modifier is a caret, ``^``, in GroupManager and PermissionsEx it is a
minus, ``-``, and in zPermissions it is by setting the permission to
``false``. The examples below revoke the permission for the default
arena.
| bPermissions: ``^mobarena.arenas.default``
| GroupManager: ``-mobarena.arenas.default``
| zPermissions: ``mobarena.arenas.default: false``
I recommend letting everyone enjoy all your arenas, but this could be
used in combination with "leveling" plugins to allow players to use
"harder" arenas at higher levels. It could also be used for
sponsors-only arenas.
Still confused? Check the `sample setup <#sample-setup>`__ at the bottom
of the page!
Classes
~~~~~~~
Did you read the note at the top? If not, read it before you continue.
Alright, if you're reading this, you want to remove permissions for
certain classes from certain users. As with the arena permissions, you
need to *negate* or *revoke* the permissions in your permissions plugin.
In bPermissions, the negation modifier is a caret, ``^``, in
GroupManager and PermissionsEx it is a minus, ``-``, and in zPermissions
it is by setting the permission to ``false``. The examples below revoke
the permission for the Knight class.
| bPermissions: ``^mobarena.classes.knight``
| GroupManager: ``-mobarena.classes.knight``
| zPermissions: ``mobarena.classes.knight: false``
**Note how the class name is lowercase.** This is important. Even if the
Knight class is called ``KnIGhT`` in your config-file, it MUST be all
lowercase in your permissions-file.
As with arenas, I recommend letting everyone enjoy all the classes,
unless you have a special reason not to.
Still confused? Check the `sample setup <#sample-setup>`__ at the bottom
of the page!
Commands
~~~~~~~~
Did you read the note at the top? If not, read it before you continue.
If you're reading this, you want to either give certain users access to
some of the admin and/or setup commands, or you want to remove some of
the user commands from some groups. If this is not the case, stop
reading and leave this page!
The first group of commands are the user commands. They are accessible
by all players by default, so don't put ``mobarena.use.*`` or something
stupid like that in your permissions-file! If you want a group to not
have access to the user commands, *negate* the permission
``mobarena.use``, which is the *parent permission node* for all the user
commands. See the classes and arenas sections for information on how to
negate permissions. If that doesn't work, negate the
``mobarena.use.join`` and ``mobarena.use.spec`` permissions. That should
be enough.
::
mobarena.use.join
mobarena.use.leave
mobarena.use.spec
mobarena.use.arenalist
mobarena.use.playerlist
mobarena.use.notready
mobarena.use.class
The admin commands are simple. They allow disabling/enabling MobArena
and individual arenas, kicking players from the arenas, restoring player
inventories if they got lost somehow, forcing arenas to start or end,
and teleporting in and out of arenas regardless of what the arena state
is. If you want to grant all of these permissions, use the *parent
permission node* ``mobarena.admin``. Don't mess around with ``*`` or
something stupid like that.
::
mobarena.admin.enable
mobarena.admin.kick
mobarena.admin.restore
mobarena.admin.force
mobarena.admin.teleport
Setup commands are only for ops, just like admin commands. **Do not**
give these permissions to random people, because they can remove your
arenas and destroy your config-files, if they do something stupid. The
setup commands allow you to manage arenas, regions, spawnpoints, chests,
leaderboards, etc. They also allow you to set up new classes in-game. If
you want to grant all of these permissions, use the *parent permission
node* ``mobarena.setup``. Don't mess around with ``*`` or something
stupid like that.
::
mobarena.setup.config
mobarena.setup.setup
mobarena.setup.setting
mobarena.setup.addarena
mobarena.setup.removearena
mobarena.setup.editarena
mobarena.setup.spawnpoints
mobarena.setup.containers
mobarena.setup.checkdata
mobarena.setup.checkspawns
mobarena.setup.classchest
mobarena.setup.classes
mobarena.setup.leaderboards
mobarena.setup.autogenerate
mobarena.setup.autodegenerate
Sample setup
~~~~~~~~~~~~
Assume you have a class called DiamondKnight that you only want your
donors to be able to use (very common use case). How do you set up your
permissions plugin when you have to revoke the class permission from the
default group, but the donor group inherits from the default group? It's
very simple: You're doing it wrong...
What you have to do instead is make an *auxiliary default*-group that
contains all your default permissions, and have your default group
inherit from that group, and furthermore revoke the DiamondKnight class
permission in MobArena. Your donor group then also inherits from the
auxiliary group, and everything is wonderful. Confusing? Here's a
pseudo-code example:
::
default-aux: <-- This is the auxiliary group that is to
permissions: be inherited by the default group and
- essentials.balance the donor group. It is not used for
- essentials.pay anything else.
- essentials.sell
default: <-- This is the default group. It inherits
inherits: default-aux from default-aux, but also revokes the
permissions: permission for the special class.
- -mobarena.classes.diamondknight
donor: <-- This is the donor group, which also
inherits: default-aux inherits from default-aux, but it
permissions: does not revoke any class permissions,
- essentials.balance.others which means it has access to all of
- essentials.kit them by default.
This sample setup is **pseudo code** and cannot be simply copy/pasted
into your own permissions file. It's your job to figure out how your
permissions plugin works, and what its syntax is.

View File

@ -1,530 +0,0 @@
##########################
Setting up the config file
##########################
**On this page:** \*
`Overview <./Setting-up-the-config-file#wiki-an-overview>`__ \*
```global-settings`` <./Setting-up-the-config-file#wiki-global-settings>`__
\* ```classes`` <./Setting-up-the-config-file#wiki-classes>`__ \* `Bring
your own items <./Setting-up-the-config-file#bring-your-own-items>`__ \*
`Slot-specific armor
nodes <./Setting-up-the-config-file#slot-specific-armor-nodes>`__ \*
`Price <./Setting-up-the-config-file#price>`__ \* `Unbreakable
items <./Setting-up-the-config-file#wiki-unbreakable-weaponsarmor>`__ \*
`Per-class
permissions <./Setting-up-the-config-file#wiki-per-class-permissions>`__
\* `Pet classes <./Setting-up-the-config-file#wiki-pet-classes>`__ \*
`Mounts <./Setting-up-the-config-file#wiki-mounts>`__ \*
```arenas`` <./Setting-up-the-config-file#wiki-arenas>`__ \*
```settings`` <./Setting-up-the-config-file#wiki-settings>`__ \*
```waves`` <./Setting-up-the-config-file#wiki-waves>`__ \*
```rewards`` <./Setting-up-the-config-file#wiki-rewards>`__ \*
```coords`` <./Setting-up-the-config-file#wiki-coords>`__
An Overview
~~~~~~~~~~~
*Note: When editing the config-file, you **MUST use spaces for
indentation**! Using tabs instead of spaces will give you errors!*
The config-file, ``plugins/MobArena/config.yml``, consists of 3
sections: ``global-settings``, ``classes``, and ``arenas``. The default
config-file that is generated when MobArena is first loaded looks
something like this:
::
[...]
global-settings:
update-notification: true
enabled: true
allowed-commands: /list
prefix: '&a[MobArena] '
classes:
Knight:
items: diamond_sword, grilled_pork:2
armor: 306,307,308,309
Archer:
items: wood_sword, bow, arrow:128, grilled_pork
armor: 298,299,300,301
[...]
arenas:
default:
settings:
prefix: ''
world: Tundra
enabled: true
protect: true
clear-wave-before-next: false
[...]
waves:
[...]
rewards:
[...]
Note about notation: ``[true|false]`` means the setting must be "true or
false", either or. ``<time>`` means the setting must be an amount of
time (in seconds or server ticks), always a whole number, and always
``0`` or greater. ``<amount>`` is similar to time.
global-settings
---------------
The ``global-settings`` are few, but important. Note that if
``enabled: false``, no arenas can be joined, regardless of their
individual ``enabled`` status.
- ``enabled: [true|false]`` - This determines if MobArena is enabled or
not. If set to ``false``, players will not be able to join any arenas
at all, regardless of what the arenas' individual statuses are.
- ``update-notification: [true|false]`` - If true, MobArena will send a
message to ops when they log on if a new version of MobArena is
available.
- ``allowed-commands: <com1>, <com2>, ...`` - A comma-separated list of
the commands that players are allowed to use while in the lobby
and/or arena. This is useful if you don't want players to use
teleport-commands, flying carpets, kill commands, etc. If you write
the command WITH its forward-slash, the entire command and all
"sub-commands" will be allowed. For instance, writing ``/kill`` will
allow both ``/kill``, ``/kill Sausageman22`` and ``/kill Notch``.
Writing the command WITHOUT its forward-slash will allow only that
specific command or "sub-command". Writing ``kill`` will thus ONLY
allow ``/kill``, but not ``/kill Sausageman22``.
- ``prefix: <prefix>`` - The prefix MobArena uses for all of its
messages. The default is the classic green ``[MobArena]``, but you
can change it to whatever you want. You can override the prefix for
specific arenas by using the arena-specific setting with the same
name.
I recommended leaving the update notifications on, and disabling
commands like ``/kill`` and ``/tp``.
classes
-------
The ``classes``-section is slightly more complicated. It is divided into
*class-branches*, where each branch denotes the *name of the class*, and
each branch has mandatory nodes ``items`` and ``armor``, as well as
optional slot-specific armor nodes and optional nodes ``price``,
``permissions``, ``lobby-permissions``, ``unbreakable-weapons``, and
``unbreakable-armor``.
**Note:** YAML is picky about how you type your items. Make sure you
read the short [[Item and Reward Syntax]]-page and fully understand it
before you attempt to modify the config file!
::
classes:
Archer:
items: wood_sword, bow, arrow:128, grilled_pork
armor: 298,299,300,301
permissions:
- EffectiveArrows.use.*
- -mobarena.use.leave
Tank:
items: iron_sword
armor: 310,311,312,313
offhand: shield
Knight:
items: '276'
armor: iron_helmet, iron_chestplate, iron_leggings, iron_boots
Wolf Master:
items: stone_sword, grilled_pork, bone:2
armor: 298,299,300,301
Crusader:
items: iron_sword, hay_block:17
armor: 302,303,304,305
price: $5
Bring your own items
~~~~~~~~~~~~~~~~~~~~
MobArena allows you to just bring your own items into the arena via the
implicit 'My Items' class. What this means is that if you just place a
sign in the lobby with the text My Class, you'll get the items that you
had before joining the arena. Items are still restored on dying in or
leaving the arena.
For a smooth, own-items-only experience, ditch the signs and set the
per-arena setting ``default-class`` to ``myitems``.
Slot-specific armor nodes
~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to use off-hand items, or if you just want to be explicit
about which items go where in the armor slots, use the optional
slot-specific armor nodes: ``offhand``, ``helmet``, ``chestplate``,
``leggings``, ``boots``. In the example above, the **Tank** class gets a
shield in its off-hand slot.
Price
~~~~~
The optional ``price`` node can be used to give classes a per-session
price. When a player tries to pick a class that has a price, they will
only be able to if they can afford it. The money is withdrawn when the
arena starts, i.e. picking different priced classes in succession will
not (necessarily) result in empty player wallets. In the example above,
the **Crusader** class costs ``$5``.
Unbreakable weapons/armor
~~~~~~~~~~~~~~~~~~~~~~~~~
The optional ``unbreakable-weapons`` and ``unbreakable-armor`` nodes can
be used to toggle on or off the unbreakability of class items and armor.
The nodes *default to true*, so they are really only necessary if you
want to toggle OFF the feature, i.e. if you want items to deteriorate
and break! If that's what you want, set the nodes to false.
.. raw:: html
<pre>
classes:
FrailTank:
items: diamond_sword
armor: 310,311,312,313
<font color="blue">unbreakable-weapons: false</font>
<font color="blue">unbreakable-armor: false</font>
</pre>
Per-class permissions
~~~~~~~~~~~~~~~~~~~~~
Using the optional ``permissions``-node, you can give classes special
permissions to customize them even more. Each permission must be listed
with a dash (-) in front of it. If you want a class to *not* have a
permission, put a dash/minus at the very beginning of the permission
node. In the example above, the **Archer** class will be able to use the
EffectiveArrows plugin, but won't be able to use ``/ma leave`` (meaning
it's impossible to leave the arena without dying).
.. raw:: html
<pre>
classes:
Archer:
items: wood_sword, bow, arrow:128, grilled_pork
armor: 298,299,300,301
<font color="blue">permissions:</font>
<font color="blue">- EffectiveArrows.use.*</font>
<font color="blue">- -mobarena.use.leave</font>
</pre>
The optional ``lobby-permissions``-node gives players special
permissions while they are in the lobby *after they have picked a
class*. This feature can be used e.g. in combination with a shop plugin
and a base class that perhaps has nothing (maybe except for a few
potions).
.. raw:: html
<pre>
classes:
Basic:
items: ''
armor: ''
<font color="blue">lobby-permissions:</font>
<font color="blue">- shop.buy</font>
</pre>
Pet classes
~~~~~~~~~~~
For every bone (Material name: ``bone``, data value: ``352``) in a
class' items-list (or class chest), one wolf pet will spawn upon arena
start. In the example above, every player that picks the **Wolf Master**
class will have 2 wolves spawn upon arena start. The wolves are
invincible, but deal less damage than normal wolves.
.. raw:: html
<pre>
classes:
Wolf Master:
items: stone_sword, grilled_pork, <font color="blue">bone:2</font>
armor: 298,299,300,301
</pre>
Mounts
~~~~~~
To give a class a horse mount, give it a hay block in the items-list (or
place a hay block in the class chest). The item stack amount (in the
first encountered stack) determines the variant and barding of the
horse. You can use this table to figure out which hay block amount you
need for your desired variant and barding:
+----------------+------------+------------+------------+---------------+
| | **None** | **Iron** | **Gold** | **Diamond** |
+================+============+============+============+===============+
| **Horse** | 1 | 9 | 17 | 25 |
+----------------+------------+------------+------------+---------------+
| **Donkey** | 2 | - | - | - |
+----------------+------------+------------+------------+---------------+
| **Mule** | 3 | - | - | - |
+----------------+------------+------------+------------+---------------+
| **Skeleton** | 4 | - | - | - |
+----------------+------------+------------+------------+---------------+
| **Zombie** | 5 | - | - | - |
+----------------+------------+------------+------------+---------------+
Note that only normal horses can have barding.
In the example above, every player that picks the **Crusader** class
will have a white horse with gold barding upon arena start. The mounts
are invincible.
.. raw:: html
<pre>
classes:
Crusader:
items: iron_sword, <font color="blue">hay_block:17</font>
armor: 302,303,304,305
</pre>
arenas
------
This section is by far the largest, and it is divided into several
smaller branches. In the above example, ``default`` denotes the *name*
of the default arena. This name can be altered, but it must contain no
spaces (use underscores instead). The arena name is significant when a
server has multiple arenas and no Master Lobby (will be featured later).
Let's go over the different branches:
settings
~~~~~~~~
The settings-branch is quite extensive, and besides the ``world``-node,
it is basically just a bunch of toggles (on/off, true/false), though a
few are number-based.
- ``prefix: <prefix>`` - An arena-specific prefix to use for
messages/announcements in this arena only. The default is the empty
string (``''``), which means the ``global-settings`` prefix will be
used.
- ``world: <name>`` - The name of the world the arena resides in.
- ``enabled: [true|false]`` - If false, players cannot join the arena.
- ``protect: [true|false]`` - If false, the arena will not be protected
from explosions and players breaking the blocks.
- ``entry-fee: [$<amount>|<item>:<amount>]`` - Follows the exact same
notation as the class items and rewards (read the [[Item and Reward
Syntax]]-page). ``$20`` will subtract 20 of whatever currency you use
from the players upon joining. ``$5, stick:2`` will require the
player to have 5 currency units and 2 sticks to join the arena. The
entry-fee will be refunded if the player leaves before the arena
starts.
- ``default-class: <class>`` - If non-empty, this class is
automatically assigned to players when they join the arena. The class
name must be all lowercase and with no spaces.
- ``clear-wave-before-next: [true|false]`` - If true, no monsters will
spawn before all monsters of the previous wave have been killed.
- ``clear-boss-before-next: [true|false]`` - If true, no new waves will
spawn before the current boss (if any) is dead.
- ``clear-wave-before-boss: [true|false]`` - If true, a boss wave will
not spawn until all previous monsters have been killed.
- ``auto-equip-armor: [true|false]`` - If true, armor pieces will
automatically be equipped upon class selection. Note that this does
not work if a class has more than 1 of an armor piece type.
- ``soft-restore: [true|false]`` - If true, all destroyed blocks will
be saved in a "repair list", which will be used to restore blocks at
arena end. No data is saved to the harddrive. Note that this setting,
if true, ignores the ``protect`` flag.
- ``soft-restore-drops: [true|false]`` - If true, blocks destroyed by
players will drop as items like they normally do (using pickaxes,
spades, etc.). Note that this makes it very easy for classes with
pickaxes to "mine the arena" and build forts.
- ``require-empty-inv-join: [true|false]`` - If false, players'
inventories will be saved upon joining, and restored upon
death/leaving.
- ``require-empty-inv-spec: [true|false]`` - If false, players can
spectate the arena without having to empty their inventories.
- ``hellhounds: [true|false]`` - If true, all pet wolves in the arena
will be in flames! This has no actual function, and is purely for the
cool-factor. Also useful for distinguishing enemy wolves and pet
wolves.
- ``pvp-enabled: [true|false]`` - If true, players can damage each
other in the arena.
- ``monster-infight: [true|false]`` - If false, monsters will no longer
damage each other.
- ``allow-teleporting: [true|false]`` - If false, all warping to and
from the arena region is blocked. Useful for preventing players from
summoning other players into the arena for help.
- ``spectate-on-death: [true|false]`` - If false, players will not get
warped to the spectator area, but instead be "kicked" from the arena
(essentially a forced /ma leave).
- ``auto-respawn: [true|false]`` - If false, players will be greeted
with the typical death screen upon dying in the arena, and will have
to click the respawn button to respawn. With this setting at false,
players will actually die in the arena, meaning plugins like Heroes
and mcMMO will properly trigger their resetting of internal data upon
respawn.
- ``share-items-in-arena: [true|false]`` - If false, players will not
be able to drop items in the arena.
- ``min-players: <amount>`` - Gives a lower limit on how many players
are required to start the arena. The default of ``0`` is the same as
``1``, which means 1 or more players may start the arena. Note that
this feature is incompatible with ``auto-start-timer`` and
``start-delay-timer``!
- ``max-players: <amount>`` - Gives an upper limit on how many players
may join the arena. The default of ``0`` means no limit.
- ``max-join-distance: <distance>`` - The maximum distance (in blocks)
from which players can join or spectate the arena. If 0 (default),
there is no limit, and players can join from any world. Note that the
distance is calculated from every corner of the arena region, and
that players not in the arena world won't be able to join or
spectate.
- ``first-wave-delay: <time>`` - The time (in seconds) before the first
wave of monsters upon arena start.
- ``wave-interval: <time>`` - The time (in seconds) between each new
wave of monsters. If clear-wave-before-next: true, this setting will
be ignored.
- ``final-wave: <number>`` - The number of the final wave before the
arena is force ended. This is useful if you want to set a cap on how
many waves an arena will have.
- ``monster-limit: <amount>`` - The maximum amount of monsters MobArena
is allowed to spawn for this arena. The next wave, if any, will not
spawn until there is room for more monsters.
- ``monster-exp: [true|false]`` - If true, monsters will drop
experience orbs. This is useful if you wish to give players the
ability to spend the gathered experience on enchants or something
else (using different plugins) during the session.
- ``keep-exp: [true|false]`` - If true, players will keep the
experience they gather in the arenas after death. This is useful if
you want to allow players to level up or gather experience in the
arenas. NOTE: If using ``display-waves-as-level`` or
``display-timer-as-level``, set ``keep-exp`` to false.
- ``food-regen: [true|false]`` - If true, a full food bar will cause
players to regenerate health while in the arena. Note that this
potentially makes tank-like classes extremely overpowered, since
diamond armor (by default) coupled with a full food bar will make a
player very hard to kill.
- ``lock-food-level: [true|false]`` - If true, the food bar will be
locked for all players in the arena, meaning they will not end up
starving, and they will be able to sprint around as they please.
- ``player-time-in-arena: <time of day>`` - When set to anything but
world, this setting will freeze the apparent world time for players
in the arena to whatever value you set. This is useful for making
time-of-day themed arenas (e.g. constant night time for a cemetery,
broad daylight for a pirate ship). Valid values are: dawn, sunrise,
morning, midday, noon, day, afternoon, evening, sunset, dusk, night,
midnight.
- ``auto-ignite-tnt: [true|false]`` - If true, TNT will be
automatically ignited when placed. This is useful for preventing
Oddjob-like classes from forting.
- ``auto-start-timer: <time>`` - The time (in seconds) before the arena
will be force started after the first player has joined the lobby
(the default of 0 means deactivated or infinite time). Non-ready
players will be removed from the lobby. This setting is useful to
prevent ill-minded players from delaying or preventing other players
from starting the arena. Note that this feature is incompatible with
``min-players``!
- ``start-delay-timer: <time>`` - The time (in seconds) before the
arena can be started after the first player has joined the lobby.
This setting is useful if you want to give your players a fixed
window of time to join the arena after the first player has joined,
so they can't just start it off right away. Note that this feature is
incompatible with ``min-players``!
- ``display-waves-as-level: [true|false]`` - When set to true, the
players' level counter (above the experience bar) will be used to
display the current wave number. If the wave announcements in the
announcements-file are silenced, this can be used to make a much less
"spammy" MobArena experience. NOTE: Do not use this if ``keep-exp``
is set to true!
- ``display-timer-as-level: [true|false]`` - When set to true, the
players' level counter (above the experience bar) will be used to
display the auto-start timer in the lobby. NOTE: Do not use this if
``keep-exp`` is set to true!
- ``auto-ready: [true|false]`` - When set to true, players are
automatically flagged as ready when they pick a class. Useful for
arenas with many players where hitting an iron block becomes
difficult.
- ``use-scoreboards: [true|false]`` - Whether to use scoreboards in
MobArena or not.
- ``isolated-chat: [true|false]`` - When set to true, all chat messages
sent by arena players will be seen only by other arena players in the
same arena. The arena players will still be able to see chat messages
from other players on the server who aren't in an arena.
- ``global-end-announce: [true|false]`` - When set to true, MobArena
will announce the ``arena-end-global`` message (see
[[Announcements]]) to all players on the server when an arena ends.
- ``global-join-announce: [true|false]`` - When set to true, MobArena
will announce the ``arena-join-global`` message (see
[[Announcements]]) to all players on the server when the first player
joins an arena.
waves
~~~~~
Please go to [[setting up the waves]] for more information.
rewards
~~~~~~~
The rewards-section denotes which rewards the arena players can win in
the arena. It uses the exact same item system as the classes-section
does, so nothing new there. You can also specify monetary rewards if you
use a major economy plugin (iConomy, BOSEconomy, Essentials Eco) in the
notation ``$<amount>``.
**Note:** YAML is picky about how you type your items. Make sure you
read the short [[Item and Reward Syntax]]-page and fully understand it
before you attempt to modify the config file!
The waves-branch is broken into ``every``- and ``after``-branches. The
``every``-branch denotes rewards that the players can receive *every* x
waves (repeated). The ``after``-branch denotes rewards that the player
can receive *after* wave x (only once) has started. Note that **only one
reward** is picked at random from the list.
In the following example, players will receive either four arrows or a
gold bar every 3 waves (3, 6, 9, 12, etc.), and a diamond every 10 waves
(10, 20, 30, etc.), as well as an iron tool on wave 7 (only on wave 7),
a diamond sword on wave 19 (only on wave 19), and 200 currency units on
wave 20:
::
rewards:
waves:
every:
'3': arrow:4, gold_ingot
'10': diamond
after:
'7': iron_spade, iron_hoe, iron_axe, iron_pickaxe
'19': diamond_sword
'20': $200
**Note:** The wave numbers **must be enclosed by apostrophes** (e.g.
``'7':``, not ``7:``), or YAML will throw errors. If you aren't sure how
to do it, just copy one of the other lines and change the wave number
and the items.
coords
~~~~~~
The coords-section does not exist when MobArena first generates the
config-file. This is because the coordinates need to be set by the user
*in-game*. See the in-game section for more details on how to set
everything up. The coords-section consists of five key points, and an
arbitrary amount of spawnpoints:
- ``p1`` and ``p2`` - These two points should span the entire arena
region (including spectator areas and the lobby associated with the
arena, if possible).
- ``l1`` and ``l2`` - [OPTIONAL] If the lobby can't properly reside
within the arena region for some reason, these two points should span
the lobby region.
- ``arena`` - This warp is where the players will be teleported upon
arena start.
- ``lobby`` - Where the players will be teleported upon joining the
arena.
- ``spectator`` - Where the players will be teleported upon death or
spectating.
- ``spawnpoints`` - A list of points where monsters can spawn from.
Note that editing these points manually can have some very unhappy
consequences. Always edit these points from within Minecraft to ensure
that they are generated properly.

View File

@ -1,493 +0,0 @@
################
Setting up waves
################
**On this page:** \* `About
Modules <./Setting-up-the-waves#wiki-about-modules>`__ \* `Wave
Branches <./Setting-up-the-waves#wiki-wave-branches>`__ \* `Common
Nodes <./Setting-up-the-waves#wiki-common-nodes>`__ \*
```recurrent`` <./Setting-up-the-waves#wiki-recurrent-waves>`__ \*
```single`` <./Setting-up-the-waves#wiki-single-waves>`__ \* `Wave
Types <./Setting-up-the-waves#wiki-wave-types>`__ \*
```default`` <./Setting-up-the-waves#wiki-default-waves>`__ \*
```special`` <./Setting-up-the-waves#wiki-special-waves>`__ \*
```swarm`` <./Setting-up-the-waves#wiki-swarm-waves>`__ \*
```supply`` <./Setting-up-the-waves#wiki-supply-waves>`__ \*
```upgrade`` <./Setting-up-the-waves#wiki-upgrade-waves>`__ \*
```boss`` <./Setting-up-the-waves#wiki-boss-waves>`__ \* `A Sample
Setup <./Setting-up-the-waves#wiki-sample-config-file-setup>`__
**Note: If you are impatient, go to the bottom of this page for an
example config-file setup to see what the waves could look like. Modify
them as you please, but make sure to read this page before asking any
questions!**
Make sure to check out `Agnate's MobArena Bosses and Waves
Thread <http://forums.bukkit.org/threads/mobarena-boss-and-wave-thread.31797/>`__
if you need inspiration for adding some cool bosses to your arenas!
About Modules
-------------
The new MobArena waves-system is extremely modular, meaning every time
you plug in a new wave, you only have to provide the nodes required by
the specific modules you are using. The modules can be broken into *wave
branches* and *wave types*. The structure of the waves-section in
config-file is the following:
::
waves:
recurrent: <-- Wave branch
<wave name>:
type: <wave type> <-- Wave type
frequency: #
priority: #
single: <-- Wave branch
<wave name>:
type: <wave type> <-- Wave type
wave: #
Wave Branches
-------------
The waves are split into two branches, ``recurrent`` and ``single``.
*Recurrent waves* (may) occur more than once (as in, they repeat), given
a frequency (how often they occur) and a priority (how "important" they
are, i.e. which wave should spawn if two recurrent waves clash). *Single
waves* occur just once, on the given wave (and always occur over
recurrent waves, should they clash).
Common Nodes
~~~~~~~~~~~~
As you can see, the two branches have one thing in common, the
``type``-node. Other than that, their other nodes differ. However, there
are two additional nodes that can be used regardless of branch and type
(doesn't work for boss waves, though):
``amount-multiplier: <decimal value>`` (optional) minimum value of 0.1,
this multiplier helps determine how many monsters spawn per wave
(minimum 1). If 8 mobs are supposed to spawn, and the value is ``0.5``,
only 4 mobs will spawn. If the value is ``3``, 24 will spawn.
``health-multiplier: <decimal value>`` (optional) minimum value of 0.1,
this multiplier helps determine the health for each monster in a wave.
If a zombie spawns with the default of 20 health points and the value is
``0.5``, the zombie will have 10 health points. If the value is ``4``,
it will be 80 health points.
These two common nodes can be used to greatly customize the difficulty
of the monsters in each wave. If you want more monsters, just set the
amount-multiplier higher than 1, and maybe adjust the health with the
health-multiplier accordingly. If you want the monsters to be tougher to
kill, just up the health-multiplier.
An additional node can be used to help determine where enemies will
spawn:
``spawnpoints: <semi-colon separated list of spawnpoints>``
For example, we can make a swarm wave spawn monsters only on spawns
``5,53,198``, ``-16,54,185``, and ``-7,53,179`` if players are in range:
::
swarm3:
type: swarm
wave: 11
monster: zombie_pigman
spawnpoints: 5,53,198; -16,54,185; -7,53,179
Note that these spawnpoints must exist in the ``spawnpoints``-list of
the ``coords``-section to work.
Recurrent Waves
~~~~~~~~~~~~~~~
``type: [default|special|swarm|supply|upgrade|boss]`` (required)
determines the wave type. Read the **Wave types** section further down
for more details.
``frequency: #`` (required) determines how often the wave will/can
spawn. With a frequency of 1, the wave can potentially spawn on every
single wave number. The implicit default waves in MobArena have a
frequency of 1, and the implicit special waves have a frequency of 4,
which means the default waves (can) spawn every wave, and the special
waves (can) spawn every 4th wave.
``priority: #`` (required) determines how "important" the wave is. If
two recurrent waves clash, the wave with the highest priority will
spawn. The implicit default waves in MobArena have a priority of 1, and
the implicit special waves have a priority of 2, which means if the
default and special waves clash, the special waves will spawn because
their priority is higher.
``wave: #`` (optional) determines the first wave number on which this
wave can/will spawn. This is useful for offsetting waves with similar
frequencies. Note that if no wave is specified, it will default to the
value of the (required) frequency-node. The implicit default waves in
MobArena have wave value of 1, and the implicit special waves have a
wave value of 4 (same as the frequency), which means the default waves
may begin spawning from wave 1, and the special waves may begin spawning
from wave 4.
Single Waves
~~~~~~~~~~~~
``type: [default|special|swarm|supply|upgrade|boss]`` (required)
determines the wave type. Read the **Wave types** section further down
for more details.
``wave: #`` (required) determines the wave on which this wave *will*
spawn. No matter what priority a recurrent wave have, if it clashes with
a single wave, the single wave will always spawn instead of the
recurrent waves. Single waves are good for extraordinary waves like
"swarm" waves, "boss" waves or even normal waves with specific monster
types, for instance.
Wave Types
----------
All MobArena waves must specify a *wave type*, which must be either
``default``, ``special``, ``swarm``, ``supply``, ``upgrade`` or
``boss``. These different wave type modules introduce some new required
and optional nodes. Note that no matter what the wave type is, any wave
*must* adhere to the requirements of the wave branch (read above).
Default Waves
~~~~~~~~~~~~~
Default waves are waves that spawn an amount of monsters picked
semi-randomly from an optional list of monsters. The amount of monsters
grow at a configurable (but optional) rate. If no growth or monster-list
is specified, default waves will consist of 5 different monster types
(zombie, skeleton, spider, creeper, wolf), all equally likely to spawn,
spawned at the "old" growth rate (player count + wave number). Nodes:
``growth: [old|slow|medium|fast|psycho]`` (optional) determines how fast
the monster count grows with every wave. ``old`` means (player count +
wave number), but the other four use a mathematical function to
determine the monster count, also based on player count and wave number.
See [[Formulas]] for more info.
``monsters: <list of <monster>: <probability>>`` (optional) determines
[[monster types]], and their individual probabilities of spawning on
each wave. Note that the probabilities are just that, probabilities.
They do not specify exact amounts, but only chance of spawning. The
following sample will statistically spawn twice as many zombies as
skeletons:
::
monsters:
zombies: 10
skeletons: 5
``fixed: [true|false]`` (optional) the probability values in the
monsters list becomes amount values instead, such that the above wave
will spawn exactly 10 zombies and 5 skeletons, regardless of player
count and wave number.
Special Waves
~~~~~~~~~~~~~
Special waves are waves that spawn *one type* of monster, and always a
fixed amount. Unlike with *default waves*, the (optional) monster list
with probabilities determines which monster out of the entire list
should spawn. The monsters-node's notation is identical to that of
*default waves*.
``monsters: <list of <monster>: <probability>>`` (optional) determines
[[monster types]], and their probabilities of spawning on each wave. The
following sample will statistically spawn powered-creepers twice as
often as slimes:
::
monsters:
powered-creepers: 4
slimes: 2
Swarm Waves
~~~~~~~~~~~
Like *special waves*, swarm waves spawn just *one type* of monster, but
in a configurable (but optional) amount. The swarm wave monsters only
have *1 health point*, meaning they will die with just one blow from
anything. Their numbers are vast compared to default and special waves,
however, so they may be a bit hard on some servers. Use with caution!
``monster: <monster>`` (required) which [[monster types]] the swarm
consists of. Note that this is different from special waves, in that
only one type is specified, and no probability value.
``amount: [low|medium|high|psycho]`` (optional) how many monsters should
spawn. Defaults to low (which is still a lot). See [[Formulas]] for more
info.
Supply Waves
~~~~~~~~~~~~
These waves spawn one monster per player, and will drop a random item
from a customized drop list (same notation as the class items). The
monster list notation is identical to that of default and special waves.
::
drops: grilled_pork, cooked_chicken, cooked_beef, cooked_fish:2
Upgrade Waves
~~~~~~~~~~~~~
These waves don't spawn any monsters, but will give or upgrade items.
The class names are optional (you don't have to give items to all
classes), and it is possible to use the ``all`` identifier to specify
items that will be given to all players regardless of class. The
``give-all-items`` flag determines if all items in the list should be
given, or just a random item off the list (like with rewards and supply
waves).
**Legacy setup**: In the following example, all players get a healing
potion, and on top of that, all Archers get 64 arrows, and all Oddjobs
get either 2 TNT or a Netherrack:
::
upgrades:
all: potion:8197:1
Archer: arrow:64
Oddjob: tnt:2, netherrack
give-all-items: false
**Advanced setup**: Since MobArena v0.95, the Upgrade Waves can be set
up to upgrade/replace certain weapons and armor, as well as add/remove
permissions. The setup follows the setup of the classes-section. In the
following example, the Knight class gets its diamond sword enchanted and
its iron chestplate replaced with a diamond chestplate. The Archer just
gets some more arrows (legacy setup) while the Wizard class gets the
permission to cast the Forcepush spell from MagicSpells:
::
classes:
Knight:
armor: iron_helmet, iron_chestplate, iron_leggings, iron_boots
items: diamond_sword
...
arenas:
...
waves:
...
upgrades:
Archer: arrow:64
Knight:
armor: diamond_chestplate
items: diamond_sword 16:2
Wizard:
permissions:
- magicspells.cast.forcepush
give-all-items: true
Explanation: Items listed in the ``armor`` node will be considered
armor, and (if valid) will replace any item currently in the armor slots
of the players. Items in the ``items`` node will be checked if they are
weapons or not; if they are weapons, then MobArena will search through
the players' inventories for weapons with the same ID, and then replace
the first weapon that matches it (automatic upgrades). If no weapon is
found, it will default to a generic item, which will just be added to
the inventory.
Boss Waves
~~~~~~~~~~
Boss waves consist of *one monster* with a configurable (but optional)
amount of health, and a configurable (but optional) list of special
abilities. The health of a boss monster is significantly higher than
that of normal monsters, and thus take much longer to kill. The special
abilities help increase the difficulty (and fun!) of a boss wave.
``monster: <monster>`` (required) the boss [[monster types]]. Note that
only one monster will spawn.
``name: <name>`` (optional) the name of the boss. Shows the given name
in a name tag above the boss' head.
``health: <amount>|[verylow|low|medium|high|veryhigh|psycho]``
(optional) how much health the boss has. Can be either a flat value,
e.g. 40 or 800, or one of the scaling values. Defaults to the scaling
value medium. See [[Formulas]] for more info about the scaling values.
``reward: <item>`` (optional) a reward for getting the killing blow on
the boss. This reward will only be given to one player (the killer, if
any).
``drops: <item list>`` (optional) a comma-separated list of items
dropped by the boss when killed. The boss will drop exactly the items
listed. This could be used to have the boss drop a "key" to advance in
the arena, or to gain access to a shed full of weapon chests or
something wonderful like that. The item syntax is the same as the one
for Supply Waves.
``potions: <potion list>`` (optional) a comma-separated list of potion
effects that will be applied to the boss when it spawns. Use this to
slow down or speed up bosses that don't move at quite the speed you
want, or perhaps to give a boss the wither effect to limit the amount of
time it will stay alive. The potion syntax is
``<effect>:<amplifier>:<seconds>``. The amplifier and duration are
optional, and will default to 0 (level 1) and pseudo-infinity,
respectively. Note that ``slow``, ``slow:0``, and ``slow:0:600`` are
identical, except the last one will only last 10 minutes (600 seconds).
Check the sample config-file at the bottom for more examples.
``abilities: <comma-separated list of boss abilities>`` (optional)
determines which (if any) boss abilities this boss has. The boss can
have several abilities; just separate each ability with a comma (e.g.
``arrows, fire-aura, throw-target``). Note that the abilities happen in
a cycle every few seconds, so the more abilities, the longer it takes
before each ability is used again. Here is an overview of the different
abilities bosses can have:
::
NAME DESCRIPTION
arrows Shoots arrows
fireballs Hurls fireballs
fire-aura Burns all nearby (5 blocks radius) players
lightning-aura Strikes lightning 4 places around itself (3-block radius)
living-bomb A random player is set on fire, and explodes after 3 seconds
obsidian-bomb Spawns an Obsidian block which explodes after 4 seconds
chain-lightning Lightning strikes the target and jumps to a nearby player
disorient-target Spins the target around 45-315 degrees
disorient-nearby Spins all nearby (5 blocks radius) players
disorient-distant Spins all distant (8+ blocks) players
root-target Locks the target in place for a couple of seconds
warp-to-player Picks a random player in the arena to warp to
shuffle-positions Swaps everyone's (including the boss) positions around
flood Places a water block on a random player's location
throw-target Throws the target backwards (if in distance)
throw-nearby Throws all nearby (5 blocks radius) players
throw-distant Throws all distant (8+ blocks) players
pull-target Pulls the target towards the boss' location
pull-nearby Pulls all nearby (5 blocks radius) players towards the boss' location
pull-distant Pulls all distant (8+ blocks) players towards the boss' location
fetch-target Warps the target to the boss' location
fetch-nearby Warps all nearby (5 blocks radius) players to the boss' location
fetch-distant Warps all distant (8+ blocks) players to the boss' location
``ability-announce: [true|false]`` (optional) should boss abilities be
announced to arena players? Defaults to true.
``ability-interval: <seconds>`` (optional) time between each ability.
Defaults to 3.
Sample config-file setup
------------------------
If you want to try a sample setup, here's one that you can use. Simply
copy this block of text, and paste it into your own config-file,
replacing the waves-section.
::
waves:
recurrent:
def1:
type: default
priority: 1
frequency: 1
monsters:
zombies: 10
skeletons: 4
exploding_sheep: 5
def2:
type: default
priority: 2
frequency: 1
wave: 5
monsters:
zombies: 10
skeletons: 6
creepers: 4
spec1:
type: special
priority: 5
frequency: 4
wave: 4
monsters:
powered_creepers: 10
angry_wolves: 10
zombie_pigmen: 10
upgrade1:
type: upgrade
priority: 7
frequency: 10
wave: 10
upgrades:
all: potion:8197:2
Archer: arrow:64
Oddjob: tnt:2, netherrack
give-all-items: true
single:
swarm1:
type: swarm
wave: 7
monster: slimes
amount: medium
boss1:
type: boss
wave: 9
monster: spider
health: medium
abilities: fire-aura, disorient-target, fireballs, throw-nearby
potions: speed:3:20, wither, increase_damage:1
ability-interval: 5
boss2:
type: boss
wave: 13
monster: zombie_pigman
health: high
abilities: root-target, arrows, fetch-distant, fire-aura
drops: lever, stone_button
upgrade2:
type: upgrade
wave: 14
upgrades:
all: potion:8197:2
Knight:
armor: diamond_helmet
items: diamond_sword 16:2;19:1
Tank:
items: iron_sword 19:3
Oddjob:
armor: iron_chestplate, iron_leggings
Wizard:
permissions:
- magicspells.cast.ChainLightning
give-all-items: true
boss3:
type: boss
wave: 16
monster: wolf
health: psycho
abilities: warp-to-player, fire-aura, throw-nearby, fireballs, fetch-target, arrows
potions: slow:1
ability-interval: 1
reward: diamond_chestplate
supply1:
type: supply
wave: 19
monsters:
cows: 10
pigs: 5
drops: grilled_pork, cooked_chicken, cooked_beef, cooked_fish:2
boss4:
type: boss
wave: 20
monster: blaze
health: low
abilities: fire-aura, throw-nearby
potions: speed
reward: diamond_helmet

View File

@ -1,137 +0,0 @@
##############
Using MobArena
##############
**On this page:** \* `Overview <#overview>`__ \* `Joining <#joining>`__
\* `Getting a list of arenas <#getting-a-list-of-arenas>`__ \* `Picking
a class <#the-class-command>`__ \* `Finding out who isn't
ready <#finding-out-who-isnt-ready>`__ \* `Leaving <#leaving>`__ \*
`Spectating <#spectating>`__ \* `A note on
inventories <#a-note-on-inventories>`__
Overview
~~~~~~~~
This page briefly describes the usage-commands of MobArena. Make sure to
check out the [[MobArena Commands]] page for a list of all the commands.
Remember that typing ``/ma help`` will get you a list of all available
commands in-game, along with a (very) brief description of what the
command does.
The commands covered on this page pertain to the usage-commands only,
i.e. the commands that the players will use to interact with MobArena.
The most basic commands are:
- ``/ma join`` - for `joining <#joining>`__ arenas to start playing
- ``/ma leave`` - for `leaving <#leaving>`__ lobbies or arenas
- ``/ma spec`` - for `spectating <#spectating>`__ arenas in progress
- ``/ma arenas`` - for getting a `list of
arenas <#getting-a-list-of-arenas>`__
Additionally, there are a few commands that might prove useful:
- ``/ma class`` - for `picking classes <#the-class-command>`__ in the
lobby instead of punching signs
- ``/ma notready`` - for finding out `who isn't ready
yet <#finding-out-who-isnt-ready>`__
Joining
~~~~~~~
To join an arena, use the ``/ma join`` command. If you have more than
one arena, you will also need to specify an arena name. Let's say we
have arenas ``cave`` and ``ship``. To join the Ship arena, simply type
``/ma join ship``.
Upon joining, you will be taken to the lobby of the given arena. In the
lobby, you will have to pick a class, which is traditionally done by
punching a sign with a class name on it. You can also `use a
command <#the-class-command>`__ directly to manually pick a class if you
know its name, or indirectly via buttons powering command blocks, or
perhaps something more complex (like NPCs).
Once you've picked a class, you will need to ready up, which is
traditionally done by punching an iron block. However, there is also a
per-arena config-file setting that automatically flags you as ready once
you've picked a class (check the [[Setting up the config-file]] page).
Getting a list of arenas
^^^^^^^^^^^^^^^^^^^^^^^^
To get a list of available arenas, you simply type ``/ma arenas``.
Arenas that have been set up and are ready for use will be green, and
unavailable arenas (disabled or not yet set up) will be gray. Note that
you won't see arenas for which you don't have permission, just like you
won't be able to actually join an arena for which you don't have
permission. Check the [[Permissions]] page for more information.
The class command
^^^^^^^^^^^^^^^^^
Normally, you would punch class signs to pick classes, but MobArena also
supports picking a class with the ``/ma class`` command. If you want to
pick the Knight class, for example, simply type ``/ma class knight``.
Forcing manual class selection like this is not recommended, because
it's unintuitive and there is no way of listing available classes.
Instead, this command is useful if you want to set up command blocks or
something more advanced like NPCs wearing the class items and armor, and
allowing players to pick the class by interacting with the NPCs.
Finding out who isn't ready
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Sometimes it's hard to keep track of who hit the iron block and who
didn't. To figure out which of the players in the lobby have not yet
readied up, you can use the ``/ma notready`` command. This is useful if
everyone thinks they've readied up and are waiting for everyone else,
but in fact one or more people haven't readied up yet.
Leaving
~~~~~~~
When you're done playing in the arena, you have to leave it with the
``/ma leave`` command (unless the ``spectate-after-death`` setting in
the config-file is set to ``false``, in which case the command will be
executed for you). You may also leave the lobby of an arena you didn't
actually want to join, or if you just don't want to play anymore, and
you don't want to wait for death.
Leaving an arena in progress has no consequences other than missing out
on the fun! You'll still be given the rewards you earned up to this
point. If you don't want your players to be able to leave arenas in
progress, you can revoke the permission on a per-class basis. See the
[[Setting up the config-file]] page for more information.
Spectating
~~~~~~~~~~
If you want to spectate an arena already in progress, use the
``/ma spec`` command, just like you would use the ``/ma join`` command.
Spectating an arena means you will be taken to the spectator area of the
arena so you have a great overlook of the arena.
Spectators cannot interact with the arena players or the monsters in the
arena, nor with each other, so there should be no hooligan fights
whatsoever.
Note that when you die in an arena, you will automatically become a
spectator, unless ``spectate-after-death`` is set to ``false``.
A note on inventories
~~~~~~~~~~~~~~~~~~~~~
Once you have joined an arena, either for playing or for spectating, you
are considered as *in the arena*. What this means is that MobArena will
hold on to your inventory until you leave. You leave by typing
``/ma leave``, either from the lobby or the arena, or after you've died.
If the ``spectate-after-death`` option in the config-file is ``false``,
you are automatically kicked out of the arena when you die, so in that
case, you won't have to do anything.
Inventories are stored on a per-player basis until they leave. Joining a
new arena (or the same arena again) after finishing a session will not
cause your inventory to be overwritten, even though it seems like you
have no items. When you leave with the ``/ma leave`` command, all of
your earned rewards from all sessions since you first joined will be
granted. Of course, with ``spectate-after-death`` set to ``false``, you
will automatically leave after every session.

View File

@ -1,92 +0,0 @@
#############
Wave formulas
#############
This page holds an overview of all the formulas used in the MobArena
waves system. Customizing the different properties of the waves should
be somewhat easier if they can be calculated, so here they all are!
About notation: Each variable used in the formulas will have its own
name. A variable that starts with a ``#`` denotes "number (of)", so
``#players`` means "number of players", and ``#wave`` means "wave
number". The function ``min(a,b)`` returns the lowest of the values
``a`` and ``b``, and ``max(a,b)`` returns the highest.
Wave growth
~~~~~~~~~~~
The wave growth node ``growth``, used in default waves, denotes how fast
monster amounts grow over time. The base is calculated by half of the
number of players, but at most 13 (i.e. there is no difference between
25 and 50 players). The amounts can be altered further using the
``amount-multiplier`` (see the [[wave setup page\|Setting up the
waves]]).
::
#monsters = base * #wave^exp
base = min(#players/2 + 1 , 13)
The ``exp`` variable is defined by the growth node, and has the
following values:
::
slow = 0.5
medium = 0.65
fast = 0.8
psycho = 1.2
Note that with the node value ``old`` (which is the default), the
monster count is ``#wave + #players``.
Swarm Amount
~~~~~~~~~~~~
The swarm amount node ``amount``, used in swarm waves, denotes how many
monsters should spawn in the swarm waves. There will always be at least
10 monsters due to the max function and the lowest multiplier value
being 10, however this can be further customized with the
``amount-multiplier`` (see the [[wave setup page\|Setting up the
waves]]).
::
#monsters = max(1, #players/2) * multiplier
The ``multiplier`` variable is defined by the amount node, and has the
following values:
::
low = 10
medium = 20
high = 30
psycho = 60
Boss Health
~~~~~~~~~~~
The boss health node ``health``, used in boss waves, denotes how much
health the boss has. Note that the ``health-multiplier`` node (see the
[[wave setup page\|Setting up the waves]]) **does NOT** affect boss
waves at all, so these are the only values that can be used. The minimum
health a boss can have is 320 health points (~160 hearts), which is with
``low`` health and only 1 player fighting. With 10 players and ``high``
health, the boss will have 5500 health points (~2750 hearts).
::
health = (#players + 1) * 20 * multiplier
The ``multiplier`` variable is defined by the health node, and has the
following values:
::
verylow = 4
low = 8
medium = 15
high = 25
veryhigh = 40
psycho = 60

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
gradlew vendored Executable file
View File

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
gradlew.bat vendored Normal file
View File

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

154
pom.xml
View File

@ -1,154 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.garbagemule</groupId>
<artifactId>mobarena</artifactId>
<name>MobArena</name>
<version>0.104.1</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<!-- Use Java 8 for compilation -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<!-- Rename the final jar-file to MobArena.jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.5</version>
<configuration>
<finalName>MobArena</finalName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<minimizeJar>true</minimizeJar>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>com.garbagemule.MobArena.metrics</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<!-- Resource filtering; use Maven version in plugin.yml -->
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<!-- Spigot repo -->
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
</repository>
<!-- Vault repo -->
<repository>
<id>vault-repo</id>
<url>http://nexus.hc.to/content/repositories/pub_releases</url>
</repository>
<repository>
<id>bstats-repo</id>
<url>http://repo.codemc.org/repository/maven-public/</url>
</repository>
<!-- JitPack for MagicSpells -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- Spigot-Bukkit API -->
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
<version>1.13-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- Vault -->
<dependency>
<groupId>net.milkbowl.vault</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>1.5</version>
<scope>compile</scope>
</dependency>
<!-- MagicSpells -->
<dependency>
<groupId>com.github.TheComputerGeek2</groupId>
<artifactId>MagicSpells</artifactId>
<version>0ad7c1f1a1</version>
<scope>provided</scope>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- Hamcrest matchers -->
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.18.3</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

235
scripts/extract-release-notes Executable file
View File

@ -0,0 +1,235 @@
#!/usr/bin/env python3
import argparse
import os
import re
import sys
VERSION_PREFIX = '## '
SECTION_PREFIX = '### '
LIST_ITEM_PREFIX = '- '
def main():
args = parse_args()
lines = extract(args.version)
output(lines, args.format)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
'version',
help='the version to extract release notes from the changelog for',
)
parser.add_argument(
'--format',
'-f',
choices=['github', 'hangar', 'spigot', 'curse'],
help='the format to output the release notes in',
)
return parser.parse_args()
def extract(target):
filename = 'changelog.md'
if not os.path.isfile(filename):
filename = os.path.join('..', filename)
if not os.path.isfile(filename):
print('error: changelog.md not found!')
sys.exit(1)
lines = []
with open(filename) as changelog:
found = False
for entry in changelog:
if entry.startswith(VERSION_PREFIX):
if found:
break
i = entry.find('[') + 1
j = entry.find(']')
version = entry[i:j]
if version == target:
if version[0].isdigit():
version = f'v{version}'
lines.append(f'{VERSION_PREFIX}{version}')
lines.append('')
found = True
continue
if not found:
continue
lines.append(entry.strip())
if not found:
print(f'error: version {target} not found!')
sys.exit(1)
return lines
def output(lines, fmt):
if fmt == 'github':
output_as_github_markdown(lines)
elif fmt == 'hangar':
output_as_hangar_markdown(lines)
elif fmt == 'spigot':
output_as_spigot_bbcode(lines)
elif fmt == 'curse':
output_as_curseforge_html(lines)
else:
output_raw(lines)
def output_as_github_markdown(lines):
"""
GitHub Releases Markdown is printed as the raw output from the changelog
except for the version header (the first line), because the version number
is already used as the release title, so we don't want it to appear twice.
"""
output_raw(lines[1:])
def output_as_hangar_markdown(lines):
"""
Hangar Versions use Markdown in the same format as GitHub Releases, so we
don't actually need to do anything else here either. Just strip the first
line so we don't get a duplicate header.
"""
output_raw(lines[1:])
def output_as_spigot_bbcode(lines):
"""
Spigot uses BBCode for resource update descriptions. It's very similar to
regular HTML, which makes it fairly easy to convert from Markdown. We just
need to use a [FONT] tag with Courier New for code bits.
"""
listing = False
for line in lines:
line = line.strip()
if line.startswith(VERSION_PREFIX):
i = len(VERSION_PREFIX)
version = line[i:]
print(f'[B]{version}[/B]')
continue
if line.startswith(SECTION_PREFIX):
if listing:
print('[/LIST]')
listing = False
i = len(SECTION_PREFIX)
section = line[i:]
print(f'[B]{section}:[/B]')
continue
if line.startswith(LIST_ITEM_PREFIX):
if not listing:
print('[LIST]')
listing = True
i = len(LIST_ITEM_PREFIX)
item = line[i:]
# Replace **bold** text
item = re.sub(r'\*\*(.*?)\*\*', r'[B]\1[/B]', item)
# Replace _italic_ text
item = re.sub(r'_(.*?)_', r'[I]\1[/I]', item)
# Replace `code` text
item = re.sub(r'`(.*?)`', r'[FONT=Courier New]\1[/FONT]', item)
# Replace [links](url)
item = re.sub(r'\[([^\]]+)]\(([^)]+)\)', r'[URL=\2]\1[/URL]', item)
print(f'[*]{item}')
continue
if len(line) > 0:
print(line)
if listing:
print('[/LIST]')
def output_as_curseforge_html(lines):
"""
CurseForge uses regular HTML for file update descriptions, which makes it
fairly easy to convert from Markdown. Angled brackets need to be replaced
with their HTML entity equivalents, but other than that it's very similar
to the Spigot BBCode conversion.
"""
listing = False
for line in lines:
line = line.strip()
if line.startswith(VERSION_PREFIX):
i = len(VERSION_PREFIX)
version = line[i:]
print(f'<p><strong>{version}</strong></p>')
continue
if line.startswith(SECTION_PREFIX):
if listing:
print('</ul>')
listing = False
i = len(SECTION_PREFIX)
section = line[i:]
print(f'<p><strong>{section}:</strong></p>')
continue
if line.startswith(LIST_ITEM_PREFIX):
if not listing:
print('<ul>')
listing = True
i = len(LIST_ITEM_PREFIX)
item = line[i:]
# Replace angled brackets
item = item.replace('<', '&lt;')
item = item.replace('>', '&gt;')
# Replace **bold** text
item = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', item)
# Replace _italic_ text
item = re.sub(r'_(.*?)_', r'<emph>\1</emph>', item)
# Replace `code` text
item = re.sub(r'`(.*?)`', r'<code>\1</code>', item)
# Replace [links](url)
item = re.sub(r'\[([^\]]+)]\(([^)]+)\)', r'<a href="\2">\1</a>', item)
print(f'<li>{item}</li>')
continue
if len(line) > 0:
print(line)
if listing:
print('</ul>')
def output_raw(lines):
[print(line.strip()) for line in lines]
if __name__ == '__main__':
main()

1
settings.gradle.kts Normal file
View File

@ -0,0 +1 @@
rootProject.name = "mobarena"

View File

@ -3,6 +3,7 @@ package com.garbagemule.MobArena;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.util.Slugs;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
@ -15,7 +16,8 @@ import java.util.stream.IntStream;
public class ArenaClass
{
private String configName, lowercaseName;
private String configName;
private String slug;
private Thing helmet, chestplate, leggings, boots, offhand;
private List<Thing> armor;
private List<Thing> items;
@ -25,6 +27,7 @@ public class ArenaClass
private boolean unbreakableWeapons, unbreakableArmor;
private Thing price;
private Location classchest;
private String petName;
/**
* Create a new, empty arena class with the given name.
@ -32,8 +35,8 @@ public class ArenaClass
*/
public ArenaClass(String name, Thing price, boolean unbreakableWeapons, boolean unbreakableArmor) {
this.configName = name;
this.lowercaseName = name.toLowerCase().replace(" ", "");
this.slug = Slugs.create(name);
this.items = new ArrayList<>();
this.armor = new ArrayList<>(4);
this.effects = new ArrayList<>();
@ -45,7 +48,7 @@ public class ArenaClass
this.price = price;
}
/**
* Get the name of the arena class as it appears in the config-file.
* @return the class name as it appears in the config-file
@ -53,15 +56,25 @@ public class ArenaClass
public String getConfigName() {
return configName;
}
/**
* Get the slug version of the arena class name.
* @return the slugified class name
*/
public String getSlug() {
return slug;
}
/**
* Get the lowercase class name.
* @return the lowercase class name
* @deprecated use {@link #getSlug()} instead
*/
@Deprecated
public String getLowercaseName() {
return lowercaseName;
return slug;
}
/**
* Set the helmet slot for the class.
* @param helmet a Thing
@ -69,7 +82,7 @@ public class ArenaClass
public void setHelmet(Thing helmet) {
this.helmet = helmet;
}
/**
* Set the chestplate slot for the class.
* @param chestplate a Thing
@ -77,7 +90,7 @@ public class ArenaClass
public void setChestplate(Thing chestplate) {
this.chestplate = chestplate;
}
/**
* Set the leggings slot for the class.
* @param leggings a Thing
@ -85,7 +98,7 @@ public class ArenaClass
public void setLeggings(Thing leggings) {
this.leggings = leggings;
}
/**
* Set the boots slot for the class.
* @param boots a Thing
@ -93,7 +106,7 @@ public class ArenaClass
public void setBoots(Thing boots) {
this.boots = boots;
}
/**
* Set the off-hand slot for the class.
* @param offhand a Thing
@ -111,7 +124,7 @@ public class ArenaClass
items.add(item);
}
}
/**
* Replace the current items list with a new list of all the items in the given list.
* This method uses the addItem() method for each item to ensure consistency.
@ -121,7 +134,7 @@ public class ArenaClass
this.items = new ArrayList<>(items.size());
items.forEach(this::addItem);
}
/**
* Replace the current armor list with the given list.
* @param armor a list of Things
@ -133,10 +146,23 @@ public class ArenaClass
public void setEffects(List<Thing> effects) {
this.effects = effects;
}
public String getPetName() {
return this.petName;
}
public void setPetName(String petName) {
this.petName = petName;
}
public boolean hasPermission(Player p) {
String perm = "mobarena.classes." + configName;
return !p.isPermissionSet(perm) || p.hasPermission(perm);
String key = "mobarena.classes." + slug;
if (p.isPermissionSet(key)) {
return p.hasPermission(key);
}
// Permissive by default.
return true;
}
/**
@ -144,7 +170,7 @@ public class ArenaClass
* The normal items will be added to the inventory normally, while the
* armor items will be verified as armor items and placed in their
* appropriate slots. If any specific armor slots are specified, they
* will overwrite any items in the armor list.
* will overwrite any items in the armor list.
* @param p a player
*/
public void grantItems(Player p) {
@ -152,7 +178,7 @@ public class ArenaClass
// Fork over the items.
items.forEach(item -> item.giveTo(p));
// Check for legacy armor-node items
armor.forEach(thing -> thing.giveTo(p));
@ -167,7 +193,7 @@ public class ArenaClass
public void grantPotionEffects(Player p) {
effects.forEach(thing -> thing.giveTo(p));
}
/**
* Add a permission value to the class.
*/
@ -212,20 +238,20 @@ public class ArenaClass
public Thing getPrice() {
return price;
}
@Override
public boolean equals(Object o) {
if (o == null) return false;
if (this == o) return true;
if (!this.getClass().equals(o.getClass())) return false;
ArenaClass other = (ArenaClass) o;
return other.lowercaseName.equals(this.lowercaseName);
return other.slug.equals(this.slug);
}
@Override
public int hashCode() {
return lowercaseName.hashCode();
return slug.hashCode();
}
public static class MyItems extends ArenaClass {
@ -269,7 +295,7 @@ public class ArenaClass
case SHULKER_SHELL:
return true;
}
return type.name().endsWith("_SHULKER_BOX");
return type.name().endsWith("SHULKER_BOX");
}
}
}

View File

@ -3,6 +3,9 @@ package com.garbagemule.MobArena;
import static com.garbagemule.MobArena.util.config.ConfigUtils.makeSection;
import com.garbagemule.MobArena.ScoreboardManager.NullScoreboardManager;
import com.garbagemule.MobArena.announce.Announcer;
import com.garbagemule.MobArena.announce.MessengerAnnouncer;
import com.garbagemule.MobArena.announce.TitleAnnouncer;
import com.garbagemule.MobArena.steps.Step;
import com.garbagemule.MobArena.steps.StepFactory;
import com.garbagemule.MobArena.steps.PlayerJoinArena;
@ -21,7 +24,9 @@ import com.garbagemule.MobArena.repairable.RepairableComparator;
import com.garbagemule.MobArena.repairable.RepairableContainer;
import com.garbagemule.MobArena.things.InvalidThingInputString;
import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.things.ThingPicker;
import com.garbagemule.MobArena.util.ClassChests;
import com.garbagemule.MobArena.util.Slugs;
import com.garbagemule.MobArena.util.inventory.InventoryManager;
import com.garbagemule.MobArena.util.timer.AutoStartTimer;
import com.garbagemule.MobArena.util.timer.StartDelayTimer;
@ -73,65 +78,70 @@ public class ArenaImpl implements Arena
// General stuff
private MobArena plugin;
private String name;
private String slug;
private World world;
private Messenger messenger;
private Announcer announcer;
// Settings section of the config-file for this arena.
private ConfigurationSection settings;
// Run-time settings and critical config settings
private boolean enabled, protect, running, edit;
// World stuff
private boolean allowMonsters, allowAnimals;
//private Difficulty spawnMonsters;
// Warps, points and locations
private ArenaRegion region;
private Leaderboard leaderboard;
// Player stuff
private InventoryManager inventoryManager;
private RewardManager rewardManager;
private ClassLimitManager limitManager;
private Map<Player,ArenaPlayer> arenaPlayerMap;
private Set<Player> arenaPlayers, lobbyPlayers, readyPlayers, specPlayers, deadPlayers;
private Set<Player> movingPlayers;
private Set<Player> leavingPlayers;
private Set<Player> randoms;
// Classes stuff
private ArenaClass defaultClass;
private Map<String,ArenaClass> classes;
// Blocks and pets
private PriorityBlockingQueue<Repairable> repairQueue;
private Set<Block> blocks;
private LinkedList<Repairable> repairables, containables;
// Monster stuff
private MonsterManager monsterManager;
// Wave stuff
private WaveManager waveManager;
private MASpawnThread spawnThread;
private SheepBouncer sheepBouncer;
private Map<Integer,List<Thing>> everyWaveMap, afterWaveMap;
private Map<Integer, ThingPicker> everyWaveMap, afterWaveMap;
// Misc
private ArenaListener eventListener;
private List<Thing> entryFee;
private AutoStartTimer autoStartTimer;
private StartDelayTimer startDelayTimer;
private boolean isolatedChat;
// Warp offsets
private double arenaWarpOffset;
// Scoreboards
private ScoreboardManager scoreboard;
// Last player standing
private Player lastStanding;
// Actions
private Map<Player, Step> histories;
private StepFactory playerJoinArena;
@ -145,13 +155,14 @@ public class ArenaImpl implements Arena
public ArenaImpl(MobArena plugin, ConfigurationSection section, String name, World world) {
if (world == null)
throw new NullPointerException("[MobArena] ERROR! World for arena '" + name + "' does not exist!");
this.name = name;
this.slug = Slugs.create(name);
this.world = world;
this.plugin = plugin;
this.settings = makeSection(section, "settings");
this.region = new ArenaRegion(section, this);
this.enabled = settings.getBoolean("enabled", false);
this.protect = settings.getBoolean("protect", true);
this.running = false;
@ -180,23 +191,24 @@ public class ArenaImpl implements Arena
String defaultClassName = settings.getString("default-class", null);
if (defaultClassName != null) {
this.defaultClass = classes.get(defaultClassName);
String slug = Slugs.create(defaultClassName);
this.defaultClass = classes.get(slug);
}
// Blocks and pets
this.repairQueue = new PriorityBlockingQueue<>(100, new RepairableComparator());
this.blocks = new HashSet<>();
this.repairables = new LinkedList<>();
this.containables = new LinkedList<>();
// Monster stuff
this.monsterManager = new MonsterManager();
// Wave stuff
this.waveManager = new WaveManager(this, section.getConfigurationSection("waves"));
this.everyWaveMap = MAUtils.getArenaRewardMap(plugin, section, name, "every");
this.afterWaveMap = MAUtils.getArenaRewardMap(plugin, section, name, "after");
// Misc
this.eventListener = new ArenaListener(this, plugin);
this.allowMonsters = world.getAllowMonsters();
@ -219,7 +231,9 @@ public class ArenaImpl implements Arena
this.startDelayTimer = new StartDelayTimer(this, autoStartTimer);
this.isolatedChat = settings.getBoolean("isolated-chat", false);
this.arenaWarpOffset = settings.getDouble("arena-warp-offset", 0.0);
// Scoreboards
this.scoreboard = (settings.getBoolean("use-scoreboards", true) ? new ScoreboardManager(this) : new NullScoreboardManager(this));
@ -227,6 +241,16 @@ public class ArenaImpl implements Arena
String prefix = settings.getString("prefix", "");
this.messenger = !prefix.isEmpty() ? new Messenger(prefix) : plugin.getGlobalMessenger();
// Announcer
String announcerType = settings.getString("announcer-type", "chat");
if (announcerType.equals("chat")) {
announcer = new MessengerAnnouncer(this.messenger);
} else if (announcerType.equals("title")) {
announcer = new TitleAnnouncer(5, 60, 10);
} else {
throw new ConfigError("Unsupported announcer type: " + announcerType);
}
// Actions
this.histories = new HashMap<>();
this.playerJoinArena = PlayerJoinArena.create(this);
@ -234,15 +258,15 @@ public class ArenaImpl implements Arena
this.spawnsPets = plugin.getArenaMaster().getSpawnsPets();
}
/*/////////////////////////////////////////////////////////////////////////
//
// NEW METHODS IN REFACTORING
//
/////////////////////////////////////////////////////////////////////////*/
@Override
public ConfigurationSection getSettings() {
return settings;
@ -307,7 +331,7 @@ public class ArenaImpl implements Arena
public int getMaxPlayers() {
return settings.getInt("max-players");
}
private int getJoinDistance() {
return settings.getInt("max-join-distance");
}
@ -318,12 +342,12 @@ public class ArenaImpl implements Arena
}
@Override
public Set<Map.Entry<Integer,List<Thing>>> getEveryWaveEntrySet() {
public Set<Map.Entry<Integer, ThingPicker>> getEveryWaveEntrySet() {
return everyWaveMap.entrySet();
}
@Override
public List<Thing> getAfterWaveReward(int wave) {
public ThingPicker getAfterWaveReward(int wave) {
return afterWaveMap.get(wave);
}
@ -416,25 +440,25 @@ public class ArenaImpl implements Arena
public MonsterManager getMonsterManager() {
return monsterManager;
}
@Override
public ClassLimitManager getClassLimitManager() {
return limitManager;
}
@Override
public ScoreboardManager getScoreboard() {
return scoreboard;
}
@Override
public Messenger getMessenger() {
@ -449,7 +473,7 @@ public class ArenaImpl implements Arena
@Override
public void announce(String msg) {
for (Player p : getAllPlayers()) {
messenger.tell(p, msg);
announcer.announce(p, msg);
}
}
@ -487,38 +511,46 @@ public class ArenaImpl implements Arena
// Store all chest contents.
storeContainerContents();
// Populate arenaPlayers and clear the lobby.
arenaPlayers.addAll(lobbyPlayers);
lobbyPlayers.clear();
readyPlayers.clear();
// Assign random classes.
for (Player p : randoms) {
assignRandomClass(p);
}
randoms.clear();
// Then check if there are still players left.
if (arenaPlayers.isEmpty()) {
return false;
}
// Initialize scoreboards
scoreboard.initialize();
// Teleport players, give full health, initialize map
for (Player p : arenaPlayers) {
// TODO figure out how people die in lobby and get sent to spectator area early
// Remove player from spec list to avoid invincibility issues
if (inSpec(p)) {
specPlayers.remove(p);
System.out.println("[MobArena] Player " + p.getName() + " joined the arena from the spec area!");
System.out.println("[MobArena] Invincibility glitch attempt stopped!");
}
movingPlayers.add(p);
p.teleport(region.getArenaWarp());
if (arenaWarpOffset > 0.01) {
Location warp = region.getArenaWarp();
double x = warp.getX() + (arenaWarpOffset * 2 * (Math.random() - 0.5));
double y = warp.getY();
double z = warp.getZ() + (arenaWarpOffset * 2 * (Math.random() - 0.5));
Location offset = new Location(warp.getWorld(), x, y, z);
p.teleport(offset);
} else {
p.teleport(region.getArenaWarp());
}
movingPlayers.remove(p);
addClassPermissions(p);
@ -528,35 +560,35 @@ public class ArenaImpl implements Arena
if (price != null) {
price.takeFrom(p);
}
scoreboard.addPlayer(p);
}
// Start spawning monsters (must happen before 'running = true;')
startSpawner();
startBouncingSheep();
// Set the boolean.
running = true;
// Spawn pets (must happen after 'running = true;')
spawnsPets.spawn(this);
// Spawn mounts
spawnMounts();
// Clear the classes in use map, as they're no longer needed
limitManager.clearClassesInUse();
// Reset rewards
rewardManager.reset();
// Initialize leaderboards and start displaying info.
leaderboard.initialize();
leaderboard.startTracking();
announce(Msg.ARENA_START);
return true;
}
@ -576,16 +608,16 @@ public class ArenaImpl implements Arena
// Reset last standing
lastStanding = null;
// Set the running boolean and disable arena if not disabled.
boolean en = enabled;
enabled = false;
running = false;
// Stop tracking leaderboards
leaderboard.stopTracking();
leaderboard.update();
// Stop spawning.
stopSpawner();
stopBouncingSheep();
@ -599,18 +631,23 @@ public class ArenaImpl implements Arena
announce(Msg.ARENA_END);
}
cleanup();
// Restore region.
if (settings.getBoolean("soft-restore", false)) {
restoreRegion();
}
// Restore chests
restoreContainerContents();
// Restore enabled status.
enabled = en;
// Auto-leave
if (settings.getBoolean("auto-leave-on-end", false)) {
specPlayers.forEach(this::playerLeave);
}
return true;
}
@ -619,12 +656,12 @@ public class ArenaImpl implements Arena
{
if (running)
return;
// Set operations.
Set<Player> tmp = new HashSet<>();
tmp.addAll(lobbyPlayers);
tmp.removeAll(readyPlayers);
// Force leave.
for (Player p : tmp) {
playerLeave(p);
@ -642,15 +679,20 @@ public class ArenaImpl implements Arena
if (players.isEmpty()) {
return;
}
players.forEach(this::playerLeave);
cleanup();
}
@Override
public boolean hasPermission(Player p) {
String perm = "mobarena.arenas." + name;
return !p.isPermissionSet(perm) || p.hasPermission(perm);
String key = "mobarena.arenas." + slug;
if (p.isPermissionSet(key)) {
return p.hasPermission(key);
}
// Permissive by default.
return true;
}
@Override
@ -700,17 +742,17 @@ public class ArenaImpl implements Arena
lobbyPlayers.add(p);
plugin.getArenaMaster().addPlayer(p, this);
arenaPlayerMap.put(p, new ArenaPlayer(p, this, plugin));
// Start the start-delay-timer if applicable
if (!autoStartTimer.isRunning()) {
startDelayTimer.start();
}
// Notify player of joining
messenger.tell(p, Msg.JOIN_PLAYER_JOINED);
// Notify player of time left
if (startDelayTimer.isRunning()) {
messenger.tell(p, Msg.ARENA_START_DELAY, "" + startDelayTimer.getRemaining() / 20l);
@ -721,11 +763,12 @@ public class ArenaImpl implements Arena
if (defaultClass != null) {
// Assign default class if applicable
if (!ClassChests.assignClassFromStoredClassChest(this, p, defaultClass)) {
assignClass(p, defaultClass.getLowercaseName());
String slug = defaultClass.getSlug();
assignClass(p, slug);
messenger.tell(p, Msg.LOBBY_CLASS_PICKED, defaultClass.getConfigName());
}
}
movingPlayers.remove(p);
return true;
}
@ -740,14 +783,14 @@ public class ArenaImpl implements Arena
}
readyPlayers.add(p);
int minPlayers = getMinPlayers();
if (minPlayers > 0 && lobbyPlayers.size() < minPlayers)
{
messenger.tell(p, Msg.LOBBY_NOT_ENOUGH_PLAYERS, "" + minPlayers);
return;
}
startArena();
}
@ -767,15 +810,18 @@ public class ArenaImpl implements Arena
}
leavingPlayers.add(p);
// Remove pets.
monsterManager.removePets(p);
// Clear inventory if player is an arena player, and unmount
if (arenaPlayers.contains(p)) {
unmount(p);
clearInv(p);
}
removePermissionAttachments(p);
removePotionEffects(p);
boolean refund = inLobby(p);
if (inLobby(p)) {
@ -789,13 +835,13 @@ public class ArenaImpl implements Arena
startDelayTimer.stop();
}
}
discardPlayer(p);
if (refund) {
refund(p);
}
endArena();
leavingPlayers.remove(p);
@ -823,12 +869,15 @@ public class ArenaImpl implements Arena
ArenaPlayerDeathEvent event = new ArenaPlayerDeathEvent(p, this, last);
plugin.getServer().getPluginManager().callEvent(event);
// Remove pets.
monsterManager.removePets(p);
// Clear the player's inventory, and unmount
if (arenaPlayers.remove(p)) {
unmount(p);
clearInv(p);
}
deadPlayers.add(p);
endArena();
}
@ -854,20 +903,30 @@ public class ArenaImpl implements Arena
@Override
public void playerRespawn(Player p) {
deadPlayers.remove(p);
plugin.getServer().getScheduler()
.scheduleSyncDelayedTask(plugin, () -> revivePlayer(p));
revivePlayer(p);
}
@Override
@SuppressWarnings("deprecation")
public void revivePlayer(Player p) {
removePermissionAttachments(p);
removePotionEffects(p);
discardPlayer(p);
specPlayers.add(p);
if (settings.getBoolean("spectate-on-death", true)) {
playerSpec(p, null);
// At this point, we know that we want players to become
// spectators when they respawn, but if we also want them
// to auto-leave on arena end, we first need to make sure
// the arena is running. If not, the session has probably
// ended, so we fall through to schedule an auto-leave.
if (!settings.getBoolean("auto-leave-on-end", false) || running) {
messenger.tell(p, Msg.SPEC_PLAYER_SPECTATE);
return;
}
}
plugin.getServer().getScheduler()
.scheduleSyncDelayedTask(plugin, () -> playerLeave(p));
}
@Override
@ -882,7 +941,7 @@ public class ArenaImpl implements Arena
}
movingPlayers.add(p);
rollback(p);
Step step = playerSpecArena.create(p);
@ -896,7 +955,7 @@ public class ArenaImpl implements Arena
specPlayers.add(p);
plugin.getArenaMaster().addPlayer(p, this);
messenger.tell(p, Msg.SPEC_PLAYER_SPECTATE);
movingPlayers.remove(p);
}
@ -985,7 +1044,7 @@ public class ArenaImpl implements Arena
default: return null;
}
}
private void startSpawner() {
if (spawnThread != null) {
spawnThread.stop();
@ -997,7 +1056,7 @@ public class ArenaImpl implements Arena
spawnThread = new MASpawnThread(plugin, this);
spawnThread.start();
}
/**
* Schedule a Runnable to be executed after the given delay in
* server ticks. The method is used by the MASpawnThread to
@ -1008,7 +1067,7 @@ public class ArenaImpl implements Arena
public void scheduleTask(Runnable r, int delay) {
Bukkit.getScheduler().runTaskLater(plugin, r, delay);
}
private void stopSpawner() {
if (spawnThread == null) {
plugin.getLogger().warning("Can't stop non-existent spawner in arena " + configName() + ". This should never happen.");
@ -1020,7 +1079,7 @@ public class ArenaImpl implements Arena
world.setSpawnFlags(allowMonsters, allowAnimals);
}
private void startBouncingSheep() {
if (sheepBouncer != null) {
sheepBouncer.stop();
@ -1067,7 +1126,7 @@ public class ArenaImpl implements Arena
plugin.getArenaMaster().removePlayer(p);
clearPlayer(p);
}
private void clearPlayer(Player p)
{
// Remove from boss health bar
@ -1077,20 +1136,17 @@ public class ArenaImpl implements Arena
boss.getHealthBar().removePlayer(p);
}
});
// Remove pets.
monsterManager.removePets(p);
// readyPlayers before lobbyPlayers because of startArena sanity-checks
readyPlayers.remove(p);
specPlayers.remove(p);
arenaPlayers.remove(p);
lobbyPlayers.remove(p);
arenaPlayerMap.remove(p);
scoreboard.removePlayer(p);
}
@Override
public void repairBlocks()
{
@ -1103,9 +1159,9 @@ public class ArenaImpl implements Arena
{
repairQueue.add(r);
}
/*////////////////////////////////////////////////////////////////////
//
// Items & Cleanup
@ -1116,11 +1172,11 @@ public class ArenaImpl implements Arena
public void assignClass(Player p, String className) {
ArenaPlayer arenaPlayer = arenaPlayerMap.get(p);
ArenaClass arenaClass = classes.get(className);
if (arenaPlayer == null || arenaClass == null) {
return;
}
InventoryManager.clearInventory(p);
removePotionEffects(p);
arenaPlayer.setArenaClass(arenaClass);
@ -1143,34 +1199,42 @@ public class ArenaImpl implements Arena
autoReady(p);
}
@Override
public void assignClassGiveInv(Player p, String className, ItemStack[] contents) {
public void assignClassGiveInv(Player p, String className, ItemStack[] source) {
ArenaPlayer arenaPlayer = arenaPlayerMap.get(p);
ArenaClass arenaClass = classes.get(className);
if (arenaPlayer == null || arenaClass == null) {
return;
}
InventoryManager.clearInventory(p);
removePermissionAttachments(p);
removePotionEffects(p);
arenaPlayer.setArenaClass(arenaClass);
PlayerInventory inv = p.getInventory();
// Clone the source array to make sure we don't modify its contents
ItemStack[] contents = new ItemStack[source.length];
for (int i = 0; i < source.length; i++) {
if (source[i] != null) {
contents[i] = source[i].clone();
}
}
// Collect armor items, because setContents() now overwrites everyhing
ItemStack helmet = null;
ItemStack chestplate = null;
ItemStack leggings = null;
ItemStack boots = null;
ItemStack offhand = null;
// Check the very last slot to see if it'll work as a helmet
int last = contents.length-1;
if (contents[last] != null) {
helmet = contents[last].clone();
helmet = contents[last];
if (arenaClass.hasUnbreakableArmor()) {
makeUnbreakable(helmet);
}
@ -1185,11 +1249,12 @@ public class ArenaImpl implements Arena
String type = parts[parts.length - 1];
if (type.equals("HELMET")) continue;
ItemStack stack = contents[i].clone();
ItemStack stack = contents[i];
if (arenaClass.hasUnbreakableArmor()) {
makeUnbreakable(stack);
}
switch (type) {
case "ELYTRA":
case "CHESTPLATE": chestplate = stack; break;
case "LEGGINGS": leggings = stack; break;
case "BOOTS": boots = stack; break;
@ -1197,11 +1262,10 @@ public class ArenaImpl implements Arena
}
contents[i] = null;
}
// Equip the fifth last slot as the off-hand
ItemStack fifth = contents[contents.length - 5];
if (fifth != null) {
offhand = fifth.clone();
offhand = contents[contents.length - 5];
if (offhand != null) {
if (arenaClass.hasUnbreakableWeapons()) {
makeUnbreakable(offhand);
}
@ -1246,14 +1310,10 @@ public class ArenaImpl implements Arena
private void autoReady(Player p) {
if (settings.getBoolean("auto-ready", false)) {
if (autoStartTimer.getRemaining() <= 0) {
playerReady(p);
} else {
readyPlayers.add(p);
}
playerReady(p);
}
}
@Override
public void addRandomPlayer(Player p) {
randoms.add(p);
@ -1271,12 +1331,12 @@ public class ArenaImpl implements Arena
playerLeave(p);
return;
}
int index = MobArena.random.nextInt(classes.size());
String className = classes.get(index).getConfigName();
assignClass(p, className);
messenger.tell(p, Msg.LOBBY_CLASS_PICKED, this.classes.get(className).getConfigName());
int index = MobArena.random.nextInt(classes.size());
String slug = classes.get(index).getSlug();
assignClass(p, slug);
messenger.tell(p, Msg.LOBBY_CLASS_PICKED, this.classes.get(slug).getConfigName());
}
private void addClassPermissions(Player player) {
@ -1299,7 +1359,7 @@ public class ArenaImpl implements Arena
.map(PermissionAttachmentInfo::getAttachment)
.forEach(PermissionAttachment::remove);
}
private void removePotionEffects(Player p) {
p.getActivePotionEffects().stream()
.map(PotionEffect::getType)
@ -1312,21 +1372,21 @@ public class ArenaImpl implements Arena
removeEntities();
clearPlayers();
}
private void removeMonsters() {
monsterManager.clear();
}
private void removeBlocks() {
for (Block b : blocks) {
b.setType(Material.AIR);
}
blocks.clear();
}
private void removeEntities() {
List<Chunk> chunks = region.getChunks();
for (Chunk c : chunks) {
for (Entity e : c.getEntities()) {
if (e == null) {
@ -1339,22 +1399,23 @@ public class ArenaImpl implements Arena
case ARROW:
case MINECART:
case BOAT:
case PRIMED_TNT:
case SHULKER_BULLET:
e.remove();
}
}
}
}
private void clearPlayers() {
arenaPlayers.clear();
arenaPlayerMap.clear();
lobbyPlayers.clear();
readyPlayers.clear();
}
/*////////////////////////////////////////////////////////////////////
//
// Initialization & Checks
@ -1365,13 +1426,13 @@ public class ArenaImpl implements Arena
public void restoreRegion()
{
Collections.sort(repairables, new RepairableComparator());
for (Repairable r : repairables)
r.repair();
}
/*////////////////////////////////////////////////////////////////////
//
// Getters & Misc
@ -1407,7 +1468,12 @@ public class ArenaImpl implements Arena
@Override
public String arenaName()
{
return MAUtils.nameConfigToArena(name);
return name;
}
@Override
public String getSlug() {
return slug;
}
@Override
@ -1435,7 +1501,7 @@ public class ArenaImpl implements Arena
result.addAll(arenaPlayers);
result.addAll(lobbyPlayers);
result.addAll(specPlayers);
return result;
}
@ -1454,10 +1520,10 @@ public class ArenaImpl implements Arena
public List<ArenaPlayerStatistics> getArenaPlayerStatistics(Comparator<ArenaPlayerStatistics> comparator)
{
List<ArenaPlayerStatistics> list = new ArrayList<ArenaPlayerStatistics>();
for (ArenaPlayer ap : arenaPlayerMap.values())
list.add(ap.getStats());
Collections.sort(list, comparator);
return list;
}*/
@ -1496,7 +1562,7 @@ public class ArenaImpl implements Arena
}
return true;
}
@Override
public boolean refund(Player p) {
entryFee.forEach(fee -> fee.giveTo(p));
@ -1526,7 +1592,7 @@ public class ArenaImpl implements Arena
else if (!canAfford(p))
messenger.tell(p, Msg.JOIN_FEE_REQUIRED, MAUtils.listToString(entryFee, plugin));
else return true;
return false;
}
@ -1545,7 +1611,7 @@ public class ArenaImpl implements Arena
else if (getJoinDistance() > 0 && !region.contains(p.getLocation(), getJoinDistance()))
messenger.tell(p, Msg.JOIN_TOO_FAR);
else return true;
return false;
}
@ -1558,7 +1624,7 @@ public class ArenaImpl implements Arena
public Player getLastPlayerStanding() {
return lastStanding;
}
/**
* The "perfect equals method" cf. "Object-Oriented Design and Patterns"
* by Cay S. Horstmann.
@ -1568,11 +1634,11 @@ public class ArenaImpl implements Arena
if (this == other) return true;
if (other == null) return false;
if (getClass() != other.getClass()) return false;
// Arenas must have different names.
if (other instanceof ArenaImpl && ((ArenaImpl)other).name.equals(name))
return true;
return false;
}

View File

@ -15,7 +15,9 @@ import com.garbagemule.MobArena.repairable.RepairableDoor;
import com.garbagemule.MobArena.repairable.RepairableSign;
import com.garbagemule.MobArena.things.ExperienceThing;
import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.things.ThingPicker;
import com.garbagemule.MobArena.util.ClassChests;
import com.garbagemule.MobArena.util.Slugs;
import com.garbagemule.MobArena.waves.MABoss;
import org.bukkit.ChatColor;
import org.bukkit.Location;
@ -28,6 +30,7 @@ import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.AbstractHorse;
import org.bukkit.entity.AnimalTamer;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Arrow;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Horse;
@ -41,6 +44,7 @@ import org.bukkit.entity.TNTPrimed;
import org.bukkit.entity.ThrownPotion;
import org.bukkit.entity.Vehicle;
import org.bukkit.event.Event.Result;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockBurnEvent;
import org.bukkit.event.block.BlockEvent;
@ -88,13 +92,11 @@ import org.bukkit.material.Attachable;
import org.bukkit.material.Bed;
import org.bukkit.material.Door;
import org.bukkit.material.Redstone;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.metadata.Metadatable;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.projectiles.ProjectileSource;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
@ -109,61 +111,55 @@ public class ArenaListener
private MonsterManager monsters;
private ClassLimitManager classLimits;
private boolean softRestore,
softRestoreDrops,
protect;
private boolean monsterExp,
monsterInfight,
pvpOn, // pvp-enabled in config
pvpEnabled = false, // activated on first wave
foodRegen,
lockFoodLevel,
useClassChests;
private boolean allowTeleport,
canShare,
autoIgniteTNT;
private boolean softRestore;
private boolean softRestoreDrops;
private boolean protect;
private boolean monsterExp;
private boolean monsterInfight;
private boolean pvpEnabled;
private boolean foodRegen;
private boolean lockFoodLevel;
private boolean useClassChests;
private boolean allowTeleport;
private boolean monsterTeleport;
private boolean canShare;
private boolean autoIgniteTNT;
private int autoIgniteFuse;
private Set<Player> banned;
private EnumSet<EntityType> excludeFromRetargeting;
public ArenaListener(Arena arena, MobArena plugin) {
this.plugin = plugin;
this.arena = arena;
this.region = arena.getRegion();
this.monsters = arena.getMonsterManager();
/*
* TODO: Figure out if this is really a good idea + It saves needing all
* those methods in Arena.java + It is relatively simple + It would be
* fairly easy to implement an observer pattern - More private fields -
* Uglier code
*/
ConfigurationSection s = arena.getSettings();
this.softRestore = s.getBoolean("soft-restore", false);
this.softRestoreDrops = s.getBoolean("soft-restore-drops", false);
this.protect = s.getBoolean("protect", true);
this.monsterExp = s.getBoolean("monster-exp", false);
this.monsterInfight = s.getBoolean("monster-infight", false);
this.pvpOn = s.getBoolean("pvp-enabled", false);
this.pvpEnabled = s.getBoolean("pvp-enabled", false);
this.foodRegen = s.getBoolean("food-regen", false);
this.lockFoodLevel = s.getBoolean("lock-food-level", true);
this.allowTeleport = s.getBoolean("allow-teleporting", false);
this.monsterTeleport = s.getBoolean("monster-teleporting", false);
this.canShare = s.getBoolean("share-items-in-arena", true);
this.autoIgniteTNT = s.getBoolean("auto-ignite-tnt", false);
this.autoIgniteFuse = s.getInt("auto-ignite-fuse", 80);
this.useClassChests = s.getBoolean("use-class-chests", false);
this.classLimits = arena.getClassLimitManager();
this.banned = new HashSet<>();
}
void pvpActivate() {
if (arena.isRunning() && !arena.getPlayersInArena().isEmpty()) {
pvpEnabled = pvpOn;
}
}
void pvpDeactivate() {
if (pvpOn) pvpEnabled = false;
this.excludeFromRetargeting = EnumSet.of(
EntityType.ELDER_GUARDIAN,
EntityType.GUARDIAN
);
}
public void onBlockBreak(BlockBreakEvent event) {
@ -177,17 +173,17 @@ public class ArenaListener
// If the arena isn't protected, care
if (!protect) return;
if (!arena.getRegion().contains(event.getBlock().getLocation()))
return;
if (!arena.inArena(event.getPlayer())) {
if (arena.inEditMode())
return;
else
event.setCancelled(true);
}
if (onBlockDestroy(event))
return;
@ -221,21 +217,18 @@ public class ArenaListener
private boolean onBlockDestroy(BlockEvent event) {
if (arena.inEditMode())
return true;
if (!arena.isRunning())
return false;
Block b = event.getBlock();
if (arena.removeBlock(b) || b.getType() == Material.TNT)
return true;
if (softRestore) {
if (arena.isProtected())
return false;
BlockState state = b.getState();
Repairable r = null;
if (state instanceof InventoryHolder)
r = new RepairableContainer(state);
else if (state instanceof Sign)
@ -246,7 +239,7 @@ public class ArenaListener
r = new RepairableBlock(state);
arena.addRepairable(r);
if (!softRestoreDrops)
b.setType(Material.AIR);
return true;
@ -284,10 +277,10 @@ public class ArenaListener
}
stack.setAmount(stack.getAmount() - 1);
TNTPrimed tnt = b.getWorld().spawn(b.getRelative(BlockFace.UP).getLocation(), TNTPrimed.class);
setPlanter(tnt, event.getPlayer());
tnt.setSource(event.getPlayer());
tnt.setFuseTicks(Math.max(0, autoIgniteFuse));
return;
}
setPlanter(b, event.getPlayer());
}
// Any other block we don't care about if we're not protecting
@ -303,20 +296,6 @@ public class ArenaListener
arena.addBlock(b.getRelative(0, 1, 0));
}
}
private void setPlanter(Metadatable tnt, Player planter) {
tnt.setMetadata("mobarena-planter", new FixedMetadataValue(plugin, planter));
}
private Player getPlanter(Metadatable tnt) {
List<MetadataValue> values = tnt.getMetadata("mobarena-planter");
for (MetadataValue value : values) {
if (value.getOwningPlugin().equals(plugin)) {
return (Player) value.value();
}
}
return null;
}
public void onBlockForm(BlockFormEvent event) {
// If the arena isn't protected, care
@ -357,16 +336,7 @@ public class ArenaListener
case FLINT_AND_STEEL:
if (arena.inEditMode()) return;
if (arena.isRunning()) {
if (b.getType() == Material.TNT) {
Player planter = getPlanter(b);
if (planter != null) {
b.setType(Material.AIR);
TNTPrimed tnt = b.getWorld().spawn(b.getLocation(), TNTPrimed.class);
setPlanter(tnt, planter);
}
} else {
arena.addBlock(event.getBlock().getRelative(BlockFace.UP));
}
arena.addBlock(b.getRelative(BlockFace.UP));
break;
}
case LIGHTNING:
@ -415,15 +385,23 @@ public class ArenaListener
* reason means MobArena didn't trigger the event. However, we
* make an exception for certain mobs that spawn as results of
* other entities spawning them, e.g. when Evokers summon Vexes.
* Note that the "spell" reason was introduced somewhere between
* 1.18 and 1.18.1, so "default" is kept only for compatibility
* with older server versions. Also note the use of `switch` as
* a workaround for the NoSuchFieldError that would occur if we
* used a simple equality check.
*/
if (reason == SpawnReason.DEFAULT) {
if (event.getEntityType() == EntityType.VEX) {
event.setCancelled(false);
monsters.addMonster(event.getEntity());
} else {
event.setCancelled(true);
switch (reason) {
case DEFAULT:
case SPELL: {
if (event.getEntityType() == EntityType.VEX) {
event.setCancelled(false);
monsters.addMonster(event.getEntity());
} else {
event.setCancelled(true);
}
return;
}
return;
}
// If not custom, we probably don't want it, so get rid of it
@ -512,9 +490,9 @@ public class ArenaListener
}
/******************************************************
*
*
* DEATH LISTENERS
*
*
******************************************************/
public void onEntityDeath(EntityDeathEvent event) {
@ -572,11 +550,11 @@ public class ArenaListener
arena.playerRespawn(p);
return true;
}
private void onMountDeath(EntityDeathEvent event) {
// Shouldn't ever happen
}
private void onMonsterDeath(EntityDeathEvent event) {
EntityDamageEvent e1 = event.getEntity().getLastDamageCause();
EntityDamageByEntityEvent e2 = (e1 instanceof EntityDamageByEntityEvent) ? (EntityDamageByEntityEvent) e1 : null;
@ -611,10 +589,13 @@ public class ArenaListener
for (Player q : arena.getPlayersInArena()) {
arena.getMessenger().tell(q, Msg.WAVE_BOSS_KILLED, p.getName());
}
Thing reward = boss.getReward();
if (reward != null) {
arena.getRewardManager().addReward(p, reward);
arena.getMessenger().tell(damager, Msg.WAVE_BOSS_REWARD_EARNED, reward.toString());
ThingPicker picker = boss.getReward();
if (picker != null) {
Thing reward = picker.pick();
if (reward != null) {
arena.getRewardManager().addReward(p, reward);
arena.getMessenger().tell(damager, Msg.WAVE_BOSS_REWARD_EARNED, reward.toString());
}
}
}
}
@ -653,9 +634,9 @@ public class ArenaListener
}
/******************************************************
*
*
* DAMAGE LISTENERS
*
*
******************************************************/
public void onEntityDamage(EntityDamageEvent event) {
@ -675,7 +656,7 @@ public class ArenaListener
}
if (damager instanceof TNTPrimed) {
damager = getPlanter(damager);
damager = ((TNTPrimed) damager).getSource();
}
}
@ -726,11 +707,21 @@ public class ArenaListener
}
if (arena.inArena(player)) {
// Cancel PvP damage if disabled
if (!pvpEnabled && damager instanceof Player && !damager.equals(player)) {
// Cancel damage from pets (and their projectiles)
if (monsters.hasPet(damager)) {
event.setCancelled(true);
return;
}
// If this is player damage (and not self-inflicted), handle PvP
if (damager instanceof Player && !damager.equals(player)) {
// PvP must be enabled, and the first wave must have spawned
if (!pvpEnabled || arena.getWaveManager().getWaveNumber() == 0) {
event.setCancelled(true);
return;
}
}
event.setCancelled(false);
arena.getArenaPlayer(player).getStats().add("dmgTaken", event.getDamage());
@ -772,7 +763,7 @@ public class ArenaListener
double progress = boss.getHealth() / boss.getMaxHealth();
boss.getHealthBar().setProgress(progress);
}
private void onMonsterDamage(EntityDamageEvent event, Entity monster, Entity damager) {
if (damager instanceof Player) {
Player p = (Player) damager;
@ -797,7 +788,7 @@ public class ArenaListener
event.setCancelled(true);
}
}
private void onGolemDamage(EntityDamageEvent event, Entity golem, Entity damager) {
if (damager instanceof Player) {
Player p = (Player) damager;
@ -805,8 +796,8 @@ public class ArenaListener
event.setCancelled(true);
return;
}
if (!pvpEnabled) {
if (!pvpEnabled || arena.getWaveManager().getWaveNumber() == 0) {
event.setCancelled(true);
}
}
@ -852,6 +843,10 @@ public class ArenaListener
private void onMonsterTarget(EntityTargetEvent event, Entity monster, Entity target) {
// Null means we lost our target or the target died, so find a new one
if (target == null) {
// ... unless the monster is excluded from retargeting
if (excludeFromRetargeting.contains(monster.getType())) {
return;
}
event.setTarget(MAUtils.getClosestPlayer(plugin, monster, arena));
return;
}
@ -895,16 +890,24 @@ public class ArenaListener
private boolean isArenaPet(Entity entity) {
return arena.hasPet(entity);
}
public void onEntityTeleport(EntityTeleportEvent event) {
if (monsters.hasPet(event.getEntity()) && region.contains(event.getTo())) {
return;
}
if (isArenaMonster(event.getEntity())) {
if (!monsterTeleport || !region.contains(event.getTo())) {
event.setCancelled(true);
}
return;
}
if (region.contains(event.getFrom()) || region.contains(event.getTo())) {
event.setCancelled(true);
}
}
public void onPotionSplash(PotionSplashEvent event) {
ThrownPotion potion = event.getPotion();
if (!region.contains(potion.getLocation())) {
@ -913,7 +916,7 @@ public class ArenaListener
if (potion.getShooter() instanceof Player) {
// Check for PvP stuff if the shooter is a player
if (!pvpEnabled) {
if (!pvpEnabled || arena.getWaveManager().getWaveNumber() == 0) {
// If a potion has harmful effects, remove all players.
for (PotionEffect effect : potion.getEffects()) {
PotionEffectType type = effect.getType();
@ -944,8 +947,32 @@ public class ArenaListener
}
public void onEntityChangeBlock(EntityChangeBlockEvent event) {
if (arena.getRegion().contains(event.getBlock().getLocation()))
event.setCancelled(true);
if (!protect) {
return;
}
Block block = event.getBlock();
if (!arena.getRegion().contains(block.getLocation())) {
return;
}
if (arena.isRunning()) {
if (block.getType() == Material.TNT) {
Entity entity = event.getEntity();
if (entity instanceof Arrow) {
Arrow arrow = (Arrow) entity;
ProjectileSource shooter = arrow.getShooter();
if (shooter instanceof Player) {
Player player = (Player) shooter;
if (arena.inArena(player)) {
return;
}
}
}
}
}
event.setCancelled(true);
}
public void onEntityRegainHealth(EntityRegainHealthEvent event) {
@ -1011,18 +1038,18 @@ public class ArenaListener
event.setCancelled(true);
}
}
// If the player is in the lobby, just cancel
else if (arena.inLobby(p)) {
arena.getMessenger().tell(p, Msg.LOBBY_DROP_ITEM);
event.setCancelled(true);
}
// Same if it's a spectator, but...
else if (arena.inSpec(p)) {
arena.getMessenger().tell(p, Msg.LOBBY_DROP_ITEM);
event.setCancelled(true);
// If the spectator isn't in the region, force them to leave
if (!region.contains(p.getLocation())) {
arena.getMessenger().tell(p, Msg.MISC_MA_LEAVE_REMINDER);
@ -1066,26 +1093,58 @@ public class ArenaListener
}
public void onPlayerInteract(PlayerInteractEvent event) {
Player p = event.getPlayer();
if (!arena.inLobby(p)) return;
if (arena.inLobby(event.getPlayer())) {
onLobbyPlayerInteract(event);
} else {
onNonLobbyPlayerInteract(event);
}
}
// Prevent placing blocks and using held items
private void onLobbyPlayerInteract(PlayerInteractEvent event) {
if (event.hasItem()) {
event.setUseItemInHand(Result.DENY);
}
// Bail if off-hand or if there's no block involved.
if (event.getHand() == EquipmentSlot.OFF_HAND || !event.hasBlock())
if (event.getHand() == EquipmentSlot.OFF_HAND) {
return;
// Iron block
if (event.getClickedBlock().getType() == Material.IRON_BLOCK) {
handleReadyBlock(p);
}
// Sign
else if (event.getClickedBlock().getState() instanceof Sign) {
Block block = event.getClickedBlock();
if (block == null) {
return;
}
if (block.getType() == Material.IRON_BLOCK) {
handleReadyBlock(event.getPlayer());
} else if (block.getState() instanceof Sign) {
if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
event.setCancelled(true);
}
Sign sign = (Sign) event.getClickedBlock().getState();
handleSign(sign, p);
handleSign(sign, event.getPlayer());
}
}
private void onNonLobbyPlayerInteract(PlayerInteractEvent event) {
if (!protect) {
return;
}
Block block = event.getClickedBlock();
if (block == null) {
return;
}
if (!region.contains(block.getLocation())) {
return;
}
if (arena.inEditMode()) {
return;
}
if (block.getState() instanceof Sign) {
if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
event.setCancelled(true);
}
}
}
@ -1107,26 +1166,27 @@ public class ArenaListener
private void handleSign(Sign sign, Player p) {
// Check if the first line is a class name.
String className = ChatColor.stripColor(sign.getLine(0)).toLowerCase().replace(" ", "");
String className = ChatColor.stripColor(sign.getLine(0));
String slug = Slugs.create(className);
if (!arena.getClasses().containsKey(className) && !className.equals("random"))
if (!arena.getClasses().containsKey(slug) && !slug.equals("random"))
return;
ArenaClass newAC = arena.getClasses().get(className);
ArenaClass newAC = arena.getClasses().get(slug);
// Check for permission.
if (!newAC.hasPermission(p) && !className.equals("random")) {
if (!newAC.hasPermission(p) && !slug.equals("random")) {
arena.getMessenger().tell(p, Msg.LOBBY_CLASS_PERMISSION);
return;
}
ArenaClass oldAC = arena.getArenaPlayer(p).getArenaClass();
// Same class, do nothing.
if (newAC.equals(oldAC)) {
return;
}
// If the new class is full, inform the player.
if (!classLimits.canPlayerJoinClass(newAC)) {
arena.getMessenger().tell(p, Msg.LOBBY_CLASS_FULL);
@ -1141,33 +1201,33 @@ public class ArenaListener
return;
}
}
// Otherwise, leave the old class, and pick the new!
classLimits.playerLeftClass(oldAC, p);
classLimits.playerPickedClass(newAC, p);
// Delay the inventory stuff to ensure that right-clicking works.
delayAssignClass(p, className, price, sign);
delayAssignClass(p, slug, price, sign);
}
/*private boolean cansPlayerJoinClass(ArenaClass ac, Player p) {
// If they can not join the class, deny them
if (!classLimits.canPlayerJoinClass(ac)) {
Messenger.tell(p, Msg.LOBBY_CLASS_FULL);
return false;
}
// Increment the "in use" in the Class Limit Manager
classLimits.playerPickedClass(ac);
return true;
}*/
private void delayAssignClass(final Player p, final String className, final Thing price, final Sign sign) {
private void delayAssignClass(final Player p, final String slug, final Thing price, final Sign sign) {
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin,new Runnable() {
public void run() {
if (!className.equalsIgnoreCase("random")) {
if (!slug.equalsIgnoreCase("random")) {
if (useClassChests) {
ArenaClass ac = plugin.getArenaMaster().getClasses().get(className.toLowerCase().replace(" ", ""));
ArenaClass ac = plugin.getArenaMaster().getClasses().get(slug);
if (ClassChests.assignClassFromStoredClassChest(arena, p, ac)) {
return;
}
@ -1176,8 +1236,8 @@ public class ArenaListener
}
// Otherwise just fall through and use the items from the config-file
}
arena.assignClass(p, className);
arena.getMessenger().tell(p, Msg.LOBBY_CLASS_PICKED, arena.getClasses().get(className).getConfigName());
arena.assignClass(p, slug);
arena.getMessenger().tell(p, Msg.LOBBY_CLASS_PICKED, arena.getClasses().get(slug).getConfigName());
if (price != null) {
arena.getMessenger().tell(p, Msg.LOBBY_CLASS_PRICE, price.toString());
}
@ -1219,7 +1279,7 @@ public class ArenaListener
}
}, ticks);
}
public TeleportResponse onPlayerTeleport(PlayerTeleportEvent event) {
if (!arena.isEnabled() || !region.isSetup() || arena.inEditMode() || allowTeleport) {
return TeleportResponse.IDGAF;
@ -1251,23 +1311,20 @@ public class ArenaListener
if (region.contains(to)) {
// Inside -> inside
if (!(arena.inArena(p) || arena.inLobby(p))) {
arena.getMessenger().tell(p, Msg.WARP_TO_ARENA);
return TeleportResponse.REJECT;
return reject(p, Msg.WARP_TO_ARENA);
}
return TeleportResponse.ALLOW;
} else {
// Inside -> outside
if (arena.getAllPlayers().contains(p)) {
arena.getMessenger().tell(p, Msg.WARP_FROM_ARENA);
return TeleportResponse.REJECT;
return reject(p, Msg.WARP_FROM_ARENA);
}
return TeleportResponse.IDGAF;
}
} else {
if (region.contains(to)) {
// Outside -> inside
arena.getMessenger().tell(p, Msg.WARP_TO_ARENA);
return TeleportResponse.REJECT;
return reject(p, Msg.WARP_TO_ARENA);
} else {
// Outside -> outside
return TeleportResponse.IDGAF;
@ -1275,6 +1332,15 @@ public class ArenaListener
}
}
private TeleportResponse reject(Player p, Msg message) {
if (p.hasPermission("mobarena.admin.teleport")) {
return TeleportResponse.IDGAF;
}
arena.getMessenger().tell(p, message);
return TeleportResponse.REJECT;
}
public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
Player p = event.getPlayer();
@ -1307,10 +1373,10 @@ public class ArenaListener
public void onPlayerPreLogin(PlayerLoginEvent event) {
Player p = event.getPlayer();
if (p == null || !p.isOnline()) return;
Arena arena = plugin.getArenaMaster().getArenaWithPlayer(p);
if (arena == null) return;
arena.playerLeave(p);
}

View File

@ -8,6 +8,7 @@ import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.things.InvalidThingInputString;
import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.util.JoinInterruptTimer;
import com.garbagemule.MobArena.util.Slugs;
import com.garbagemule.MobArena.util.config.ConfigUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
@ -137,7 +138,7 @@ public class ArenaMasterImpl implements ArenaMaster
public List<Arena> getEnabledArenas(List<Arena> arenas) {
List<Arena> result = new ArrayList<>(arenas.size());
for (Arena arena : arenas)
if (arena.isEnabled())
if (arena.isEnabled())
result.add(arena);
return result;
}
@ -232,8 +233,9 @@ public class ArenaMasterImpl implements ArenaMaster
}
public Arena getArenaWithName(Collection<Arena> arenas, String configName) {
String slug = Slugs.create(configName);
for (Arena arena : arenas)
if (arena.configName().equalsIgnoreCase(configName))
if (arena.getSlug().equalsIgnoreCase(slug))
return arena;
return null;
}
@ -280,6 +282,9 @@ public class ArenaMasterImpl implements ArenaMaster
spawnsPets.clear();
ConfigurationSection items = settings.getConfigurationSection("pet-items");
if (items == null) {
return;
}
for (String key : items.getKeys(false)) {
EntityType entity;
@ -334,14 +339,13 @@ public class ArenaMasterImpl implements ArenaMaster
private ArenaClass loadClass(String classname) {
FileConfiguration config = plugin.getConfig();
ConfigurationSection section = config.getConfigurationSection("classes." + classname);
String lowercase = classname.toLowerCase().replace(" ", "");
// If the section doesn't exist, the class doesn't either.
if (section == null) {
// We may not have a class entry for My Items, but that's fine
if (classname.equals("My Items")) {
ArenaClass myItems = new ArenaClass.MyItems(null, false, false, this);
classes.put(lowercase, myItems);
classes.put(myItems.getSlug(), myItems);
return myItems;
}
plugin.getLogger().severe("Failed to load class '" + classname + "'.");
@ -389,8 +393,12 @@ public class ArenaMasterImpl implements ArenaMaster
throw new ConfigError("Failed to parse classchest location for class " + classname + " because: " + e.getMessage());
}
// Load pet name
String petName = section.getString("pet-name", "<display-name>'s pet");
arenaClass.setPetName(petName);
// Finally add the class to the classes map.
classes.put(lowercase, arenaClass);
classes.put(arenaClass.getSlug(), arenaClass);
return arenaClass;
}

View File

@ -17,7 +17,7 @@ public class ArenaPlayerStatistics
public ArenaPlayerStatistics(ArenaPlayer player) {
this.player = player;
this.playerName = player.getPlayer().getName();
this.className = player.getArenaClass().getLowercaseName();
this.className = player.getArenaClass().getConfigName();
reset();
}

View File

@ -16,7 +16,7 @@ public class ClassLimitManager
private HashMap<ArenaClass, HashSet<String>> classesInUse;
private ConfigurationSection limits;
private Map<String,ArenaClass> classes;
public ClassLimitManager(Arena arena, Map<String,ArenaClass> classes, ConfigurationSection limits) {
this.limits = limits;
this.classes = classes;
@ -26,7 +26,7 @@ public class ClassLimitManager
loadLimitMap(arena.getPlugin());
initInUseMap();
}
private void loadLimitMap(Plugin plugin) {
// If the config-section is empty, create and populate it.
if (limits.getKeys(false).isEmpty()) {
@ -35,20 +35,20 @@ public class ClassLimitManager
}
plugin.saveConfig();
}
// Populate the limits map using the values in the config-file.
for (ArenaClass ac : classes.values()) {
classLimits.put(ac, new MutableInt(limits.getInt(ac.getConfigName(), -1)));
}
}
private void initInUseMap() {
// Initialize the in-use map with zeros.
for (ArenaClass ac : classes.values()) {
classesInUse.put(ac, new HashSet<>());
}
}
/**
* This is the class a player is changing to
* @param ac the new ArenaClass
@ -56,7 +56,7 @@ public class ClassLimitManager
public void playerPickedClass(ArenaClass ac, Player p) {
classesInUse.get(ac).add(p.getName());
}
/**
* This is the class a player left
* @param ac the current/old ArenaClass
@ -66,7 +66,7 @@ public class ClassLimitManager
classesInUse.get(ac).remove(p.getName());
}
}
/**
* Checks to see if a player can pick a specific class
* @param ac the ArenaClass to check
@ -78,13 +78,13 @@ public class ClassLimitManager
classLimits.put(ac, new MutableInt(-1));
classesInUse.put(ac, new HashSet<>());
}
if (classLimits.get(ac).value() <= -1)
return true;
return classesInUse.get(ac).size() < classLimits.get(ac).value();
}
/**
* returns a set of Player Names who have picked an ArenaClass
* @param ac the ArenaClass in question
@ -93,7 +93,7 @@ public class ClassLimitManager
public HashSet<String> getPlayersWithClass(ArenaClass ac) {
return classesInUse.get(ac);
}
/**
* Clear the classes in use map and reinitialize it for the next match
*/
@ -101,4 +101,4 @@ public class ClassLimitManager
classesInUse.clear();
initInUseMap();
}
}
}

View File

@ -8,6 +8,7 @@ import com.garbagemule.MobArena.healthbar.HealthBar;
import com.garbagemule.MobArena.region.ArenaRegion;
import com.garbagemule.MobArena.things.ExperienceThing;
import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.things.ThingPicker;
import com.garbagemule.MobArena.waves.MABoss;
import com.garbagemule.MobArena.waves.MACreature;
import com.garbagemule.MobArena.waves.Wave;
@ -41,6 +42,7 @@ public class MASpawnThread implements Runnable
private int playerCount, monsterLimit;
private boolean waveClear, bossClear, preBossClear, wavesAsLevel;
private int clearLeeway;
private int waveInterval;
private int nextWaveDelay;
@ -75,6 +77,7 @@ public class MASpawnThread implements Runnable
waveClear = arena.getSettings().getBoolean("clear-wave-before-next", false);
bossClear = arena.getSettings().getBoolean("clear-boss-before-next", false);
preBossClear = arena.getSettings().getBoolean("clear-wave-before-boss", false);
clearLeeway = arena.getSettings().getInt("clear-wave-leeway", 0);
wavesAsLevel = arena.getSettings().getBoolean("display-waves-as-level", false);
waveInterval = arena.getSettings().getInt("wave-interval", 3);
nextWaveDelay = arena.getSettings().getInt("next-wave-delay", 0);
@ -88,10 +91,7 @@ public class MASpawnThread implements Runnable
}
int delay = arena.getSettings().getInt("first-wave-delay", 5) * 20;
task = Bukkit.getScheduler().runTaskLater(plugin, () -> {
arena.getEventListener().pvpActivate();
this.run();
}, delay);
task = Bukkit.getScheduler().runTaskLater(plugin, this, delay);
}
public void stop() {
@ -100,8 +100,6 @@ public class MASpawnThread implements Runnable
return;
}
arena.getEventListener().pvpDeactivate();
task.cancel();
task = null;
}
@ -184,9 +182,9 @@ public class MASpawnThread implements Runnable
Wave w = waveManager.next();
w.announce(arena, wave);
arena.getScoreboard().updateWave(wave);
// Set the players' level to the wave number
if (wavesAsLevel) {
for (Player p : arena.getPlayersInArena()) {
@ -241,13 +239,18 @@ public class MASpawnThread implements Runnable
} else {
e.setCustomName("SPIGOT ERROR");
}
for (Player p : plugin.getServer().getOnlinePlayers()) {
if (p.hasPermission("mobarena.admin.errors")) {
arena.getMessenger().tell(p, "Failed to set boss health (" + health + ") in arena " + arena.configName() + " (wave " + wave + ") because Spigot 'maxHealth' is too low. See console for details.");
}
}
}
// Switch on the type.
switch (w.getType()){
case BOSS:
BossWave bw = (BossWave) w;
double maxHealth = bw.getMaxHealth(playerCount);
double maxHealth = bw.getHealth().evaluate(arena);
MABoss boss = monsterManager.addBoss(e, maxHealth);
HealthBar healthbar = createsHealthBar.create(e, bw.getBossName());
arena.getPlayersInArena().forEach(healthbar::addPlayer);
@ -282,8 +285,8 @@ public class MASpawnThread implements Runnable
UpgradeWave uw = (UpgradeWave) w;
for (Player p : arena.getPlayersInArena()) {
String className = arena.getArenaPlayer(p).getArenaClass().getLowercaseName();
uw.grantItems(p, className);
String slug = arena.getArenaPlayer(p).getArenaClass().getSlug();
uw.grantItems(p, slug);
uw.grantItems(p, "all");
}
@ -307,18 +310,18 @@ public class MASpawnThread implements Runnable
return false;
}
// Check for wave and pre boss clear
if (waveClear && !monsterManager.getMonsters().isEmpty()) {
// Check for wave clear
if (waveClear && monsterManager.getMonsters().size() > clearLeeway) {
return false;
}
// Check for pre boss clear
if (preBossClear && waveManager.getNext().getType() == WaveType.BOSS && !monsterManager.getMonsters().isEmpty()) {
if (preBossClear && waveManager.getNext().getType() == WaveType.BOSS && monsterManager.getMonsters().size() > clearLeeway) {
return false;
}
// Check for final wave
if (!monsterManager.getMonsters().isEmpty() && waveManager.getWaveNumber() == waveManager.getFinalWave()) {
if (monsterManager.getMonsters().size() > clearLeeway && waveManager.getWaveNumber() == waveManager.getFinalWave()) {
return false;
}
@ -345,7 +348,7 @@ public class MASpawnThread implements Runnable
if (region.contains(p.getLocation())) {
continue;
}
arena.getMessenger().tell(p, "Leaving so soon?");
p.getInventory().clear();
arena.playerLeave(p);
@ -353,13 +356,13 @@ public class MASpawnThread implements Runnable
}
private void grantRewards(int wave) {
for (Map.Entry<Integer, List<Thing>> entry : arena.getEveryWaveEntrySet()) {
for (Map.Entry<Integer, ThingPicker> entry : arena.getEveryWaveEntrySet()) {
if (wave > 0 && wave % entry.getKey() == 0) {
addReward(entry.getValue());
}
}
List<Thing> after = arena.getAfterWaveReward(wave);
ThingPicker after = arena.getAfterWaveReward(wave);
if (after != null) {
addReward(after);
}
@ -388,18 +391,13 @@ public class MASpawnThread implements Runnable
/**
* Rewards all players with an item from the input String.
*/
private void addReward(List<Thing> rewards) {
private void addReward(ThingPicker picker) {
for (Player p : arena.getPlayersInArena()) {
Thing reward = rewards.get(MobArena.random.nextInt(rewards.size()));
rewardManager.addReward(p, reward);
if (reward == null) {
arena.getMessenger().tell(p, "ERROR! Problem with rewards. Notify server host!");
plugin.getLogger().warning("Could not add null reward. Please check the config-file!");
}
else {
Thing reward = picker.pick();
if (reward != null) {
rewardManager.addReward(p, reward);
arena.getMessenger().tell(p, Msg.WAVE_REWARD, reward.toString());
}
}
}
}
}

View File

@ -4,8 +4,8 @@ import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.region.ArenaRegion;
import com.garbagemule.MobArena.things.InvalidThingInputString;
import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.util.TextUtils;
import com.garbagemule.MobArena.things.ThingPicker;
import com.garbagemule.MobArena.util.Materials;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
@ -22,73 +22,69 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MAUtils
{
{
/* ///////////////////////////////////////////////////////////////////// //
INITIALIZATION METHODS
// ///////////////////////////////////////////////////////////////////// */
/**
* Generates a map of wave numbers and rewards based on the
* type of wave ("after" or "every") and the config-file. If
* no keys exist in the config-file, an empty map is returned.
*/
public static Map<Integer,List<Thing>> getArenaRewardMap(MobArena plugin, ConfigurationSection config, String arena, String type)
*/
public static Map<Integer, ThingPicker> getArenaRewardMap(MobArena plugin, ConfigurationSection config, String arena, String type)
{
//String arenaPath = "arenas." + arena + ".rewards.waves.";
Map<Integer,List<Thing>> result = new HashMap<>();
Map<Integer, ThingPicker> result = new HashMap<>();
String typePath = "rewards.waves." + type;
if (!config.contains(typePath)) return result;
//Set<String> waves = config.getKeys(arenaPath + type);
Set<String> waves = config.getConfigurationSection(typePath).getKeys(false);
if (waves == null) return result;
for (String n : waves)
{
if (!n.matches("[0-9]+"))
continue;
int wave = Integer.parseInt(n);
String path = typePath + "." + wave;
String rewards = config.getString(path);
List<Thing> things = new ArrayList<>();
for (String reward : rewards.split(",")) {
try {
Thing thing = plugin.getThingManager().parse(reward.trim());
things.add(thing);
} catch (InvalidThingInputString e) {
throw new ConfigError("Failed to parse reward for wave " + wave + " in the '" + type + "' branch of arena " + arena + ": " + e.getInput());
}
try {
String wrapped = "random(" + rewards + ")";
ThingPicker picker = plugin.getThingPickerManager().parse(wrapped);
result.put(wave, picker);
} catch (InvalidThingInputString e) {
throw new ConfigError("Failed to parse reward for wave " + wave + " in the '" + type + "' branch of arena " + arena + ": " + e.getInput());
}
result.put(wave, things);
}
return result;
}
/* ///////////////////////////////////////////////////////////////////// //
MISC METHODS
// ///////////////////////////////////////////////////////////////////// */
public static Player getClosestPlayer(MobArena plugin, Entity e, Arena arena) {
// Set up the comparison variable and the result.
double current = Double.POSITIVE_INFINITY;
Player result = null;
/* Iterate through the ArrayList, and update current and result every
* time a squared distance smaller than current is found. */
List<Player> players = new ArrayList<>(arena.getPlayersInArena());
@ -99,7 +95,7 @@ public class MAUtils
arena.getMessenger().tell(p, "You warped out of the arena world.");
continue;
}
double dist = distanceSquared(plugin, p, e.getLocation());
if (dist < current && dist < 256D) {
current = dist;
@ -108,7 +104,7 @@ public class MAUtils
}
return result;
}
public static double distanceSquared(MobArena plugin, Player p, Location l) {
try {
return p.getLocation().distanceSquared(l);
@ -121,50 +117,19 @@ public class MAUtils
return Double.MAX_VALUE;
}
}
/**
* Convert a config-name to a proper spaced and capsed arena name.
* The input String is split around all underscores, and every part
* of the String array is properly capsed.
*/
public static String nameConfigToArena(String name)
{
String[] parts = name.split("_");
if (parts.length == 1) {
return toCamelCase(parts[0]);
}
String separator = " ";
StringBuffer buffy = new StringBuffer(name.length());
for (String part : parts) {
buffy.append(toCamelCase(part));
buffy.append(separator);
}
buffy.replace(buffy.length()-1, buffy.length(), "");
return buffy.toString();
}
/**
* Returns the input String with a capital first letter, and all the
* other letters become lower case.
*/
public static String toCamelCase(String name) {
return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
}
/**
* Turn a list into a space-separated string-representation of the list.
*/
*/
public static <E> String listToString(Collection<E> list, boolean none, MobArena plugin)
{
if (list == null || list.isEmpty()) {
return (none ? Msg.MISC_NONE.toString() : "");
}
StringBuffer buffy = new StringBuffer();
int trimLength = 0;
E type = list.iterator().next();
if (type instanceof Player) {
for (E e : list) {
@ -192,23 +157,7 @@ public class MAUtils
return buffy.toString().substring(0, buffy.length() - trimLength);
}
public static <E> String listToString(Collection<E> list, JavaPlugin plugin) { return listToString(list, true, (MobArena) plugin); }
/**
* Returns a String-list version of a comma-separated list.
*/
public static List<String> stringToList(String list)
{
List<String> result = new LinkedList<>();
if (list == null) return result;
String[] parts = list.trim().split(",");
for (String part : parts)
result.add(part.trim());
return result;
}
/**
* Stand back, I'm going to try science!
*/
@ -216,11 +165,11 @@ public class MAUtils
{
// Grab the Configuration and ArenaMaster
ArenaMaster am = plugin.getArenaMaster();
// Create the arena node in the config-file.
World world = loc.getWorld();
Arena arena = am.createArenaNode(name, world);
// Get the hippie bounds.
int x1 = (int)loc.getX() - radius;
int x2 = (int)loc.getX() + radius;
@ -228,14 +177,14 @@ public class MAUtils
int y2 = (int)loc.getY() - 1;
int z1 = (int)loc.getZ() - radius;
int z2 = (int)loc.getZ() + radius;
int lx1 = x1;
int lx2 = x1 + am.getClasses().size() + 3;
int ly1 = y1-6;
int ly2 = y1-2;
int lz1 = z1;
int lz2 = z1 + 6;
// Build some monster walls.
for (int i = x1; i <= x2; i++)
{
@ -253,7 +202,7 @@ public class MAUtils
world.getBlockAt(x2,j,k).setType(Material.SANDSTONE);
}
}
// Add some hippie light.
for (int i = x1; i <= x2; i++)
{
@ -265,7 +214,7 @@ public class MAUtils
world.getBlockAt(x1,y1+2,k).setType(Material.GLOWSTONE);
world.getBlockAt(x2,y1+2,k).setType(Material.GLOWSTONE);
}
// Build a monster floor, and some Obsidian foundation.
for (int i = x1; i <= x2; i++)
{
@ -275,20 +224,20 @@ public class MAUtils
world.getBlockAt(i,y1-1,k).setType(Material.OBSIDIAN);
}
}
// Make a hippie roof.
for (int i = x1; i <= x2; i++)
{
for (int k = z1; k <= z2; k++)
world.getBlockAt(i,y2,k).setType(Material.GLASS);
}
// Monster bulldoze
for (int i = x1+1; i < x2; i++)
for (int j = y1+1; j < y2; j++)
for (int k = z1+1; k < z2; k++)
world.getBlockAt(i,j,k).setType(Material.AIR);
// Build a hippie lobby
for (int i = lx1; i <= lx2; i++) // Walls
{
@ -322,37 +271,37 @@ public class MAUtils
for (int j = ly1+1; j <= ly2; j++)
for (int k = lz1+1; k < lz2; k++)
world.getBlockAt(i,j,k).setType(Material.AIR);
// Place the hippie signs
//Iterator<String> iterator = am.getClasses().iterator();
Iterator<String> iterator = am.getClasses().keySet().iterator();
Rotatable signData = (Rotatable) Material.SIGN.createBlockData();
Iterator<ArenaClass> iterator = am.getClasses().values().iterator();
Rotatable signData = (Rotatable) Materials.SIGN.createBlockData();
signData.setRotation(BlockFace.NORTH);
for (int i = lx1+2; i <= lx2-2; i++) // Signs
{
world.getBlockAt(i,ly1+1,lz2-1).setBlockData(signData);
Sign sign = (Sign) world.getBlockAt(i,ly1+1,lz2-1).getState();
sign.setLine(0, TextUtils.camelCase(iterator.next()));
sign.setLine(0, iterator.next().getConfigName());
sign.update();
}
world.getBlockAt(lx2-2,ly1+1,lz1+2).setType(Material.IRON_BLOCK);
// Set up the monster points.
// Set up the monster points.
ArenaRegion region = arena.getRegion();
region.set("p1", new Location(world, x1, ly1, z1));
region.set("p2", new Location(world, x2, y2+1, z2));
region.set("arena", new Location(world, loc.getX(), y1+1, loc.getZ()));
region.set("lobby", new Location(world, x1+2, ly1+1, z1+2));
region.set("spectator", new Location(world, loc.getX(), y2+1, loc.getZ()));
region.addSpawn("s1", new Location(world, x1+3, y1+2, z1+3));
region.addSpawn("s2", new Location(world, x1+3, y1+2, z2-3));
region.addSpawn("s3", new Location(world, x2-3, y1+2, z1+3));
region.addSpawn("s4", new Location(world, x2-3, y1+2, z2-3));
region.save();
am.reloadConfig();
return true;
}
}
}

View File

@ -2,10 +2,14 @@ package com.garbagemule.MobArena;
import com.garbagemule.MobArena.commands.CommandHandler;
import com.garbagemule.MobArena.config.LoadsConfigFile;
import com.garbagemule.MobArena.events.MobArenaPreReloadEvent;
import com.garbagemule.MobArena.events.MobArenaReloadEvent;
import com.garbagemule.MobArena.formula.FormulaMacros;
import com.garbagemule.MobArena.formula.FormulaManager;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.items.SavedItemsManager;
import com.garbagemule.MobArena.listeners.MAGlobalListener;
import com.garbagemule.MobArena.listeners.MagicSpellsListener;
import com.garbagemule.MobArena.metrics.ArenaCountChart;
import com.garbagemule.MobArena.metrics.ClassChestsChart;
import com.garbagemule.MobArena.metrics.ClassCountChart;
@ -14,10 +18,13 @@ import com.garbagemule.MobArena.metrics.IsolatedChatChart;
import com.garbagemule.MobArena.metrics.MonsterInfightChart;
import com.garbagemule.MobArena.metrics.PvpEnabledChart;
import com.garbagemule.MobArena.metrics.VaultChart;
import com.garbagemule.MobArena.signs.ArenaSign;
import com.garbagemule.MobArena.signs.SignBootstrap;
import com.garbagemule.MobArena.signs.SignListeners;
import com.garbagemule.MobArena.things.NothingPickerParser;
import com.garbagemule.MobArena.things.RandomThingPickerParser;
import com.garbagemule.MobArena.things.ThingGroupPickerParser;
import com.garbagemule.MobArena.things.ThingManager;
import com.garbagemule.MobArena.things.ThingPickerManager;
import com.garbagemule.MobArena.util.config.ConfigUtils;
import com.garbagemule.MobArena.waves.ability.AbilityManager;
import net.milkbowl.vault.economy.Economy;
@ -26,7 +33,6 @@ import org.bukkit.ChatColor;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.RegisteredServiceProvider;
@ -58,12 +64,24 @@ public class MobArena extends JavaPlugin
private Messenger messenger;
private ThingManager thingman;
private ThingPickerManager pickman;
private FormulaManager formman;
private FormulaMacros macros;
private SavedItemsManager itemman;
private SignListeners signListeners;
@Override
public void onLoad() {
thingman = new ThingManager(this);
pickman = new ThingPickerManager(thingman);
pickman.register(new ThingGroupPickerParser(pickman));
pickman.register(new RandomThingPickerParser(pickman, random));
pickman.register(new NothingPickerParser());
formman = FormulaManager.createDefault();
}
public void onEnable() {
@ -85,18 +103,17 @@ public class MobArena extends JavaPlugin
arenaMaster = null;
}
loadsConfigFile = null;
ConfigurationSerialization.unregisterClass(ArenaSign.class);
}
private void setup() {
try {
createDataFolder();
setupFormulaMacros();
setupSavedItemsManager();
setupArenaMaster();
setupCommandHandler();
registerConfigurationSerializers();
setupVault();
setupMagicSpells();
setupBossAbilities();
setupListeners();
setupMetrics();
@ -116,7 +133,15 @@ public class MobArena extends JavaPlugin
}
}
}
private void setupFormulaMacros() {
macros = FormulaMacros.create(this);
}
private void setupSavedItemsManager() {
itemman = new SavedItemsManager(this);
}
private void setupArenaMaster() {
arenaMaster = new ArenaMasterImpl(this);
}
@ -125,10 +150,6 @@ public class MobArena extends JavaPlugin
getCommand("ma").setExecutor(new CommandHandler(this));
}
private void registerConfigurationSerializers() {
ConfigurationSerialization.registerClass(ArenaSign.class);
}
private void setupVault() {
Plugin vaultPlugin = this.getServer().getPluginManager().getPlugin("Vault");
if (vaultPlugin == null) {
@ -147,16 +168,6 @@ public class MobArena extends JavaPlugin
}
}
private void setupMagicSpells() {
Plugin spells = this.getServer().getPluginManager().getPlugin("MagicSpells");
if (spells == null) {
return;
}
getLogger().info("MagicSpells found, loading config-file.");
this.getServer().getPluginManager().registerEvents(new MagicSpellsListener(this), this);
}
private void setupBossAbilities() {
AbilityManager.loadCoreAbilities();
AbilityManager.loadCustomAbilities(getDataFolder());
@ -168,7 +179,7 @@ public class MobArena extends JavaPlugin
}
private void setupMetrics() {
Metrics metrics = new Metrics(this);
Metrics metrics = new Metrics(this, 2572);
metrics.addCustomChart(new VaultChart(this));
metrics.addCustomChart(new ArenaCountChart(this));
metrics.addCustomChart(new ClassCountChart(this));
@ -180,9 +191,14 @@ public class MobArena extends JavaPlugin
}
public void reload() {
MobArenaPreReloadEvent pre = new MobArenaPreReloadEvent(this);
getServer().getPluginManager().callEvent(pre);
try {
reloadConfig();
reloadGlobalMessenger();
reloadFormulaMacros();
reloadSavedItemsManager();
reloadArenaMaster();
reloadAnnouncementsFile();
reloadSigns();
@ -190,6 +206,9 @@ public class MobArena extends JavaPlugin
setLastFailureCauseAndRethrow(e);
}
lastFailureCause = null;
MobArenaReloadEvent post = new MobArenaReloadEvent(this);
getServer().getPluginManager().callEvent(post);
}
@Override
@ -208,6 +227,18 @@ public class MobArena extends JavaPlugin
messenger = new Messenger(prefix);
}
private void reloadFormulaMacros() {
try {
macros.reload();
} catch (IOException e) {
throw new RuntimeException("There was an error reloading the formulas-file:\n" + e.getMessage());
}
}
private void reloadSavedItemsManager() {
itemman.reload();
}
private void reloadArenaMaster() {
arenaMaster.getArenas().forEach(Arena::forceEnd);
arenaMaster.initialize();
@ -264,10 +295,6 @@ public class MobArena extends JavaPlugin
return lastFailureCause;
}
public File getPluginFile() {
return getFile();
}
@Override
public FileConfiguration getConfig() {
if (config == null) {
@ -291,4 +318,20 @@ public class MobArena extends JavaPlugin
public ThingManager getThingManager() {
return thingman;
}
public ThingPickerManager getThingPickerManager() {
return pickman;
}
public FormulaManager getFormulaManager() {
return formman;
}
public FormulaMacros getFormulaMacros() {
return macros;
}
public SavedItemsManager getSavedItemsManager() {
return itemman;
}
}

View File

@ -10,7 +10,7 @@ import org.bukkit.entity.Player;
public class MobArenaHandler
{
private MobArena plugin;
/**
* Primary constructor.
* The field 'plugin' is initalized, if the server is running MobArena.
@ -18,15 +18,15 @@ public class MobArenaHandler
public MobArenaHandler() {
plugin = (MobArena) Bukkit.getServer().getPluginManager().getPlugin("MobArena");
}
/*//////////////////////////////////////////////////////////////////
REGION/LOCATION METHODS
//////////////////////////////////////////////////////////////////*/
/**
* Check if a Location is inside of any arena region.
* @param loc A location.
@ -41,7 +41,7 @@ public class MobArenaHandler
return false;
}
/**
* Check if a Location is inside of a specific arena region (by arena object).
* @param arena An Arena object
@ -51,7 +51,7 @@ public class MobArenaHandler
public boolean inRegion(Arena arena, Location loc) {
return (arena != null && arena.getRegion().contains(loc));
}
/**
* Check if a Location is inside of a specific arena region (by arena name).
* @param arenaName The name of an arena
@ -65,7 +65,7 @@ public class MobArenaHandler
return arena.getRegion().contains(loc);
}
/**
* Check if a Location is inside of the region of an arena that is currently running.
* @param loc A location.
@ -74,7 +74,7 @@ public class MobArenaHandler
public boolean inRunningRegion(Location loc) {
return inRegion(loc, false, true);
}
/**
* Check if a Location is inside of the region of an arena that is currently enabled.
* @param loc A location.
@ -83,7 +83,7 @@ public class MobArenaHandler
public boolean inEnabledRegion(Location loc) {
return inRegion(loc, true, false);
}
/**
* Private helper method for inRunningRegion and inEnabledRegion
* @param loc A location
@ -106,15 +106,15 @@ public class MobArenaHandler
return false;
}
/*//////////////////////////////////////////////////////////////////
PLAYER/MONSTER/PET METHODS
//////////////////////////////////////////////////////////////////*/
/**
* Check if a player is in a MobArena arena (by Player).
* @param player The player
@ -123,7 +123,7 @@ public class MobArenaHandler
public boolean isPlayerInArena(Player player) {
return (plugin.getArenaMaster().getArenaWithPlayer(player) != null);
}
/**
* Check if a player is in a MobArena arena (by name).
* @param playerName The name of the player
@ -132,7 +132,7 @@ public class MobArenaHandler
public boolean isPlayerInArena(String playerName) {
return (plugin.getArenaMaster().getArenaWithPlayer(playerName) != null);
}
/**
* Get the MobArena class of a given player.
* @param player The player
@ -144,7 +144,7 @@ public class MobArenaHandler
return getPlayerClass(arena, player);
}
/**
* Get the MobArena class of a given player in a given arena.
* This method is faster than the above method, granted the Arena object is known.
@ -155,13 +155,13 @@ public class MobArenaHandler
public String getPlayerClass(Arena arena, Player player) {
ArenaPlayer ap = arena.getArenaPlayer(player);
if (ap == null) return null;
ArenaClass ac = ap.getArenaClass();
if (ac == null) return null;
return ac.getLowercaseName();
return ac.getSlug();
}
/**
* Check if a monster is in a MobArena arena.
* @param entity The monster entity
@ -170,7 +170,7 @@ public class MobArenaHandler
public boolean isMonsterInArena(LivingEntity entity) {
return plugin.getArenaMaster().getArenaWithMonster(entity) != null;
}
/**
* Check if a pet is in a MobArena arena.
* @param wolf The pet wolf
@ -179,15 +179,15 @@ public class MobArenaHandler
public boolean isPetInArena(LivingEntity wolf) {
return plugin.getArenaMaster().getArenaWithPet(wolf) != null;
}
/*//////////////////////////////////////////////////////////////////
ARENA GETTERS
//////////////////////////////////////////////////////////////////*/
/**
* Get an Arena object at the given location.
* @param loc A location
@ -196,7 +196,7 @@ public class MobArenaHandler
public Arena getArenaAtLocation(Location loc) {
return plugin.getArenaMaster().getArenaAtLocation(loc);
}
/**
* Get the Arena object that the given player is currently in.
* @param p A player
@ -205,7 +205,7 @@ public class MobArenaHandler
public Arena getArenaWithPlayer(Player p) {
return plugin.getArenaMaster().getArenaWithPlayer(p);
}
/**
* Get the Arena object that the given pet is currently in.
* @param wolf A pet wolf
@ -214,7 +214,7 @@ public class MobArenaHandler
public Arena getArenaWithPet(Entity wolf) {
return plugin.getArenaMaster().getArenaWithPet(wolf);
}
/**
* Get the Arena object that the given monster is currently in.
* @param monster A monster

View File

@ -24,7 +24,7 @@ public class MonsterManager
private Set<LivingEntity> mounts;
private Map<Entity, Player> petToPlayer;
private Map<Player, Set<Entity>> playerToPets;
public MonsterManager() {
this.monsters = new HashSet<>();
this.sheep = new HashSet<>();
@ -35,7 +35,7 @@ public class MonsterManager
this.petToPlayer = new HashMap<>();
this.playerToPets = new HashMap<>();
}
public void reset() {
monsters.clear();
sheep.clear();
@ -46,7 +46,7 @@ public class MonsterManager
petToPlayer.clear();
playerToPets.clear();
}
public void clear() {
bosses.values().stream()
.map(MABoss::getHealthBar)
@ -60,10 +60,10 @@ public class MonsterManager
removeAll(suppliers.keySet());
removeAll(mounts);
removeAll(petToPlayer.keySet());
reset();
}
private void removeAll(Collection<? extends Entity> collection) {
for (Entity e : collection) {
if (e != null) {
@ -71,7 +71,7 @@ public class MonsterManager
}
}
}
public void remove(Entity e) {
if (monsters.remove(e)) {
sheep.remove(e);
@ -83,50 +83,50 @@ public class MonsterManager
}
}
}
public Set<LivingEntity> getMonsters() {
return monsters;
}
public void addMonster(LivingEntity e) {
monsters.add(e);
}
public boolean removeMonster(Entity e) {
return monsters.remove(e);
}
public Set<LivingEntity> getExplodingSheep() {
return sheep;
}
public void addExplodingSheep(LivingEntity e) {
sheep.add(e);
}
public boolean removeExplodingSheep(LivingEntity e) {
return sheep.remove(e);
}
public Set<LivingEntity> getGolems() {
return golems;
}
public void addGolem(LivingEntity e) {
golems.add(e);
}
public boolean removeGolem(LivingEntity e) {
return golems.remove(e);
}
public void addPet(Player player, Entity pet) {
petToPlayer.put(pet, player);
playerToPets
.computeIfAbsent(player, (key) -> new HashSet<>())
.add(pet);
}
public boolean hasPet(Entity e) {
return petToPlayer.containsKey(e);
}
@ -154,7 +154,7 @@ public class MonsterManager
}
return Collections.emptySet();
}
public void removePets(Player p) {
Set<Entity> pets = playerToPets.remove(p);
if (pets != null) {
@ -162,7 +162,7 @@ public class MonsterManager
pets.clear();
}
}
public void addMount(LivingEntity e) {
mounts.add(e);
}
@ -180,29 +180,29 @@ public class MonsterManager
e.remove();
}
}
public void addSupplier(LivingEntity e, List<ItemStack> drops) {
suppliers.put(e, drops);
}
public List<ItemStack> getLoot(Entity e) {
return suppliers.get(e);
}
public MABoss addBoss(LivingEntity e, double maxHealth) {
MABoss b = new MABoss(e, maxHealth);
bosses.put(e, b);
return b;
}
public MABoss removeBoss(LivingEntity e) {
return bosses.remove(e);
}
public MABoss getBoss(LivingEntity e) {
return bosses.get(e);
}
public Set<LivingEntity> getBossMonsters() {
return bosses.keySet();
}

View File

@ -78,6 +78,7 @@ public enum Msg {
WAVE_BOSS_KILLED("&a%&r killed the boss!"),
WAVE_BOSS_REWARD_EARNED("You earned: &e%"),
WAVE_REWARD("You just earned a reward: &e%&r"),
MISC_REWARD_ADDED("You were just given a reward: &e%&r"),
MISC_LIST_PLAYERS("Live players: &a%&r"),
MISC_LIST_ARENAS("Available arenas: %"),
MISC_COMMAND_NOT_ALLOWED("You can't use that command in the arena!"),
@ -124,4 +125,4 @@ public enum Msg {
}
return yaml;
}
}
}

View File

@ -5,7 +5,6 @@ import com.garbagemule.MobArena.things.Thing;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -14,43 +13,32 @@ import java.util.Set;
public class RewardManager
{
@SuppressWarnings("unused")
private MobArena plugin;
@SuppressWarnings("unused")
private Arena arena;
private Map<Player,List<Thing>> players;
private Set<Player> rewarded;
public RewardManager(Arena arena) {
this.plugin = arena.getPlugin();
this.arena = arena;
this.players = new HashMap<>();
this.rewarded = new HashSet<>();
}
public void reset() {
players.clear();
rewarded.clear();
}
public void addReward(Player p, Thing thing) {
if (!players.containsKey(p)) {
players.put(p, new ArrayList<>());
}
players.get(p).add(thing);
}
public List<Thing> getRewards(Player p) {
List<Thing> rewards = players.get(p);
return (rewards == null ? new ArrayList<>(1) : Collections.unmodifiableList(rewards));
}
public void grantRewards(Player p) {
if (rewarded.contains(p)) return;
List<Thing> rewards = players.get(p);
if (rewards == null) return;
for (Thing reward : rewards) {
if (reward == null) {
continue;
@ -59,4 +47,4 @@ public class RewardManager
}
rewarded.add(p);
}
}
}

View File

@ -20,7 +20,7 @@ public class ScoreboardManager {
private Objective kills;
private Map<Player, Scoreboard> scoreboards;
/**
* Create a new scoreboard for the given arena.
* @param arena an arena
@ -30,7 +30,7 @@ public class ScoreboardManager {
scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
scoreboards = new HashMap<>();
}
/**
* Add a player to the scoreboard by setting the player's scoreboard
* and giving him an initial to-be-reset non-zero score.
@ -44,7 +44,7 @@ public class ScoreboardManager {
player.setScoreboard(scoreboard);
kills.getScore(player.getName()).setScore(8);
}
/**
* Remove a player from the scoreboard by setting the player's scoreboard
* to the main server scoreboard.
@ -108,7 +108,7 @@ public class ScoreboardManager {
fake.setScore(value);
}
}
/**
* Update the scoreboard to display the given wave number.
* @param wave a wave number
@ -116,7 +116,7 @@ public class ScoreboardManager {
void updateWave(int wave) {
kills.setDisplayName(DISPLAY_NAME + wave);
}
/**
* Initialize the scoreboard by resetting the kills objective and
* setting all player scores to 0.
@ -129,7 +129,7 @@ public class ScoreboardManager {
resetKills();
arena.scheduleTask(this::resetPlayerScores, 1);
}
private void resetKills() {
if (kills != null) {
kills.unregister();

View File

@ -1,6 +1,7 @@
package com.garbagemule.MobArena;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
@ -42,11 +43,11 @@ public class SpawnsPets {
}
for (Map.Entry<Material, EntityType> entry : materialToEntity.entrySet()) {
spawnPetsFor(player, arena, entry.getKey(), entry.getValue());
spawnPetsFor(player, arena, entry.getKey(), entry.getValue(), ac.getPetName());
}
}
private void spawnPetsFor(Player player, Arena arena, Material material, EntityType entity) {
private void spawnPetsFor(Player player, Arena arena, Material material, EntityType entity, String petName) {
PlayerInventory inv = player.getInventory();
int index = inv.first(material);
@ -57,8 +58,14 @@ public class SpawnsPets {
int amount = inv.getItem(index).getAmount();
for (int i = 0; i < amount; i++) {
Entity pet = arena.getWorld().spawn(player.getLocation(), entity.getEntityClass());
pet.setCustomName(player.getDisplayName() + "'s pet");
pet.setCustomNameVisible(true);
if (!petName.isEmpty()) {
String resolved = petName
.replace("<player-name>", player.getName())
.replace("<display-name>", player.getDisplayName());
String colorized = ChatColor.translateAlternateColorCodes('&', resolved);
pet.setCustomName(colorized);
pet.setCustomNameVisible(true);
}
if (pet instanceof Tameable) {
Tameable tameable = (Tameable) pet;
tameable.setTamed(true);

View File

@ -0,0 +1,15 @@
package com.garbagemule.MobArena.announce;
import org.bukkit.entity.Player;
public interface Announcer {
/**
* Announce the given message to the given player.
*
* @param player a player to send a message to, non-null
* @param message the message to send, non-null
*/
void announce(Player player, String message);
}

View File

@ -0,0 +1,19 @@
package com.garbagemule.MobArena.announce;
import com.garbagemule.MobArena.Messenger;
import org.bukkit.entity.Player;
public class MessengerAnnouncer implements Announcer {
private final Messenger messenger;
public MessengerAnnouncer(Messenger messenger) {
this.messenger = messenger;
}
@Override
public void announce(Player player, String message) {
messenger.tell(player, message);
}
}

View File

@ -0,0 +1,22 @@
package com.garbagemule.MobArena.announce;
import org.bukkit.entity.Player;
public class TitleAnnouncer implements Announcer {
private final int fadeIn;
private final int stay;
private final int fadeOut;
public TitleAnnouncer(int fadeIn, int stay, int fadeOut) {
this.fadeIn = fadeIn;
this.stay = stay;
this.fadeOut = fadeOut;
}
@Override
public void announce(Player player, String message) {
player.sendTitle(" ", message, fadeIn, stay, fadeOut);
}
}

View File

@ -4,6 +4,7 @@ import com.garbagemule.MobArena.ConfigError;
import com.garbagemule.MobArena.Messenger;
import com.garbagemule.MobArena.MobArena;
import com.garbagemule.MobArena.Msg;
import com.garbagemule.MobArena.commands.admin.AddRewardCommand;
import com.garbagemule.MobArena.commands.admin.DisableCommand;
import com.garbagemule.MobArena.commands.admin.EnableCommand;
import com.garbagemule.MobArena.commands.admin.ForceCommand;
@ -14,12 +15,15 @@ import com.garbagemule.MobArena.commands.setup.AutoGenerateCommand;
import com.garbagemule.MobArena.commands.setup.CheckDataCommand;
import com.garbagemule.MobArena.commands.setup.CheckSpawnsCommand;
import com.garbagemule.MobArena.commands.setup.ClassChestCommand;
import com.garbagemule.MobArena.commands.setup.DeleteItemCommand;
import com.garbagemule.MobArena.commands.setup.EditArenaCommand;
import com.garbagemule.MobArena.commands.setup.ListClassesCommand;
import com.garbagemule.MobArena.commands.setup.LoadItemCommand;
import com.garbagemule.MobArena.commands.setup.RemoveArenaCommand;
import com.garbagemule.MobArena.commands.setup.RemoveContainerCommand;
import com.garbagemule.MobArena.commands.setup.RemoveLeaderboardCommand;
import com.garbagemule.MobArena.commands.setup.RemoveSpawnpointCommand;
import com.garbagemule.MobArena.commands.setup.SaveItemCommand;
import com.garbagemule.MobArena.commands.setup.SettingCommand;
import com.garbagemule.MobArena.commands.setup.SetupCommand;
import com.garbagemule.MobArena.commands.user.ArenaListCommand;
@ -53,7 +57,7 @@ public class CommandHandler implements CommandExecutor, TabCompleter
private Messenger fallbackMessenger;
private Map<String,Command> commands;
public CommandHandler(MobArena plugin) {
this.plugin = plugin;
this.fallbackMessenger = new Messenger("&a[MobArena] ");
@ -98,7 +102,7 @@ public class CommandHandler implements CommandExecutor, TabCompleter
// Get all commands that match the base.
List<Command> matches = getMatchingCommands(base);
// If there's more than one match, display them.
if (matches.size() > 1) {
am.getGlobalMessenger().tell(sender, Msg.MISC_MULTIPLE_MATCHES);
@ -107,29 +111,29 @@ public class CommandHandler implements CommandExecutor, TabCompleter
}
return true;
}
// If there are no matches at all, notify.
if (matches.size() == 0) {
am.getGlobalMessenger().tell(sender, Msg.MISC_NO_MATCHES);
return true;
}
// Grab the only match.
Command command = matches.get(0);
CommandInfo info = command.getClass().getAnnotation(CommandInfo.class);
// First check if the sender has permission.
if (!sender.hasPermission(info.permission())) {
am.getGlobalMessenger().tell(sender, Msg.MISC_NO_ACCESS);
return true;
}
// Check if the last argument is a ?, in which case, display usage and description
if (last.equals("?") || last.equals("help")) {
showUsage(command, sender, true);
return true;
}
// Otherwise, execute the command!
String[] params = trimFirstArg(args);
if (!command.execute(am, sender, params)) {
@ -162,7 +166,7 @@ public class CommandHandler implements CommandExecutor, TabCompleter
}
return true;
}
/**
* Get all commands that match a given string.
* @param arg the given string
@ -170,17 +174,17 @@ public class CommandHandler implements CommandExecutor, TabCompleter
*/
private List<Command> getMatchingCommands(String arg) {
List<Command> result = new ArrayList<>();
// Grab the commands that match the argument.
for (Entry<String,Command> entry : commands.entrySet()) {
if (arg.matches(entry.getKey())) {
result.add(entry.getValue());
}
}
return result;
}
/**
* Show the usage and description messages of a command to a player.
* The usage will only be shown, if the player has permission for the command.
@ -193,7 +197,7 @@ public class CommandHandler implements CommandExecutor, TabCompleter
sender.sendMessage((prefix ? "Usage: " : "") + info.usage() + " " + ChatColor.YELLOW + info.desc());
}
/**
* Remove the first argument of a string. This is because the very first
* element of the arguments array will be the command itself.
@ -203,7 +207,7 @@ public class CommandHandler implements CommandExecutor, TabCompleter
private String[] trimFirstArg(String[] args) {
return Arrays.copyOfRange(args, 1, args.length);
}
/**
* List all the available MobArena commands for the CommandSender.
* @param sender a player or the console
@ -240,7 +244,7 @@ public class CommandHandler implements CommandExecutor, TabCompleter
if (setup.length() > 0) am.getGlobalMessenger().tell(sender, "Setup commands: " + setup.toString());
}
}
@Override
public List<String> onTabComplete(CommandSender sender, org.bukkit.command.Command bcmd, String alias, String[] args) {
// Only players can tab complete
@ -302,7 +306,7 @@ public class CommandHandler implements CommandExecutor, TabCompleter
*/
private void registerCommands() {
commands = new LinkedHashMap<>();
// mobarena.use
register(JoinCommand.class);
register(LeaveCommand.class);
@ -319,7 +323,8 @@ public class CommandHandler implements CommandExecutor, TabCompleter
register(ForceCommand.class);
register(KickCommand.class);
register(RestoreCommand.class);
register(AddRewardCommand.class);
// mobarena.setup
register(SetupCommand.class);
register(SettingCommand.class);
@ -338,8 +343,12 @@ public class CommandHandler implements CommandExecutor, TabCompleter
register(RemoveLeaderboardCommand.class);
register(AutoGenerateCommand.class);
register(SaveItemCommand.class);
register(DeleteItemCommand.class);
register(LoadItemCommand.class);
}
/**
* Register a command.
* The Command's CommandInfo annotation is queried to find its pattern
@ -347,14 +356,34 @@ public class CommandHandler implements CommandExecutor, TabCompleter
* @param c a Command
*/
public void register(Class<? extends Command> c) {
CommandInfo info = c.getAnnotation(CommandInfo.class);
if (info == null) return;
try {
commands.put(info.pattern(), c.newInstance());
}
catch (Exception e) {
e.printStackTrace();
Command command = c.newInstance();
register(command);
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("Failed to instantiate Command class: " + c.getName(), e);
}
}
/**
* Register a command instance.
* <p>
* Adds the given command to MobArena's internal command handler as a
* subcommand, overwriting any existing subcommand mappings. This means
* that the method is safe to call on reloads as long as a reload does
* not change the pattern of a registered command.
*
* @param command the Command instance to register
* @throws IllegalArgumentException if the CommandInfo annotation is
* missing from the class of the Command instance
*/
public void register(Command command) {
Class<?> cls = command.getClass();
CommandInfo info = cls.getAnnotation(CommandInfo.class);
if (info == null) {
throw new IllegalArgumentException("Missing CommandInfo annotation on class " + cls.getName());
}
commands.put(info.pattern(), command);
}
}

View File

@ -10,24 +10,24 @@ public @interface CommandInfo
* The actual name of the command. Not really used anywhere.
*/
String name();
/**
* A regex pattern that allows minor oddities and alternatives to the command name.
*/
String pattern();
/**
* The usage message, i.e. how the command should be used.
*/
String usage();
/**
* A description of what the command does.
*/
String desc();
/**
* The permission required to execute this command.
*/
String permission();
}
}

View File

@ -34,7 +34,7 @@ public class Commands
public static boolean isPlayer(CommandSender sender) {
return (sender instanceof Player);
}
public static Arena getArenaToJoinOrSpec(ArenaMaster am, Player p, String arg1) {
// Check if MobArena is enabled first.
if (!am.isEnabled()) {
@ -48,17 +48,17 @@ public class Commands
am.getGlobalMessenger().tell(p, Msg.JOIN_NO_PERMISSION);
return null;
}
// Then check if we have any enabled arenas.
arenas = am.getEnabledArenas(arenas);
if (arenas.isEmpty()) {
am.getGlobalMessenger().tell(p, Msg.JOIN_NOT_ENABLED);
return null;
}
// The arena to join.
Arena arena = null;
// Branch on whether there's an argument or not.
if (arg1 != null) {
arena = am.getArenaWithName(arg1);
@ -66,7 +66,7 @@ public class Commands
am.getGlobalMessenger().tell(p, Msg.ARENA_DOES_NOT_EXIST);
return null;
}
if (!arenas.contains(arena)) {
am.getGlobalMessenger().tell(p, Msg.JOIN_ARENA_NOT_ENABLED);
return null;
@ -80,18 +80,18 @@ public class Commands
}
arena = arenas.get(0);
}
// If player is in a boat/minecart, eject!
if (p.isInsideVehicle()) {
p.leaveVehicle();
}
// If player is in a bed, unbed!
if (p.isSleeping()) {
p.kickPlayer("Banned for life... Nah, just don't join from a bed ;)");
return null;
}
return arena;
}
}

View File

@ -0,0 +1,91 @@
package com.garbagemule.MobArena.commands.admin;
import com.garbagemule.MobArena.Msg;
import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.things.ThingPicker;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@CommandInfo(
name = "addreward",
pattern = "addreward",
usage = "/ma addreward <player> <thing>",
desc = "add a reward to an arena player's rewards list",
permission = "mobarena.admin.addreward"
)
public class AddRewardCommand implements Command {
@Override
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
if (args.length < 2) {
return false;
}
Player player = am.getPlugin().getServer().getPlayer(args[0]);
if (player == null) {
am.getGlobalMessenger().tell(sender, "Player not found.");
return true;
}
Arena arena = am.getArenaWithPlayer(player);
if (arena == null) {
am.getGlobalMessenger().tell(sender, "That player is not in an arena.");
return true;
}
if (!arena.isRunning()) {
am.getGlobalMessenger().tell(sender, "That arena is not running.");
return true;
}
if (!arena.getPlayersInArena().contains(player)) {
am.getGlobalMessenger().tell(sender, "That player is not an arena player.");
return true;
}
String[] rest = Arrays.copyOfRange(args, 1, args.length);
String input = String.join(" ", rest);
Thing thing;
try {
ThingPicker picker = am.getPlugin().getThingPickerManager().parse(input);
thing = picker.pick();
} catch (Exception e) {
am.getGlobalMessenger().tell(sender, e.getMessage());
return true;
}
arena.getRewardManager().addReward(player, thing);
arena.getMessenger().tell(player, Msg.MISC_REWARD_ADDED, thing.toString());
String msg = "Added " + ChatColor.YELLOW + thing + ChatColor.RESET + " to " + ChatColor.YELLOW + player.getName() + "'s" + ChatColor.RESET + " rewards.";
am.getGlobalMessenger().tell(sender, msg);
return true;
}
@Override
public List<String> tab(ArenaMaster am, Player player, String... args) {
if (args.length > 1) {
return Collections.emptyList();
}
String prefix = args[0].toLowerCase();
List<Player> players = am.getAllPlayers();
return players.stream()
.filter(p -> p.getName().toLowerCase().startsWith(prefix))
.map(Player::getName)
.collect(Collectors.toList());
}
}

View File

@ -26,14 +26,14 @@ public class DisableCommand implements Command
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
// Grab the argument, if any.
String arg1 = (args.length > 0 ? args[0] : "");
if (arg1.equals("all")) {
for (Arena arena : am.getArenas()) {
disable(arena, sender);
}
return true;
}
if (!arg1.equals("")) {
Arena arena = am.getArenaWithName(arg1);
if (arena == null) {
@ -43,13 +43,13 @@ public class DisableCommand implements Command
disable(arena, sender);
return true;
}
am.setEnabled(false);
am.saveConfig();
am.getGlobalMessenger().tell(sender, "MobArena " + ChatColor.RED + "disabled");
return true;
}
private void disable(Arena arena, CommandSender sender) {
arena.setEnabled(false);
arena.getPlugin().saveConfig();
@ -67,8 +67,8 @@ public class DisableCommand implements Command
List<Arena> arenas = am.getArenas();
return arenas.stream()
.filter(arena -> arena.configName().toLowerCase().startsWith(prefix))
.map(Arena::configName)
.filter(arena -> arena.getSlug().startsWith(prefix))
.map(Arena::getSlug)
.collect(Collectors.toList());
}
}

View File

@ -26,14 +26,14 @@ public class EnableCommand implements Command
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
// Grab the argument, if any.
String arg1 = (args.length > 0 ? args[0] : "");
if (arg1.equals("all")) {
for (Arena arena : am.getArenas()) {
enable(arena, sender);
}
return true;
}
if (!arg1.equals("")) {
Arena arena = am.getArenaWithName(arg1);
if (arena == null) {
@ -43,13 +43,13 @@ public class EnableCommand implements Command
enable(arena, sender);
return true;
}
am.setEnabled(true);
am.saveConfig();
am.getGlobalMessenger().tell(sender, "MobArena " + ChatColor.GREEN + "enabled");
return true;
}
private void enable(Arena arena, CommandSender sender) {
arena.setEnabled(true);
arena.getPlugin().saveConfig();
@ -67,8 +67,8 @@ public class EnableCommand implements Command
List<Arena> arenas = am.getArenas();
return arenas.stream()
.filter(arena -> arena.configName().toLowerCase().startsWith(prefix))
.map(Arena::configName)
.filter(arena -> arena.getSlug().startsWith(prefix))
.map(Arena::getSlug)
.collect(Collectors.toList());
}
}

View File

@ -30,7 +30,7 @@ public class ForceCommand implements Command
// Grab the argument, if any.
String arg1 = (args.length > 0 ? args[0] : "");
String arg2 = (args.length > 1 ? args[1] : "");
if (arg1.equals("end")) {
// With no arguments, end all.
if (arg2.equals("")) {
@ -41,46 +41,46 @@ public class ForceCommand implements Command
am.resetArenaMap();
return true;
}
// Otherwise, grab the arena in question.
Arena arena = am.getArenaWithName(arg2);
if (arena == null) {
am.getGlobalMessenger().tell(sender, Msg.ARENA_DOES_NOT_EXIST);
return true;
}
if (arena.getAllPlayers().isEmpty()) {
am.getGlobalMessenger().tell(sender, Msg.FORCE_END_EMPTY);
return true;
}
// And end it!
arena.forceEnd();
am.getGlobalMessenger().tell(sender, Msg.FORCE_END_ENDED);
return true;
}
if (arg1.equals("start")) {
// Require argument.
if (arg2.equals("")) return false;
// Grab the arena.
Arena arena = am.getArenaWithName(arg2);
if (arena == null) {
am.getGlobalMessenger().tell(sender, Msg.ARENA_DOES_NOT_EXIST);
return true;
}
if (arena.isRunning()) {
am.getGlobalMessenger().tell(sender, Msg.FORCE_START_RUNNING);
return true;
}
if (arena.getReadyPlayersInLobby().isEmpty()) {
am.getGlobalMessenger().tell(sender, Msg.FORCE_START_NOT_READY);
return true;
}
// And start it!
arena.forceStart();
am.getGlobalMessenger().tell(sender, Msg.FORCE_START_STARTED);
@ -117,9 +117,9 @@ public class ForceCommand implements Command
List<Arena> arenas = am.getArenas();
return arenas.stream()
.filter(arena -> arena.configName().toLowerCase().startsWith(prefix))
.filter(arena -> arena.getSlug().startsWith(prefix))
.filter(arena -> start != arena.isRunning())
.map(Arena::configName)
.map(Arena::getSlug)
.collect(Collectors.toList());
}
}

View File

@ -24,16 +24,16 @@ public class KickCommand implements Command
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
// Require a player name
if (args.length != 1) return false;
Arena arena = am.getArenaWithPlayer(args[0]);
if (arena == null) {
am.getGlobalMessenger().tell(sender, "That player is not in an arena.");
return true;
}
// Grab the Player object.
Player bp = am.getPlugin().getServer().getPlayer(args[0]);
// Force leave.
arena.playerLeave(bp);
am.getGlobalMessenger().tell(sender, "Player '" + args[0] + "' was kicked from arena '" + arena.configName() + "'.");

View File

@ -25,7 +25,7 @@ public class RestoreCommand implements Command
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
// Require a player name
if (args.length != 1) return false;
Player player = am.getPlugin().getServer().getPlayer(args[0]);
if (player == null) {
am.getGlobalMessenger().tell(sender, "Player not found.");
@ -35,7 +35,7 @@ public class RestoreCommand implements Command
am.getGlobalMessenger().tell(sender, "Player is currently in an arena.");
return true;
}
if (InventoryManager.restoreFromFile(am.getPlugin(), player)) {
am.getGlobalMessenger().tell(sender, "Restored " + args[0] + "'s inventory!");
} else {

View File

@ -6,9 +6,13 @@ import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.commands.Commands;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.util.Slugs;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.stream.Collectors;
@CommandInfo(
name = "addarena",
pattern = "(add|new)arena",
@ -26,18 +30,20 @@ public class AddArenaCommand implements Command
}
// Require an arena name
if (args.length != 1) return false;
if (args.length < 1) return false;
// Unwrap the sender.
Player p = Commands.unwrap(sender);
Arena arena = am.getArenaWithName(args[0]);
String name = String.join(" ", args);
String slug = Slugs.create(name);
Arena arena = am.getArenaWithName(slug);
if (arena != null) {
am.getGlobalMessenger().tell(sender, "An arena with that name already exists.");
return true;
}
am.createArenaNode(args[0], p.getWorld());
am.getGlobalMessenger().tell(sender, "New arena with name '" + args[0] + "' created!");
am.createArenaNode(name, p.getWorld());
am.getGlobalMessenger().tell(sender, "New arena with name '" + name + "' created!");
return true;
}
}

View File

@ -7,6 +7,7 @@ import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.commands.Commands;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.util.Slugs;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@ -27,24 +28,26 @@ public class AutoGenerateCommand implements Command
}
// Require an arena name
if (args.length != 1) return false;
if (args.length < 1) return false;
// Unwrap the sender.
Player p = Commands.unwrap(sender);
// Check if arena already exists.
Arena arena = am.getArenaWithName(args[0]);
String name = String.join(" ", args);
String slug = Slugs.create(name);
Arena arena = am.getArenaWithName(slug);
if (arena != null) {
am.getGlobalMessenger().tell(sender, "An arena with that name already exists.");
return true;
}
if (!MAUtils.doooooItHippieMonster(p.getLocation(), 13, args[0], am.getPlugin())) {
if (!MAUtils.doooooItHippieMonster(p.getLocation(), 13, name, am.getPlugin())) {
am.getGlobalMessenger().tell(sender, "Could not auto-generate arena.");
return true;
}
am.getGlobalMessenger().tell(sender, "Arena with name '" + args[0] + "' generated.");
am.getGlobalMessenger().tell(sender, "Arena with name '" + name + "' generated.");
return true;
}
}

View File

@ -8,6 +8,7 @@ import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.commands.Commands;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.util.Slugs;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
@ -37,7 +38,8 @@ public class ClassChestCommand implements Command {
// Require a class name
if (args.length != 1) return false;
ArenaClass ac = am.getClasses().get(args[0].toLowerCase());
String slug = Slugs.create(args[0]);
ArenaClass ac = am.getClasses().get(slug);
if (ac == null) {
am.getGlobalMessenger().tell(sender, "Class not found.");
return true;
@ -69,13 +71,13 @@ public class ClassChestCommand implements Command {
return Collections.emptyList();
}
String prefix = args[0].toLowerCase();
String prefix = Slugs.create(args[0]);
Collection<ArenaClass> classes = am.getClasses().values();
return classes.stream()
.filter(cls -> cls.getConfigName().toLowerCase().startsWith(prefix))
.map(ArenaClass::getConfigName)
.filter(cls -> cls.getSlug().startsWith(prefix))
.map(ArenaClass::getSlug)
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,60 @@
package com.garbagemule.MobArena.commands.setup;
import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.items.SavedItemsManager;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@CommandInfo(
name = "delete-item",
pattern = "delete(-)?item",
usage = "/ma delete-item <identifier>",
desc = "delete the item with the given identifier",
permission = "mobarena.setup.deleteitem"
)
public class DeleteItemCommand implements Command {
@Override
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
if (args.length != 1) {
return false;
}
String key = args[0];
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
try {
items.deleteItem(key);
} catch (Exception e) {
am.getGlobalMessenger().tell(sender, "Couldn't delete " + ChatColor.YELLOW + key + ChatColor.RESET + ", because: " + ChatColor.RED + e.getMessage());
return true;
}
am.getGlobalMessenger().tell(sender, "Saved item " + ChatColor.YELLOW + key + ChatColor.RESET + " deleted.");
return true;
}
@Override
public List<String> tab(ArenaMaster am, Player player, String... args) {
if (args.length > 1) {
return Collections.emptyList();
}
String prefix = args[0].toLowerCase();
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
List<String> keys = items.getKeys();
return keys.stream()
.filter(key -> key.startsWith(prefix))
.collect(Collectors.toList());
}
}

View File

@ -69,8 +69,8 @@ public class EditArenaCommand implements Command
List<Arena> arenas = am.getArenas();
return arenas.stream()
.filter(arena -> arena.configName().toLowerCase().startsWith(prefix))
.map(Arena::configName)
.filter(arena -> arena.getSlug().startsWith(prefix))
.map(Arena::getSlug)
.collect(Collectors.toList());
}
}

View File

@ -24,7 +24,7 @@ public class ListClassesCommand implements Command
am.getGlobalMessenger().tell(sender, "<none>");
return true;
}
for (String c : classes) {
am.getGlobalMessenger().tell(sender, "- " + c);
}

View File

@ -0,0 +1,70 @@
package com.garbagemule.MobArena.commands.setup;
import com.garbagemule.MobArena.Msg;
import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.commands.Commands;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.items.SavedItemsManager;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@CommandInfo(
name = "load-item",
pattern = "load(-)?item",
usage = "/ma load-item <identifier>",
desc = "load the item saved by the given identifier into your hand",
permission = "mobarena.setup.loaditem"
)
public class LoadItemCommand implements Command {
@Override
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
if (!Commands.isPlayer(sender)) {
am.getGlobalMessenger().tell(sender, Msg.MISC_NOT_FROM_CONSOLE);
return true;
}
if (args.length != 1) {
return false;
}
String key = args[0];
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
ItemStack stack = items.getItem(key);
if (stack == null) {
am.getGlobalMessenger().tell(sender, "No saved item with identifier " + ChatColor.YELLOW + key + ChatColor.RESET + " found.");
return true;
}
Player player = Commands.unwrap(sender);
player.getInventory().setItemInMainHand(stack);
am.getGlobalMessenger().tell(sender, "Saved item " + ChatColor.YELLOW + key + ChatColor.RESET + " loaded.");
return true;
}
@Override
public List<String> tab(ArenaMaster am, Player player, String... args) {
if (args.length > 1) {
return Collections.emptyList();
}
String prefix = args[0].toLowerCase();
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
List<String> keys = items.getKeys();
return keys.stream()
.filter(key -> key.startsWith(prefix))
.collect(Collectors.toList());
}
}

View File

@ -24,12 +24,12 @@ public class RemoveArenaCommand implements Command
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
// Require an arena name
if (args.length != 1) return false;
if (am.getArenas().size() == 1) {
am.getGlobalMessenger().tell(sender, "At least one arena must exist.");
return true;
}
Arena arena = am.getArenaWithName(args[0]);
if (arena == null) {
am.getGlobalMessenger().tell(sender, "There is no arena with that name.");
@ -51,8 +51,8 @@ public class RemoveArenaCommand implements Command
List<Arena> arenas = am.getArenas();
return arenas.stream()
.filter(arena -> arena.configName().toLowerCase().startsWith(prefix))
.map(Arena::configName)
.filter(arena -> arena.getSlug().startsWith(prefix))
.map(Arena::getSlug)
.collect(Collectors.toList());
}
}

View File

@ -41,4 +41,4 @@ public class RemoveLeaderboardCommand implements Command
}
return true;
}
}
}

View File

@ -0,0 +1,80 @@
package com.garbagemule.MobArena.commands.setup;
import com.garbagemule.MobArena.Msg;
import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.commands.Commands;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.items.SavedItemsManager;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@CommandInfo(
name = "save-item",
pattern = "save(-)?item",
usage = "/ma save-item <identifier>",
desc = "save the currently held item for use in the config-file",
permission = "mobarena.setup.saveitem"
)
public class SaveItemCommand implements Command {
@Override
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
if (!Commands.isPlayer(sender)) {
am.getGlobalMessenger().tell(sender, Msg.MISC_NOT_FROM_CONSOLE);
return true;
}
if (args.length != 1) {
return false;
}
String key = args[0];
if (!key.matches("^[\\p{IsAlphabetic}\\d_]+$")) {
am.getGlobalMessenger().tell(sender, "The identifier must contain only letters, numbers, and underscores");
return true;
}
Player player = Commands.unwrap(sender);
ItemStack stack = player.getInventory().getItemInMainHand();
if (stack.getType() == Material.AIR) {
am.getGlobalMessenger().tell(sender, "You must be holding an item.");
return true;
}
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
try {
items.saveItem(key, stack);
} catch (Exception e) {
am.getGlobalMessenger().tell(sender, "Couldn't save " + ChatColor.YELLOW + key + ChatColor.RESET + ", because: " + ChatColor.RED + e.getMessage());
return true;
}
am.getGlobalMessenger().tell(sender, "Item saved as " + ChatColor.YELLOW + key + ChatColor.RESET + ".");
return true;
}
@Override
public List<String> tab(ArenaMaster am, Player player, String... args) {
if (args.length > 1) {
return Collections.emptyList();
}
String prefix = args[0].toLowerCase();
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
List<String> keys = items.getKeys();
return keys.stream()
.filter(key -> key.startsWith(prefix))
.collect(Collectors.toList());
}
}

View File

@ -1,5 +1,6 @@
package com.garbagemule.MobArena.commands.setup;
import com.garbagemule.MobArena.ConfigError;
import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.framework.Arena;
@ -89,7 +90,13 @@ public class SettingCommand implements Command {
// Save config-file and reload arena
am.saveConfig();
am.reloadArena(args[0]);
try {
am.reloadArena(args[0]);
} catch (ConfigError e) {
am.getGlobalMessenger().tell(sender, "Failed to reload arena after changing setting, reason:\n" + ChatColor.RED + e.getMessage());
am.getGlobalMessenger().tell(sender, "Fix the error in your config-file, then run " + ChatColor.YELLOW + "/ma reload");
return true;
}
// Notify the sender
StringBuilder buffy = new StringBuilder();
@ -113,9 +120,9 @@ public class SettingCommand implements Command {
String prefix = args[0].toLowerCase();
return arenas.stream()
.filter(arena -> arena.configName().toLowerCase().startsWith(prefix))
.filter(arena -> arena.getSlug().startsWith(prefix))
.filter(arena -> !arena.isRunning())
.map(Arena::configName)
.map(Arena::getSlug)
.collect(Collectors.toList());
}

View File

@ -101,8 +101,8 @@ public class SetupCommand implements Command, Listener {
List<Arena> arenas = am.getArenas();
return arenas.stream()
.filter(arena -> arena.configName().toLowerCase().startsWith(prefix))
.map(Arena::configName)
.filter(arena -> arena.getSlug().startsWith(prefix))
.map(Arena::getSlug)
.collect(Collectors.toList());
}

View File

@ -24,14 +24,14 @@ public class ArenaListCommand implements Command
@Override
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
List<Arena> arenas;
if (Commands.isPlayer(sender)) {
Player p = Commands.unwrap(sender);
arenas = am.getPermittedArenas(p);
arenas = am.getPermittedArenas(p);
} else {
arenas = am.getArenas();
}
String list = MAUtils.listToString(arenas, am.getPlugin());
am.getGlobalMessenger().tell(sender, Msg.MISC_LIST_ARENAS.format(list));
return true;

View File

@ -29,7 +29,7 @@ public class JoinCommand implements Command
am.getGlobalMessenger().tell(sender, Msg.MISC_NOT_FROM_CONSOLE);
return true;
}
// Unwrap the sender, grab the argument, if any.
Player p = Commands.unwrap(sender);
String arg1 = (args.length > 0 ? args[0] : null);
@ -39,7 +39,7 @@ public class JoinCommand implements Command
if (toArena == null || !canJoin(p, toArena)) {
return true;
}
// Join the arena!
int seconds = toArena.getSettings().getInt("join-interrupt-timer", 0);
if (seconds > 0) {
@ -94,9 +94,9 @@ public class JoinCommand implements Command
return arenas.stream()
.filter(Arena::isEnabled)
.filter(arena -> arena.configName().toLowerCase().startsWith(prefix))
.filter(arena -> arena.getSlug().startsWith(prefix))
.filter(arena -> arena.getRegion().isSetup())
.map(Arena::configName)
.map(Arena::getSlug)
.collect(Collectors.toList());
}
}

View File

@ -24,11 +24,11 @@ public class LeaveCommand implements Command
am.getGlobalMessenger().tell(sender, Msg.MISC_NOT_FROM_CONSOLE);
return true;
}
// Unwrap the sender.
Player p = Commands.unwrap(sender);
Arena arena = am.getArenaWithPlayer(p);
Arena arena = am.getArenaWithPlayer(p);
if (arena == null) {
arena = am.getArenaWithSpectator(p);
if (arena == null) {
@ -36,7 +36,7 @@ public class LeaveCommand implements Command
return true;
}
}
if (arena.playerLeave(p)) {
arena.getMessenger().tell(p, Msg.LEAVE_PLAYER_LEFT);
}

View File

@ -23,10 +23,10 @@ public class NotReadyCommand implements Command
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
// Grab the argument, if any
String arg1 = (args.length > 0 ? args[0] : "");
// The arena to query.
Arena arena = null;
if (!arg1.equals("")) {
arena = am.getArenaWithName(arg1);
if (arena == null) {
@ -36,7 +36,7 @@ public class NotReadyCommand implements Command
} else if (Commands.isPlayer(sender)) {
Player p = Commands.unwrap(sender);
arena = am.getArenaWithPlayer(p);
if (arena == null) {
am.getGlobalMessenger().tell(sender, Msg.LEAVE_NOT_PLAYING);
return true;
@ -44,7 +44,7 @@ public class NotReadyCommand implements Command
} else {
return false;
}
String list = MAUtils.listToString(arena.getNonreadyPlayers(), am.getPlugin());
arena.getMessenger().tell(sender, Msg.MISC_LIST_PLAYERS.format(list));
return true;

View File

@ -10,6 +10,7 @@ import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.util.ClassChests;
import com.garbagemule.MobArena.util.Slugs;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@ -36,7 +37,7 @@ public class PickClassCommand implements Command
// Require a class name
if (args.length != 1) return false;
// Unwrap the sender
Player p = Commands.unwrap(sender);
@ -51,15 +52,15 @@ public class PickClassCommand implements Command
}
// Grab the ArenaClass, if it exists
String lowercase = args[0].toLowerCase();
ArenaClass ac = am.getClasses().get(lowercase);
String slug = Slugs.create(args[0]);
ArenaClass ac = am.getClasses().get(slug);
if (ac == null) {
arena.getMessenger().tell(p, Msg.LOBBY_NO_SUCH_CLASS, lowercase);
arena.getMessenger().tell(p, Msg.LOBBY_NO_SUCH_CLASS, slug);
return true;
}
// Check for permission.
if (!ac.hasPermission(p) && !lowercase.equals("random")) {
if (!ac.hasPermission(p) && !slug.equals("random")) {
arena.getMessenger().tell(p, Msg.LOBBY_CLASS_PERMISSION);
return true;
}
@ -88,15 +89,15 @@ public class PickClassCommand implements Command
clm.playerLeftClass(oldAC, p);
clm.playerPickedClass(ac, p);
if (!lowercase.equalsIgnoreCase("random")) {
if (!slug.equalsIgnoreCase("random")) {
if (arena.getSettings().getBoolean("use-class-chests", false)) {
if (ClassChests.assignClassFromStoredClassChest(arena, p, ac)) {
return true;
}
// No linked chest? Fall through to config-file
}
arena.assignClass(p, lowercase);
arena.getMessenger().tell(p, Msg.LOBBY_CLASS_PICKED, arena.getClasses().get(lowercase).getConfigName());
arena.assignClass(p, slug);
arena.getMessenger().tell(p, Msg.LOBBY_CLASS_PICKED, arena.getClasses().get(slug).getConfigName());
if (price != null) {
arena.getMessenger().tell(p, Msg.LOBBY_CLASS_PRICE, price.toString());
}
@ -118,9 +119,9 @@ public class PickClassCommand implements Command
Collection<ArenaClass> classes = am.getClasses().values();
return classes.stream()
.filter(cls -> cls.getConfigName().toLowerCase().startsWith(prefix))
.filter(cls -> cls.getSlug().startsWith(prefix))
.filter(cls -> cls.hasPermission(player))
.map(ArenaClass::getConfigName)
.map(ArenaClass::getSlug)
.collect(Collectors.toList());
}
}

View File

@ -14,7 +14,7 @@ import java.util.List;
@CommandInfo(
name = "playerlist",
pattern = "player.*|listp.*",
pattern = "players|playerlist|player-list",
usage = "/ma players (<arena>)",
desc = "lists players in an arena",
permission = "mobarena.use.playerlist"
@ -25,29 +25,29 @@ public class PlayerListCommand implements Command
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
// Grab the argument, if any.
String arg1 = (args.length > 0 ? args[0] : "");
String list = null;
if (!arg1.equals("")) {
Arena arena = am.getArenaWithName(arg1);
if (arena == null) {
am.getGlobalMessenger().tell(sender, Msg.ARENA_DOES_NOT_EXIST);
return false;
}
list = MAUtils.listToString(arena.getPlayersInArena(), am.getPlugin());
} else {
StringBuilder buffy = new StringBuilder();
List<Player> players = new LinkedList<>();
for (Arena arena : am.getArenas()) {
players.addAll(arena.getPlayersInArena());
}
buffy.append(MAUtils.listToString(players, am.getPlugin()));
list = buffy.toString();
}
am.getGlobalMessenger().tell(sender, Msg.MISC_LIST_PLAYERS.format(list));
return true;
}

View File

@ -29,11 +29,11 @@ public class SpecCommand implements Command
am.getGlobalMessenger().tell(sender, Msg.MISC_NOT_FROM_CONSOLE);
return false;
}
// Unwrap the sender, grab the argument, if any.
Player p = Commands.unwrap(sender);
String arg1 = (args.length > 0 ? args[0] : null);
// Run some rough sanity checks, and grab the arena to spec.
Arena toArena = Commands.getArenaToJoinOrSpec(am, p, arg1);
if (toArena == null || !canSpec(p, toArena)) {
@ -88,9 +88,9 @@ public class SpecCommand implements Command
return arenas.stream()
.filter(Arena::isEnabled)
.filter(arena -> arena.configName().toLowerCase().startsWith(prefix))
.filter(arena -> arena.getSlug().startsWith(prefix))
.filter(arena -> arena.getRegion().isSetup())
.map(Arena::configName)
.map(Arena::getSlug)
.collect(Collectors.toList());
}
}

View File

@ -10,12 +10,12 @@ public class ArenaEndEvent extends Event implements Cancellable
private static final HandlerList handlers = new HandlerList();
private Arena arena;
private boolean cancelled;
public ArenaEndEvent(Arena arena) {
this.arena = arena;
this.cancelled = false;
}
public Arena getArena() {
return arena;
}
@ -29,12 +29,12 @@ public class ArenaEndEvent extends Event implements Cancellable
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
}

View File

@ -11,17 +11,17 @@ public class ArenaPlayerDeathEvent extends Event
private Player player;
private Arena arena;
private boolean last;
public ArenaPlayerDeathEvent(Player player, Arena arena, boolean last) {
this.player = player;
this.arena = arena;
this.last = last;
}
public Player getPlayer() {
return player;
}
public Arena getArena() {
return arena;
}
@ -29,12 +29,12 @@ public class ArenaPlayerDeathEvent extends Event
public boolean wasLastPlayerStanding() {
return last;
}
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
}

View File

@ -12,17 +12,17 @@ public class ArenaPlayerJoinEvent extends Event implements Cancellable
private Player player;
private Arena arena;
private boolean cancelled;
public ArenaPlayerJoinEvent(Player player, Arena arena) {
this.player = player;
this.arena = arena;
this.cancelled = false;
}
public Player getPlayer() {
return player;
}
public Arena getArena() {
return arena;
}
@ -36,12 +36,12 @@ public class ArenaPlayerJoinEvent extends Event implements Cancellable
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
}

View File

@ -12,17 +12,17 @@ public class ArenaPlayerLeaveEvent extends Event implements Cancellable
private Player player;
private Arena arena;
private boolean cancelled;
public ArenaPlayerLeaveEvent(Player player, Arena arena) {
this.player = player;
this.arena = arena;
this.cancelled = false;
}
public Player getPlayer() {
return player;
}
public Arena getArena() {
return arena;
}
@ -36,12 +36,12 @@ public class ArenaPlayerLeaveEvent extends Event implements Cancellable
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
}

View File

@ -18,11 +18,11 @@ public class ArenaPlayerReadyEvent extends Event implements Cancellable
this.arena = arena;
this.cancelled = false;
}
public Player getPlayer() {
return player;
}
public Arena getArena() {
return arena;
}
@ -36,12 +36,12 @@ public class ArenaPlayerReadyEvent extends Event implements Cancellable
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
}

View File

@ -10,12 +10,12 @@ public class ArenaStartEvent extends Event implements Cancellable
private static final HandlerList handlers = new HandlerList();
private Arena arena;
private boolean cancelled;
public ArenaStartEvent(Arena arena) {
this.arena = arena;
this.cancelled = false;
}
public Arena getArena() {
return arena;
}
@ -29,12 +29,12 @@ public class ArenaStartEvent extends Event implements Cancellable
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
}

View File

@ -0,0 +1,50 @@
package com.garbagemule.MobArena.events;
import com.garbagemule.MobArena.MobArena;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* Called right before MobArena begins reloading.
* <p>
* Use this event to re-register components that need to be set up and in
* place <i>before</i> MobArena dips into its config-file. This is mostly
* relevant for plugins that <code>loadbefore</code> MobArena to register
* {@link com.garbagemule.MobArena.things.ThingParser}s and such.
* <p>
* This event is <i>not</i> suitable for working with the "current" state
* of arenas, classes, etc., because MobArena's state at the time of this
* event is about to become stale. To work with the "current" state after
* a reload, use the {@link MobArenaReloadEvent} instead.
*
* @see MobArenaReloadEvent
*/
public class MobArenaPreReloadEvent extends Event {
private static final HandlerList handlers = new HandlerList();
private final MobArena plugin;
public MobArenaPreReloadEvent(MobArena plugin) {
this.plugin = plugin;
}
/**
* Get the current MobArena plugin instance.
*
* @return the MobArena plugin instance
*/
public MobArena getPlugin() {
return plugin;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,52 @@
package com.garbagemule.MobArena.events;
import com.garbagemule.MobArena.MobArena;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* Called right after MobArena has reloaded.
* <p>
* The primary goal of this event is to allow extension plugins to "ride
* along" with the <code>/ma reload</code> command so they don't have to
* implement their own commands. <i>One command to rule them all.</i>
* <p>
* This event is useful if you need to work with arenas, classes, etc. in
* their "current" state. This is typical of plugins that (soft)depend on
* MobArena and use its API after the initialization phase.
* <p>
* This event is <i>not</i> suitable for re-registering components that
* need to be in place <i>before</i> MobArena reloads. For that, see the
* {@link MobArenaPreReloadEvent}.
*
* @see MobArenaPreReloadEvent
*/
public class MobArenaReloadEvent extends Event {
private static final HandlerList handlers = new HandlerList();
private final MobArena plugin;
public MobArenaReloadEvent(MobArena plugin) {
this.plugin = plugin;
}
/**
* Get the current MobArena plugin instance.
*
* @return the MobArena plugin instance
*/
public MobArena getPlugin() {
return plugin;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -11,10 +11,10 @@ public class NewWaveEvent extends Event implements Cancellable
private static final HandlerList handlers = new HandlerList();
private Arena arena;
private boolean cancelled;
private Wave wave;
private int waveNo;
public NewWaveEvent(Arena arena, Wave wave, int waveNo) {
this.arena = arena;
this.wave = wave;
@ -24,11 +24,11 @@ public class NewWaveEvent extends Event implements Cancellable
public Wave getWave() {
return wave;
}
public int getWaveNumber() {
return waveNo;
}
public Arena getArena() {
return arena;
}
@ -42,11 +42,11 @@ public class NewWaveEvent extends Event implements Cancellable
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}

View File

@ -0,0 +1,36 @@
package com.garbagemule.MobArena.formula;
class ArgumentMismatch extends FormulaError {
private static final String few = "Not enough arguments for %s(%s), expected %d, got %d";
private static final String many = "Too many arguments for %s(%s), expected %d, got %d";
ArgumentMismatch(Lexeme lexeme, int expected, int counted, String input) {
super(message(lexeme, expected, counted), input, lexeme.pos);
}
private static String message(Lexeme lexeme, int expected, int counted) {
String name = lexeme.value;
String args = args(expected);
String template = (counted < expected) ? few : many;
return String.format(template, name, args, expected, counted);
}
private static String args(int count) {
if (count == 0) {
return "";
}
char current = 'a';
StringBuilder result = new StringBuilder();
result.append(current);
for (int i = 1; i < count; i++) {
current++;
result.append(",").append(current);
}
return result.toString();
}
}

View File

@ -0,0 +1,24 @@
package com.garbagemule.MobArena.formula;
import com.garbagemule.MobArena.framework.Arena;
class BinaryFormula implements Formula {
private final BinaryOperation operation;
private final Formula left;
private final Formula right;
BinaryFormula(BinaryOperation operation, Formula left, Formula right) {
this.operation = operation;
this.left = left;
this.right = right;
}
@Override
public double evaluate(Arena arena) {
double a = left.evaluate(arena);
double b = right.evaluate(arena);
return operation.apply(a, b);
}
}

View File

@ -0,0 +1,17 @@
package com.garbagemule.MobArena.formula;
class BinaryFunction {
final String name;
final BinaryOperation operation;
BinaryFunction(String name, BinaryOperation operation) {
this.name = name;
this.operation = operation;
}
Formula create(Formula left, Formula right) {
return new BinaryFormula(operation, left, right);
}
}

View File

@ -0,0 +1,8 @@
package com.garbagemule.MobArena.formula;
@FunctionalInterface
public interface BinaryOperation {
double apply(double left, double right);
}

View File

@ -0,0 +1,21 @@
package com.garbagemule.MobArena.formula;
class BinaryOperator {
final String symbol;
final int precedence;
final boolean left;
final BinaryOperation operation;
BinaryOperator(String symbol, int precedence, boolean left, BinaryOperation operation) {
this.symbol = symbol;
this.precedence = precedence;
this.left = left;
this.operation = operation;
}
Formula create(Formula left, Formula right) {
return new BinaryFormula(operation, left, right);
}
}

View File

@ -0,0 +1,170 @@
package com.garbagemule.MobArena.formula;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Environment {
final List<Token> unary;
final List<Token> binary;
final List<Token> symbols;
final Map<String, Double> constants;
final Map<String, Formula> variables;
final Map<String, UnaryOperator> unaryOperators;
final Map<String, BinaryOperator> binaryOperators;
final Map<String, UnaryFunction> unaryFunctions;
final Map<String, BinaryFunction> binaryFunctions;
private Environment() {
unary = new ArrayList<>();
binary = new ArrayList<>();
symbols = Arrays.asList(
Token.LEFT_PAREN,
Token.RIGHT_PAREN,
Token.COMMA
);
constants = new HashMap<>();
variables = new HashMap<>();
unaryOperators = new HashMap<>();
binaryOperators = new HashMap<>();
unaryFunctions = new HashMap<>();
binaryFunctions = new HashMap<>();
}
void registerConstant(String name, double value) {
constants.put(name, value);
}
void registerVariable(String name, Formula formula) {
variables.put(name, formula);
}
void registerUnaryOperator(String symbol, int precedence, UnaryOperation operation) {
unaryOperators.put(symbol, new UnaryOperator(symbol, precedence, operation));
registerOperatorToken(TokenType.UNARY_OPERATOR, symbol, unary);
}
void registerBinaryOperator(String symbol, int precedence, boolean left, BinaryOperation operation) {
binaryOperators.put(symbol, new BinaryOperator(symbol, precedence, left, operation));
registerOperatorToken(TokenType.BINARY_OPERATOR, symbol, binary);
}
void registerUnaryFunction(String name, UnaryOperation operation) {
unaryFunctions.put(name, new UnaryFunction(name, operation));
}
void registerBinaryFunction(String name, BinaryOperation operation) {
binaryFunctions.put(name, new BinaryFunction(name, operation));
}
boolean isConstant(String identifier) {
return constants.containsKey(identifier);
}
boolean isVariable(String identifier) {
return variables.containsKey(identifier);
}
boolean isUnaryOperator(String symbol) {
return unaryOperators.containsKey(symbol);
}
boolean isBinaryOperator(String symbol) {
return binaryOperators.containsKey(symbol);
}
boolean isUnaryFunction(String identifier) {
return unaryFunctions.containsKey(identifier);
}
boolean isBinaryFunction(String identifier) {
return binaryFunctions.containsKey(identifier);
}
boolean isFunction(String identifier) {
return isUnaryFunction(identifier)
|| isBinaryFunction(identifier);
}
double getConstant(String identifier) {
return constants.get(identifier);
}
Formula getVariable(String identifier) {
return variables.get(identifier);
}
UnaryOperator getUnaryOperator(String symbol) {
return unaryOperators.get(symbol);
}
BinaryOperator getBinaryOperator(String symbol) {
return binaryOperators.get(symbol);
}
UnaryFunction getUnaryFunction(String identifier) {
return unaryFunctions.get(identifier);
}
BinaryFunction getBinaryFunction(String identifier) {
return binaryFunctions.get(identifier);
}
private void registerOperatorToken(TokenType type, String operator, List<Token> operators) {
int i;
for (i = 0; i < operators.size(); i++) {
if (operators.get(i).symbol.length() < operator.length()) {
break;
}
}
String escaped = operator.replaceAll("", "\\\\");
String trimmed = escaped.substring(0, escaped.length() - 1);
Token token = new Token(type, trimmed, operator);
operators.add(i, token);
}
@SuppressWarnings("Convert2MethodRef")
static Environment createDefault() {
Environment result = new Environment();
// Constants
result.registerConstant("pi", Math.PI);
result.registerConstant("e", Math.E);
// Unary operators
result.registerUnaryOperator("+", 4, value -> +value);
result.registerUnaryOperator("-", 4, value -> -value);
// Binary operators
result.registerBinaryOperator("+", 2, true, (a, b) -> a + b);
result.registerBinaryOperator("-", 2, true, (a, b) -> a - b);
result.registerBinaryOperator("*", 3, true, (a, b) -> a * b);
result.registerBinaryOperator("/", 3, true, (a, b) -> a / b);
result.registerBinaryOperator("%", 3, true, (a, b) -> a % b);
result.registerBinaryOperator("^", 4, false, (a, b) -> Math.pow(a, b));
// Unary functions
result.registerUnaryFunction("sqrt", Math::sqrt);
result.registerUnaryFunction("abs", Math::abs);
result.registerUnaryFunction("ceil", Math::ceil);
result.registerUnaryFunction("floor", Math::floor);
result.registerUnaryFunction("round", value -> (double) Math.round(value));
result.registerUnaryFunction("sin", Math::sin);
result.registerUnaryFunction("cos", Math::cos);
result.registerUnaryFunction("tan", Math::tan);
// Binary functions
result.registerBinaryFunction("min", Math::min);
result.registerBinaryFunction("max", Math::max);
return result;
}
}

View File

@ -0,0 +1,9 @@
package com.garbagemule.MobArena.formula;
import com.garbagemule.MobArena.framework.Arena;
public interface Formula {
double evaluate(Arena arena);
}

View File

@ -0,0 +1,30 @@
package com.garbagemule.MobArena.formula;
import java.util.Arrays;
class FormulaError extends IllegalArgumentException {
private final String input;
private final int pos;
FormulaError(String message, String input, int pos) {
super(message);
this.input = input;
this.pos = pos;
}
@Override
public String getMessage() {
String arrow = arrow(pos + 1);
String template = "%s\n%s\n%s";
return String.format(template, super.getMessage(), input, arrow);
}
private String arrow(int length) {
char[] value = new char[length];
Arrays.fill(value, ' ');
value[length - 1] = '^';
return new String(value);
}
}

View File

@ -0,0 +1,98 @@
package com.garbagemule.MobArena.formula;
import com.garbagemule.MobArena.MobArena;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class FormulaMacros {
private static final String FILENAME = "formulas.yml";
private final Path file;
private final Map<String, Map<String, String>> macros;
private FormulaMacros(Path file) {
this.file = file;
this.macros = new HashMap<>();
}
public void reload() throws IOException {
byte[] bytes = Files.readAllBytes(file);
String content = new String(bytes);
Yaml yaml = new Yaml();
Map<?, ?> raw = yaml.load(content);
Map<String, Map<String, String>> converted = convert(raw);
macros.clear();
macros.putAll(converted);
}
private Map<String, Map<String, String>> convert(Map<?, ?> raw) {
Map<String, Map<String, String>> result = new HashMap<>();
for (Map.Entry<?, ?> entry : raw.entrySet()) {
String section = String.valueOf(entry.getKey());
Map<String, String> macros = convert(entry.getValue());
if (macros != null) {
result.put(section, macros);
}
}
return result;
}
private Map<String, String> convert(Object raw) {
if (raw instanceof Map) {
Map<String, String> macros = new HashMap<>();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) raw).entrySet()) {
String macro = String.valueOf(entry.getKey());
String formula = String.valueOf(entry.getValue());
macros.put(macro, formula);
}
return macros;
}
return null;
}
public String get(String key) {
return get(null, key);
}
public String get(String section, String key) {
if (key == null) {
return null;
}
if (section != null) {
String macro = lookup(section, key);
if (macro != null) {
return macro;
}
}
return lookup("global", key);
}
private String lookup(String section, String key) {
Map<String, String> specific = macros.get(section);
if (specific == null) {
return null;
}
return specific.get(key);
}
public static FormulaMacros create(MobArena plugin) {
File file = new File(plugin.getDataFolder(), FILENAME);
if (!file.exists()) {
plugin.getLogger().info(FILENAME + " not found, creating default...");
plugin.saveResource(FILENAME, false);
}
return new FormulaMacros(file.toPath());
}
}

View File

@ -0,0 +1,81 @@
package com.garbagemule.MobArena.formula;
import com.garbagemule.MobArena.framework.Arena;
import java.util.List;
public class FormulaManager {
private final Environment env;
private final Lexer lexer;
private final Parser parser;
FormulaManager(
Environment env,
Lexer lexer,
Parser parser
) {
this.env = env;
this.lexer = lexer;
this.parser = parser;
}
@SuppressWarnings("unused")
public void registerConstant(String name, double value) {
env.registerConstant(name, value);
}
@SuppressWarnings("unused")
public void registerVariable(String name, Formula formula) {
env.registerVariable(name, formula);
}
@SuppressWarnings("unused")
public void registerUnaryOperator(String symbol, int precedence, UnaryOperation operation) {
env.registerUnaryOperator(symbol, precedence, operation);
}
@SuppressWarnings("unused")
public void registerBinaryOperator(String symbol, int precedence, boolean left, BinaryOperation operation) {
env.registerBinaryOperator(symbol, precedence, left, operation);
}
@SuppressWarnings("unused")
public void registerUnaryFunction(String name, UnaryOperation operation) {
env.registerUnaryFunction(name, operation);
}
@SuppressWarnings("unused")
public void registerBinaryFunction(String name, BinaryOperation operation) {
env.registerBinaryFunction(name, operation);
}
public Formula parse(String input) {
List<Lexeme> infix = lexer.tokenize(input);
return parser.parse(input, infix);
}
public static FormulaManager createDefault() {
Environment env = Environment.createDefault();
// Wave number variables
env.registerVariable("current-wave", a -> a.getWaveManager().getWaveNumber());
env.registerVariable("final-wave", a -> a.getWaveManager().getFinalWave());
// Player count variables
env.registerVariable("initial-players", Arena::getPlayerCount);
env.registerVariable("live-players", a -> a.getPlayersInArena().size());
env.registerVariable("dead-players", a -> a.getPlayerCount() - a.getPlayersInArena().size());
env.registerVariable("min-players", Arena::getMinPlayers);
env.registerVariable("max-players", Arena::getMaxPlayers);
// Monster count variables
env.registerVariable("live-monsters", a -> a.getMonsterManager().getMonsters().size());
Lexer lexer = new Lexer(env);
Parser parser = new Parser(env);
return new FormulaManager(env, lexer, parser);
}
}

View File

@ -0,0 +1,37 @@
package com.garbagemule.MobArena.formula;
public class Formulas {
/**
* Default "old" style wave growth. Equivalent to the formula:
* <pre>{@code <initial-players> + <current-wave>}</pre>
*/
public static final Formula DEFAULT_WAVE_GROWTH = (arena) -> {
int players = arena.getPlayerCount();
int wave = arena.getWaveManager().getWaveNumber();
return players + wave;
};
/**
* Default "low" swarm amount. Equivalent to the formula:
* <pre>{@code max(1, <initial-players> / 2) * 10}</pre>
*/
public static final Formula DEFAULT_SWARM_AMOUNT = (arena) -> {
int players = arena.getPlayerCount();
return Math.max(1, players / 2) * 10;
};
/**
* Default "medium" boss health. Equivalent to the formula:
* <pre>{@code (<initial-players> + 1) 20 * 15}</pre>
*/
public static final Formula DEFAULT_BOSS_HEALTH = (arena) -> {
int players = arena.getPlayerCount();
return (players + 1) * 20 * 8;
};
private Formulas() {
// OK BOSS
}
}

Some files were not shown because too many files have changed in this diff Show More